123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777 |
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Threading;
- using System.Threading.Tasks;
- using Jellyfin.Data.Entities.Libraries;
- using Jellyfin.Data.Enums;
- using Jellyfin.Extensions;
- using Jellyfin.LiveTv.Configuration;
- using MediaBrowser.Common.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.Model.Entities;
- using MediaBrowser.Model.IO;
- using MediaBrowser.Model.LiveTv;
- using Microsoft.Extensions.Logging;
- namespace Jellyfin.LiveTv.Guide;
- /// <inheritdoc />
- public class GuideManager : IGuideManager
- {
- private const int MaxGuideDays = 14;
- private const string EtagKey = "ProgramEtag";
- private const string ExternalServiceTag = "ExternalServiceId";
- private static readonly ParallelOptions _cacheParallelOptions = new() { MaxDegreeOfParallelism = Math.Min(Environment.ProcessorCount, 10) };
- private readonly ILogger<GuideManager> _logger;
- private readonly IConfigurationManager _config;
- private readonly IFileSystem _fileSystem;
- private readonly IItemRepository _itemRepo;
- private readonly ILibraryManager _libraryManager;
- private readonly ILiveTvManager _liveTvManager;
- private readonly ITunerHostManager _tunerHostManager;
- private readonly IRecordingsManager _recordingsManager;
- private readonly LiveTvDtoService _tvDtoService;
- /// <summary>
- /// Amount of days images are pre-cached from external sources.
- /// </summary>
- public const int MaxCacheDays = 2;
- /// <summary>
- /// Initializes a new instance of the <see cref="GuideManager"/> class.
- /// </summary>
- /// <param name="logger">The <see cref="ILogger{TCategoryName}"/>.</param>
- /// <param name="config">The <see cref="IConfigurationManager"/>.</param>
- /// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
- /// <param name="itemRepo">The <see cref="IItemRepository"/>.</param>
- /// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
- /// <param name="liveTvManager">The <see cref="ILiveTvManager"/>.</param>
- /// <param name="tunerHostManager">The <see cref="ITunerHostManager"/>.</param>
- /// <param name="recordingsManager">The <see cref="IRecordingsManager"/>.</param>
- /// <param name="tvDtoService">The <see cref="LiveTvDtoService"/>.</param>
- public GuideManager(
- ILogger<GuideManager> logger,
- IConfigurationManager config,
- IFileSystem fileSystem,
- IItemRepository itemRepo,
- ILibraryManager libraryManager,
- ILiveTvManager liveTvManager,
- ITunerHostManager tunerHostManager,
- IRecordingsManager recordingsManager,
- LiveTvDtoService tvDtoService)
- {
- _logger = logger;
- _config = config;
- _fileSystem = fileSystem;
- _itemRepo = itemRepo;
- _libraryManager = libraryManager;
- _liveTvManager = liveTvManager;
- _tunerHostManager = tunerHostManager;
- _recordingsManager = recordingsManager;
- _tvDtoService = tvDtoService;
- }
- /// <inheritdoc />
- public GuideInfo GetGuideInfo()
- {
- var startDate = DateTime.UtcNow;
- var endDate = startDate.AddDays(GetGuideDays());
- return new GuideInfo
- {
- StartDate = startDate,
- EndDate = endDate
- };
- }
- /// <inheritdoc />
- public async Task RefreshGuide(IProgress<double> progress, CancellationToken cancellationToken)
- {
- ArgumentNullException.ThrowIfNull(progress);
- await _recordingsManager.CreateRecordingFolders().ConfigureAwait(false);
- await _tunerHostManager.ScanForTunerDeviceChanges(cancellationToken).ConfigureAwait(false);
- var numComplete = 0;
- double progressPerService = _liveTvManager.Services.Count == 0
- ? 0
- : 1.0 / _liveTvManager.Services.Count;
- var newChannelIdList = new List<Guid>();
- var newProgramIdList = new List<Guid>();
- var cleanDatabase = true;
- foreach (var service in _liveTvManager.Services)
- {
- cancellationToken.ThrowIfCancellationRequested();
- _logger.LogDebug("Refreshing guide from {Name}", service.Name);
- try
- {
- var innerProgress = new Progress<double>(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 /= _liveTvManager.Services.Count;
- progress.Report(100 * percent);
- }
- if (cleanDatabase)
- {
- CleanDatabase(newChannelIdList.ToArray(), [BaseItemKind.LiveTvChannel], progress, cancellationToken);
- CleanDatabase(newProgramIdList.ToArray(), [BaseItemKind.LiveTvProgram], progress, cancellationToken);
- }
- var coreService = _liveTvManager.Services.OfType<DefaultLiveTvService>().FirstOrDefault();
- if (coreService is not null)
- {
- await coreService.RefreshSeriesTimers(cancellationToken).ConfigureAwait(false);
- await coreService.RefreshTimers(cancellationToken).ConfigureAwait(false);
- }
- progress.Report(100);
- }
- private double GetGuideDays()
- {
- var config = _config.GetLiveTvConfiguration();
- return config.GuideDays.HasValue
- ? Math.Clamp(config.GuideDays.Value, 1, MaxGuideDays)
- : 7;
- }
- private async Task<Tuple<List<Guid>, List<Guid>>> RefreshChannelsInternal(ILiveTvService service, IProgress<double> progress, CancellationToken cancellationToken)
- {
- progress.Report(10);
- var allChannelsList = (await service.GetChannelsAsync(cancellationToken).ConfigureAwait(false))
- .Select(i => new Tuple<string, ChannelInfo>(service.Name, i))
- .ToList();
- var list = new List<LiveTvChannel>();
- var numComplete = 0;
- var parentFolder = _liveTvManager.GetInternalLiveTvFolder(cancellationToken);
- foreach (var channelInfo in allChannelsList)
- {
- cancellationToken.ThrowIfCancellationRequested();
- try
- {
- var item = await GetChannel(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<LiveTvProgram>();
- var channels = new List<Guid>();
- var guideDays = GetGuideDays();
- _logger.LogInformation("Refreshing guide with {Days} days of guide data", guideDays);
- var maxCacheDate = DateTime.UtcNow.AddDays(MaxCacheDays);
- foreach (var currentChannel in list)
- {
- cancellationToken.ThrowIfCancellationRequested();
- channels.Add(currentChannel.Id);
- 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 = [BaseItemKind.LiveTvProgram],
- ChannelIds = [currentChannel.Id],
- DtoOptions = new DtoOptions(true)
- }).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
- var newPrograms = new List<Guid>();
- var updatedPrograms = new List<Guid>();
- foreach (var program in channelPrograms)
- {
- var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel);
- var id = programItem.Id;
- if (isNew)
- {
- newPrograms.Add(id);
- }
- else if (isUpdated)
- {
- updatedPrograms.Add(id);
- }
- programs.Add(programItem);
- isMovie |= program.IsMovie;
- isSeries |= program.IsSeries;
- isSports |= program.IsSports;
- isNews |= program.IsNews;
- isKids |= program.IsKids;
- }
- _logger.LogDebug(
- "Channel {Name} has {NewCount} new programs and {UpdatedCount} updated programs",
- currentChannel.Name,
- newPrograms.Count,
- updatedPrograms.Count);
- if (newPrograms.Count > 0)
- {
- var newProgramDtos = programs.Where(b => newPrograms.Contains(b.Id)).ToList();
- _libraryManager.CreateOrUpdateItems(newProgramDtos, null, cancellationToken);
- }
- if (updatedPrograms.Count > 0)
- {
- var updatedProgramDtos = programs.Where(b => updatedPrograms.Contains(b.Id)).ToList();
- await _libraryManager.UpdateItemsAsync(
- updatedProgramDtos,
- currentChannel,
- ItemUpdateType.MetadataImport,
- cancellationToken).ConfigureAwait(false);
- }
- await PreCacheImages(programs, maxCacheDate).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);
- var programIds = programs.Select(p => p.Id).ToList();
- return new Tuple<List<Guid>, List<Guid>>(channels, programIds);
- }
- private void CleanDatabase(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> 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.IsEmpty())
- {
- // 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 is not null)
- {
- _libraryManager.DeleteItem(
- item,
- new DeleteOptions
- {
- DeleteFileLocation = false,
- DeleteFromExternalProvider = false
- },
- false);
- }
- }
- numComplete++;
- double percent = numComplete / (double)list.Count;
- progress.Report(100 * percent);
- }
- }
- private async Task<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);
- if (_libraryManager.GetItemById(id) is not LiveTvChannel item)
- {
- item = new LiveTvChannel
- {
- Name = channelInfo.Name,
- Id = id,
- DateCreated = DateTime.UtcNow
- };
- isNew = true;
- }
- if (channelInfo.Tags is not 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<Guid, LiveTvProgram> allExistingPrograms,
- LiveTvChannel channel)
- {
- var id = _tvDtoService.GetInternalProgramId(info.Id);
- var isNew = false;
- var forceUpdate = false;
- if (!allExistingPrograms.TryGetValue(id, out var item))
- {
- isNew = true;
- item = new LiveTvProgram
- {
- Name = info.Name,
- Id = id,
- DateCreated = DateTime.UtcNow,
- DateModified = DateTime.UtcNow
- };
- item.TrySetProviderId(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<string>();
- 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;
- forceUpdate = forceUpdate || UpdateImages(item, info);
- if (isNew)
- {
- item.OnMetadataChanged();
- return (item, isNew, false);
- }
- var isUpdated = false;
- 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 (isUpdated)
- {
- item.OnMetadataChanged();
- }
- return (item, isNew, isUpdated);
- }
- private static bool UpdateImages(BaseItem item, ProgramInfo info)
- {
- var updated = false;
- // Primary
- updated |= UpdateImage(ImageType.Primary, item, info);
- // Thumbnail
- updated |= UpdateImage(ImageType.Thumb, item, info);
- // Logo
- updated |= UpdateImage(ImageType.Logo, item, info);
- // Backdrop
- return updated || UpdateImage(ImageType.Backdrop, item, info);
- }
- private static bool UpdateImage(ImageType imageType, BaseItem item, ProgramInfo info)
- {
- var image = item.GetImages(imageType).FirstOrDefault();
- var currentImagePath = image?.Path;
- var newImagePath = imageType switch
- {
- ImageType.Primary => info.ImagePath,
- _ => string.Empty
- };
- var newImageUrl = imageType switch
- {
- ImageType.Backdrop => info.BackdropImageUrl,
- ImageType.Logo => info.LogoImageUrl,
- ImageType.Primary => info.ImageUrl,
- ImageType.Thumb => info.ThumbImageUrl,
- _ => string.Empty
- };
- var differentImage = newImageUrl?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false
- || newImagePath?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false;
- if (!differentImage)
- {
- return false;
- }
- if (!string.IsNullOrWhiteSpace(newImagePath))
- {
- item.SetImage(
- new ItemImageInfo
- {
- Path = newImagePath,
- Type = imageType
- },
- 0);
- return true;
- }
- if (!string.IsNullOrWhiteSpace(newImageUrl))
- {
- item.SetImage(
- new ItemImageInfo
- {
- Path = newImageUrl,
- Type = imageType
- },
- 0);
- return true;
- }
- item.RemoveImage(image);
- return false;
- }
- private async Task PreCacheImages(IReadOnlyList<BaseItem> programs, DateTime maxCacheDate)
- {
- await Parallel.ForEachAsync(
- programs
- .Where(p => p.EndDate.HasValue && p.EndDate.Value < maxCacheDate)
- .DistinctBy(p => p.Id),
- _cacheParallelOptions,
- async (program, cancellationToken) =>
- {
- for (var i = 0; i < program.ImageInfos.Length; i++)
- {
- if (cancellationToken.IsCancellationRequested)
- {
- return;
- }
- var imageInfo = program.ImageInfos[i];
- if (!imageInfo.IsLocalFile)
- {
- try
- {
- program.ImageInfos[i] = await _libraryManager.ConvertImageToLocal(
- program,
- imageInfo,
- imageIndex: 0,
- removeOnFailure: false)
- .ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "Unable to pre-cache {Url}", imageInfo.Path);
- }
- }
- }
- }).ConfigureAwait(false);
- }
- }
|