#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
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.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
namespace Emby.Server.Implementations.LiveTv
{
///
/// Class LiveTvManager.
///
public class LiveTvManager : ILiveTvManager, IDisposable
{
private const int MaxGuideDays = 14;
private const string ExternalServiceTag = "ExternalServiceId";
private const string EtagKey = "ProgramEtag";
private readonly IServerConfigurationManager _config;
private readonly ILogger _logger;
private readonly IItemRepository _itemRepo;
private readonly IUserManager _userManager;
private readonly IDtoService _dtoService;
private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager;
private readonly ITaskManager _taskManager;
private readonly ILocalizationManager _localization;
private readonly IFileSystem _fileSystem;
private readonly IChannelManager _channelManager;
private readonly LiveTvDtoService _tvDtoService;
private ILiveTvService[] _services = Array.Empty();
private ITunerHost[] _tunerHosts = Array.Empty();
private IListingsProvider[] _listingProviders = Array.Empty();
private bool _disposed = false;
public LiveTvManager(
IServerConfigurationManager config,
ILogger logger,
IItemRepository itemRepo,
IUserDataManager userDataManager,
IDtoService dtoService,
IUserManager userManager,
ILibraryManager libraryManager,
ITaskManager taskManager,
ILocalizationManager localization,
IFileSystem fileSystem,
IChannelManager channelManager,
LiveTvDtoService liveTvDtoService)
{
_config = config;
_logger = logger;
_itemRepo = itemRepo;
_userManager = userManager;
_libraryManager = libraryManager;
_taskManager = taskManager;
_localization = localization;
_fileSystem = fileSystem;
_dtoService = dtoService;
_userDataManager = userDataManager;
_channelManager = channelManager;
_tvDtoService = liveTvDtoService;
}
public event EventHandler> SeriesTimerCancelled;
public event EventHandler> TimerCancelled;
public event EventHandler> TimerCreated;
public event EventHandler> SeriesTimerCreated;
///
/// Gets the services.
///
/// The services.
public IReadOnlyList Services => _services;
public ITunerHost[] TunerHosts => _tunerHosts;
public IListingsProvider[] ListingProviders => _listingProviders;
private LiveTvOptions GetConfiguration()
{
return _config.GetConfiguration("livetv");
}
public string GetEmbyTvActiveRecordingPath(string id)
{
return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id);
}
///
/// 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)
{
if (service is EmbyTV.EmbyTV embyTv)
{
embyTv.TimerCreated += OnEmbyTvTimerCreated;
embyTv.TimerCancelled += OnEmbyTvTimerCancelled;
}
}
}
private void OnEmbyTvTimerCancelled(object sender, GenericEventArgs e)
{
var timerId = e.Argument;
TimerCancelled?.Invoke(this, new GenericEventArgs(new TimerEventInfo(timerId)));
}
private void OnEmbyTvTimerCreated(object sender, GenericEventArgs e)
{
var timer = e.Argument;
TimerCreated?.Invoke(this, new GenericEventArgs(
new TimerEventInfo(timer.Id)
{
ProgramId = _tvDtoService.GetInternalProgramId(timer.ProgramId)
}));
}
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);
}
public QueryResult GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken)
{
var user = query.UserId == 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[] { nameof(LiveTvChannel) },
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 => (i, query.SortOrder ?? SortOrder.Ascending)));
if (query.EnableFavoriteSorting)
{
orderBy.Insert(0, (ItemSortBy.IsFavoriteOrLiked, SortOrder.Descending));
}
if (!internalQuery.OrderBy.Any(i => string.Equals(i.Item1, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase)))
{
orderBy.Add((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;
var 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", CultureInfo.InvariantCulture) + "_";
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)
=> Array.Find(_services, x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))
?? throw new KeyNotFoundException(
string.Format(
CultureInfo.InvariantCulture,
"No service with the name '{0}' can be found.",
name));
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 not 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 async Task GetChannelAsync(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)
{
await _libraryManager.UpdateItemAsync(item, parentFolder, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
}
return item;
}
private (LiveTvProgram item, bool isNew, bool isUpdated) GetProgram(ProgramInfo info, Dictionary allExistingPrograms, LiveTvChannel channel)
{
var id = _tvDtoService.GetInternalProgramId(info.Id);
var isNew = false;
var forceUpdate = false;
if (!allExistingPrograms.TryGetValue(id, out LiveTvProgram 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.Audio = info.Audio;
item.ChannelId = channel.Id;
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 ??= info.OfficialRating;
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 (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.Count == 0)
{
// Unless something else was specified, order by start date to take advantage of a specialized index
query.OrderBy = new[]
{
(ItemSortBy.StartDate, SortOrder.Ascending)
};
}
RemoveFields(options);
var internalQuery = new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { nameof(LiveTvProgram) },
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", CultureInfo.InvariantCulture), 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);
return new QueryResult
{
Items = returnArray,
TotalRecordCount = queryResult.TotalRecordCount
};
}
public QueryResult GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken)
{
var user = query.User;
var topFolder = GetInternalLiveTvFolder(cancellationToken);
var internalQuery = new InternalItemsQuery(user)
{
IncludeItemTypes = new[] { nameof(LiveTvProgram) },
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[] { (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.Count;
var 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)
{
IReadOnlyList timerList = null;
IReadOnlyList seriesTimerList = null;
foreach (var programTuple in programs)
{
var program = programTuple.Item1;
var externalProgramId = programTuple.Item2;
string externalSeriesId = programTuple.Item3;
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", CultureInfo.InvariantCulture);
foundSeriesTimer = true;
}
}
if (foundSeriesTimer || string.IsNullOrWhiteSpace(externalSeriesId))
{
continue;
}
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", CultureInfo.InvariantCulture);
}
}
}
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[] { nameof(LiveTvChannel) }, progress, cancellationToken);
CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { nameof(LiveTvProgram) }, progress, cancellationToken);
}
var coreService = _services.OfType().FirstOrDefault();
if (coreService != null)
{
await coreService.RefreshSeriesTimers(cancellationToken).ConfigureAwait(false);
await coreService.RefreshTimers(cancellationToken).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 = await GetChannelAsync(channelInfo.Item2, channelInfo.Item1, parentFolder, cancellationToken).ConfigureAwait(false);
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[] { nameof(LiveTvProgram) },
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);
var programItem = programTuple.item;
if (programTuple.isNew)
{
newPrograms.Add(programItem);
}
else if (programTuple.isUpdated)
{
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)
{
await _libraryManager.UpdateItemsAsync(
updatedPrograms,
currentChannel,
ItemUpdateType.MetadataImport,
cancellationToken).ConfigureAwait(false);
}
currentChannel.IsMovie = isMovie;
currentChannel.IsNews = isNews;
currentChannel.IsSports = isSports;
currentChannel.IsSeries = iSSeries;
if (isKids)
{
currentChannel.AddTag("Kids");
}
await currentChannel.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
await currentChannel.RefreshMetadata(
new MetadataRefreshOptions(new DirectoryService(_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 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(nameof(Movie));
}
else
{
excludeItemTypes.Add(nameof(Movie));
}
}
if (query.IsSeries.HasValue)
{
if (query.IsSeries.Value)
{
includeItemTypes.Add(nameof(Episode));
}
else
{
excludeItemTypes.Add(nameof(Episode));
}
}
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[] { (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