diff options
| author | Brian Howe <howe.m.brian@gmail.com> | 2024-02-27 21:07:30 -0600 |
|---|---|---|
| committer | Brian Howe <howe.m.brian@gmail.com> | 2024-02-27 21:07:30 -0600 |
| commit | 54eb81395ef8d3d4cb064b56361ce94fc72b38b5 (patch) | |
| tree | 73240b556055557b0ae034ef5d5ba60cb5cb051e /Emby.Server.Implementations | |
| parent | 7f1fec688cc1a6f7f69fa5b059af01cf9c456d3f (diff) | |
| parent | 4786901bb796c3e912f13b686571fde8d16f49c5 (diff) | |
Merge branch 'master' into bhowe34/fix-replace-missing-metadata-for-music
Diffstat (limited to 'Emby.Server.Implementations')
122 files changed, 372 insertions, 13756 deletions
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index dce56e0a4..5870fed76 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -15,7 +15,6 @@ using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Emby.Naming.Common; using Emby.Photos; -using Emby.Server.Implementations.Channels; using Emby.Server.Implementations.Collections; using Emby.Server.Implementations.Configuration; using Emby.Server.Implementations.Cryptography; @@ -25,7 +24,6 @@ using Emby.Server.Implementations.Dto; using Emby.Server.Implementations.HttpServer.Security; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Library; -using Emby.Server.Implementations.LiveTv; using Emby.Server.Implementations.Localization; using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.Plugins; @@ -76,6 +74,7 @@ using MediaBrowser.Controller.TV; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.MediaEncoding.Subtitles; +using MediaBrowser.MediaEncoding.Transcoding; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; @@ -503,8 +502,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(_xmlSerializer); - serviceCollection.AddSingleton<IStreamHelper, StreamHelper>(); - serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>(); serviceCollection.AddSingleton<ISocketFactory, SocketFactory>(); @@ -556,8 +553,6 @@ namespace Emby.Server.Implementations serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>)); serviceCollection.AddSingleton<IDtoService, DtoService>(); - serviceCollection.AddSingleton<IChannelManager, ChannelManager>(); - serviceCollection.AddSingleton<ISessionManager, SessionManager>(); serviceCollection.AddSingleton<ICollectionManager, CollectionManager>(); @@ -566,9 +561,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>(); - serviceCollection.AddSingleton<LiveTvDtoService>(); - serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>(); - serviceCollection.AddSingleton<IUserViewManager, UserViewManager>(); serviceCollection.AddSingleton<IChapterManager, ChapterManager>(); @@ -583,7 +575,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); - serviceCollection.AddSingleton<TranscodingJobHelper>(); + serviceCollection.AddSingleton<ITranscodeManager, TranscodeManager>(); serviceCollection.AddScoped<MediaInfoHelper>(); serviceCollection.AddScoped<AudioHelper>(); serviceCollection.AddScoped<DynamicHlsHelper>(); @@ -703,7 +695,7 @@ namespace Emby.Server.Implementations GetExports<IMetadataSaver>(), GetExports<IExternalId>()); - Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>()); + Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<IListingsProvider>()); Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>()); } diff --git a/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs b/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs deleted file mode 100644 index 3e149cc82..000000000 --- a/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Channels; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Dto; - -namespace Emby.Server.Implementations.Channels -{ - /// <summary> - /// A media source provider for channels. - /// </summary> - public class ChannelDynamicMediaSourceProvider : IMediaSourceProvider - { - private readonly ChannelManager _channelManager; - - /// <summary> - /// Initializes a new instance of the <see cref="ChannelDynamicMediaSourceProvider"/> class. - /// </summary> - /// <param name="channelManager">The channel manager.</param> - public ChannelDynamicMediaSourceProvider(IChannelManager channelManager) - { - _channelManager = (ChannelManager)channelManager; - } - - /// <inheritdoc /> - public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(BaseItem item, CancellationToken cancellationToken) - { - return item.SourceType == SourceType.Channel - ? _channelManager.GetDynamicMediaSources(item, cancellationToken) - : Task.FromResult(Enumerable.Empty<MediaSourceInfo>()); - } - - /// <inheritdoc /> - public Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - } -} diff --git a/Emby.Server.Implementations/Channels/ChannelImageProvider.cs b/Emby.Server.Implementations/Channels/ChannelImageProvider.cs deleted file mode 100644 index 25cbfcf14..000000000 --- a/Emby.Server.Implementations/Channels/ChannelImageProvider.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Channels; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; - -namespace Emby.Server.Implementations.Channels -{ - /// <summary> - /// An image provider for channels. - /// </summary> - public class ChannelImageProvider : IDynamicImageProvider, IHasItemChangeMonitor - { - private readonly IChannelManager _channelManager; - - /// <summary> - /// Initializes a new instance of the <see cref="ChannelImageProvider"/> class. - /// </summary> - /// <param name="channelManager">The channel manager.</param> - public ChannelImageProvider(IChannelManager channelManager) - { - _channelManager = channelManager; - } - - /// <inheritdoc /> - public string Name => "Channel Image Provider"; - - /// <inheritdoc /> - public IEnumerable<ImageType> GetSupportedImages(BaseItem item) - { - return GetChannel(item).GetSupportedChannelImages(); - } - - /// <inheritdoc /> - public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken) - { - var channel = GetChannel(item); - - return channel.GetChannelImage(type, cancellationToken); - } - - /// <inheritdoc /> - public bool Supports(BaseItem item) - { - return item is Channel; - } - - private IChannel GetChannel(BaseItem item) - { - var channel = (Channel)item; - - return ((ChannelManager)_channelManager).GetChannelProvider(channel); - } - - /// <inheritdoc /> - public bool HasChanged(BaseItem item, IDirectoryService directoryService) - { - return GetSupportedImages(item).Any(i => !item.HasImage(i)); - } - } -} diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs deleted file mode 100644 index 8279acb05..000000000 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ /dev/null @@ -1,1244 +0,0 @@ -#nullable disable - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Data.Entities; -using Jellyfin.Data.Enums; -using Jellyfin.Extensions; -using Jellyfin.Extensions.Json; -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.Entities.Audio; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Channels; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Querying; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Logging; -using Episode = MediaBrowser.Controller.Entities.TV.Episode; -using Movie = MediaBrowser.Controller.Entities.Movies.Movie; -using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; -using Season = MediaBrowser.Controller.Entities.TV.Season; -using Series = MediaBrowser.Controller.Entities.TV.Series; - -namespace Emby.Server.Implementations.Channels -{ - /// <summary> - /// The LiveTV channel manager. - /// </summary> - public class ChannelManager : IChannelManager, IDisposable - { - private readonly IUserManager _userManager; - private readonly IUserDataManager _userDataManager; - private readonly IDtoService _dtoService; - private readonly ILibraryManager _libraryManager; - private readonly ILogger<ChannelManager> _logger; - private readonly IServerConfigurationManager _config; - private readonly IFileSystem _fileSystem; - private readonly IProviderManager _providerManager; - private readonly IMemoryCache _memoryCache; - private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1); - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - private bool _disposed = false; - - /// <summary> - /// Initializes a new instance of the <see cref="ChannelManager"/> class. - /// </summary> - /// <param name="userManager">The user manager.</param> - /// <param name="dtoService">The dto service.</param> - /// <param name="libraryManager">The library manager.</param> - /// <param name="logger">The logger.</param> - /// <param name="config">The server configuration manager.</param> - /// <param name="fileSystem">The filesystem.</param> - /// <param name="userDataManager">The user data manager.</param> - /// <param name="providerManager">The provider manager.</param> - /// <param name="memoryCache">The memory cache.</param> - /// <param name="channels">The channels.</param> - public ChannelManager( - IUserManager userManager, - IDtoService dtoService, - ILibraryManager libraryManager, - ILogger<ChannelManager> logger, - IServerConfigurationManager config, - IFileSystem fileSystem, - IUserDataManager userDataManager, - IProviderManager providerManager, - IMemoryCache memoryCache, - IEnumerable<IChannel> channels) - { - _userManager = userManager; - _dtoService = dtoService; - _libraryManager = libraryManager; - _logger = logger; - _config = config; - _fileSystem = fileSystem; - _userDataManager = userDataManager; - _providerManager = providerManager; - _memoryCache = memoryCache; - Channels = channels.ToArray(); - } - - internal IChannel[] Channels { get; } - - private static TimeSpan CacheLength => TimeSpan.FromHours(3); - - /// <inheritdoc /> - public bool EnableMediaSourceDisplay(BaseItem item) - { - var internalChannel = _libraryManager.GetItemById(item.ChannelId); - var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id)); - - return channel is not IDisableMediaSourceDisplay; - } - - /// <inheritdoc /> - public bool CanDelete(BaseItem item) - { - var internalChannel = _libraryManager.GetItemById(item.ChannelId); - var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id)); - - return channel is ISupportsDelete supportsDelete && supportsDelete.CanDelete(item); - } - - /// <inheritdoc /> - public bool EnableMediaProbe(BaseItem item) - { - var internalChannel = _libraryManager.GetItemById(item.ChannelId); - var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id)); - - return channel is ISupportsMediaProbe; - } - - /// <inheritdoc /> - public Task DeleteItem(BaseItem item) - { - var internalChannel = _libraryManager.GetItemById(item.ChannelId); - if (internalChannel is null) - { - throw new ArgumentException(nameof(item.ChannelId)); - } - - var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id)); - - if (channel is not ISupportsDelete supportsDelete) - { - throw new ArgumentException(nameof(channel)); - } - - return supportsDelete.DeleteItem(item.ExternalId, CancellationToken.None); - } - - private IEnumerable<IChannel> GetAllChannels() - { - return Channels - .OrderBy(i => i.Name); - } - - /// <summary> - /// Get the installed channel IDs. - /// </summary> - /// <returns>An <see cref="IEnumerable{T}"/> containing installed channel IDs.</returns> - public IEnumerable<Guid> GetInstalledChannelIds() - { - return GetAllChannels().Select(i => GetInternalChannelId(i.Name)); - } - - /// <inheritdoc /> - public async Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query) - { - var user = query.UserId.Equals(default) - ? null - : _userManager.GetUserById(query.UserId); - - var channels = await GetAllChannelEntitiesAsync() - .OrderBy(i => i.SortName) - .ToListAsync() - .ConfigureAwait(false); - - if (query.IsRecordingsFolder.HasValue) - { - var val = query.IsRecordingsFolder.Value; - channels = channels.Where(i => - { - try - { - return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes - && hasAttributes.Attributes.Contains("Recordings", StringComparison.OrdinalIgnoreCase)) == val; - } - catch - { - return false; - } - }).ToList(); - } - - if (query.SupportsLatestItems.HasValue) - { - var val = query.SupportsLatestItems.Value; - channels = channels.Where(i => - { - try - { - return GetChannelProvider(i) is ISupportsLatestMedia == val; - } - catch - { - return false; - } - }).ToList(); - } - - if (query.SupportsMediaDeletion.HasValue) - { - var val = query.SupportsMediaDeletion.Value; - channels = channels.Where(i => - { - try - { - return GetChannelProvider(i) is ISupportsDelete == val; - } - catch - { - return false; - } - }).ToList(); - } - - if (query.IsFavorite.HasValue) - { - var val = query.IsFavorite.Value; - channels = channels.Where(i => _userDataManager.GetUserData(user, i).IsFavorite == val) - .ToList(); - } - - if (user is not null) - { - var userId = user.Id.ToString("N", CultureInfo.InvariantCulture); - channels = channels.Where(i => - { - if (!i.IsVisible(user)) - { - return false; - } - - try - { - return GetChannelProvider(i).IsEnabledFor(userId); - } - catch - { - return false; - } - }).ToList(); - } - - var all = channels; - var totalCount = all.Count; - - if (query.StartIndex.HasValue || query.Limit.HasValue) - { - int startIndex = query.StartIndex ?? 0; - int count = query.Limit is null ? totalCount - startIndex : Math.Min(query.Limit.Value, totalCount - startIndex); - all = all.GetRange(startIndex, count); - } - - if (query.RefreshLatestChannelItems) - { - foreach (var item in all) - { - await RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).ConfigureAwait(false); - } - } - - return new QueryResult<Channel>( - query.StartIndex, - totalCount, - all); - } - - /// <inheritdoc /> - public async Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query) - { - var user = query.UserId.Equals(default) - ? null - : _userManager.GetUserById(query.UserId); - - var internalResult = await GetChannelsInternalAsync(query).ConfigureAwait(false); - - var dtoOptions = new DtoOptions(); - - // TODO Fix The co-variant conversion (internalResult.Items) between Folder[] and BaseItem[], this can generate runtime issues. - var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user); - - var result = new QueryResult<BaseItemDto>( - query.StartIndex, - internalResult.TotalRecordCount, - returnItems); - - return result; - } - - /// <summary> - /// Refreshes the associated channels. - /// </summary> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> - /// <returns>The completed task.</returns> - public async Task RefreshChannels(IProgress<double> progress, CancellationToken cancellationToken) - { - var allChannelsList = GetAllChannels().ToList(); - - var numComplete = 0; - - foreach (var channelInfo in allChannelsList) - { - cancellationToken.ThrowIfCancellationRequested(); - - try - { - await GetChannel(channelInfo, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting channel information for {0}", channelInfo.Name); - } - - numComplete++; - double percent = (double)numComplete / allChannelsList.Count; - progress.Report(100 * percent); - } - - progress.Report(100); - } - - private async IAsyncEnumerable<Channel> GetAllChannelEntitiesAsync() - { - foreach (IChannel channel in GetAllChannels()) - { - yield return GetChannel(GetInternalChannelId(channel.Name)) ?? await GetChannel(channel, CancellationToken.None).ConfigureAwait(false); - } - } - - private MediaSourceInfo[] GetSavedMediaSources(BaseItem item) - { - var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json"); - - try - { - var bytes = File.ReadAllBytes(path); - return JsonSerializer.Deserialize<MediaSourceInfo[]>(bytes, _jsonOptions) - ?? Array.Empty<MediaSourceInfo>(); - } - catch - { - return Array.Empty<MediaSourceInfo>(); - } - } - - private async Task SaveMediaSources(BaseItem item, List<MediaSourceInfo> mediaSources) - { - var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json"); - - if (mediaSources is null || mediaSources.Count == 0) - { - try - { - _fileSystem.DeleteFile(path); - } - catch - { - } - - return; - } - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - FileStream createStream = File.Create(path); - await using (createStream.ConfigureAwait(false)) - { - await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false); - } - } - - /// <inheritdoc /> - public IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken) - { - IEnumerable<MediaSourceInfo> results = GetSavedMediaSources(item); - - return results - .Select(i => NormalizeMediaSource(item, i)) - .ToList(); - } - - /// <summary> - /// Gets the dynamic media sources based on the provided item. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> - /// <returns>The task representing the operation to get the media sources.</returns> - public async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, CancellationToken cancellationToken) - { - var channel = GetChannel(item.ChannelId); - var channelPlugin = GetChannelProvider(channel); - - IEnumerable<MediaSourceInfo> results; - - if (channelPlugin is IRequiresMediaInfoCallback requiresCallback) - { - results = await GetChannelItemMediaSourcesInternal(requiresCallback, item.ExternalId, cancellationToken) - .ConfigureAwait(false); - } - else - { - results = Enumerable.Empty<MediaSourceInfo>(); - } - - return results - .Select(i => NormalizeMediaSource(item, i)) - .ToList(); - } - - private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken) - { - if (_memoryCache.TryGetValue(id, out List<MediaSourceInfo> cachedInfo)) - { - return cachedInfo; - } - - var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken) - .ConfigureAwait(false); - var list = mediaInfo.ToList(); - _memoryCache.Set(id, list, DateTimeOffset.UtcNow.AddMinutes(5)); - - return list; - } - - private static MediaSourceInfo NormalizeMediaSource(BaseItem item, MediaSourceInfo info) - { - info.RunTimeTicks ??= item.RunTimeTicks; - - return info; - } - - private async Task<Channel> GetChannel(IChannel channelInfo, CancellationToken cancellationToken) - { - var parentFolderId = Guid.Empty; - - var id = GetInternalChannelId(channelInfo.Name); - - var path = Channel.GetInternalMetadataPath(_config.ApplicationPaths.InternalMetadataPath, id); - - var isNew = false; - var forceUpdate = false; - - var item = _libraryManager.GetItemById(id) as Channel; - - if (item is null) - { - item = new Channel - { - Name = channelInfo.Name, - Id = id, - DateCreated = _fileSystem.GetCreationTimeUtc(path), - DateModified = _fileSystem.GetLastWriteTimeUtc(path) - }; - - isNew = true; - } - - if (!string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase)) - { - isNew = true; - } - - item.Path = path; - - if (!item.ChannelId.Equals(id)) - { - forceUpdate = true; - } - - item.ChannelId = id; - - if (!item.ParentId.Equals(parentFolderId)) - { - forceUpdate = true; - } - - item.ParentId = parentFolderId; - - item.OfficialRating = GetOfficialRating(channelInfo.ParentalRating); - item.Overview = channelInfo.Description; - - if (string.IsNullOrWhiteSpace(item.Name)) - { - item.Name = channelInfo.Name; - } - - if (isNew) - { - item.OnMetadataChanged(); - _libraryManager.CreateItem(item, null); - } - - await item.RefreshMetadata( - new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - ForceSave = !isNew && forceUpdate - }, - cancellationToken).ConfigureAwait(false); - - return item; - } - - private static string GetOfficialRating(ChannelParentalRating rating) - { - return rating switch - { - ChannelParentalRating.Adult => "XXX", - ChannelParentalRating.UsR => "R", - ChannelParentalRating.UsPG13 => "PG-13", - ChannelParentalRating.UsPG => "PG", - _ => null - }; - } - - /// <summary> - /// Gets a channel with the provided Guid. - /// </summary> - /// <param name="id">The Guid.</param> - /// <returns>The corresponding channel.</returns> - public Channel GetChannel(Guid id) - { - return _libraryManager.GetItemById(id) as Channel; - } - - /// <inheritdoc /> - public Channel GetChannel(string id) - { - return _libraryManager.GetItemById(id) as Channel; - } - - /// <inheritdoc /> - public ChannelFeatures[] GetAllChannelFeatures() - { - return _libraryManager.GetItemIds( - new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.Channel }, - OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) } - }).Select(i => GetChannelFeatures(i)).ToArray(); - } - - /// <inheritdoc /> - public ChannelFeatures GetChannelFeatures(Guid? id) - { - if (!id.HasValue) - { - throw new ArgumentNullException(nameof(id)); - } - - var channel = GetChannel(id.Value); - var channelProvider = GetChannelProvider(channel); - - return GetChannelFeaturesDto(channel, channelProvider, channelProvider.GetChannelFeatures()); - } - - /// <summary> - /// Checks whether the provided Guid supports external transfer. - /// </summary> - /// <param name="channelId">The Guid.</param> - /// <returns>Whether or not the provided Guid supports external transfer.</returns> - public bool SupportsExternalTransfer(Guid channelId) - { - var channelProvider = GetChannelProvider(channelId); - - return channelProvider.GetChannelFeatures().SupportsContentDownloading; - } - - /// <summary> - /// Gets the provided channel's supported features. - /// </summary> - /// <param name="channel">The channel.</param> - /// <param name="provider">The provider.</param> - /// <param name="features">The features.</param> - /// <returns>The supported features.</returns> - public ChannelFeatures GetChannelFeaturesDto( - Channel channel, - IChannel provider, - InternalChannelFeatures features) - { - var supportsLatest = provider is ISupportsLatestMedia; - - return new ChannelFeatures(channel.Name, channel.Id) - { - CanFilter = !features.MaxPageSize.HasValue, - CanSearch = provider is ISearchableChannel, - ContentTypes = features.ContentTypes.ToArray(), - DefaultSortFields = features.DefaultSortFields.ToArray(), - MaxPageSize = features.MaxPageSize, - MediaTypes = features.MediaTypes.ToArray(), - SupportsSortOrderToggle = features.SupportsSortOrderToggle, - SupportsLatestMedia = supportsLatest, - SupportsContentDownloading = features.SupportsContentDownloading, - AutoRefreshLevels = features.AutoRefreshLevels - }; - } - - private Guid GetInternalChannelId(string name) - { - ArgumentException.ThrowIfNullOrEmpty(name); - - return _libraryManager.GetNewItemId("Channel " + name, typeof(Channel)); - } - - /// <inheritdoc /> - public async Task<QueryResult<BaseItemDto>> GetLatestChannelItems(InternalItemsQuery query, CancellationToken cancellationToken) - { - var internalResult = await GetLatestChannelItemsInternal(query, cancellationToken).ConfigureAwait(false); - - var items = internalResult.Items; - var totalRecordCount = internalResult.TotalRecordCount; - - var returnItems = _dtoService.GetBaseItemDtos(items, query.DtoOptions, query.User); - - var result = new QueryResult<BaseItemDto>( - query.StartIndex, - totalRecordCount, - returnItems); - - return result; - } - - /// <inheritdoc /> - public async Task<QueryResult<BaseItem>> GetLatestChannelItemsInternal(InternalItemsQuery query, CancellationToken cancellationToken) - { - var channels = GetAllChannels().Where(i => i is ISupportsLatestMedia).ToArray(); - - if (query.ChannelIds.Count > 0) - { - // Avoid implicitly captured closure - var ids = query.ChannelIds; - channels = channels - .Where(i => ids.Contains(GetInternalChannelId(i.Name))) - .ToArray(); - } - - if (channels.Length == 0) - { - return new QueryResult<BaseItem>(); - } - - foreach (var channel in channels) - { - await RefreshLatestChannelItems(channel, cancellationToken).ConfigureAwait(false); - } - - query.IsFolder = false; - - // hack for trailers, figure out a better way later - var sortByPremiereDate = channels.Length == 1 && channels[0].GetType().Name.Contains("Trailer", StringComparison.Ordinal); - - if (sortByPremiereDate) - { - query.OrderBy = new[] - { - (ItemSortBy.PremiereDate, SortOrder.Descending), - (ItemSortBy.ProductionYear, SortOrder.Descending), - (ItemSortBy.DateCreated, SortOrder.Descending) - }; - } - else - { - query.OrderBy = new[] - { - (ItemSortBy.DateCreated, SortOrder.Descending) - }; - } - - return _libraryManager.GetItemsResult(query); - } - - private async Task RefreshLatestChannelItems(IChannel channel, CancellationToken cancellationToken) - { - var internalChannel = await GetChannel(channel, cancellationToken).ConfigureAwait(false); - - var query = new InternalItemsQuery - { - Parent = internalChannel, - EnableTotalRecordCount = false, - ChannelIds = new Guid[] { internalChannel.Id } - }; - - var result = await GetChannelItemsInternal(query, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false); - - foreach (var item in result.Items) - { - if (item is Folder folder) - { - await GetChannelItemsInternal( - new InternalItemsQuery - { - Parent = folder, - EnableTotalRecordCount = false, - ChannelIds = new Guid[] { internalChannel.Id } - }, - new SimpleProgress<double>(), - cancellationToken).ConfigureAwait(false); - } - } - } - - /// <inheritdoc /> - public async Task<QueryResult<BaseItem>> GetChannelItemsInternal(InternalItemsQuery query, IProgress<double> progress, CancellationToken cancellationToken) - { - // Get the internal channel entity - var channel = GetChannel(query.ChannelIds[0]); - - // Find the corresponding channel provider plugin - var channelProvider = GetChannelProvider(channel); - - var parentItem = query.ParentId.Equals(default) - ? channel - : _libraryManager.GetItemById(query.ParentId); - - var itemsResult = await GetChannelItems( - channelProvider, - query.User, - parentItem is Channel ? null : parentItem.ExternalId, - null, - false, - cancellationToken) - .ConfigureAwait(false); - - if (query.ParentId.Equals(default)) - { - query.Parent = channel; - } - - query.ChannelIds = Array.Empty<Guid>(); - - // Not yet sure why this is causing a problem - query.GroupByPresentationUniqueKey = false; - - // null if came from cache - if (itemsResult is not null) - { - var items = itemsResult.Items; - var itemsLen = items.Count; - var internalItems = new Guid[itemsLen]; - for (int i = 0; i < itemsLen; i++) - { - internalItems[i] = (await GetChannelItemEntityAsync( - items[i], - channelProvider, - channel.Id, - parentItem, - cancellationToken).ConfigureAwait(false)).Id; - } - - var existingIds = _libraryManager.GetItemIds(query); - var deadIds = existingIds.Except(internalItems) - .ToArray(); - - foreach (var deadId in deadIds) - { - var deadItem = _libraryManager.GetItemById(deadId); - if (deadItem is not null) - { - _libraryManager.DeleteItem( - deadItem, - new DeleteOptions - { - DeleteFileLocation = false, - DeleteFromExternalProvider = false - }, - parentItem, - false); - } - } - } - - return _libraryManager.GetItemsResult(query); - } - - /// <inheritdoc /> - public async Task<QueryResult<BaseItemDto>> GetChannelItems(InternalItemsQuery query, CancellationToken cancellationToken) - { - var internalResult = await GetChannelItemsInternal(query, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false); - - var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, query.DtoOptions, query.User); - - var result = new QueryResult<BaseItemDto>( - query.StartIndex, - internalResult.TotalRecordCount, - returnItems); - - return result; - } - - private async Task<ChannelItemResult> GetChannelItems( - IChannel channel, - User user, - string externalFolderId, - ChannelItemSortField? sortField, - bool sortDescending, - CancellationToken cancellationToken) - { - var userId = user?.Id.ToString("N", CultureInfo.InvariantCulture); - - var cacheLength = CacheLength; - var cachePath = GetChannelDataCachePath(channel, userId, externalFolderId, sortField, sortDescending); - - try - { - if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) - { - await using FileStream jsonStream = AsyncFile.OpenRead(cachePath); - var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); - if (cachedResult is not null) - { - return null; - } - } - } - catch (FileNotFoundException) - { - } - catch (IOException) - { - } - - await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - try - { - if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) - { - await using FileStream jsonStream = AsyncFile.OpenRead(cachePath); - var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); - if (cachedResult is not null) - { - return null; - } - } - } - catch (FileNotFoundException) - { - } - catch (IOException) - { - } - - var query = new InternalChannelItemQuery - { - UserId = user?.Id ?? Guid.Empty, - SortBy = sortField, - SortDescending = sortDescending, - FolderId = externalFolderId - }; - - query.FolderId = externalFolderId; - - var result = await channel.GetChannelItems(query, cancellationToken).ConfigureAwait(false); - - if (result is null) - { - throw new InvalidOperationException("Channel returned a null result from GetChannelItems"); - } - - await CacheResponse(result, cachePath); - - return result; - } - finally - { - _resourcePool.Release(); - } - } - - private async Task CacheResponse(ChannelItemResult result, string path) - { - try - { - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - await using FileStream createStream = File.Create(path); - await JsonSerializer.SerializeAsync(createStream, result, _jsonOptions).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error writing to channel cache file: {Path}", path); - } - } - - private string GetChannelDataCachePath( - IChannel channel, - string userId, - string externalFolderId, - ChannelItemSortField? sortField, - bool sortDescending) - { - var channelId = GetInternalChannelId(channel.Name).ToString("N", CultureInfo.InvariantCulture); - - var userCacheKey = string.Empty; - - if (channel is IHasCacheKey hasCacheKey) - { - userCacheKey = hasCacheKey.GetCacheKey(userId) ?? string.Empty; - } - - var filename = string.IsNullOrEmpty(externalFolderId) ? "root" : externalFolderId.GetMD5().ToString("N", CultureInfo.InvariantCulture); - filename += userCacheKey; - - var version = ((channel.DataVersion ?? string.Empty) + "2").GetMD5().ToString("N", CultureInfo.InvariantCulture); - - if (sortField.HasValue) - { - filename += "-sortField-" + sortField.Value; - } - - if (sortDescending) - { - filename += "-sortDescending"; - } - - filename = filename.GetMD5().ToString("N", CultureInfo.InvariantCulture); - - return Path.Combine( - _config.ApplicationPaths.CachePath, - "channels", - channelId, - version, - filename + ".json"); - } - - private static string GetIdToHash(string externalId, string channelName) - { - // Increment this as needed to force new downloads - // Incorporate Name because it's being used to convert channel entity to provider - return externalId + (channelName ?? string.Empty) + "16"; - } - - private T GetItemById<T>(string idString, string channelName, out bool isNew) - where T : BaseItem, new() - { - var id = _libraryManager.GetNewItemId(GetIdToHash(idString, channelName), typeof(T)); - - T item = null; - - try - { - item = _libraryManager.GetItemById(id) as T; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error retrieving channel item from database"); - } - - if (item is null) - { - item = new T(); - isNew = true; - } - else - { - isNew = false; - } - - item.Id = id; - return item; - } - - private async Task<BaseItem> GetChannelItemEntityAsync(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, BaseItem parentFolder, CancellationToken cancellationToken) - { - var parentFolderId = parentFolder.Id; - - BaseItem item; - bool isNew; - bool forceUpdate = false; - - if (info.Type == ChannelItemType.Folder) - { - item = info.FolderType switch - { - ChannelFolderType.MusicAlbum => GetItemById<MusicAlbum>(info.Id, channelProvider.Name, out isNew), - ChannelFolderType.MusicArtist => GetItemById<MusicArtist>(info.Id, channelProvider.Name, out isNew), - ChannelFolderType.PhotoAlbum => GetItemById<PhotoAlbum>(info.Id, channelProvider.Name, out isNew), - ChannelFolderType.Series => GetItemById<Series>(info.Id, channelProvider.Name, out isNew), - ChannelFolderType.Season => GetItemById<Season>(info.Id, channelProvider.Name, out isNew), - _ => GetItemById<Folder>(info.Id, channelProvider.Name, out isNew) - }; - } - else if (info.MediaType == ChannelMediaType.Audio) - { - item = info.ContentType == ChannelMediaContentType.Podcast - ? GetItemById<AudioBook>(info.Id, channelProvider.Name, out isNew) - : GetItemById<Audio>(info.Id, channelProvider.Name, out isNew); - } - else - { - item = info.ContentType switch - { - ChannelMediaContentType.Episode => GetItemById<Episode>(info.Id, channelProvider.Name, out isNew), - ChannelMediaContentType.Movie => GetItemById<Movie>(info.Id, channelProvider.Name, out isNew), - var x when x == ChannelMediaContentType.Trailer || info.ExtraType == ExtraType.Trailer - => GetItemById<Trailer>(info.Id, channelProvider.Name, out isNew), - _ => GetItemById<Video>(info.Id, channelProvider.Name, out isNew) - }; - } - - var enableMediaProbe = channelProvider is ISupportsMediaProbe; - - if (info.IsLiveStream) - { - item.RunTimeTicks = null; - } - else if (isNew || !enableMediaProbe) - { - item.RunTimeTicks = info.RunTimeTicks; - } - - if (isNew) - { - item.Name = info.Name; - item.Genres = info.Genres.ToArray(); - item.Studios = info.Studios.ToArray(); - item.CommunityRating = info.CommunityRating; - item.Overview = info.Overview; - item.IndexNumber = info.IndexNumber; - item.ParentIndexNumber = info.ParentIndexNumber; - item.PremiereDate = info.PremiereDate; - item.ProductionYear = info.ProductionYear; - item.ProviderIds = info.ProviderIds; - item.OfficialRating = info.OfficialRating; - item.DateCreated = info.DateCreated ?? DateTime.UtcNow; - item.Tags = info.Tags.ToArray(); - item.OriginalTitle = info.OriginalTitle; - } - else if (info.Type == ChannelItemType.Folder && info.FolderType == ChannelFolderType.Container) - { - // At least update names of container folders - if (item.Name != info.Name) - { - item.Name = info.Name; - forceUpdate = true; - } - } - - if (item is IHasArtist hasArtists) - { - hasArtists.Artists = info.Artists.ToArray(); - } - - if (item is IHasAlbumArtist hasAlbumArtists) - { - hasAlbumArtists.AlbumArtists = info.AlbumArtists.ToArray(); - } - - if (item is Trailer trailer) - { - if (!info.TrailerTypes.SequenceEqual(trailer.TrailerTypes)) - { - _logger.LogDebug("Forcing update due to TrailerTypes {0}", item.Name); - forceUpdate = true; - } - - trailer.TrailerTypes = info.TrailerTypes.ToArray(); - } - - if (info.DateModified > item.DateModified) - { - item.DateModified = info.DateModified; - _logger.LogDebug("Forcing update due to DateModified {0}", item.Name); - forceUpdate = true; - } - - if (!internalChannelId.Equals(item.ChannelId)) - { - forceUpdate = true; - _logger.LogDebug("Forcing update due to ChannelId {0}", item.Name); - } - - item.ChannelId = internalChannelId; - - if (!item.ParentId.Equals(parentFolderId)) - { - forceUpdate = true; - _logger.LogDebug("Forcing update due to parent folder Id {0}", item.Name); - } - - item.ParentId = parentFolderId; - - if (item is IHasSeries hasSeries) - { - if (!string.Equals(hasSeries.SeriesName, info.SeriesName, StringComparison.OrdinalIgnoreCase)) - { - forceUpdate = true; - _logger.LogDebug("Forcing update due to SeriesName {0}", item.Name); - } - - hasSeries.SeriesName = info.SeriesName; - } - - if (!string.Equals(item.ExternalId, info.Id, StringComparison.OrdinalIgnoreCase)) - { - forceUpdate = true; - _logger.LogDebug("Forcing update due to ExternalId {0}", item.Name); - } - - item.ExternalId = info.Id; - - if (item is Audio channelAudioItem) - { - channelAudioItem.ExtraType = info.ExtraType; - - var mediaSource = info.MediaSources.FirstOrDefault(); - item.Path = mediaSource?.Path; - } - - if (item is Video channelVideoItem) - { - channelVideoItem.ExtraType = info.ExtraType; - - var mediaSource = info.MediaSources.FirstOrDefault(); - item.Path = mediaSource?.Path; - } - - if (!string.IsNullOrEmpty(info.ImageUrl) && !item.HasImage(ImageType.Primary)) - { - item.SetImagePath(ImageType.Primary, info.ImageUrl); - _logger.LogDebug("Forcing update due to ImageUrl {0}", item.Name); - forceUpdate = true; - } - - if (!info.IsLiveStream) - { - if (item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase)) - { - item.Tags = item.Tags.Except(new[] { "livestream" }, StringComparer.OrdinalIgnoreCase).ToArray(); - _logger.LogDebug("Forcing update due to Tags {0}", item.Name); - forceUpdate = true; - } - } - else - { - if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase)) - { - item.Tags = item.Tags.Concat(new[] { "livestream" }).ToArray(); - _logger.LogDebug("Forcing update due to Tags {0}", item.Name); - forceUpdate = true; - } - } - - item.OnMetadataChanged(); - - if (isNew) - { - _libraryManager.CreateItem(item, parentFolder); - - if (info.People is not null && info.People.Count > 0) - { - await _libraryManager.UpdatePeopleAsync(item, info.People, cancellationToken).ConfigureAwait(false); - } - } - else if (forceUpdate) - { - await item.UpdateToRepositoryAsync(ItemUpdateType.None, cancellationToken).ConfigureAwait(false); - } - - if ((isNew || forceUpdate) && info.Type == ChannelItemType.Media) - { - if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol) - { - await SaveMediaSources(item, new List<MediaSourceInfo>()).ConfigureAwait(false); - } - else - { - await SaveMediaSources(item, info.MediaSources).ConfigureAwait(false); - } - } - - if (isNew || forceUpdate || item.DateLastRefreshed == default) - { - _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal); - } - - return item; - } - - internal IChannel GetChannelProvider(Channel channel) - { - ArgumentNullException.ThrowIfNull(channel); - - var result = GetAllChannels() - .FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(channel.ChannelId) || string.Equals(i.Name, channel.Name, StringComparison.OrdinalIgnoreCase)); - - if (result is null) - { - throw new ResourceNotFoundException("No channel provider found for channel " + channel.Name); - } - - return result; - } - - internal IChannel GetChannelProvider(Guid internalChannelId) - { - var result = GetAllChannels() - .FirstOrDefault(i => internalChannelId.Equals(GetInternalChannelId(i.Name))); - - if (result is null) - { - throw new ResourceNotFoundException("No channel provider found for channel id " + internalChannelId); - } - - return result; - } - - /// <inheritdoc /> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Releases unmanaged and optionally managed resources. - /// </summary> - /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - _resourcePool?.Dispose(); - } - - _disposed = true; - } - } -} diff --git a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs deleted file mode 100644 index b358ba4d5..000000000 --- a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Data.Enums; -using MediaBrowser.Controller.Channels; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Channels -{ - /// <summary> - /// A task to remove all non-installed channels from the database. - /// </summary> - public class ChannelPostScanTask - { - private readonly IChannelManager _channelManager; - private readonly ILogger _logger; - private readonly ILibraryManager _libraryManager; - - /// <summary> - /// Initializes a new instance of the <see cref="ChannelPostScanTask"/> class. - /// </summary> - /// <param name="channelManager">The channel manager.</param> - /// <param name="logger">The logger.</param> - /// <param name="libraryManager">The library manager.</param> - public ChannelPostScanTask(IChannelManager channelManager, ILogger logger, ILibraryManager libraryManager) - { - _channelManager = channelManager; - _logger = logger; - _libraryManager = libraryManager; - } - - /// <summary> - /// Runs this task. - /// </summary> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>The completed task.</returns> - public Task Run(IProgress<double> progress, CancellationToken cancellationToken) - { - CleanDatabase(cancellationToken); - - progress.Report(100); - return Task.CompletedTask; - } - - private void CleanDatabase(CancellationToken cancellationToken) - { - var installedChannelIds = ((ChannelManager)_channelManager).GetInstalledChannelIds(); - - var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.Channel }, - ExcludeItemIds = installedChannelIds.ToArray() - }); - - foreach (var channel in uninstalledChannels) - { - cancellationToken.ThrowIfCancellationRequested(); - - CleanChannel((Channel)channel, cancellationToken); - } - } - - private void CleanChannel(Channel channel, CancellationToken cancellationToken) - { - _logger.LogInformation("Cleaning channel {0} from database", channel.Id); - - // Delete all channel items - var items = _libraryManager.GetItemList(new InternalItemsQuery - { - ChannelIds = new[] { channel.Id } - }); - - foreach (var item in items) - { - cancellationToken.ThrowIfCancellationRequested(); - - _libraryManager.DeleteItem( - item, - new DeleteOptions - { - DeleteFileLocation = false - }, - false); - } - - // Finally, delete the channel itself - _libraryManager.DeleteItem( - channel, - new DeleteOptions - { - DeleteFileLocation = false - }, - false); - } - } -} diff --git a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs deleted file mode 100644 index cfd08e653..000000000 --- a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Progress; -using MediaBrowser.Controller.Channels; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Tasks; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Channels -{ - /// <summary> - /// The "Refresh Channels" scheduled task. - /// </summary> - public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask - { - private readonly IChannelManager _channelManager; - private readonly ILogger<RefreshChannelsScheduledTask> _logger; - private readonly ILibraryManager _libraryManager; - private readonly ILocalizationManager _localization; - - /// <summary> - /// Initializes a new instance of the <see cref="RefreshChannelsScheduledTask"/> class. - /// </summary> - /// <param name="channelManager">The channel manager.</param> - /// <param name="logger">The logger.</param> - /// <param name="libraryManager">The library manager.</param> - /// <param name="localization">The localization manager.</param> - public RefreshChannelsScheduledTask( - IChannelManager channelManager, - ILogger<RefreshChannelsScheduledTask> logger, - ILibraryManager libraryManager, - ILocalizationManager localization) - { - _channelManager = channelManager; - _logger = logger; - _libraryManager = libraryManager; - _localization = localization; - } - - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TasksRefreshChannels"); - - /// <inheritdoc /> - public string Description => _localization.GetLocalizedString("TasksRefreshChannelsDescription"); - - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksChannelsCategory"); - - /// <inheritdoc /> - public bool IsHidden => ((ChannelManager)_channelManager).Channels.Length == 0; - - /// <inheritdoc /> - public bool IsEnabled => true; - - /// <inheritdoc /> - public bool IsLogged => true; - - /// <inheritdoc /> - public string Key => "RefreshInternetChannels"; - - /// <inheritdoc /> - public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) - { - var manager = (ChannelManager)_channelManager; - - await manager.RefreshChannels(new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false); - - await new ChannelPostScanTask(_channelManager, _logger, _libraryManager).Run(progress, cancellationToken) - .ConfigureAwait(false); - } - - /// <inheritdoc /> - public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() - { - return new[] - { - // Every so often - new TaskTriggerInfo - { - Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks - } - }; - } - } -} diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index 01b5fdaee..25ef57d27 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -104,6 +104,13 @@ namespace Emby.Server.Implementations.Data if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult)) { + // If the resulting DateTimeKind is Unspecified it is actually Utc. + // This is required downstream for the Json serializer. + if (dateTimeResult.Kind == DateTimeKind.Unspecified) + { + dateTimeResult = DateTime.SpecifyKind(dateTimeResult, DateTimeKind.Utc); + } + result = dateTimeResult; return true; } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index d0772654c..a6336f145 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -699,7 +699,7 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBindNull("@EndDate"); } - saveItemStatement.TryBind("@ChannelId", item.ChannelId.Equals(default) ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture)); + saveItemStatement.TryBind("@ChannelId", item.ChannelId.IsEmpty() ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture)); if (item is IHasProgramAttributes hasProgramAttributes) { @@ -729,7 +729,7 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBind("@ProductionYear", item.ProductionYear); var parentId = item.ParentId; - if (parentId.Equals(default)) + if (parentId.IsEmpty()) { saveItemStatement.TryBindNull("@ParentId"); } @@ -925,7 +925,7 @@ namespace Emby.Server.Implementations.Data { saveItemStatement.TryBind("@SeasonName", episode.SeasonName); - var nullableSeasonId = episode.SeasonId.Equals(default) ? (Guid?)null : episode.SeasonId; + var nullableSeasonId = episode.SeasonId.IsEmpty() ? (Guid?)null : episode.SeasonId; saveItemStatement.TryBind("@SeasonId", nullableSeasonId); } @@ -937,7 +937,7 @@ namespace Emby.Server.Implementations.Data if (item is IHasSeries hasSeries) { - var nullableSeriesId = hasSeries.SeriesId.Equals(default) ? (Guid?)null : hasSeries.SeriesId; + var nullableSeriesId = hasSeries.SeriesId.IsEmpty() ? (Guid?)null : hasSeries.SeriesId; saveItemStatement.TryBind("@SeriesId", nullableSeriesId); saveItemStatement.TryBind("@SeriesPresentationUniqueKey", hasSeries.SeriesPresentationUniqueKey); @@ -1010,7 +1010,7 @@ namespace Emby.Server.Implementations.Data } Guid ownerId = item.OwnerId; - if (ownerId.Equals(default)) + if (ownerId.IsEmpty()) { saveItemStatement.TryBindNull("@OwnerId"); } @@ -1266,7 +1266,7 @@ namespace Emby.Server.Implementations.Data /// <exception cref="ArgumentException"><paramr name="id"/> is <seealso cref="Guid.Empty"/>.</exception> public BaseItem RetrieveItem(Guid id) { - if (id.Equals(default)) + if (id.IsEmpty()) { throw new ArgumentException("Guid can't be empty", nameof(id)); } @@ -1970,7 +1970,7 @@ namespace Emby.Server.Implementations.Data { CheckDisposed(); - if (id.Equals(default)) + if (id.IsEmpty()) { throw new ArgumentNullException(nameof(id)); } @@ -3230,7 +3230,7 @@ namespace Emby.Server.Implementations.Data whereClauses.Add($"ChannelId in ({inClause})"); } - if (!query.ParentId.Equals(default)) + if (!query.ParentId.IsEmpty()) { whereClauses.Add("ParentId=@ParentId"); statement?.TryBind("@ParentId", query.ParentId); @@ -4452,7 +4452,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type public void DeleteItem(Guid id) { - if (id.Equals(default)) + if (id.IsEmpty()) { throw new ArgumentNullException(nameof(id)); } @@ -4583,13 +4583,13 @@ AND Type = @InternalPersonType)"); statement?.TryBind("@UserId", query.User.InternalId); } - if (!query.ItemId.Equals(default)) + if (!query.ItemId.IsEmpty()) { whereClauses.Add("ItemId=@ItemId"); statement?.TryBind("@ItemId", query.ItemId); } - if (!query.AppearsInItemId.Equals(default)) + if (!query.AppearsInItemId.IsEmpty()) { whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)"); statement?.TryBind("@AppearsInItemId", query.AppearsInItemId); @@ -4640,7 +4640,7 @@ AND Type = @InternalPersonType)"); private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, SqliteConnection db, SqliteCommand deleteAncestorsStatement) { - if (itemId.Equals(default)) + if (itemId.IsEmpty()) { throw new ArgumentNullException(nameof(itemId)); } @@ -5156,7 +5156,7 @@ AND Type = @InternalPersonType)"); private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, SqliteConnection db) { - if (itemId.Equals(default)) + if (itemId.IsEmpty()) { throw new ArgumentNullException(nameof(itemId)); } @@ -5228,7 +5228,7 @@ AND Type = @InternalPersonType)"); public void UpdatePeople(Guid itemId, List<PersonInfo> people) { - if (itemId.Equals(default)) + if (itemId.IsEmpty()) { throw new ArgumentNullException(nameof(itemId)); } @@ -5378,7 +5378,7 @@ AND Type = @InternalPersonType)"); { CheckDisposed(); - if (id.Equals(default)) + if (id.IsEmpty()) { throw new ArgumentNullException(nameof(id)); } @@ -5758,7 +5758,7 @@ AND Type = @InternalPersonType)"); CancellationToken cancellationToken) { CheckDisposed(); - if (id.Equals(default)) + if (id.IsEmpty()) { throw new ArgumentException("Guid can't be empty.", nameof(id)); } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 44b97e8b8..d0d5bb81c 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -418,15 +418,6 @@ namespace Emby.Server.Implementations.Dto { dto.PlayAccess = item.GetPlayAccess(user); } - - if (options.ContainsField(ItemFields.BasicSyncInfo)) - { - var userCanSync = user is not null && user.HasPermission(PermissionKind.EnableContentDownloading); - if (userCanSync && item.SupportsExternalTransfer) - { - dto.SupportsSync = true; - } - } } private static int GetChildCount(Folder folder, User user) diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index b3344bb9f..34276355a 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -22,7 +22,6 @@ <ItemGroup> <PackageReference Include="DiscUtils.Udf" /> - <PackageReference Include="Jellyfin.XmlTv" /> <PackageReference Include="Microsoft.Data.Sqlite" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" /> diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index a83d7a410..83e7b230d 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Events; +using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -241,7 +242,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint { var userIds = _sessionManager.Sessions .Select(i => i.UserId) - .Where(i => !i.Equals(default)) + .Where(i => !i.IsEmpty()) .Distinct() .ToArray(); diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs deleted file mode 100644 index e0ca02d98..000000000 --- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs +++ /dev/null @@ -1,93 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Data.Enums; -using Jellyfin.Data.Events; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Session; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.EntryPoints -{ - public sealed class RecordingNotifier : IServerEntryPoint - { - private readonly ILiveTvManager _liveTvManager; - private readonly ISessionManager _sessionManager; - private readonly IUserManager _userManager; - private readonly ILogger<RecordingNotifier> _logger; - - public RecordingNotifier( - ISessionManager sessionManager, - IUserManager userManager, - ILogger<RecordingNotifier> logger, - ILiveTvManager liveTvManager) - { - _sessionManager = sessionManager; - _userManager = userManager; - _logger = logger; - _liveTvManager = liveTvManager; - } - - /// <inheritdoc /> - public Task RunAsync() - { - _liveTvManager.TimerCancelled += OnLiveTvManagerTimerCancelled; - _liveTvManager.SeriesTimerCancelled += OnLiveTvManagerSeriesTimerCancelled; - _liveTvManager.TimerCreated += OnLiveTvManagerTimerCreated; - _liveTvManager.SeriesTimerCreated += OnLiveTvManagerSeriesTimerCreated; - - return Task.CompletedTask; - } - - private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e) - { - await SendMessage(SessionMessageType.SeriesTimerCreated, e.Argument).ConfigureAwait(false); - } - - private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e) - { - await SendMessage(SessionMessageType.TimerCreated, e.Argument).ConfigureAwait(false); - } - - private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e) - { - await SendMessage(SessionMessageType.SeriesTimerCancelled, e.Argument).ConfigureAwait(false); - } - - private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e) - { - await SendMessage(SessionMessageType.TimerCancelled, e.Argument).ConfigureAwait(false); - } - - private async Task SendMessage(SessionMessageType name, TimerEventInfo info) - { - var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList(); - - try - { - await _sessionManager.SendMessageToUserSessions(users, name, info, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error sending message"); - } - } - - /// <inheritdoc /> - public void Dispose() - { - _liveTvManager.TimerCancelled -= OnLiveTvManagerTimerCancelled; - _liveTvManager.SeriesTimerCancelled -= OnLiveTvManagerSeriesTimerCancelled; - _liveTvManager.TimerCreated -= OnLiveTvManagerTimerCreated; - _liveTvManager.SeriesTimerCreated -= OnLiveTvManagerSeriesTimerCreated; - } - } -} diff --git a/Emby.Server.Implementations/IO/StreamHelper.cs b/Emby.Server.Implementations/IO/StreamHelper.cs deleted file mode 100644 index 6eaf22ce4..000000000 --- a/Emby.Server.Implementations/IO/StreamHelper.cs +++ /dev/null @@ -1,150 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Buffers; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; - -namespace Emby.Server.Implementations.IO -{ - public class StreamHelper : IStreamHelper - { - public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action? onStarted, CancellationToken cancellationToken) - { - byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize); - try - { - int read; - while ((read = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0) - { - cancellationToken.ThrowIfCancellationRequested(); - - await destination.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); - - if (onStarted is not null) - { - onStarted(); - onStarted = null; - } - } - } - finally - { - ArrayPool<byte>.Shared.Return(buffer); - } - } - - public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, int emptyReadLimit, CancellationToken cancellationToken) - { - byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize); - try - { - if (emptyReadLimit <= 0) - { - int read; - while ((read = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0) - { - cancellationToken.ThrowIfCancellationRequested(); - - await destination.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false); - } - - return; - } - - var eofCount = 0; - - while (eofCount < emptyReadLimit) - { - cancellationToken.ThrowIfCancellationRequested(); - - var bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - - if (bytesRead == 0) - { - eofCount++; - await Task.Delay(50, cancellationToken).ConfigureAwait(false); - } - else - { - eofCount = 0; - - await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false); - } - } - } - finally - { - ArrayPool<byte>.Shared.Return(buffer); - } - } - - public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) - { - byte[] buffer = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize); - try - { - int bytesRead; - - while ((bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0) - { - var bytesToWrite = Math.Min(bytesRead, copyLength); - - if (bytesToWrite > 0) - { - await destination.WriteAsync(buffer.AsMemory(0, Convert.ToInt32(bytesToWrite)), cancellationToken).ConfigureAwait(false); - } - - copyLength -= bytesToWrite; - - if (copyLength <= 0) - { - break; - } - } - } - finally - { - ArrayPool<byte>.Shared.Return(buffer); - } - } - - public async Task CopyUntilCancelled(Stream source, Stream target, int bufferSize, CancellationToken cancellationToken) - { - byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize); - try - { - while (!cancellationToken.IsCancellationRequested) - { - var bytesRead = await CopyToAsyncInternal(source, target, buffer, cancellationToken).ConfigureAwait(false); - - if (bytesRead == 0) - { - await Task.Delay(100, cancellationToken).ConfigureAwait(false); - } - } - } - finally - { - ArrayPool<byte>.Shared.Return(buffer); - } - } - - private static async Task<int> CopyToAsyncInternal(Stream source, Stream destination, byte[] buffer, CancellationToken cancellationToken) - { - int bytesRead; - int totalBytesRead = 0; - - while ((bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0) - { - await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false); - - totalBytesRead += bytesRead; - } - - return totalBytesRead; - } - } -} diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs index 6e8f77977..34c722e41 100644 --- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs @@ -32,26 +32,26 @@ namespace Emby.Server.Implementations.Images switch (viewType) { - case CollectionType.Movies: + case CollectionType.movies: includeItemTypes = new[] { BaseItemKind.Movie }; break; - case CollectionType.TvShows: + case CollectionType.tvshows: includeItemTypes = new[] { BaseItemKind.Series }; break; - case CollectionType.Music: + case CollectionType.music: includeItemTypes = new[] { BaseItemKind.MusicAlbum }; break; - case CollectionType.MusicVideos: + case CollectionType.musicvideos: includeItemTypes = new[] { BaseItemKind.MusicVideo }; break; - case CollectionType.Books: + case CollectionType.books: includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook }; break; - case CollectionType.BoxSets: + case CollectionType.boxsets: includeItemTypes = new[] { BaseItemKind.BoxSet }; break; - case CollectionType.HomeVideos: - case CollectionType.Photos: + case CollectionType.homevideos: + case CollectionType.photos: includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo }; break; default: @@ -59,7 +59,7 @@ namespace Emby.Server.Implementations.Images break; } - var recursive = viewType != CollectionType.Playlists; + var recursive = viewType != CollectionType.playlists; return view.GetItemList(new InternalItemsQuery { diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs index 5de53df73..6b2ae23b3 100644 --- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs @@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Images var view = (UserView)item; var isUsingCollectionStrip = IsUsingCollectionStrip(view); - var recursive = isUsingCollectionStrip && view?.ViewType is not null && view.ViewType != CollectionType.BoxSets && view.ViewType != CollectionType.Playlists; + var recursive = isUsingCollectionStrip && view?.ViewType is not null && view.ViewType != CollectionType.boxsets && view.ViewType != CollectionType.playlists; var result = view.GetItemList(new InternalItemsQuery { @@ -114,9 +114,9 @@ namespace Emby.Server.Implementations.Images { CollectionType[] collectionStripViewTypes = { - CollectionType.Movies, - CollectionType.TvShows, - CollectionType.Playlists + CollectionType.movies, + CollectionType.tvshows, + CollectionType.playlists }; return view?.ViewType is not null && collectionStripViewTypes.Contains(view.ViewType.Value); diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs deleted file mode 100644 index 868071a99..000000000 --- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs +++ /dev/null @@ -1,55 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Dto; - -namespace Emby.Server.Implementations.Library -{ - public class ExclusiveLiveStream : ILiveStream - { - private readonly Func<Task> _closeFn; - - public ExclusiveLiveStream(MediaSourceInfo mediaSource, Func<Task> closeFn) - { - MediaSource = mediaSource; - EnableStreamSharing = false; - _closeFn = closeFn; - ConsumerCount = 1; - UniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - } - - public int ConsumerCount { get; set; } - - public string OriginalStreamId { get; set; } - - public string TunerHostId => null; - - public bool EnableStreamSharing { get; set; } - - public MediaSourceInfo MediaSource { get; set; } - - public string UniqueId { get; } - - public Task Close() - { - return _closeFn(); - } - - public Stream GetStream() - { - throw new NotSupportedException(); - } - - public Task Open(CancellationToken openCancellationToken) - { - return Task.CompletedTask; - } - } -} diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index f40177fa7..8ae913dad 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -732,7 +732,7 @@ namespace Emby.Server.Implementations.Library Path = path }; - if (folder.Id.Equals(default)) + if (folder.Id.IsEmpty()) { if (string.IsNullOrEmpty(folder.Path)) { @@ -1219,7 +1219,7 @@ namespace Emby.Server.Implementations.Library /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception> public BaseItem GetItemById(Guid id) { - if (id.Equals(default)) + if (id.IsEmpty()) { throw new ArgumentException("Guid can't be empty", nameof(id)); } @@ -1241,7 +1241,7 @@ namespace Emby.Server.Implementations.Library public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent) { - if (query.Recursive && !query.ParentId.Equals(default)) + if (query.Recursive && !query.ParentId.IsEmpty()) { var parent = GetItemById(query.ParentId); if (parent is not null) @@ -1272,7 +1272,7 @@ namespace Emby.Server.Implementations.Library public int GetCount(InternalItemsQuery query) { - if (query.Recursive && !query.ParentId.Equals(default)) + if (query.Recursive && !query.ParentId.IsEmpty()) { var parent = GetItemById(query.ParentId); if (parent is not null) @@ -1430,7 +1430,7 @@ namespace Emby.Server.Implementations.Library public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query) { - if (query.Recursive && !query.ParentId.Equals(default)) + if (query.Recursive && !query.ParentId.IsEmpty()) { var parent = GetItemById(query.ParentId); if (parent is not null) @@ -1486,7 +1486,7 @@ namespace Emby.Server.Implementations.Library private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true) { if (query.AncestorIds.Length == 0 && - query.ParentId.Equals(default) && + query.ParentId.IsEmpty() && query.ChannelIds.Count == 0 && query.TopParentIds.Length == 0 && string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) && @@ -1514,13 +1514,13 @@ namespace Emby.Server.Implementations.Library { if (item is UserView view) { - if (view.ViewType == CollectionType.LiveTv) + if (view.ViewType == CollectionType.livetv) { return new[] { view.Id }; } // Translate view into folders - if (!view.DisplayParentId.Equals(default)) + if (!view.DisplayParentId.IsEmpty()) { var displayParent = GetItemById(view.DisplayParentId); if (displayParent is not null) @@ -1531,7 +1531,7 @@ namespace Emby.Server.Implementations.Library return Array.Empty<Guid>(); } - if (!view.ParentId.Equals(default)) + if (!view.ParentId.IsEmpty()) { var displayParent = GetItemById(view.ParentId); if (displayParent is not null) @@ -1543,7 +1543,7 @@ namespace Emby.Server.Implementations.Library } // Handle grouping - if (user is not null && view.ViewType != CollectionType.Unknown && UserView.IsEligibleForGrouping(view.ViewType) + if (user is not null && view.ViewType != CollectionType.unknown && UserView.IsEligibleForGrouping(view.ViewType) && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0) { return GetUserRootFolder() @@ -2137,7 +2137,7 @@ namespace Emby.Server.Implementations.Library return null; } - while (!item.ParentId.Equals(default)) + while (!item.ParentId.IsEmpty()) { var parent = item.GetParent(); if (parent is null || parent is AggregateFolder) @@ -2215,7 +2215,7 @@ namespace Emby.Server.Implementations.Library CollectionType? viewType, string sortName) { - var parentIdString = parentId.Equals(default) + var parentIdString = parentId.IsEmpty() ? null : parentId.ToString("N", CultureInfo.InvariantCulture); var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty); @@ -2251,7 +2251,7 @@ namespace Emby.Server.Implementations.Library var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - if (!refresh && !item.DisplayParentId.Equals(default)) + if (!refresh && !item.DisplayParentId.IsEmpty()) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; @@ -2315,7 +2315,7 @@ namespace Emby.Server.Implementations.Library var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - if (!refresh && !item.DisplayParentId.Equals(default)) + if (!refresh && !item.DisplayParentId.IsEmpty()) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; @@ -2345,7 +2345,7 @@ namespace Emby.Server.Implementations.Library { ArgumentException.ThrowIfNullOrEmpty(name); - var parentIdString = parentId.Equals(default) + var parentIdString = parentId.IsEmpty() ? null : parentId.ToString("N", CultureInfo.InvariantCulture); var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty); @@ -2391,7 +2391,7 @@ namespace Emby.Server.Implementations.Library var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - if (!refresh && !item.DisplayParentId.Equals(default)) + if (!refresh && !item.DisplayParentId.IsEmpty()) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; @@ -2419,7 +2419,7 @@ namespace Emby.Server.Implementations.Library return GetItemById(parentId.Value); } - if (userId.HasValue && !userId.Equals(default)) + if (!userId.IsNullOrEmpty()) { return GetUserRootFolder(); } diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 96fad9bca..c38f1af91 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -11,14 +11,16 @@ using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using EasyCaching.Core.Configurations; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; @@ -37,6 +39,7 @@ namespace Emby.Server.Implementations.Library // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message. private const char LiveStreamIdDelimeter = '_'; + private readonly IServerApplicationHost _appHost; private readonly IItemRepository _itemRepo; private readonly IUserManager _userManager; private readonly ILibraryManager _libraryManager; @@ -55,6 +58,7 @@ namespace Emby.Server.Implementations.Library private IMediaSourceProvider[] _providers; public MediaSourceManager( + IServerApplicationHost appHost, IItemRepository itemRepo, IApplicationPaths applicationPaths, ILocalizationManager localizationManager, @@ -66,6 +70,7 @@ namespace Emby.Server.Implementations.Library IMediaEncoder mediaEncoder, IDirectoryService directoryService) { + _appHost = appHost; _itemRepo = itemRepo; _userManager = userManager; _libraryManager = libraryManager; @@ -520,10 +525,10 @@ namespace Emby.Server.Implementations.Library _logger.LogInformation("Live stream opened: {@MediaSource}", mediaSource); var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions); - if (!request.UserId.Equals(default)) + if (!request.UserId.IsEmpty()) { var user = _userManager.GetUserById(request.UserId); - var item = request.ItemId.Equals(default) + var item = request.ItemId.IsEmpty() ? null : _libraryManager.GetItemById(request.ItemId); SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user); @@ -799,6 +804,35 @@ namespace Emby.Server.Implementations.Library return result.Item1; } + public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken) + { + var stream = new MediaSourceInfo + { + EncoderPath = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveRecordings/" + info.Id + "/stream", + EncoderProtocol = MediaProtocol.Http, + Path = info.Path, + Protocol = MediaProtocol.File, + Id = info.Id, + SupportsDirectPlay = false, + SupportsDirectStream = true, + SupportsTranscoding = true, + IsInfiniteStream = true, + RequiresOpening = false, + RequiresClosing = false, + BufferMs = 0, + IgnoreDts = true, + IgnoreIndex = true + }; + + await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths) + .AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false); + + return new List<MediaSourceInfo> + { + stream + }; + } + public async Task CloseLiveStream(string id) { ArgumentException.ThrowIfNullOrEmpty(id); diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index b2439a87e..078f4ad21 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -80,7 +81,7 @@ namespace Emby.Server.Implementations.Library { return Guid.Empty; } - }).Where(i => !i.Equals(default)).ToArray(); + }).Where(i => !i.IsEmpty()).ToArray(); return GetInstantMixFromGenreIds(genreIds, user, dtoOptions); } diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index ac423ed09..dbf05c1db 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio List<FileSystemMetadata> files, CollectionType? collectionType) { - if (collectionType == CollectionType.Books) + if (collectionType == CollectionType.books) { return ResolveMultipleAudio(parent, files, true); } @@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var collectionType = args.GetCollectionType(); - var isBooksCollectionType = collectionType == CollectionType.Books; + var isBooksCollectionType = collectionType == CollectionType.books; if (args.IsDirectory) { @@ -112,7 +112,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio MediaBrowser.Controller.Entities.Audio.Audio item = null; - var isMusicCollectionType = collectionType == CollectionType.Music; + var isMusicCollectionType = collectionType == CollectionType.music; // Use regular audio type for mixed libraries, owned items and music if (isMixedCollectionType || diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index 06e292f4c..0bfb7fbe6 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -55,7 +55,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio protected override MusicAlbum Resolve(ItemResolveArgs args) { var collectionType = args.GetCollectionType(); - var isMusicMediaFolder = collectionType == CollectionType.Music; + var isMusicMediaFolder = collectionType == CollectionType.music; // If there's a collection type and it's not music, don't allow it. if (!isMusicMediaFolder) diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index 7d6f97b12..1bdae7f62 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var collectionType = args.GetCollectionType(); - var isMusicMediaFolder = collectionType == CollectionType.Music; + var isMusicMediaFolder = collectionType == CollectionType.music; // If there's a collection type and it's not music, it can't be a music artist if (!isMusicMediaFolder) diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index b76bfe427..464a548ab 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books var collectionType = args.GetCollectionType(); // Only process items that are in a collection folder containing books - if (collectionType != CollectionType.Books) + if (collectionType != CollectionType.books) { return null; } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 50fd8b877..1a210e3cc 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -31,11 +31,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies private static readonly CollectionType[] _validCollectionTypes = new[] { - CollectionType.Movies, - CollectionType.HomeVideos, - CollectionType.MusicVideos, - CollectionType.TvShows, - CollectionType.Photos + CollectionType.movies, + CollectionType.homevideos, + CollectionType.musicvideos, + CollectionType.tvshows, + CollectionType.photos }; /// <summary> @@ -100,12 +100,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies Video movie = null; var files = args.GetActualFileSystemChildren().ToList(); - if (collectionType == CollectionType.MusicVideos) + if (collectionType == CollectionType.musicvideos) { movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false); } - if (collectionType == CollectionType.HomeVideos) + if (collectionType == CollectionType.homevideos) { movie = FindMovie<Video>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false); } @@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true); } - if (collectionType == CollectionType.Movies) + if (collectionType == CollectionType.movies) { movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true); } @@ -147,17 +147,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies Video item = null; - if (collectionType == CollectionType.MusicVideos) + if (collectionType == CollectionType.musicvideos) { item = ResolveVideo<MusicVideo>(args, false); } // To find a movie file, the collection type must be movies or boxsets - else if (collectionType == CollectionType.Movies) + else if (collectionType == CollectionType.movies) { item = ResolveVideo<Movie>(args, true); } - else if (collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos) + else if (collectionType == CollectionType.homevideos || collectionType == CollectionType.photos) { item = ResolveVideo<Video>(args, false); } @@ -195,12 +195,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return null; } - if (collectionType is CollectionType.MusicVideos) + if (collectionType is CollectionType.musicvideos) { return ResolveVideos<MusicVideo>(parent, files, true, collectionType, false); } - if (collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos) + if (collectionType == CollectionType.homevideos || collectionType == CollectionType.photos) { return ResolveVideos<Video>(parent, files, false, collectionType, false); } @@ -221,12 +221,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return ResolveVideos<Movie>(parent, files, false, collectionType, true); } - if (collectionType == CollectionType.Movies) + if (collectionType == CollectionType.movies) { return ResolveVideos<Movie>(parent, files, true, collectionType, true); } - if (collectionType == CollectionType.TvShows) + if (collectionType == CollectionType.tvshows) { return ResolveVideos<Episode>(parent, files, false, collectionType, true); } @@ -403,7 +403,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies var multiDiscFolders = new List<FileSystemMetadata>(); var libraryOptions = args.LibraryOptions; - var supportPhotos = collectionType == CollectionType.HomeVideos && libraryOptions.EnablePhotos; + var supportPhotos = collectionType == CollectionType.homevideos && libraryOptions.EnablePhotos; var photos = new List<FileSystemMetadata>(); // Search for a folder rip @@ -459,7 +459,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ?? new MultiItemResolverResult(); - var isPhotosCollection = collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos; + var isPhotosCollection = collectionType == CollectionType.homevideos || collectionType == CollectionType.photos; if (!isPhotosCollection && result.Items.Count == 1) { var videoPath = result.Items[0].Path; diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs index 29d540700..c0b00caaf 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs @@ -46,8 +46,8 @@ namespace Emby.Server.Implementations.Library.Resolvers // Must be an image file within a photo collection var collectionType = args.GetCollectionType(); - if (collectionType == CollectionType.Photos - || (collectionType == CollectionType.HomeVideos && args.LibraryOptions.EnablePhotos)) + if (collectionType == CollectionType.photos + || (collectionType == CollectionType.homevideos && args.LibraryOptions.EnablePhotos)) { if (HasPhotos(args)) { diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index d166ac37f..0934555b2 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -61,8 +61,8 @@ namespace Emby.Server.Implementations.Library.Resolvers // Must be an image file within a photo collection var collectionType = args.CollectionType; - if (collectionType == CollectionType.Photos - || (collectionType == CollectionType.HomeVideos && args.LibraryOptions.EnablePhotos)) + if (collectionType == CollectionType.photos + || (collectionType == CollectionType.homevideos && args.LibraryOptions.EnablePhotos)) { if (IsImageFile(args.Path, _imageProcessor)) { diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index d4b3722c9..a50435ae6 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers private CollectionType?[] _musicPlaylistCollectionTypes = { null, - CollectionType.Music + CollectionType.music }; /// <inheritdoc/> diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index 8274881be..5fd23c9f5 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -50,7 +50,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV // If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something // Also handle flat tv folders if (season is not null - || args.GetCollectionType() == CollectionType.TvShows + || args.GetCollectionType() == CollectionType.tvshows || args.HasParent<Series>()) { var episode = ResolveVideo<Episode>(args, false); diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index 2ae1138a5..1484c34bc 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -60,11 +60,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path); var collectionType = args.GetCollectionType(); - if (collectionType == CollectionType.TvShows) + if (collectionType == CollectionType.tvshows) { // TODO refactor into separate class or something, this is copied from LibraryManager.GetConfiguredContentType var configuredContentType = args.GetConfiguredContentType(); - if (configuredContentType != CollectionType.TvShows) + if (configuredContentType != CollectionType.tvshows) { return new Series { diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index b916b9170..020cb517d 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query) { User user = null; - if (!query.UserId.Equals(default)) + if (!query.UserId.IsEmpty()) { user = _userManager.GetUserById(query.UserId); } @@ -177,7 +177,7 @@ namespace Emby.Server.Implementations.Library if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist) { - if (!searchQuery.ParentId.Equals(default)) + if (!searchQuery.ParentId.IsEmpty()) { searchQuery.AncestorIds = new[] { searchQuery.ParentId }; searchQuery.ParentId = Guid.Empty; diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index a0a90b129..8beeb8041 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -81,6 +81,53 @@ namespace Emby.Server.Implementations.Library }); } + public void SaveUserData(User user, BaseItem item, UpdateUserItemDataDto userDataDto, UserDataSaveReason reason) + { + ArgumentNullException.ThrowIfNull(user); + ArgumentNullException.ThrowIfNull(item); + ArgumentNullException.ThrowIfNull(reason); + ArgumentNullException.ThrowIfNull(userDataDto); + + var userData = GetUserData(user, item); + + if (userDataDto.PlaybackPositionTicks.HasValue) + { + userData.PlaybackPositionTicks = userDataDto.PlaybackPositionTicks.Value; + } + + if (userDataDto.PlayCount.HasValue) + { + userData.PlayCount = userDataDto.PlayCount.Value; + } + + if (userDataDto.IsFavorite.HasValue) + { + userData.IsFavorite = userDataDto.IsFavorite.Value; + } + + if (userDataDto.Likes.HasValue) + { + userData.Likes = userDataDto.Likes.Value; + } + + if (userDataDto.Played.HasValue) + { + userData.Played = userDataDto.Played.Value; + } + + if (userDataDto.LastPlayedDate.HasValue) + { + userData.LastPlayedDate = userDataDto.LastPlayedDate.Value; + } + + if (userDataDto.Rating.HasValue) + { + userData.Rating = userDataDto.Rating.Value; + } + + SaveUserData(user, item, userData, reason, CancellationToken.None); + } + /// <summary> /// Save the provided user data for the given user. Batch operation. Does not fire any events or update the cache. /// </summary> diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index 113370fc3..83a66c8e4 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; @@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.Library var folderViewType = collectionFolder?.CollectionType; // Playlist library requires special handling because the folder only references user playlists - if (folderViewType == CollectionType.Playlists) + if (folderViewType == CollectionType.playlists) { var items = folder.GetItemList(new InternalItemsQuery(user) { @@ -99,14 +100,14 @@ namespace Emby.Server.Implementations.Library } } - foreach (var viewType in new[] { CollectionType.Movies, CollectionType.TvShows }) + foreach (var viewType in new[] { CollectionType.movies, CollectionType.tvshows }) { var parents = groupedFolders.Where(i => i.CollectionType == viewType || i.CollectionType is null) .ToList(); if (parents.Count > 0) { - var localizationKey = viewType == CollectionType.TvShows + var localizationKey = viewType == CollectionType.tvshows ? "TvShows" : "Movies"; @@ -117,7 +118,7 @@ namespace Emby.Server.Implementations.Library if (_config.Configuration.EnableFolderView) { var name = _localizationManager.GetLocalizedString("Folders"); - list.Add(_libraryManager.GetNamedView(name, CollectionType.Folders, string.Empty)); + list.Add(_libraryManager.GetNamedView(name, CollectionType.folders, string.Empty)); } if (query.IncludeExternalContent) @@ -151,7 +152,7 @@ namespace Emby.Server.Implementations.Library var index = Array.IndexOf(orders, i.Id); if (index == -1 && i is UserView view - && !view.DisplayParentId.Equals(default)) + && !view.DisplayParentId.IsEmpty()) { index = Array.IndexOf(orders, view.DisplayParentId); } @@ -253,7 +254,7 @@ namespace Emby.Server.Implementations.Library var parents = new List<BaseItem>(); - if (!parentId.Equals(default)) + if (!parentId.IsEmpty()) { var parentItem = _libraryManager.GetItemById(parentId); if (parentItem is Channel) @@ -279,7 +280,7 @@ namespace Emby.Server.Implementations.Library var isPlayed = request.IsPlayed; - if (parents.OfType<ICollectionFolder>().Any(i => i.CollectionType == CollectionType.Music)) + if (parents.OfType<ICollectionFolder>().Any(i => i.CollectionType == CollectionType.music)) { isPlayed = null; } @@ -305,11 +306,11 @@ namespace Emby.Server.Implementations.Library var hasCollectionType = parents.OfType<UserView>().ToArray(); if (hasCollectionType.Length > 0) { - if (hasCollectionType.All(i => i.CollectionType == CollectionType.Movies)) + if (hasCollectionType.All(i => i.CollectionType == CollectionType.movies)) { includeItemTypes = new[] { BaseItemKind.Movie }; } - else if (hasCollectionType.All(i => i.CollectionType == CollectionType.TvShows)) + else if (hasCollectionType.All(i => i.CollectionType == CollectionType.tvshows)) { includeItemTypes = new[] { BaseItemKind.Episode }; } @@ -324,18 +325,18 @@ namespace Emby.Server.Implementations.Library { switch (parent.CollectionType) { - case CollectionType.Books: + case CollectionType.books: mediaTypes.Add(MediaType.Book); mediaTypes.Add(MediaType.Audio); break; - case CollectionType.Music: + case CollectionType.music: mediaTypes.Add(MediaType.Audio); break; - case CollectionType.Photos: + case CollectionType.photos: mediaTypes.Add(MediaType.Photo); mediaTypes.Add(MediaType.Video); break; - case CollectionType.HomeVideos: + case CollectionType.homevideos: mediaTypes.Add(MediaType.Photo); mediaTypes.Add(MediaType.Video); break; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs deleted file mode 100644 index 49833de73..000000000 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ /dev/null @@ -1,103 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.IO; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Api.Helpers; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.IO; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.LiveTv.EmbyTV -{ - public class DirectRecorder : IRecorder - { - private readonly ILogger _logger; - private readonly IHttpClientFactory _httpClientFactory; - private readonly IStreamHelper _streamHelper; - - public DirectRecorder(ILogger logger, IHttpClientFactory httpClientFactory, IStreamHelper streamHelper) - { - _logger = logger; - _httpClientFactory = httpClientFactory; - _streamHelper = streamHelper; - } - - public string GetOutputPath(MediaSourceInfo mediaSource, string targetFile) - { - return targetFile; - } - - public Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) - { - if (directStreamProvider is not null) - { - return RecordFromDirectStreamProvider(directStreamProvider, targetFile, duration, onStarted, cancellationToken); - } - - return RecordFromMediaSource(mediaSource, targetFile, duration, onStarted, cancellationToken); - } - - private async Task RecordFromDirectStreamProvider(IDirectStreamProvider directStreamProvider, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) - { - Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile))); - - await using (var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) - { - onStarted(); - - _logger.LogInformation("Copying recording to file {FilePath}", targetFile); - - // The media source is infinite so we need to handle stopping ourselves - using var durationToken = new CancellationTokenSource(duration); - using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); - var linkedCancellationToken = cancellationTokenSource.Token; - var fileStream = new ProgressiveFileStream(directStreamProvider.GetStream()); - await using (fileStream.ConfigureAwait(false)) - { - await _streamHelper.CopyToAsync( - fileStream, - output, - IODefaults.CopyToBufferSize, - 1000, - linkedCancellationToken).ConfigureAwait(false); - } - } - - _logger.LogInformation("Recording completed: {FilePath}", targetFile); - } - - private async Task RecordFromMediaSource(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) - { - using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .GetAsync(mediaSource.Path, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - - _logger.LogInformation("Opened recording stream from tuner provider"); - - Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile))); - - await using var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.CopyToBufferSize, FileOptions.Asynchronous); - - onStarted(); - - _logger.LogInformation("Copying recording stream to file {0}", targetFile); - - // The media source if infinite so we need to handle stopping ourselves - using var durationToken = new CancellationTokenSource(duration); - using var linkedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); - cancellationToken = linkedCancellationToken.Token; - - await _streamHelper.CopyUntilCancelled( - await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), - output, - IODefaults.CopyToBufferSize, - cancellationToken).ConfigureAwait(false); - - _logger.LogInformation("Recording completed to file {0}", targetFile); - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs deleted file mode 100644 index 74b62ca3f..000000000 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ /dev/null @@ -1,2665 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Xml; -using Emby.Server.Implementations.Library; -using Jellyfin.Data.Enums; -using Jellyfin.Data.Events; -using Jellyfin.Extensions; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Progress; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.LiveTv; -using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Querying; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.LiveTv.EmbyTV -{ - public class EmbyTV : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds, IDisposable - { - public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss"; - - private const int TunerDiscoveryDurationMs = 3000; - - private readonly IServerApplicationHost _appHost; - private readonly ILogger<EmbyTV> _logger; - private readonly IHttpClientFactory _httpClientFactory; - private readonly IServerConfigurationManager _config; - - private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider; - private readonly TimerManager _timerProvider; - - private readonly LiveTvManager _liveTvManager; - private readonly IFileSystem _fileSystem; - - private readonly ILibraryMonitor _libraryMonitor; - private readonly ILibraryManager _libraryManager; - private readonly IProviderManager _providerManager; - private readonly IMediaEncoder _mediaEncoder; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IStreamHelper _streamHelper; - - private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings = - new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase); - - private readonly ConcurrentDictionary<string, EpgChannelData> _epgChannels = - new ConcurrentDictionary<string, EpgChannelData>(StringComparer.OrdinalIgnoreCase); - - private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1); - - private bool _disposed = false; - - public EmbyTV( - IServerApplicationHost appHost, - IStreamHelper streamHelper, - IMediaSourceManager mediaSourceManager, - ILogger<EmbyTV> logger, - IHttpClientFactory httpClientFactory, - IServerConfigurationManager config, - ILiveTvManager liveTvManager, - IFileSystem fileSystem, - ILibraryManager libraryManager, - ILibraryMonitor libraryMonitor, - IProviderManager providerManager, - IMediaEncoder mediaEncoder) - { - Current = this; - - _appHost = appHost; - _logger = logger; - _httpClientFactory = httpClientFactory; - _config = config; - _fileSystem = fileSystem; - _libraryManager = libraryManager; - _libraryMonitor = libraryMonitor; - _providerManager = providerManager; - _mediaEncoder = mediaEncoder; - _liveTvManager = (LiveTvManager)liveTvManager; - _mediaSourceManager = mediaSourceManager; - _streamHelper = streamHelper; - - _seriesTimerProvider = new SeriesTimerManager(_logger, Path.Combine(DataPath, "seriestimers.json")); - _timerProvider = new TimerManager(_logger, Path.Combine(DataPath, "timers.json")); - _timerProvider.TimerFired += OnTimerProviderTimerFired; - - _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated; - } - - public event EventHandler<GenericEventArgs<TimerInfo>> TimerCreated; - - public event EventHandler<GenericEventArgs<string>> TimerCancelled; - - public static EmbyTV Current { get; private set; } - - /// <inheritdoc /> - public string Name => "Emby"; - - public string DataPath => Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv"); - - /// <inheritdoc /> - public string HomePageUrl => "https://github.com/jellyfin/jellyfin"; - - private string DefaultRecordingPath => Path.Combine(DataPath, "recordings"); - - private string RecordingPath - { - get - { - var path = GetConfiguration().RecordingPath; - - return string.IsNullOrWhiteSpace(path) - ? DefaultRecordingPath - : path; - } - } - - private async void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) - { - if (string.Equals(e.Key, "livetv", StringComparison.OrdinalIgnoreCase)) - { - await CreateRecordingFolders().ConfigureAwait(false); - } - } - - public Task Start() - { - _timerProvider.RestartTimers(); - - return CreateRecordingFolders(); - } - - internal async Task CreateRecordingFolders() - { - try - { - var recordingFolders = GetRecordingFolders().ToArray(); - var virtualFolders = _libraryManager.GetVirtualFolders(); - - var allExistingPaths = virtualFolders.SelectMany(i => i.Locations).ToList(); - - var pathsAdded = new List<string>(); - - foreach (var recordingFolder in recordingFolders) - { - var pathsToCreate = recordingFolder.Locations - .Where(i => !allExistingPaths.Any(p => _fileSystem.AreEqual(p, i))) - .ToList(); - - if (pathsToCreate.Count == 0) - { - continue; - } - - var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo(i)).ToArray(); - - var libraryOptions = new LibraryOptions - { - PathInfos = mediaPathInfos - }; - try - { - await _libraryManager.AddVirtualFolder(recordingFolder.Name, recordingFolder.CollectionType, libraryOptions, true).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating virtual folder"); - } - - pathsAdded.AddRange(pathsToCreate); - } - - var config = GetConfiguration(); - - var pathsToRemove = config.MediaLocationsCreated - .Except(recordingFolders.SelectMany(i => i.Locations)) - .ToList(); - - if (pathsAdded.Count > 0 || pathsToRemove.Count > 0) - { - pathsAdded.InsertRange(0, config.MediaLocationsCreated); - config.MediaLocationsCreated = pathsAdded.Except(pathsToRemove).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); - _config.SaveConfiguration("livetv", config); - } - - foreach (var path in pathsToRemove) - { - await RemovePathFromLibraryAsync(path).ConfigureAwait(false); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating recording folders"); - } - } - - private async Task RemovePathFromLibraryAsync(string path) - { - _logger.LogDebug("Removing path from library: {0}", path); - - var requiresRefresh = false; - var virtualFolders = _libraryManager.GetVirtualFolders(); - - foreach (var virtualFolder in virtualFolders) - { - if (!virtualFolder.Locations.Contains(path, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - if (virtualFolder.Locations.Length == 1) - { - // remove entire virtual folder - try - { - await _libraryManager.RemoveVirtualFolder(virtualFolder.Name, true).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error removing virtual folder"); - } - } - else - { - try - { - _libraryManager.RemoveMediaPath(virtualFolder.Name, path); - requiresRefresh = true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error removing media path"); - } - } - } - - if (requiresRefresh) - { - await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false); - } - } - - public async Task RefreshSeriesTimers(CancellationToken cancellationToken) - { - var seriesTimers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false); - - foreach (var timer in seriesTimers) - { - UpdateTimersForSeriesTimer(timer, false, true); - } - } - - public async Task RefreshTimers(CancellationToken cancellationToken) - { - var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false); - - var tempChannelCache = new Dictionary<Guid, LiveTvChannel>(); - - foreach (var timer in timers) - { - if (DateTime.UtcNow > timer.EndDate && !_activeRecordings.ContainsKey(timer.Id)) - { - OnTimerOutOfDate(timer); - continue; - } - - if (string.IsNullOrWhiteSpace(timer.ProgramId) || string.IsNullOrWhiteSpace(timer.ChannelId)) - { - continue; - } - - var program = GetProgramInfoFromCache(timer); - if (program is null) - { - OnTimerOutOfDate(timer); - continue; - } - - CopyProgramInfoToTimerInfo(program, timer, tempChannelCache); - _timerProvider.Update(timer); - } - } - - private void OnTimerOutOfDate(TimerInfo timer) - { - _timerProvider.Delete(timer); - } - - private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationToken) - { - var list = new List<ChannelInfo>(); - - foreach (var hostInstance in _liveTvManager.TunerHosts) - { - try - { - var channels = await hostInstance.GetChannels(enableCache, cancellationToken).ConfigureAwait(false); - - list.AddRange(channels); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting channels"); - } - } - - foreach (var provider in GetListingProviders()) - { - var enabledChannels = list - .Where(i => IsListingProviderEnabledForTuner(provider.Item2, i.TunerHostId)) - .ToList(); - - if (enabledChannels.Count > 0) - { - try - { - await AddMetadata(provider.Item1, provider.Item2, enabledChannels, enableCache, cancellationToken).ConfigureAwait(false); - } - catch (NotSupportedException) - { - } - catch (Exception ex) - { - _logger.LogError(ex, "Error adding metadata"); - } - } - } - - return list; - } - - private async Task AddMetadata( - IListingsProvider provider, - ListingsProviderInfo info, - IEnumerable<ChannelInfo> tunerChannels, - bool enableCache, - CancellationToken cancellationToken) - { - var epgChannels = await GetEpgChannels(provider, info, enableCache, cancellationToken).ConfigureAwait(false); - - foreach (var tunerChannel in tunerChannels) - { - var epgChannel = GetEpgChannelFromTunerChannel(info, tunerChannel, epgChannels); - - if (epgChannel is not null) - { - if (!string.IsNullOrWhiteSpace(epgChannel.Name)) - { - // tunerChannel.Name = epgChannel.Name; - } - - if (!string.IsNullOrWhiteSpace(epgChannel.ImageUrl)) - { - tunerChannel.ImageUrl = epgChannel.ImageUrl; - } - } - } - } - - private async Task<EpgChannelData> GetEpgChannels( - IListingsProvider provider, - ListingsProviderInfo info, - bool enableCache, - CancellationToken cancellationToken) - { - if (!enableCache || !_epgChannels.TryGetValue(info.Id, out var result)) - { - var channels = await provider.GetChannels(info, cancellationToken).ConfigureAwait(false); - - foreach (var channel in channels) - { - _logger.LogInformation("Found epg channel in {0} {1} {2} {3}", provider.Name, info.ListingsId, channel.Name, channel.Id); - } - - result = new EpgChannelData(channels); - _epgChannels.AddOrUpdate(info.Id, result, (_, _) => result); - } - - return result; - } - - private async Task<ChannelInfo> GetEpgChannelFromTunerChannel(IListingsProvider provider, ListingsProviderInfo info, ChannelInfo tunerChannel, CancellationToken cancellationToken) - { - var epgChannels = await GetEpgChannels(provider, info, true, cancellationToken).ConfigureAwait(false); - - return GetEpgChannelFromTunerChannel(info, tunerChannel, epgChannels); - } - - private static string GetMappedChannel(string channelId, NameValuePair[] mappings) - { - foreach (NameValuePair mapping in mappings) - { - if (string.Equals(mapping.Name, channelId, StringComparison.OrdinalIgnoreCase)) - { - return mapping.Value; - } - } - - return channelId; - } - - internal ChannelInfo GetEpgChannelFromTunerChannel(NameValuePair[] mappings, ChannelInfo tunerChannel, List<ChannelInfo> epgChannels) - { - return GetEpgChannelFromTunerChannel(mappings, tunerChannel, new EpgChannelData(epgChannels)); - } - - private ChannelInfo GetEpgChannelFromTunerChannel(ListingsProviderInfo info, ChannelInfo tunerChannel, EpgChannelData epgChannels) - { - return GetEpgChannelFromTunerChannel(info.ChannelMappings, tunerChannel, epgChannels); - } - - private ChannelInfo GetEpgChannelFromTunerChannel( - NameValuePair[] mappings, - ChannelInfo tunerChannel, - EpgChannelData epgChannelData) - { - if (!string.IsNullOrWhiteSpace(tunerChannel.Id)) - { - var mappedTunerChannelId = GetMappedChannel(tunerChannel.Id, mappings); - - if (string.IsNullOrWhiteSpace(mappedTunerChannelId)) - { - mappedTunerChannelId = tunerChannel.Id; - } - - var channel = epgChannelData.GetChannelById(mappedTunerChannelId); - - if (channel is not null) - { - return channel; - } - } - - if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId)) - { - var tunerChannelId = tunerChannel.TunerChannelId; - if (tunerChannelId.Contains(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase)) - { - tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I'); - } - - var mappedTunerChannelId = GetMappedChannel(tunerChannelId, mappings); - - if (string.IsNullOrWhiteSpace(mappedTunerChannelId)) - { - mappedTunerChannelId = tunerChannelId; - } - - var channel = epgChannelData.GetChannelById(mappedTunerChannelId); - - if (channel is not null) - { - return channel; - } - } - - if (!string.IsNullOrWhiteSpace(tunerChannel.Number)) - { - var tunerChannelNumber = GetMappedChannel(tunerChannel.Number, mappings); - - if (string.IsNullOrWhiteSpace(tunerChannelNumber)) - { - tunerChannelNumber = tunerChannel.Number; - } - - var channel = epgChannelData.GetChannelByNumber(tunerChannelNumber); - - if (channel is not null) - { - return channel; - } - } - - if (!string.IsNullOrWhiteSpace(tunerChannel.Name)) - { - var normalizedName = EpgChannelData.NormalizeName(tunerChannel.Name); - - var channel = epgChannelData.GetChannelByName(normalizedName); - - if (channel is not null) - { - return channel; - } - } - - return null; - } - - public async Task<List<ChannelInfo>> GetChannelsForListingsProvider(ListingsProviderInfo listingsProvider, CancellationToken cancellationToken) - { - var list = new List<ChannelInfo>(); - - foreach (var hostInstance in _liveTvManager.TunerHosts) - { - try - { - var channels = await hostInstance.GetChannels(false, cancellationToken).ConfigureAwait(false); - - list.AddRange(channels); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting channels"); - } - } - - return list - .Where(i => IsListingProviderEnabledForTuner(listingsProvider, i.TunerHostId)) - .ToList(); - } - - 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)) - .ToList(); - - foreach (var timer in timers) - { - CancelTimerInternal(timer.Id, true, true); - } - - var remove = _seriesTimerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase)); - if (remove is not null) - { - _seriesTimerProvider.Delete(remove); - } - - return Task.CompletedTask; - } - - private void CancelTimerInternal(string timerId, bool isSeriesCancelled, bool isManualCancellation) - { - var timer = _timerProvider.GetTimer(timerId); - if (timer is not null) - { - var statusChanging = timer.Status != RecordingStatus.Cancelled; - timer.Status = RecordingStatus.Cancelled; - - if (isManualCancellation) - { - timer.IsManual = true; - } - - if (string.IsNullOrWhiteSpace(timer.SeriesTimerId) || isSeriesCancelled) - { - _timerProvider.Delete(timer); - } - else - { - _timerProvider.AddOrUpdate(timer, false); - } - - if (statusChanging && TimerCancelled is not null) - { - TimerCancelled(this, new GenericEventArgs<string>(timerId)); - } - } - - if (_activeRecordings.TryGetValue(timerId, out var activeRecordingInfo)) - { - activeRecordingInfo.Timer = timer; - activeRecordingInfo.CancellationTokenSource.Cancel(); - } - } - - public Task CancelTimerAsync(string timerId, CancellationToken cancellationToken) - { - CancelTimerInternal(timerId, false, true); - return Task.CompletedTask; - } - - public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public Task<string> CreateTimer(TimerInfo info, CancellationToken cancellationToken) - { - var existingTimer = string.IsNullOrWhiteSpace(info.ProgramId) ? - null : - _timerProvider.GetTimerByProgramId(info.ProgramId); - - if (existingTimer is not null) - { - if (existingTimer.Status == RecordingStatus.Cancelled - || existingTimer.Status == RecordingStatus.Completed) - { - existingTimer.Status = RecordingStatus.New; - existingTimer.IsManual = true; - _timerProvider.Update(existingTimer); - return Task.FromResult(existingTimer.Id); - } - - throw new ArgumentException("A scheduled recording already exists for this program."); - } - - info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - - LiveTvProgram programInfo = null; - - if (!string.IsNullOrWhiteSpace(info.ProgramId)) - { - programInfo = GetProgramInfoFromCache(info); - } - - if (programInfo is null) - { - _logger.LogInformation("Unable to find program with Id {0}. Will search using start date", info.ProgramId); - programInfo = GetProgramInfoFromCache(info.ChannelId, info.StartDate); - } - - if (programInfo is not null) - { - CopyProgramInfoToTimerInfo(programInfo, info); - } - - info.IsManual = true; - _timerProvider.Add(info); - - TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(info)); - - return Task.FromResult(info.Id); - } - - public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken) - { - info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - - // populate info.seriesID - var program = GetProgramInfoFromCache(info.ProgramId); - - if (program is not null) - { - info.SeriesId = program.ExternalSeriesId; - } - else - { - throw new InvalidOperationException("SeriesId for program not found"); - } - - // If any timers have already been manually created, make sure they don't get cancelled - var existingTimers = (await GetTimersAsync(CancellationToken.None).ConfigureAwait(false)) - .Where(i => - { - if (string.Equals(i.ProgramId, info.ProgramId, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(info.ProgramId)) - { - return true; - } - - if (string.Equals(i.SeriesId, info.SeriesId, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(info.SeriesId)) - { - return true; - } - - return false; - }) - .ToList(); - - _seriesTimerProvider.Add(info); - - foreach (var timer in existingTimers) - { - timer.SeriesTimerId = info.Id; - timer.IsManual = true; - - _timerProvider.AddOrUpdate(timer, false); - } - - UpdateTimersForSeriesTimer(info, true, false); - - return info.Id; - } - - public Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) - { - var instance = _seriesTimerProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase)); - - if (instance is not null) - { - instance.ChannelId = info.ChannelId; - instance.Days = info.Days; - instance.EndDate = info.EndDate; - instance.IsPostPaddingRequired = info.IsPostPaddingRequired; - instance.IsPrePaddingRequired = info.IsPrePaddingRequired; - instance.PostPaddingSeconds = info.PostPaddingSeconds; - instance.PrePaddingSeconds = info.PrePaddingSeconds; - instance.Priority = info.Priority; - instance.RecordAnyChannel = info.RecordAnyChannel; - instance.RecordAnyTime = info.RecordAnyTime; - instance.RecordNewOnly = info.RecordNewOnly; - instance.SkipEpisodesInLibrary = info.SkipEpisodesInLibrary; - instance.KeepUpTo = info.KeepUpTo; - instance.KeepUntil = info.KeepUntil; - instance.StartDate = info.StartDate; - - _seriesTimerProvider.Update(instance); - - UpdateTimersForSeriesTimer(instance, true, true); - } - - return Task.CompletedTask; - } - - public Task UpdateTimerAsync(TimerInfo updatedTimer, CancellationToken cancellationToken) - { - var existingTimer = _timerProvider.GetTimer(updatedTimer.Id); - - if (existingTimer is null) - { - throw new ResourceNotFoundException(); - } - - // Only update if not currently active - if (!_activeRecordings.TryGetValue(updatedTimer.Id, out _)) - { - existingTimer.PrePaddingSeconds = updatedTimer.PrePaddingSeconds; - existingTimer.PostPaddingSeconds = updatedTimer.PostPaddingSeconds; - existingTimer.IsPostPaddingRequired = updatedTimer.IsPostPaddingRequired; - existingTimer.IsPrePaddingRequired = updatedTimer.IsPrePaddingRequired; - - _timerProvider.Update(existingTimer); - } - - return Task.CompletedTask; - } - - private static void UpdateExistingTimerWithNewMetadata(TimerInfo existingTimer, TimerInfo updatedTimer) - { - // Update the program info but retain the status - existingTimer.ChannelId = updatedTimer.ChannelId; - existingTimer.CommunityRating = updatedTimer.CommunityRating; - existingTimer.EndDate = updatedTimer.EndDate; - existingTimer.EpisodeNumber = updatedTimer.EpisodeNumber; - existingTimer.EpisodeTitle = updatedTimer.EpisodeTitle; - existingTimer.Genres = updatedTimer.Genres; - existingTimer.IsMovie = updatedTimer.IsMovie; - existingTimer.IsSeries = updatedTimer.IsSeries; - existingTimer.Tags = updatedTimer.Tags; - existingTimer.IsProgramSeries = updatedTimer.IsProgramSeries; - existingTimer.IsRepeat = updatedTimer.IsRepeat; - existingTimer.Name = updatedTimer.Name; - existingTimer.OfficialRating = updatedTimer.OfficialRating; - existingTimer.OriginalAirDate = updatedTimer.OriginalAirDate; - existingTimer.Overview = updatedTimer.Overview; - existingTimer.ProductionYear = updatedTimer.ProductionYear; - existingTimer.ProgramId = updatedTimer.ProgramId; - existingTimer.SeasonNumber = updatedTimer.SeasonNumber; - existingTimer.StartDate = updatedTimer.StartDate; - existingTimer.ShowId = updatedTimer.ShowId; - existingTimer.ProviderIds = updatedTimer.ProviderIds; - existingTimer.SeriesProviderIds = updatedTimer.SeriesProviderIds; - } - - public string GetActiveRecordingPath(string id) - { - if (_activeRecordings.TryGetValue(id, out var info)) - { - return info.Path; - } - - return null; - } - - public ActiveRecordingInfo GetActiveRecordingInfo(string path) - { - if (string.IsNullOrWhiteSpace(path) || _activeRecordings.IsEmpty) - { - return null; - } - - foreach (var (_, recordingInfo) in _activeRecordings) - { - if (string.Equals(recordingInfo.Path, path, StringComparison.Ordinal) && !recordingInfo.CancellationTokenSource.IsCancellationRequested) - { - var timer = recordingInfo.Timer; - if (timer.Status != RecordingStatus.InProgress) - { - return null; - } - - return recordingInfo; - } - } - - return null; - } - - public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken) - { - var excludeStatues = new List<RecordingStatus> - { - RecordingStatus.Completed - }; - - var timers = _timerProvider.GetAll() - .Where(i => !excludeStatues.Contains(i.Status)); - - return Task.FromResult(timers); - } - - 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 = true, - RecordNewOnly = true, - - Days = new List<DayOfWeek> - { - DayOfWeek.Sunday, - DayOfWeek.Monday, - DayOfWeek.Tuesday, - DayOfWeek.Wednesday, - DayOfWeek.Thursday, - DayOfWeek.Friday, - DayOfWeek.Saturday - } - }; - - if (program is not null) - { - defaults.SeriesId = program.SeriesId; - defaults.ProgramId = program.Id; - defaults.RecordNewOnly = !program.IsRepeat; - defaults.Name = program.Name; - } - - defaults.SkipEpisodesInLibrary = defaults.RecordNewOnly; - defaults.KeepUntil = KeepUntil.UntilDeleted; - - return Task.FromResult(defaults); - } - - public Task<IEnumerable<SeriesTimerInfo>> GetSeriesTimersAsync(CancellationToken cancellationToken) - { - return Task.FromResult((IEnumerable<SeriesTimerInfo>)_seriesTimerProvider.GetAll()); - } - - private bool IsListingProviderEnabledForTuner(ListingsProviderInfo info, string tunerHostId) - { - if (info.EnableAllTuners) - { - return true; - } - - if (string.IsNullOrWhiteSpace(tunerHostId)) - { - throw new ArgumentNullException(nameof(tunerHostId)); - } - - return info.EnabledTuners.Contains(tunerHostId, StringComparison.OrdinalIgnoreCase); - } - - 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()) - { - if (!IsListingProviderEnabledForTuner(provider.Item2, channel.TunerHostId)) - { - _logger.LogDebug("Skipping getting programs for channel {0}-{1} from {2}-{3}, because it's not enabled for this tuner.", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty); - continue; - } - - _logger.LogDebug("Getting programs for channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty); - - var epgChannel = await GetEpgChannelFromTunerChannel(provider.Item1, provider.Item2, channel, cancellationToken).ConfigureAwait(false); - - if (epgChannel is null) - { - _logger.LogDebug("EPG channel not found for tuner channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty); - continue; - } - - List<ProgramInfo> programs = (await provider.Item1.GetProgramsAsync(provider.Item2, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken) - .ConfigureAwait(false)).ToList(); - - // Replace the value that came from the provider with a normalized value - foreach (var program in programs) - { - program.ChannelId = channelId; - - program.Id += "_" + channelId; - } - - if (programs.Count > 0) - { - return programs; - } - } - - return Enumerable.Empty<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 is null ? null : new Tuple<IListingsProvider, ListingsProviderInfo>(provider, i); - }) - .Where(i => i is not null) - .ToList(); - } - - public Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public async Task<ILiveStream> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) - { - _logger.LogInformation("Streaming Channel {Id}", channelId); - - var result = string.IsNullOrEmpty(streamId) ? - null : - currentLiveStreams.FirstOrDefault(i => string.Equals(i.OriginalStreamId, streamId, StringComparison.OrdinalIgnoreCase)); - - if (result is not null && result.EnableStreamSharing) - { - result.ConsumerCount++; - - _logger.LogInformation("Live stream {0} consumer count is now {1}", streamId, result.ConsumerCount); - - return result; - } - - foreach (var hostInstance in _liveTvManager.TunerHosts) - { - try - { - result = await hostInstance.GetChannelStream(channelId, streamId, currentLiveStreams, cancellationToken).ConfigureAwait(false); - - var openedMediaSource = result.MediaSource; - - result.OriginalStreamId = streamId; - - _logger.LogInformation("Returning mediasource streamId {0}, mediaSource.Id {1}, mediaSource.LiveStreamId {2}", streamId, openedMediaSource.Id, openedMediaSource.LiveStreamId); - - return result; - } - catch (FileNotFoundException) - { - } - catch (OperationCanceledException) - { - } - } - - throw new ResourceNotFoundException("Tuner not found."); - } - - public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(channelId)) - { - throw new ArgumentNullException(nameof(channelId)); - } - - 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 async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken) - { - var stream = new MediaSourceInfo - { - EncoderPath = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveRecordings/" + info.Id + "/stream", - EncoderProtocol = MediaProtocol.Http, - Path = info.Path, - Protocol = MediaProtocol.File, - Id = info.Id, - SupportsDirectPlay = false, - SupportsDirectStream = true, - SupportsTranscoding = true, - IsInfiniteStream = true, - RequiresOpening = false, - RequiresClosing = false, - BufferMs = 0, - IgnoreDts = true, - IgnoreIndex = true - }; - - await new LiveStreamHelper(_mediaEncoder, _logger, _config.CommonApplicationPaths) - .AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false); - - return new List<MediaSourceInfo> - { - stream - }; - } - - public Task CloseLiveStream(string id, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - - public Task RecordLiveStream(string id, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - - public Task ResetTuner(string id, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - - private async void OnTimerProviderTimerFired(object sender, GenericEventArgs<TimerInfo> e) - { - var timer = e.Argument; - - _logger.LogInformation("Recording timer fired for {0}.", timer.Name); - - try - { - var recordingEndDate = timer.EndDate.AddSeconds(timer.PostPaddingSeconds); - - if (recordingEndDate <= DateTime.UtcNow) - { - _logger.LogWarning("Recording timer fired for updatedTimer {0}, Id: {1}, but the program has already ended.", timer.Name, timer.Id); - OnTimerOutOfDate(timer); - return; - } - - var activeRecordingInfo = new ActiveRecordingInfo - { - CancellationTokenSource = new CancellationTokenSource(), - Timer = timer, - Id = timer.Id - }; - - if (!_activeRecordings.ContainsKey(timer.Id)) - { - await RecordStream(timer, recordingEndDate, activeRecordingInfo).ConfigureAwait(false); - } - else - { - _logger.LogInformation("Skipping RecordStream because it's already in progress."); - } - } - catch (OperationCanceledException) - { - } - catch (Exception ex) - { - _logger.LogError(ex, "Error recording stream"); - } - } - - private string GetRecordingPath(TimerInfo timer, RemoteSearchResult metadata, out string seriesPath) - { - var recordPath = RecordingPath; - var config = GetConfiguration(); - seriesPath = null; - - if (timer.IsProgramSeries) - { - var customRecordingPath = config.SeriesRecordingPath; - var allowSubfolder = true; - if (!string.IsNullOrWhiteSpace(customRecordingPath)) - { - allowSubfolder = string.Equals(customRecordingPath, recordPath, StringComparison.OrdinalIgnoreCase); - recordPath = customRecordingPath; - } - - if (allowSubfolder && config.EnableRecordingSubfolders) - { - recordPath = Path.Combine(recordPath, "Series"); - } - - // trim trailing period from the folder name - var folderName = _fileSystem.GetValidFilename(timer.Name).Trim().TrimEnd('.').Trim(); - - if (metadata is not null && metadata.ProductionYear.HasValue) - { - folderName += " (" + metadata.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")"; - } - - // Can't use the year here in the folder name because it is the year of the episode, not the series. - recordPath = Path.Combine(recordPath, folderName); - - seriesPath = recordPath; - - if (timer.SeasonNumber.HasValue) - { - folderName = string.Format( - CultureInfo.InvariantCulture, - "Season {0}", - timer.SeasonNumber.Value); - recordPath = Path.Combine(recordPath, folderName); - } - } - else if (timer.IsMovie) - { - var customRecordingPath = config.MovieRecordingPath; - var allowSubfolder = true; - if (!string.IsNullOrWhiteSpace(customRecordingPath)) - { - allowSubfolder = string.Equals(customRecordingPath, recordPath, StringComparison.OrdinalIgnoreCase); - recordPath = customRecordingPath; - } - - if (allowSubfolder && config.EnableRecordingSubfolders) - { - recordPath = Path.Combine(recordPath, "Movies"); - } - - var folderName = _fileSystem.GetValidFilename(timer.Name).Trim(); - if (timer.ProductionYear.HasValue) - { - folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")"; - } - - // trim trailing period from the folder name - folderName = folderName.TrimEnd('.').Trim(); - - recordPath = Path.Combine(recordPath, folderName); - } - else if (timer.IsKids) - { - if (config.EnableRecordingSubfolders) - { - recordPath = Path.Combine(recordPath, "Kids"); - } - - var folderName = _fileSystem.GetValidFilename(timer.Name).Trim(); - if (timer.ProductionYear.HasValue) - { - folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")"; - } - - // trim trailing period from the folder name - folderName = folderName.TrimEnd('.').Trim(); - - recordPath = Path.Combine(recordPath, folderName); - } - else if (timer.IsSports) - { - if (config.EnableRecordingSubfolders) - { - recordPath = Path.Combine(recordPath, "Sports"); - } - - recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim()); - } - else - { - if (config.EnableRecordingSubfolders) - { - recordPath = Path.Combine(recordPath, "Other"); - } - - recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim()); - } - - var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer)).Trim() + ".ts"; - - return Path.Combine(recordPath, recordingFileName); - } - - private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate, ActiveRecordingInfo activeRecordingInfo) - { - ArgumentNullException.ThrowIfNull(timer); - - LiveTvProgram programInfo = null; - - if (!string.IsNullOrWhiteSpace(timer.ProgramId)) - { - programInfo = GetProgramInfoFromCache(timer); - } - - if (programInfo is null) - { - _logger.LogInformation("Unable to find program with Id {0}. Will search using start date", timer.ProgramId); - programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate); - } - - if (programInfo is not null) - { - CopyProgramInfoToTimerInfo(programInfo, timer); - } - - var remoteMetadata = await FetchInternetMetadata(timer, CancellationToken.None).ConfigureAwait(false); - var recordPath = GetRecordingPath(timer, remoteMetadata, out string seriesPath); - - var channelItem = _liveTvManager.GetLiveTvChannel(timer, this); - - string liveStreamId = null; - RecordingStatus recordingStatus; - try - { - var allMediaSources = await _mediaSourceManager.GetPlaybackMediaSources(channelItem, null, true, false, CancellationToken.None).ConfigureAwait(false); - - var mediaStreamInfo = allMediaSources[0]; - IDirectStreamProvider directStreamProvider = null; - - if (mediaStreamInfo.RequiresOpening) - { - var liveStreamResponse = await _mediaSourceManager.OpenLiveStreamInternal( - new LiveStreamRequest - { - ItemId = channelItem.Id, - OpenToken = mediaStreamInfo.OpenToken - }, - CancellationToken.None).ConfigureAwait(false); - - mediaStreamInfo = liveStreamResponse.Item1.MediaSource; - liveStreamId = mediaStreamInfo.LiveStreamId; - directStreamProvider = liveStreamResponse.Item2; - } - - var recorder = GetRecorder(mediaStreamInfo); - - recordPath = recorder.GetOutputPath(mediaStreamInfo, recordPath); - recordPath = EnsureFileUnique(recordPath, timer.Id); - - _libraryMonitor.ReportFileSystemChangeBeginning(recordPath); - - var duration = recordingEndDate - DateTime.UtcNow; - - _logger.LogInformation("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture)); - - _logger.LogInformation("Writing file to: {Path}", recordPath); - - Action onStarted = async () => - { - activeRecordingInfo.Path = recordPath; - - _activeRecordings.TryAdd(timer.Id, activeRecordingInfo); - - timer.Status = RecordingStatus.InProgress; - _timerProvider.AddOrUpdate(timer, false); - - await SaveRecordingMetadata(timer, recordPath, seriesPath).ConfigureAwait(false); - - await CreateRecordingFolders().ConfigureAwait(false); - - TriggerRefresh(recordPath); - await EnforceKeepUpTo(timer, seriesPath).ConfigureAwait(false); - }; - - await recorder.Record(directStreamProvider, mediaStreamInfo, recordPath, duration, onStarted, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false); - - recordingStatus = RecordingStatus.Completed; - _logger.LogInformation("Recording completed: {RecordPath}", recordPath); - } - catch (OperationCanceledException) - { - _logger.LogInformation("Recording stopped: {RecordPath}", recordPath); - recordingStatus = RecordingStatus.Completed; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error recording to {RecordPath}", recordPath); - recordingStatus = RecordingStatus.Error; - } - - if (!string.IsNullOrWhiteSpace(liveStreamId)) - { - try - { - await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error closing live stream"); - } - } - - DeleteFileIfEmpty(recordPath); - - TriggerRefresh(recordPath); - _libraryMonitor.ReportFileSystemChangeComplete(recordPath, false); - - _activeRecordings.TryRemove(timer.Id, out _); - - if (recordingStatus != RecordingStatus.Completed && DateTime.UtcNow < timer.EndDate && timer.RetryCount < 10) - { - const int RetryIntervalSeconds = 60; - _logger.LogInformation("Retrying recording in {0} seconds.", RetryIntervalSeconds); - - timer.Status = RecordingStatus.New; - timer.PrePaddingSeconds = 0; - timer.StartDate = DateTime.UtcNow.AddSeconds(RetryIntervalSeconds); - timer.RetryCount++; - _timerProvider.AddOrUpdate(timer); - } - else if (File.Exists(recordPath)) - { - timer.RecordingPath = recordPath; - timer.Status = RecordingStatus.Completed; - _timerProvider.AddOrUpdate(timer, false); - OnSuccessfulRecording(timer, recordPath); - } - else - { - _timerProvider.Delete(timer); - } - } - - private async Task<RemoteSearchResult> FetchInternetMetadata(TimerInfo timer, CancellationToken cancellationToken) - { - if (timer.IsSeries) - { - if (timer.SeriesProviderIds.Count == 0) - { - return null; - } - - var query = new RemoteSearchQuery<SeriesInfo>() - { - SearchInfo = new SeriesInfo - { - ProviderIds = timer.SeriesProviderIds, - Name = timer.Name, - MetadataCountryCode = _config.Configuration.MetadataCountryCode, - MetadataLanguage = _config.Configuration.PreferredMetadataLanguage - } - }; - - var results = await _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(query, cancellationToken).ConfigureAwait(false); - - return results.FirstOrDefault(); - } - - return null; - } - - private void DeleteFileIfEmpty(string path) - { - var file = _fileSystem.GetFileInfo(path); - - if (file.Exists && file.Length == 0) - { - try - { - _fileSystem.DeleteFile(path); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting 0-byte failed recording file {Path}", path); - } - } - } - - private void TriggerRefresh(string path) - { - _logger.LogInformation("Triggering refresh on {Path}", path); - - var item = GetAffectedBaseItem(Path.GetDirectoryName(path)); - - if (item is not null) - { - _logger.LogInformation("Refreshing recording parent {Path}", item.Path); - - _providerManager.QueueRefresh( - item.Id, - new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - RefreshPaths = new string[] - { - path, - Path.GetDirectoryName(path), - Path.GetDirectoryName(Path.GetDirectoryName(path)) - } - }, - RefreshPriority.High); - } - } - - private BaseItem GetAffectedBaseItem(string path) - { - BaseItem item = null; - - var parentPath = Path.GetDirectoryName(path); - - while (item is null && !string.IsNullOrEmpty(path)) - { - item = _libraryManager.FindByPath(path, null); - - path = Path.GetDirectoryName(path); - } - - if (item is not null) - { - if (item.GetType() == typeof(Folder) && string.Equals(item.Path, parentPath, StringComparison.OrdinalIgnoreCase)) - { - var parentItem = item.GetParent(); - if (parentItem is not null && parentItem is not AggregateFolder) - { - item = parentItem; - } - } - } - - return item; - } - - private async Task EnforceKeepUpTo(TimerInfo timer, string seriesPath) - { - if (string.IsNullOrWhiteSpace(timer.SeriesTimerId)) - { - return; - } - - if (string.IsNullOrWhiteSpace(seriesPath)) - { - return; - } - - var seriesTimerId = timer.SeriesTimerId; - var seriesTimer = _seriesTimerProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, seriesTimerId, StringComparison.OrdinalIgnoreCase)); - - if (seriesTimer is null || seriesTimer.KeepUpTo <= 0) - { - return; - } - - if (_disposed) - { - return; - } - - await _recordingDeleteSemaphore.WaitAsync().ConfigureAwait(false); - - try - { - if (_disposed) - { - return; - } - - var timersToDelete = _timerProvider.GetAll() - .Where(i => i.Status == RecordingStatus.Completed && !string.IsNullOrWhiteSpace(i.RecordingPath)) - .Where(i => string.Equals(i.SeriesTimerId, seriesTimerId, StringComparison.OrdinalIgnoreCase)) - .OrderByDescending(i => i.EndDate) - .Where(i => File.Exists(i.RecordingPath)) - .Skip(seriesTimer.KeepUpTo - 1) - .ToList(); - - DeleteLibraryItemsForTimers(timersToDelete); - - if (_libraryManager.FindByPath(seriesPath, true) is not Folder librarySeries) - { - return; - } - - var episodesToDelete = librarySeries.GetItemList( - new InternalItemsQuery - { - OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) }, - IsVirtualItem = false, - IsFolder = false, - Recursive = true, - DtoOptions = new DtoOptions(true) - }) - .Where(i => i.IsFileProtocol && File.Exists(i.Path)) - .Skip(seriesTimer.KeepUpTo - 1) - .ToList(); - - foreach (var item in episodesToDelete) - { - try - { - _libraryManager.DeleteItem( - item, - new DeleteOptions - { - DeleteFileLocation = true - }, - true); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting item"); - } - } - } - finally - { - _recordingDeleteSemaphore.Release(); - } - } - - private void DeleteLibraryItemsForTimers(List<TimerInfo> timers) - { - foreach (var timer in timers) - { - if (_disposed) - { - return; - } - - try - { - DeleteLibraryItemForTimer(timer); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting recording"); - } - } - } - - private void DeleteLibraryItemForTimer(TimerInfo timer) - { - var libraryItem = _libraryManager.FindByPath(timer.RecordingPath, false); - - if (libraryItem is not null) - { - _libraryManager.DeleteItem( - libraryItem, - new DeleteOptions - { - DeleteFileLocation = true - }, - true); - } - else if (File.Exists(timer.RecordingPath)) - { - _fileSystem.DeleteFile(timer.RecordingPath); - } - - _timerProvider.Delete(timer); - } - - private string EnsureFileUnique(string path, string timerId) - { - var originalPath = path; - var index = 1; - - while (FileExists(path, timerId)) - { - var parent = Path.GetDirectoryName(originalPath); - var name = Path.GetFileNameWithoutExtension(originalPath); - name += " - " + index.ToString(CultureInfo.InvariantCulture); - - path = Path.ChangeExtension(Path.Combine(parent, name), Path.GetExtension(originalPath)); - index++; - } - - return path; - } - - private bool FileExists(string path, string timerId) - { - if (File.Exists(path)) - { - return true; - } - - return _activeRecordings - .Any(i => string.Equals(i.Value.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Value.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase)); - } - - private IRecorder GetRecorder(MediaSourceInfo mediaSource) - { - if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http)) - { - return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _config); - } - - return new DirectRecorder(_logger, _httpClientFactory, _streamHelper); - } - - private void OnSuccessfulRecording(TimerInfo timer, string path) - { - PostProcessRecording(timer, path); - } - - private void PostProcessRecording(TimerInfo timer, string path) - { - var options = GetConfiguration(); - if (string.IsNullOrWhiteSpace(options.RecordingPostProcessor)) - { - return; - } - - try - { - var process = new Process - { - StartInfo = new ProcessStartInfo - { - Arguments = GetPostProcessArguments(path, options.RecordingPostProcessorArguments), - CreateNoWindow = true, - ErrorDialog = false, - FileName = options.RecordingPostProcessor, - WindowStyle = ProcessWindowStyle.Hidden, - UseShellExecute = false - }, - EnableRaisingEvents = true - }; - - _logger.LogInformation("Running recording post processor {0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); - - process.Exited += OnProcessExited; - process.Start(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error running recording post processor"); - } - } - - private static string GetPostProcessArguments(string path, string arguments) - { - return arguments.Replace("{path}", path, StringComparison.OrdinalIgnoreCase); - } - - private void OnProcessExited(object sender, EventArgs e) - { - using (var process = (Process)sender) - { - _logger.LogInformation("Recording post-processing script completed with exit code {ExitCode}", process.ExitCode); - } - } - - private async Task SaveRecordingImage(string recordingPath, LiveTvProgram program, ItemImageInfo image) - { - if (!image.IsLocalFile) - { - image = await _libraryManager.ConvertImageToLocal(program, image, 0).ConfigureAwait(false); - } - - string imageSaveFilenameWithoutExtension = image.Type switch - { - ImageType.Primary => program.IsSeries ? Path.GetFileNameWithoutExtension(recordingPath) + "-thumb" : "poster", - ImageType.Logo => "logo", - ImageType.Thumb => program.IsSeries ? Path.GetFileNameWithoutExtension(recordingPath) + "-thumb" : "landscape", - ImageType.Backdrop => "fanart", - _ => null - }; - - if (imageSaveFilenameWithoutExtension is null) - { - return; - } - - var imageSavePath = Path.Combine(Path.GetDirectoryName(recordingPath), imageSaveFilenameWithoutExtension); - - // preserve original image extension - imageSavePath = Path.ChangeExtension(imageSavePath, Path.GetExtension(image.Path)); - - File.Copy(image.Path, imageSavePath, true); - } - - private async Task SaveRecordingImages(string recordingPath, LiveTvProgram program) - { - var image = program.IsSeries ? - (program.GetImageInfo(ImageType.Thumb, 0) ?? program.GetImageInfo(ImageType.Primary, 0)) : - (program.GetImageInfo(ImageType.Primary, 0) ?? program.GetImageInfo(ImageType.Thumb, 0)); - - if (image is not null) - { - try - { - await SaveRecordingImage(recordingPath, program, image).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error saving recording image"); - } - } - - if (!program.IsSeries) - { - image = program.GetImageInfo(ImageType.Backdrop, 0); - if (image is not null) - { - try - { - await SaveRecordingImage(recordingPath, program, image).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error saving recording image"); - } - } - - image = program.GetImageInfo(ImageType.Thumb, 0); - if (image is not null) - { - try - { - await SaveRecordingImage(recordingPath, program, image).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error saving recording image"); - } - } - - image = program.GetImageInfo(ImageType.Logo, 0); - if (image is not null) - { - try - { - await SaveRecordingImage(recordingPath, program, image).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error saving recording image"); - } - } - } - } - - private async Task SaveRecordingMetadata(TimerInfo timer, string recordingPath, string seriesPath) - { - try - { - var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, - Limit = 1, - ExternalId = timer.ProgramId, - DtoOptions = new DtoOptions(true) - }).FirstOrDefault() as LiveTvProgram; - - // dummy this up - if (program is null) - { - program = new LiveTvProgram - { - Name = timer.Name, - Overview = timer.Overview, - Genres = timer.Genres, - CommunityRating = timer.CommunityRating, - OfficialRating = timer.OfficialRating, - ProductionYear = timer.ProductionYear, - PremiereDate = timer.OriginalAirDate, - IndexNumber = timer.EpisodeNumber, - ParentIndexNumber = timer.SeasonNumber - }; - } - - if (timer.IsSports) - { - program.AddGenre("Sports"); - } - - if (timer.IsKids) - { - program.AddGenre("Kids"); - program.AddGenre("Children"); - } - - if (timer.IsNews) - { - program.AddGenre("News"); - } - - var config = GetConfiguration(); - - if (config.SaveRecordingNFO) - { - if (timer.IsProgramSeries) - { - await SaveSeriesNfoAsync(timer, seriesPath).ConfigureAwait(false); - await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false); - } - else if (!timer.IsMovie || timer.IsSports || timer.IsNews) - { - await SaveVideoNfoAsync(timer, recordingPath, program, true).ConfigureAwait(false); - } - else - { - await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false); - } - } - - if (config.SaveRecordingImages) - { - await SaveRecordingImages(recordingPath, program).ConfigureAwait(false); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error saving nfo"); - } - } - - private async Task SaveSeriesNfoAsync(TimerInfo timer, string seriesPath) - { - var nfoPath = Path.Combine(seriesPath, "tvshow.nfo"); - - if (File.Exists(nfoPath)) - { - return; - } - - var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); - await using (stream.ConfigureAwait(false)) - { - var settings = new XmlWriterSettings - { - Indent = true, - Encoding = Encoding.UTF8, - Async = true - }; - - var writer = XmlWriter.Create(stream, settings); - await using (writer.ConfigureAwait(false)) - { - await writer.WriteStartDocumentAsync(true).ConfigureAwait(false); - await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false); - if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out var id)) - { - await writer.WriteElementStringAsync(null, "id", null, id).ConfigureAwait(false); - } - - if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out id)) - { - await writer.WriteElementStringAsync(null, "imdb_id", null, id).ConfigureAwait(false); - } - - if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out id)) - { - await writer.WriteElementStringAsync(null, "tmdbid", null, id).ConfigureAwait(false); - } - - if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Zap2It.ToString(), out id)) - { - await writer.WriteElementStringAsync(null, "zap2itid", null, id).ConfigureAwait(false); - } - - if (!string.IsNullOrWhiteSpace(timer.Name)) - { - await writer.WriteElementStringAsync(null, "title", null, timer.Name).ConfigureAwait(false); - } - - if (!string.IsNullOrWhiteSpace(timer.OfficialRating)) - { - await writer.WriteElementStringAsync(null, "mpaa", null, timer.OfficialRating).ConfigureAwait(false); - } - - foreach (var genre in timer.Genres) - { - await writer.WriteElementStringAsync(null, "genre", null, genre).ConfigureAwait(false); - } - - await writer.WriteEndElementAsync().ConfigureAwait(false); - await writer.WriteEndDocumentAsync().ConfigureAwait(false); - } - } - } - - private async Task SaveVideoNfoAsync(TimerInfo timer, string recordingPath, BaseItem item, bool lockData) - { - var nfoPath = Path.ChangeExtension(recordingPath, ".nfo"); - - if (File.Exists(nfoPath)) - { - return; - } - - var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); - await using (stream.ConfigureAwait(false)) - { - var settings = new XmlWriterSettings - { - Indent = true, - Encoding = Encoding.UTF8, - Async = true - }; - - var options = _config.GetNfoConfiguration(); - - var isSeriesEpisode = timer.IsProgramSeries; - - var writer = XmlWriter.Create(stream, settings); - await using (writer.ConfigureAwait(false)) - { - await writer.WriteStartDocumentAsync(true).ConfigureAwait(false); - - if (isSeriesEpisode) - { - await writer.WriteStartElementAsync(null, "episodedetails", null).ConfigureAwait(false); - - if (!string.IsNullOrWhiteSpace(timer.EpisodeTitle)) - { - await writer.WriteElementStringAsync(null, "title", null, timer.EpisodeTitle).ConfigureAwait(false); - } - - var premiereDate = item.PremiereDate ?? (!timer.IsRepeat ? DateTime.UtcNow : null); - - if (premiereDate.HasValue) - { - var formatString = options.ReleaseDateFormat; - - await writer.WriteElementStringAsync( - null, - "aired", - null, - premiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)).ConfigureAwait(false); - } - - if (item.IndexNumber.HasValue) - { - await writer.WriteElementStringAsync(null, "episode", null, item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); - } - - if (item.ParentIndexNumber.HasValue) - { - await writer.WriteElementStringAsync(null, "season", null, item.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); - } - } - else - { - await writer.WriteStartElementAsync(null, "movie", null).ConfigureAwait(false); - - if (!string.IsNullOrWhiteSpace(item.Name)) - { - await writer.WriteElementStringAsync(null, "title", null, item.Name).ConfigureAwait(false); - } - - if (!string.IsNullOrWhiteSpace(item.OriginalTitle)) - { - await writer.WriteElementStringAsync(null, "originaltitle", null, item.OriginalTitle).ConfigureAwait(false); - } - - if (item.PremiereDate.HasValue) - { - var formatString = options.ReleaseDateFormat; - - await writer.WriteElementStringAsync( - null, - "premiered", - null, - item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)).ConfigureAwait(false); - await writer.WriteElementStringAsync( - null, - "releasedate", - null, - item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)).ConfigureAwait(false); - } - } - - await writer.WriteElementStringAsync( - null, - "dateadded", - null, - DateTime.Now.ToString(DateAddedFormat, CultureInfo.InvariantCulture)).ConfigureAwait(false); - - if (item.ProductionYear.HasValue) - { - await writer.WriteElementStringAsync(null, "year", null, item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); - } - - if (!string.IsNullOrEmpty(item.OfficialRating)) - { - await writer.WriteElementStringAsync(null, "mpaa", null, item.OfficialRating).ConfigureAwait(false); - } - - var overview = (item.Overview ?? string.Empty) - .StripHtml() - .Replace(""", "'", StringComparison.Ordinal); - - await writer.WriteElementStringAsync(null, "plot", null, overview).ConfigureAwait(false); - - if (item.CommunityRating.HasValue) - { - await writer.WriteElementStringAsync(null, "rating", null, item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); - } - - foreach (var genre in item.Genres) - { - await writer.WriteElementStringAsync(null, "genre", null, genre).ConfigureAwait(false); - } - - var people = item.Id.Equals(default) ? new List<PersonInfo>() : _libraryManager.GetPeople(item); - - var directors = people - .Where(i => i.IsType(PersonKind.Director)) - .Select(i => i.Name) - .ToList(); - - foreach (var person in directors) - { - await writer.WriteElementStringAsync(null, "director", null, person).ConfigureAwait(false); - } - - var writers = people - .Where(i => i.IsType(PersonKind.Writer)) - .Select(i => i.Name) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToList(); - - foreach (var person in writers) - { - await writer.WriteElementStringAsync(null, "writer", null, person).ConfigureAwait(false); - } - - foreach (var person in writers) - { - await writer.WriteElementStringAsync(null, "credits", null, person).ConfigureAwait(false); - } - - var tmdbCollection = item.GetProviderId(MetadataProvider.TmdbCollection); - - if (!string.IsNullOrEmpty(tmdbCollection)) - { - await writer.WriteElementStringAsync(null, "collectionnumber", null, tmdbCollection).ConfigureAwait(false); - } - - var imdb = item.GetProviderId(MetadataProvider.Imdb); - if (!string.IsNullOrEmpty(imdb)) - { - if (!isSeriesEpisode) - { - await writer.WriteElementStringAsync(null, "id", null, imdb).ConfigureAwait(false); - } - - await writer.WriteElementStringAsync(null, "imdbid", null, imdb).ConfigureAwait(false); - - // No need to lock if we have identified the content already - lockData = false; - } - - var tvdb = item.GetProviderId(MetadataProvider.Tvdb); - if (!string.IsNullOrEmpty(tvdb)) - { - await writer.WriteElementStringAsync(null, "tvdbid", null, tvdb).ConfigureAwait(false); - - // No need to lock if we have identified the content already - lockData = false; - } - - var tmdb = item.GetProviderId(MetadataProvider.Tmdb); - if (!string.IsNullOrEmpty(tmdb)) - { - await writer.WriteElementStringAsync(null, "tmdbid", null, tmdb).ConfigureAwait(false); - - // No need to lock if we have identified the content already - lockData = false; - } - - if (lockData) - { - await writer.WriteElementStringAsync(null, "lockdata", null, "true").ConfigureAwait(false); - } - - if (item.CriticRating.HasValue) - { - await writer.WriteElementStringAsync(null, "criticrating", null, item.CriticRating.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); - } - - if (!string.IsNullOrWhiteSpace(item.Tagline)) - { - await writer.WriteElementStringAsync(null, "tagline", null, item.Tagline).ConfigureAwait(false); - } - - foreach (var studio in item.Studios) - { - await writer.WriteElementStringAsync(null, "studio", null, studio).ConfigureAwait(false); - } - - await writer.WriteEndElementAsync().ConfigureAwait(false); - await writer.WriteEndDocumentAsync().ConfigureAwait(false); - } - } - } - - private LiveTvProgram GetProgramInfoFromCache(string programId) - { - var query = new InternalItemsQuery - { - ItemIds = new[] { _liveTvManager.GetInternalProgramId(programId) }, - Limit = 1, - DtoOptions = new DtoOptions() - }; - - return _libraryManager.GetItemList(query).Cast<LiveTvProgram>().FirstOrDefault(); - } - - private LiveTvProgram GetProgramInfoFromCache(TimerInfo timer) - { - return GetProgramInfoFromCache(timer.ProgramId); - } - - private LiveTvProgram GetProgramInfoFromCache(string channelId, DateTime startDateUtc) - { - var query = new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, - Limit = 1, - DtoOptions = new DtoOptions(true) - { - EnableImages = false - }, - MinStartDate = startDateUtc.AddMinutes(-3), - MaxStartDate = startDateUtc.AddMinutes(3), - OrderBy = new[] { (ItemSortBy.StartDate, SortOrder.Ascending) } - }; - - if (!string.IsNullOrWhiteSpace(channelId)) - { - query.ChannelIds = new[] { _liveTvManager.GetInternalChannelId(Name, channelId) }; - } - - return _libraryManager.GetItemList(query).Cast<LiveTvProgram>().FirstOrDefault(); - } - - private LiveTvOptions GetConfiguration() - { - return _config.GetConfiguration<LiveTvOptions>("livetv"); - } - - private bool ShouldCancelTimerForSeriesTimer(SeriesTimerInfo seriesTimer, TimerInfo timer) - { - if (timer.IsManual) - { - return false; - } - - if (!seriesTimer.RecordAnyTime - && Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMinutes(10).Ticks) - { - return true; - } - - if (seriesTimer.RecordNewOnly && timer.IsRepeat) - { - return true; - } - - if (!seriesTimer.RecordAnyChannel - && !string.Equals(timer.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - return seriesTimer.SkipEpisodesInLibrary && IsProgramAlreadyInLibrary(timer); - } - - private void HandleDuplicateShowIds(List<TimerInfo> timers) - { - // sort showings by HD channels first, then by startDate, record earliest showing possible - foreach (var timer in timers.OrderByDescending(t => _liveTvManager.GetLiveTvChannel(t, this).IsHD).ThenBy(t => t.StartDate).Skip(1)) - { - timer.Status = RecordingStatus.Cancelled; - _timerProvider.Update(timer); - } - } - - private void SearchForDuplicateShowIds(IEnumerable<TimerInfo> timers) - { - var groups = timers.ToLookup(i => i.ShowId ?? string.Empty).ToList(); - - foreach (var group in groups) - { - if (string.IsNullOrWhiteSpace(group.Key)) - { - continue; - } - - var groupTimers = group.ToList(); - - if (groupTimers.Count < 2) - { - continue; - } - - // Skip ShowId without SubKey from duplicate removal actions - https://github.com/jellyfin/jellyfin/issues/5856 - if (group.Key.EndsWith("0000", StringComparison.Ordinal)) - { - continue; - } - - HandleDuplicateShowIds(groupTimers); - } - } - - private void UpdateTimersForSeriesTimer(SeriesTimerInfo seriesTimer, bool updateTimerSettings, bool deleteInvalidTimers) - { - var allTimers = GetTimersForSeries(seriesTimer).ToList(); - - var enabledTimersForSeries = new List<TimerInfo>(); - foreach (var timer in allTimers) - { - var existingTimer = _timerProvider.GetTimer(timer.Id) - ?? (string.IsNullOrWhiteSpace(timer.ProgramId) - ? null - : _timerProvider.GetTimerByProgramId(timer.ProgramId)); - - if (existingTimer is null) - { - if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer)) - { - timer.Status = RecordingStatus.Cancelled; - } - else - { - enabledTimersForSeries.Add(timer); - } - - _timerProvider.Add(timer); - - TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(timer)); - } - - // Only update if not currently active - test both new timer and existing in case Id's are different - // Id's could be different if the timer was created manually prior to series timer creation - else if (!_activeRecordings.TryGetValue(timer.Id, out _) && !_activeRecordings.TryGetValue(existingTimer.Id, out _)) - { - UpdateExistingTimerWithNewMetadata(existingTimer, timer); - - // Needed by ShouldCancelTimerForSeriesTimer - timer.IsManual = existingTimer.IsManual; - - if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer)) - { - existingTimer.Status = RecordingStatus.Cancelled; - } - else if (!existingTimer.IsManual) - { - existingTimer.Status = RecordingStatus.New; - } - - if (existingTimer.Status != RecordingStatus.Cancelled) - { - enabledTimersForSeries.Add(existingTimer); - } - - if (updateTimerSettings) - { - existingTimer.KeepUntil = seriesTimer.KeepUntil; - existingTimer.IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired; - existingTimer.IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired; - existingTimer.PostPaddingSeconds = seriesTimer.PostPaddingSeconds; - existingTimer.PrePaddingSeconds = seriesTimer.PrePaddingSeconds; - existingTimer.Priority = seriesTimer.Priority; - existingTimer.SeriesTimerId = seriesTimer.Id; - } - - existingTimer.SeriesTimerId = seriesTimer.Id; - _timerProvider.Update(existingTimer); - } - } - - SearchForDuplicateShowIds(enabledTimersForSeries); - - if (deleteInvalidTimers) - { - var allTimerIds = allTimers - .Select(i => i.Id) - .ToList(); - - var deleteStatuses = new[] - { - RecordingStatus.New - }; - - var deletes = _timerProvider.GetAll() - .Where(i => string.Equals(i.SeriesTimerId, seriesTimer.Id, StringComparison.OrdinalIgnoreCase)) - .Where(i => !allTimerIds.Contains(i.Id, StringComparison.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow) - .Where(i => deleteStatuses.Contains(i.Status)) - .ToList(); - - foreach (var timer in deletes) - { - CancelTimerInternal(timer.Id, false, false); - } - } - } - - private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer) - { - ArgumentNullException.ThrowIfNull(seriesTimer); - - var query = new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, - ExternalSeriesId = seriesTimer.SeriesId, - DtoOptions = new DtoOptions(true) - { - EnableImages = false - }, - MinEndDate = DateTime.UtcNow - }; - - if (string.IsNullOrEmpty(seriesTimer.SeriesId)) - { - query.Name = seriesTimer.Name; - } - - if (!seriesTimer.RecordAnyChannel) - { - query.ChannelIds = new[] { _liveTvManager.GetInternalChannelId(Name, seriesTimer.ChannelId) }; - } - - var tempChannelCache = new Dictionary<Guid, LiveTvChannel>(); - - return _libraryManager.GetItemList(query).Cast<LiveTvProgram>().Select(i => CreateTimer(i, seriesTimer, tempChannelCache)); - } - - private TimerInfo CreateTimer(LiveTvProgram parent, SeriesTimerInfo seriesTimer, Dictionary<Guid, LiveTvChannel> tempChannelCache) - { - string channelId = seriesTimer.RecordAnyChannel ? null : seriesTimer.ChannelId; - - if (string.IsNullOrWhiteSpace(channelId) && !parent.ChannelId.Equals(default)) - { - if (!tempChannelCache.TryGetValue(parent.ChannelId, out LiveTvChannel channel)) - { - channel = _libraryManager.GetItemList( - new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel }, - ItemIds = new[] { parent.ChannelId }, - DtoOptions = new DtoOptions() - }).FirstOrDefault() as LiveTvChannel; - - if (channel is not null && !string.IsNullOrWhiteSpace(channel.ExternalId)) - { - tempChannelCache[parent.ChannelId] = channel; - } - } - - if (channel is not null || tempChannelCache.TryGetValue(parent.ChannelId, out channel)) - { - channelId = channel.ExternalId; - } - } - - var timer = new TimerInfo - { - ChannelId = channelId, - Id = (seriesTimer.Id + parent.ExternalId).GetMD5().ToString("N", CultureInfo.InvariantCulture), - StartDate = parent.StartDate, - EndDate = parent.EndDate.Value, - ProgramId = parent.ExternalId, - PrePaddingSeconds = seriesTimer.PrePaddingSeconds, - PostPaddingSeconds = seriesTimer.PostPaddingSeconds, - IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired, - IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired, - KeepUntil = seriesTimer.KeepUntil, - Priority = seriesTimer.Priority, - Name = parent.Name, - Overview = parent.Overview, - SeriesId = parent.ExternalSeriesId, - SeriesTimerId = seriesTimer.Id, - ShowId = parent.ShowId - }; - - CopyProgramInfoToTimerInfo(parent, timer, tempChannelCache); - - return timer; - } - - private void CopyProgramInfoToTimerInfo(LiveTvProgram programInfo, TimerInfo timerInfo) - { - var tempChannelCache = new Dictionary<Guid, LiveTvChannel>(); - CopyProgramInfoToTimerInfo(programInfo, timerInfo, tempChannelCache); - } - - private void CopyProgramInfoToTimerInfo(LiveTvProgram programInfo, TimerInfo timerInfo, Dictionary<Guid, LiveTvChannel> tempChannelCache) - { - string channelId = null; - - if (!programInfo.ChannelId.Equals(default)) - { - if (!tempChannelCache.TryGetValue(programInfo.ChannelId, out LiveTvChannel channel)) - { - channel = _libraryManager.GetItemList( - new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel }, - ItemIds = new[] { programInfo.ChannelId }, - DtoOptions = new DtoOptions() - }).FirstOrDefault() as LiveTvChannel; - - if (channel is not null && !string.IsNullOrWhiteSpace(channel.ExternalId)) - { - tempChannelCache[programInfo.ChannelId] = channel; - } - } - - if (channel is not null || tempChannelCache.TryGetValue(programInfo.ChannelId, out channel)) - { - channelId = channel.ExternalId; - } - } - - timerInfo.Name = programInfo.Name; - timerInfo.StartDate = programInfo.StartDate; - timerInfo.EndDate = programInfo.EndDate.Value; - - if (!string.IsNullOrWhiteSpace(channelId)) - { - timerInfo.ChannelId = channelId; - } - - timerInfo.SeasonNumber = programInfo.ParentIndexNumber; - timerInfo.EpisodeNumber = programInfo.IndexNumber; - timerInfo.IsMovie = programInfo.IsMovie; - timerInfo.ProductionYear = programInfo.ProductionYear; - timerInfo.EpisodeTitle = programInfo.EpisodeTitle; - timerInfo.OriginalAirDate = programInfo.PremiereDate; - timerInfo.IsProgramSeries = programInfo.IsSeries; - - timerInfo.IsSeries = programInfo.IsSeries; - - timerInfo.CommunityRating = programInfo.CommunityRating; - timerInfo.Overview = programInfo.Overview; - timerInfo.OfficialRating = programInfo.OfficialRating; - timerInfo.IsRepeat = programInfo.IsRepeat; - timerInfo.SeriesId = programInfo.ExternalSeriesId; - timerInfo.ProviderIds = programInfo.ProviderIds; - timerInfo.Tags = programInfo.Tags; - - var seriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - foreach (var providerId in timerInfo.ProviderIds) - { - const string Search = "Series"; - if (providerId.Key.StartsWith(Search, StringComparison.OrdinalIgnoreCase)) - { - seriesProviderIds[providerId.Key.Substring(Search.Length)] = providerId.Value; - } - } - - timerInfo.SeriesProviderIds = seriesProviderIds; - } - - private bool IsProgramAlreadyInLibrary(TimerInfo program) - { - if ((program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) || !string.IsNullOrWhiteSpace(program.EpisodeTitle)) - { - var seriesIds = _libraryManager.GetItemIds( - new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.Series }, - Name = program.Name - }).ToArray(); - - if (seriesIds.Length == 0) - { - return false; - } - - if (program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) - { - var result = _libraryManager.GetItemIds(new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.Episode }, - ParentIndexNumber = program.SeasonNumber.Value, - IndexNumber = program.EpisodeNumber.Value, - AncestorIds = seriesIds, - IsVirtualItem = false, - Limit = 1 - }); - - if (result.Count > 0) - { - return true; - } - } - } - - return false; - } - - /// <inheritdoc /> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - _recordingDeleteSemaphore.Dispose(); - } - - foreach (var pair in _activeRecordings.ToList()) - { - pair.Value.CancellationTokenSource.Cancel(); - } - - _disposed = true; - } - - public IEnumerable<VirtualFolderInfo> GetRecordingFolders() - { - var defaultFolder = RecordingPath; - var defaultName = "Recordings"; - - if (Directory.Exists(defaultFolder)) - { - yield return new VirtualFolderInfo - { - Locations = new string[] { defaultFolder }, - Name = defaultName - }; - } - - var customPath = GetConfiguration().MovieRecordingPath; - if (!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase) && Directory.Exists(customPath)) - { - yield return new VirtualFolderInfo - { - Locations = new string[] { customPath }, - Name = "Recorded Movies", - CollectionType = CollectionTypeOptions.Movies - }; - } - - customPath = GetConfiguration().SeriesRecordingPath; - if (!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase) && Directory.Exists(customPath)) - { - yield return new VirtualFolderInfo - { - Locations = new string[] { customPath }, - Name = "Recorded Shows", - CollectionType = CollectionTypeOptions.TvShows - }; - } - } - - public async Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken) - { - var list = new List<TunerHostInfo>(); - - var configuredDeviceIds = GetConfiguration().TunerHosts - .Where(i => !string.IsNullOrWhiteSpace(i.DeviceId)) - .Select(i => i.DeviceId) - .ToList(); - - foreach (var host in _liveTvManager.TunerHosts) - { - var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false); - - if (newDevicesOnly) - { - discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparison.OrdinalIgnoreCase)) - .ToList(); - } - - list.AddRange(discoveredDevices); - } - - return list; - } - - public async Task ScanForTunerDeviceChanges(CancellationToken cancellationToken) - { - foreach (var host in _liveTvManager.TunerHosts) - { - await ScanForTunerDeviceChanges(host, cancellationToken).ConfigureAwait(false); - } - } - - private async Task ScanForTunerDeviceChanges(ITunerHost host, CancellationToken cancellationToken) - { - var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false); - - var configuredDevices = GetConfiguration().TunerHosts - .Where(i => string.Equals(i.Type, host.Type, StringComparison.OrdinalIgnoreCase)) - .ToList(); - - foreach (var device in discoveredDevices) - { - var configuredDevice = configuredDevices.FirstOrDefault(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase)); - - if (configuredDevice is not null && !string.Equals(device.Url, configuredDevice.Url, StringComparison.OrdinalIgnoreCase)) - { - _logger.LogInformation("Tuner url has changed from {PreviousUrl} to {NewUrl}", configuredDevice.Url, device.Url); - - configuredDevice.Url = device.Url; - await _liveTvManager.SaveTunerHost(configuredDevice).ConfigureAwait(false); - } - } - } - - private async Task<List<TunerHostInfo>> DiscoverDevices(ITunerHost host, int discoveryDurationMs, CancellationToken cancellationToken) - { - try - { - var discoveredDevices = await host.DiscoverDevices(discoveryDurationMs, cancellationToken).ConfigureAwait(false); - - foreach (var device in discoveredDevices) - { - _logger.LogInformation("Discovered tuner device {0} at {1}", host.Name, device.Url); - } - - return discoveredDevices; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error discovering tuner devices"); - - return new List<TunerHostInfo>(); - } - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs deleted file mode 100644 index 5369c9b3d..000000000 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ /dev/null @@ -1,362 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Extensions; -using Jellyfin.Extensions.Json; -using MediaBrowser.Common; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.IO; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.LiveTv.EmbyTV -{ - public class EncodedRecorder : IRecorder, IDisposable - { - private readonly ILogger _logger; - private readonly IMediaEncoder _mediaEncoder; - private readonly IServerApplicationPaths _appPaths; - private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); - private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - private bool _hasExited; - private Stream _logFileStream; - private string _targetPath; - private Process _process; - private bool _disposed = false; - - public EncodedRecorder( - ILogger logger, - IMediaEncoder mediaEncoder, - IServerApplicationPaths appPaths, - IServerConfigurationManager serverConfigurationManager) - { - _logger = logger; - _mediaEncoder = mediaEncoder; - _appPaths = appPaths; - _serverConfigurationManager = serverConfigurationManager; - } - - private static bool CopySubtitles => false; - - public string GetOutputPath(MediaSourceInfo mediaSource, string targetFile) - { - return Path.ChangeExtension(targetFile, ".ts"); - } - - public async Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) - { - // The media source is infinite so we need to handle stopping ourselves - using var durationToken = new CancellationTokenSource(duration); - using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); - - await RecordFromFile(mediaSource, mediaSource.Path, targetFile, onStarted, cancellationTokenSource.Token).ConfigureAwait(false); - - _logger.LogInformation("Recording completed to file {Path}", targetFile); - } - - private async Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, Action onStarted, CancellationToken cancellationToken) - { - _targetPath = targetFile; - Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); - - var processStartInfo = new ProcessStartInfo - { - CreateNoWindow = true, - UseShellExecute = false, - - RedirectStandardError = true, - RedirectStandardInput = true, - - FileName = _mediaEncoder.EncoderPath, - Arguments = GetCommandLineArgs(mediaSource, inputFile, targetFile), - - WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false - }; - - _logger.LogInformation("{Filename} {Arguments}", processStartInfo.FileName, processStartInfo.Arguments); - - var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "record-transcode-" + Guid.NewGuid() + ".txt"); - Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); - - // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. - _logFileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); - - await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false); - await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + processStartInfo.FileName + " " + processStartInfo.Arguments + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false); - - _process = new Process - { - StartInfo = processStartInfo, - EnableRaisingEvents = true - }; - _process.Exited += (_, _) => OnFfMpegProcessExited(_process); - - _process.Start(); - - cancellationToken.Register(Stop); - - onStarted(); - - // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback - _ = StartStreamingLog(_process.StandardError.BaseStream, _logFileStream); - - _logger.LogInformation("ffmpeg recording process started for {Path}", _targetPath); - - // Block until ffmpeg exits - await _taskCompletionSource.Task.ConfigureAwait(false); - } - - private string GetCommandLineArgs(MediaSourceInfo mediaSource, string inputTempFile, string targetFile) - { - string videoArgs; - if (EncodeVideo(mediaSource)) - { - const int MaxBitrate = 25000000; - videoArgs = string.Format( - CultureInfo.InvariantCulture, - "-codec:v:0 libx264 -force_key_frames \"expr:gte(t,n_forced*5)\" {0} -pix_fmt yuv420p -preset superfast -crf 23 -b:v {1} -maxrate {1} -bufsize ({1}*2) -vsync -1 -profile:v high -level 41", - GetOutputSizeParam(), - MaxBitrate); - } - else - { - videoArgs = "-codec:v:0 copy"; - } - - videoArgs += " -fflags +genpts"; - - var flags = new List<string>(); - if (mediaSource.IgnoreDts) - { - flags.Add("+igndts"); - } - - if (mediaSource.IgnoreIndex) - { - flags.Add("+ignidx"); - } - - if (mediaSource.GenPtsInput) - { - flags.Add("+genpts"); - } - - var inputModifier = "-async 1 -vsync -1"; - - if (flags.Count > 0) - { - inputModifier += " -fflags " + string.Join(string.Empty, flags); - } - - if (mediaSource.ReadAtNativeFramerate) - { - inputModifier += " -re"; - } - - if (mediaSource.RequiresLooping) - { - inputModifier += " -stream_loop -1 -reconnect_at_eof 1 -reconnect_streamed 1 -reconnect_delay_max 2"; - } - - var analyzeDurationSeconds = 5; - var analyzeDuration = " -analyzeduration " + - (analyzeDurationSeconds * 1000000).ToString(CultureInfo.InvariantCulture); - inputModifier += analyzeDuration; - - var subtitleArgs = CopySubtitles ? " -codec:s copy" : " -sn"; - - // var outputParam = string.Equals(Path.GetExtension(targetFile), ".mp4", StringComparison.OrdinalIgnoreCase) ? - // " -f mp4 -movflags frag_keyframe+empty_moov" : - // string.Empty; - - var outputParam = string.Empty; - - var threads = EncodingHelper.GetNumberOfThreads(null, _serverConfigurationManager.GetEncodingOptions(), null); - var commandLineArgs = string.Format( - CultureInfo.InvariantCulture, - "-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"", - inputTempFile, - targetFile.Replace("\"", "\\\"", StringComparison.Ordinal), // Escape quotes in filename - videoArgs, - GetAudioArgs(mediaSource), - subtitleArgs, - outputParam, - threads); - - return inputModifier + " " + commandLineArgs; - } - - private static string GetAudioArgs(MediaSourceInfo mediaSource) - { - return "-codec:a:0 copy"; - - // var audioChannels = 2; - // var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); - // if (audioStream is not null) - // { - // audioChannels = audioStream.Channels ?? audioChannels; - // } - // return "-codec:a:0 aac -strict experimental -ab 320000"; - } - - private static bool EncodeVideo(MediaSourceInfo mediaSource) - { - return false; - } - - protected string GetOutputSizeParam() - => "-vf \"yadif=0:-1:0\""; - - private void Stop() - { - if (!_hasExited) - { - try - { - _logger.LogInformation("Stopping ffmpeg recording process for {Path}", _targetPath); - - _process.StandardInput.WriteLine("q"); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error stopping recording transcoding job for {Path}", _targetPath); - } - - if (_hasExited) - { - return; - } - - try - { - _logger.LogInformation("Calling recording process.WaitForExit for {Path}", _targetPath); - - if (_process.WaitForExit(10000)) - { - return; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error waiting for recording process to exit for {Path}", _targetPath); - } - - if (_hasExited) - { - return; - } - - try - { - _logger.LogInformation("Killing ffmpeg recording process for {Path}", _targetPath); - - _process.Kill(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error killing recording transcoding job for {Path}", _targetPath); - } - } - } - - /// <summary> - /// Processes the exited. - /// </summary> - private void OnFfMpegProcessExited(Process process) - { - using (process) - { - _hasExited = true; - - _logFileStream?.Dispose(); - _logFileStream = null; - - var exitCode = process.ExitCode; - - _logger.LogInformation("FFMpeg recording exited with code {ExitCode} for {Path}", exitCode, _targetPath); - - if (exitCode == 0) - { - _taskCompletionSource.TrySetResult(true); - } - else - { - _taskCompletionSource.TrySetException( - new FfmpegException( - string.Format( - CultureInfo.InvariantCulture, - "Recording for {0} failed. Exit code {1}", - _targetPath, - exitCode))); - } - } - } - - private async Task StartStreamingLog(Stream source, Stream target) - { - try - { - using (var reader = new StreamReader(source)) - { - await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) - { - var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line); - - await target.WriteAsync(bytes.AsMemory()).ConfigureAwait(false); - await target.FlushAsync().ConfigureAwait(false); - } - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reading ffmpeg recording log"); - } - } - - /// <inheritdoc /> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Releases unmanaged and optionally managed resources. - /// </summary> - /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - _logFileStream?.Dispose(); - _process?.Dispose(); - } - - _logFileStream = null; - _process = null; - - _disposed = true; - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs deleted file mode 100644 index a2ec2df37..000000000 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs +++ /dev/null @@ -1,21 +0,0 @@ -#pragma warning disable CS1591 - -using System.Threading.Tasks; -using MediaBrowser.Controller.Plugins; - -namespace Emby.Server.Implementations.LiveTv.EmbyTV -{ - public sealed class EntryPoint : IServerEntryPoint - { - /// <inheritdoc /> - public Task RunAsync() - { - return EmbyTV.Current.Start(); - } - - /// <inheritdoc /> - public void Dispose() - { - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs deleted file mode 100644 index 20a8213a7..000000000 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs +++ /dev/null @@ -1,54 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using MediaBrowser.Controller.LiveTv; - -namespace Emby.Server.Implementations.LiveTv.EmbyTV -{ - internal class EpgChannelData - { - private readonly Dictionary<string, ChannelInfo> _channelsById; - - private readonly Dictionary<string, ChannelInfo> _channelsByNumber; - - private readonly Dictionary<string, ChannelInfo> _channelsByName; - - public EpgChannelData(IEnumerable<ChannelInfo> channels) - { - _channelsById = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); - _channelsByNumber = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); - _channelsByName = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase); - - foreach (var channel in channels) - { - _channelsById[channel.Id] = channel; - - if (!string.IsNullOrEmpty(channel.Number)) - { - _channelsByNumber[channel.Number] = channel; - } - - var normalizedName = NormalizeName(channel.Name ?? string.Empty); - if (!string.IsNullOrWhiteSpace(normalizedName)) - { - _channelsByName[normalizedName] = channel; - } - } - } - - public ChannelInfo? GetChannelById(string id) - => _channelsById.GetValueOrDefault(id); - - public ChannelInfo? GetChannelByNumber(string number) - => _channelsByNumber.GetValueOrDefault(number); - - public ChannelInfo? GetChannelByName(string name) - => _channelsByName.GetValueOrDefault(name); - - public static string NormalizeName(string value) - { - return value.Replace(" ", string.Empty, StringComparison.Ordinal).Replace("-", string.Empty, StringComparison.Ordinal); - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs deleted file mode 100644 index 7705132da..000000000 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs +++ /dev/null @@ -1,27 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Dto; - -namespace Emby.Server.Implementations.LiveTv.EmbyTV -{ - public interface IRecorder - { - /// <summary> - /// Records the specified media source. - /// </summary> - /// <param name="directStreamProvider">The direct stream provider, or <c>null</c>.</param> - /// <param name="mediaSource">The media source.</param> - /// <param name="targetFile">The target file.</param> - /// <param name="duration">The duration to record.</param> - /// <param name="onStarted">An action to perform when recording starts.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>A <see cref="Task"/> that represents the recording operation.</returns> - Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken); - - string GetOutputPath(MediaSourceInfo mediaSource, string targetFile); - } -} diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs deleted file mode 100644 index d5a6feb47..000000000 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs +++ /dev/null @@ -1,163 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Text.Json; -using Jellyfin.Extensions.Json; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.LiveTv.EmbyTV -{ - public class ItemDataProvider<T> - where T : class - { - private readonly string _dataPath; - private readonly object _fileDataLock = new object(); - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - private T[]? _items; - - public ItemDataProvider( - ILogger logger, - string dataPath, - Func<T, T, bool> equalityComparer) - { - Logger = logger; - _dataPath = dataPath; - EqualityComparer = equalityComparer; - } - - protected ILogger Logger { get; } - - protected Func<T, T, bool> EqualityComparer { get; } - - [MemberNotNull(nameof(_items))] - private void EnsureLoaded() - { - if (_items is not null) - { - return; - } - - if (File.Exists(_dataPath)) - { - Logger.LogInformation("Loading live tv data from {Path}", _dataPath); - - try - { - var bytes = File.ReadAllBytes(_dataPath); - _items = JsonSerializer.Deserialize<T[]>(bytes, _jsonOptions); - if (_items is null) - { - Logger.LogError("Error deserializing {Path}, data was null", _dataPath); - _items = Array.Empty<T>(); - } - - return; - } - catch (JsonException ex) - { - Logger.LogError(ex, "Error deserializing {Path}", _dataPath); - } - } - - _items = Array.Empty<T>(); - } - - private void SaveList() - { - Directory.CreateDirectory(Path.GetDirectoryName(_dataPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(_dataPath))); - var jsonString = JsonSerializer.Serialize(_items, _jsonOptions); - File.WriteAllText(_dataPath, jsonString); - } - - public IReadOnlyList<T> GetAll() - { - lock (_fileDataLock) - { - EnsureLoaded(); - return (T[])_items.Clone(); - } - } - - public virtual void Update(T item) - { - ArgumentNullException.ThrowIfNull(item); - - lock (_fileDataLock) - { - EnsureLoaded(); - - var index = Array.FindIndex(_items, i => EqualityComparer(i, item)); - if (index == -1) - { - throw new ArgumentException("item not found"); - } - - _items[index] = item; - - SaveList(); - } - } - - public virtual void Add(T item) - { - ArgumentNullException.ThrowIfNull(item); - - lock (_fileDataLock) - { - EnsureLoaded(); - - if (_items.Any(i => EqualityComparer(i, item))) - { - throw new ArgumentException("item already exists", nameof(item)); - } - - int oldLen = _items.Length; - var newList = new T[oldLen + 1]; - _items.CopyTo(newList, 0); - newList[oldLen] = item; - _items = newList; - - SaveList(); - } - } - - public virtual void AddOrUpdate(T item) - { - lock (_fileDataLock) - { - EnsureLoaded(); - - int index = Array.FindIndex(_items, i => EqualityComparer(i, item)); - if (index == -1) - { - int oldLen = _items.Length; - var newList = new T[oldLen + 1]; - _items.CopyTo(newList, 0); - newList[oldLen] = item; - _items = newList; - } - else - { - _items[index] = item; - } - - SaveList(); - } - } - - public virtual void Delete(T item) - { - lock (_fileDataLock) - { - EnsureLoaded(); - _items = _items.Where(i => !EqualityComparer(i, item)).ToArray(); - - SaveList(); - } - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs deleted file mode 100644 index 83f5e8413..000000000 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.Configuration; - -namespace Emby.Server.Implementations.LiveTv.EmbyTV -{ - /// <summary> - /// Class containing extension methods for working with the nfo configuration. - /// </summary> - public static class NfoConfigurationExtensions - { - /// <summary> - /// Gets the nfo configuration. - /// </summary> - /// <param name="configurationManager">The configuration manager.</param> - /// <returns>The nfo configuration.</returns> - public static XbmcMetadataOptions GetNfoConfiguration(this IConfigurationManager configurationManager) - => configurationManager.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata"); - } -} diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs deleted file mode 100644 index 7bbeae866..000000000 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs +++ /dev/null @@ -1,83 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.Text; -using MediaBrowser.Controller.LiveTv; - -namespace Emby.Server.Implementations.LiveTv.EmbyTV -{ - internal static class RecordingHelper - { - public static DateTime GetStartTime(TimerInfo timer) - { - return timer.StartDate.AddSeconds(-timer.PrePaddingSeconds); - } - - public static string GetRecordingName(TimerInfo info) - { - var name = info.Name; - - if (info.IsProgramSeries) - { - var addHyphen = true; - - if (info.SeasonNumber.HasValue && info.EpisodeNumber.HasValue) - { - name += string.Format( - CultureInfo.InvariantCulture, - " 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) - { - if (info.OriginalAirDate.Value.Date.Equals(info.StartDate.Date)) - { - name += " " + GetDateString(info.StartDate); - } - else - { - name += " " + info.OriginalAirDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); - } - } - else - { - name += " " + GetDateString(info.StartDate); - } - - if (!string.IsNullOrWhiteSpace(info.EpisodeTitle)) - { - var tmpName = name; - if (addHyphen) - { - tmpName += " -"; - } - - tmpName += " " + info.EpisodeTitle; - // Since the filename will be used with file ext. (.mp4, .ts, etc) - if (Encoding.UTF8.GetByteCount(tmpName) < 250) - { - name = tmpName; - } - } - } - else if (info.IsMovie && info.ProductionYear is not null) - { - name += " (" + info.ProductionYear + ")"; - } - else - { - name += " " + GetDateString(info.StartDate); - } - - return name; - } - - private static string GetDateString(DateTime date) - { - return date.ToLocalTime().ToString("yyyy_MM_dd_HH_mm_ss", CultureInfo.InvariantCulture); - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs deleted file mode 100644 index bf28f3b67..000000000 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs +++ /dev/null @@ -1,24 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Controller.LiveTv; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.LiveTv.EmbyTV -{ - public class SeriesTimerManager : ItemDataProvider<SeriesTimerInfo> - { - public SeriesTimerManager(ILogger logger, string dataPath) - : base(logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)) - { - } - - /// <inheritdoc /> - public override void Add(SeriesTimerInfo item) - { - ArgumentException.ThrowIfNullOrEmpty(item.Id); - - base.Add(item); - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs deleted file mode 100644 index 9f8441fa4..000000000 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ /dev/null @@ -1,181 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Concurrent; -using System.Globalization; -using System.Linq; -using System.Threading; -using Jellyfin.Data.Events; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.LiveTv; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.LiveTv.EmbyTV -{ - public class TimerManager : ItemDataProvider<TimerInfo> - { - private readonly ConcurrentDictionary<string, Timer> _timers = new ConcurrentDictionary<string, Timer>(StringComparer.OrdinalIgnoreCase); - - public TimerManager(ILogger logger, string dataPath) - : base(logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)) - { - } - - public event EventHandler<GenericEventArgs<TimerInfo>>? TimerFired; - - public void RestartTimers() - { - StopTimers(); - - foreach (var item in GetAll()) - { - AddOrUpdateSystemTimer(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); - AddOrUpdateSystemTimer(item); - } - - public void AddOrUpdate(TimerInfo item, bool resetTimer) - { - if (resetTimer) - { - AddOrUpdate(item); - return; - } - - base.AddOrUpdate(item); - } - - public override void AddOrUpdate(TimerInfo item) - { - base.AddOrUpdate(item); - AddOrUpdateSystemTimer(item); - } - - public override void Add(TimerInfo item) - { - ArgumentException.ThrowIfNullOrEmpty(item.Id); - - base.Add(item); - AddOrUpdateSystemTimer(item); - } - - private static bool ShouldStartTimer(TimerInfo item) - { - if (item.Status == RecordingStatus.Completed - || item.Status == RecordingStatus.Cancelled) - { - return false; - } - - return true; - } - - private void AddOrUpdateSystemTimer(TimerInfo item) - { - StopTimer(item); - - if (!ShouldStartTimer(item)) - { - return; - } - - var startDate = RecordingHelper.GetStartTime(item); - var now = DateTime.UtcNow; - - if (startDate < now) - { - TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo>(item)); - return; - } - - var dueTime = startDate - now; - StartTimer(item, dueTime); - } - - private void StartTimer(TimerInfo item, TimeSpan dueTime) - { - var timer = new Timer(TimerCallback, item.Id, dueTime, TimeSpan.Zero); - - if (_timers.TryAdd(item.Id, timer)) - { - if (item.IsSeries) - { - Logger.LogInformation( - "Creating recording timer for {Id}, {Name} {SeasonNumber}x{EpisodeNumber:D2} on channel {ChannelId}. Timer will fire in {Minutes} minutes at {StartDate}", - item.Id, - item.Name, - item.SeasonNumber, - item.EpisodeNumber, - item.ChannelId, - dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture), - item.StartDate); - } - else - { - Logger.LogInformation( - "Creating recording timer for {Id}, {Name} on channel {ChannelId}. Timer will fire in {Minutes} minutes at {StartDate}", - item.Id, - item.Name, - item.ChannelId, - dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture), - item.StartDate); - } - } - else - { - timer.Dispose(); - Logger.LogWarning("Timer already exists for item {Id}", item.Id); - } - } - - private void StopTimer(TimerInfo item) - { - if (_timers.TryRemove(item.Id, out var timer)) - { - timer.Dispose(); - } - } - - private void TimerCallback(object? state) - { - var timerId = (string?)state ?? throw new ArgumentNullException(nameof(state)); - - var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase)); - if (timer is not null) - { - TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo>(timer)); - } - } - - public TimerInfo? GetTimer(string id) - { - return GetAll().FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase)); - } - - public TimerInfo? GetTimerByProgramId(string programId) - { - return GetAll().FirstOrDefault(r => string.Equals(r.ProgramId, programId, StringComparison.OrdinalIgnoreCase)); - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs deleted file mode 100644 index 6b0520ad0..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ /dev/null @@ -1,808 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Json; -using System.Net.Mime; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos; -using Jellyfin.Extensions; -using Jellyfin.Extensions.Json; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.LiveTv; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.LiveTv.Listings -{ - public class SchedulesDirect : IListingsProvider, IDisposable - { - private const string ApiUrl = "https://json.schedulesdirect.org/20141201"; - - private readonly ILogger<SchedulesDirect> _logger; - private readonly IHttpClientFactory _httpClientFactory; - private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1); - - private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>(); - private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; - private DateTime _lastErrorResponse; - private bool _disposed = false; - - public SchedulesDirect( - ILogger<SchedulesDirect> logger, - IHttpClientFactory httpClientFactory) - { - _logger = logger; - _httpClientFactory = httpClientFactory; - } - - /// <inheritdoc /> - public string Name => "Schedules Direct"; - - /// <inheritdoc /> - public string Type => nameof(SchedulesDirect); - - private static List<string> GetScheduleRequestDates(DateTime startDateUtc, DateTime endDateUtc) - { - var dates = new List<string>(); - - var start = new[] { startDateUtc, startDateUtc.ToLocalTime() }.Min().Date; - var end = new[] { endDateUtc, endDateUtc.ToLocalTime() }.Max().Date; - - while (start <= end) - { - dates.Add(start.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); - start = start.AddDays(1); - } - - return dates; - } - - public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) - { - ArgumentException.ThrowIfNullOrEmpty(channelId); - - // Normalize incoming input - channelId = channelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I'); - - var token = await GetToken(info, cancellationToken).ConfigureAwait(false); - - if (string.IsNullOrEmpty(token)) - { - _logger.LogWarning("SchedulesDirect token is empty, returning empty program list"); - - return Enumerable.Empty<ProgramInfo>(); - } - - var dates = GetScheduleRequestDates(startDateUtc, endDateUtc); - - _logger.LogInformation("Channel Station ID is: {ChannelID}", channelId); - var requestList = new List<RequestScheduleForChannelDto>() - { - new RequestScheduleForChannelDto() - { - StationId = channelId, - Date = dates - } - }; - - _logger.LogDebug("Request string for schedules is: {@RequestString}", requestList); - - using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/schedules"); - options.Content = JsonContent.Create(requestList, options: _jsonOptions); - options.Headers.TryAddWithoutValidation("token", token); - using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false); - var dailySchedules = await response.Content.ReadFromJsonAsync<IReadOnlyList<DayDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false); - if (dailySchedules is null) - { - return Array.Empty<ProgramInfo>(); - } - - _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId); - - using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs"); - programRequestOptions.Headers.TryAddWithoutValidation("token", token); - - var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct(); - programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions); - - using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false); - var programDetails = await innerResponse.Content.ReadFromJsonAsync<IReadOnlyList<ProgramDetailsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false); - if (programDetails is null) - { - return Array.Empty<ProgramInfo>(); - } - - var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y); - - var programIdsWithImages = programDetails - .Where(p => p.HasImageArtwork).Select(p => p.ProgramId) - .ToList(); - - var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false); - - var programsInfo = new List<ProgramInfo>(); - foreach (ProgramDto schedule in dailySchedules.SelectMany(d => d.Programs)) - { - // _logger.LogDebug("Proccesing Schedule for statio ID " + stationID + - // " which corresponds to channel " + channelNumber + " and program id " + - // schedule.ProgramId + " which says it has images? " + - // programDict[schedule.ProgramId].hasImageArtwork); - - if (string.IsNullOrEmpty(schedule.ProgramId)) - { - continue; - } - - if (images is not null) - { - var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]); - if (imageIndex > -1) - { - var programEntry = programDict[schedule.ProgramId]; - - var allImages = images[imageIndex].Data; - var imagesWithText = allImages.Where(i => string.Equals(i.Text, "yes", StringComparison.OrdinalIgnoreCase)).ToList(); - var imagesWithoutText = allImages.Where(i => string.Equals(i.Text, "no", StringComparison.OrdinalIgnoreCase)).ToList(); - - const double DesiredAspect = 2.0 / 3; - - programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect, token) ?? - GetProgramImage(ApiUrl, allImages, DesiredAspect, token); - - const double WideAspect = 16.0 / 9; - - programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect, token); - - // Don't supply the same image twice - if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal)) - { - programEntry.ThumbImage = null; - } - - programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect, token); - - // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ?? - // GetProgramImage(ApiUrl, data, "Banner-L1", false) ?? - // GetProgramImage(ApiUrl, data, "Banner-LO", false) ?? - // GetProgramImage(ApiUrl, data, "Banner-LOT", false); - } - } - - programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.ProgramId])); - } - - return programsInfo; - } - - private static int GetSizeOrder(ImageDataDto image) - { - if (int.TryParse(image.Height, out int value)) - { - return value; - } - - return 0; - } - - private static string GetChannelNumber(MapDto map) - { - var channelNumber = map.LogicalChannelNumber; - - if (string.IsNullOrWhiteSpace(channelNumber)) - { - channelNumber = map.Channel; - } - - if (string.IsNullOrWhiteSpace(channelNumber)) - { - channelNumber = map.AtscMajor + "." + map.AtscMinor; - } - - return channelNumber.TrimStart('0'); - } - - private static bool IsMovie(ProgramDetailsDto programInfo) - { - return string.Equals(programInfo.EntityType, "movie", StringComparison.OrdinalIgnoreCase); - } - - private ProgramInfo GetProgram(string channelId, ProgramDto programInfo, ProgramDetailsDto details) - { - if (programInfo.AirDateTime is null) - { - return null; - } - - var startAt = programInfo.AirDateTime.Value; - var endAt = startAt.AddSeconds(programInfo.Duration); - var audioType = ProgramAudio.Stereo; - - var programId = programInfo.ProgramId ?? string.Empty; - - string newID = programId + "T" + startAt.Ticks + "C" + channelId; - - if (programInfo.AudioProperties.Count != 0) - { - if (programInfo.AudioProperties.Contains("atmos", StringComparison.OrdinalIgnoreCase)) - { - audioType = ProgramAudio.Atmos; - } - else if (programInfo.AudioProperties.Contains("dd 5.1", StringComparison.OrdinalIgnoreCase)) - { - audioType = ProgramAudio.DolbyDigital; - } - else if (programInfo.AudioProperties.Contains("dd", StringComparison.OrdinalIgnoreCase)) - { - audioType = ProgramAudio.DolbyDigital; - } - else if (programInfo.AudioProperties.Contains("stereo", StringComparison.OrdinalIgnoreCase)) - { - audioType = ProgramAudio.Stereo; - } - else - { - audioType = ProgramAudio.Mono; - } - } - - string episodeTitle = null; - if (details.EpisodeTitle150 is not null) - { - episodeTitle = details.EpisodeTitle150; - } - - var info = new ProgramInfo - { - ChannelId = channelId, - Id = newID, - StartDate = startAt, - EndDate = endAt, - Name = details.Titles[0].Title120 ?? "Unknown", - OfficialRating = null, - CommunityRating = null, - EpisodeTitle = episodeTitle, - Audio = audioType, - // IsNew = programInfo.@new ?? false, - IsRepeat = programInfo.New is null, - IsSeries = string.Equals(details.EntityType, "episode", StringComparison.OrdinalIgnoreCase), - ImageUrl = details.PrimaryImage, - ThumbImageUrl = details.ThumbImage, - IsKids = string.Equals(details.Audience, "children", StringComparison.OrdinalIgnoreCase), - IsSports = string.Equals(details.EntityType, "sports", StringComparison.OrdinalIgnoreCase), - IsMovie = IsMovie(details), - Etag = programInfo.Md5, - IsLive = string.Equals(programInfo.LiveTapeDelay, "live", StringComparison.OrdinalIgnoreCase), - IsPremiere = programInfo.Premiere || (programInfo.IsPremiereOrFinale ?? string.Empty).IndexOf("premiere", StringComparison.OrdinalIgnoreCase) != -1 - }; - - var showId = programId; - - if (!info.IsSeries) - { - // It's also a series if it starts with SH - info.IsSeries = showId.StartsWith("SH", StringComparison.OrdinalIgnoreCase) && showId.Length >= 14; - } - - // According to SchedulesDirect, these are generic, unidentified episodes - // SH005316560000 - var hasUniqueShowId = !showId.StartsWith("SH", StringComparison.OrdinalIgnoreCase) || - !showId.EndsWith("0000", StringComparison.OrdinalIgnoreCase); - - if (!hasUniqueShowId) - { - showId = newID; - } - - info.ShowId = showId; - - if (programInfo.VideoProperties is not null) - { - info.IsHD = programInfo.VideoProperties.Contains("hdtv", StringComparison.OrdinalIgnoreCase); - info.Is3D = programInfo.VideoProperties.Contains("3d", StringComparison.OrdinalIgnoreCase); - } - - if (details.ContentRating is not null && details.ContentRating.Count > 0) - { - info.OfficialRating = details.ContentRating[0].Code.Replace("TV", "TV-", StringComparison.Ordinal) - .Replace("--", "-", StringComparison.Ordinal); - - var invalid = new[] { "N/A", "Approved", "Not Rated", "Passed" }; - if (invalid.Contains(info.OfficialRating, StringComparison.OrdinalIgnoreCase)) - { - info.OfficialRating = null; - } - } - - if (details.Descriptions is not null) - { - if (details.Descriptions.Description1000 is not null && details.Descriptions.Description1000.Count > 0) - { - info.Overview = details.Descriptions.Description1000[0].Description; - } - else if (details.Descriptions.Description100 is not null && details.Descriptions.Description100.Count > 0) - { - info.Overview = details.Descriptions.Description100[0].Description; - } - } - - if (info.IsSeries) - { - info.SeriesId = programId.Substring(0, 10); - - info.SeriesProviderIds[MetadataProvider.Zap2It.ToString()] = info.SeriesId; - - if (details.Metadata is not null) - { - foreach (var metadataProgram in details.Metadata) - { - var gracenote = metadataProgram.Gracenote; - if (gracenote is not null) - { - info.SeasonNumber = gracenote.Season; - - if (gracenote.Episode > 0) - { - info.EpisodeNumber = gracenote.Episode; - } - - break; - } - } - } - } - - if (details.OriginalAirDate is not null) - { - info.OriginalAirDate = details.OriginalAirDate; - info.ProductionYear = info.OriginalAirDate.Value.Year; - } - - if (details.Movie is not null) - { - if (!string.IsNullOrEmpty(details.Movie.Year) - && int.TryParse(details.Movie.Year, out int year)) - { - info.ProductionYear = year; - } - } - - if (details.Genres is not null) - { - info.Genres = details.Genres.Where(g => !string.IsNullOrWhiteSpace(g)).ToList(); - info.IsNews = details.Genres.Contains("news", StringComparison.OrdinalIgnoreCase); - - if (info.Genres.Contains("children", StringComparison.OrdinalIgnoreCase)) - { - info.IsKids = true; - } - } - - return info; - } - - private static string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, double desiredAspect, string token) - { - var match = images - .OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i))) - .ThenByDescending(i => GetSizeOrder(i)) - .FirstOrDefault(); - - if (match is null) - { - return null; - } - - var uri = match.Uri; - - if (string.IsNullOrWhiteSpace(uri)) - { - return null; - } - - if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1) - { - return uri; - } - - return apiUrl + "/image/" + uri + "?token=" + token; - } - - private static double GetAspectRatio(ImageDataDto i) - { - int width = 0; - int height = 0; - - if (!string.IsNullOrWhiteSpace(i.Width)) - { - _ = int.TryParse(i.Width, out width); - } - - if (!string.IsNullOrWhiteSpace(i.Height)) - { - _ = int.TryParse(i.Height, out height); - } - - if (height == 0 || width == 0) - { - return 0; - } - - double result = width; - result /= height; - return result; - } - - private async Task<IReadOnlyList<ShowImagesDto>> GetImageForPrograms( - ListingsProviderInfo info, - IReadOnlyList<string> programIds, - CancellationToken cancellationToken) - { - var token = await GetToken(info, cancellationToken).ConfigureAwait(false); - - if (programIds.Count == 0) - { - return Array.Empty<ShowImagesDto>(); - } - - StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13)); - foreach (var i in programIds) - { - str.Append('"') - .Append(i[..10]) - .Append("\","); - } - - // Remove last , - str.Length--; - str.Append(']'); - - using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs") - { - Content = new StringContent(str.ToString(), Encoding.UTF8, MediaTypeNames.Application.Json) - }; - message.Headers.TryAddWithoutValidation("token", token); - - try - { - using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false); - return await innerResponse2.Content.ReadFromJsonAsync<IReadOnlyList<ShowImagesDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting image info from schedules direct"); - - return Array.Empty<ShowImagesDto>(); - } - } - - public async Task<List<NameIdPair>> GetHeadends(ListingsProviderInfo info, string country, string location, CancellationToken cancellationToken) - { - var token = await GetToken(info, cancellationToken).ConfigureAwait(false); - - var lineups = new List<NameIdPair>(); - - if (string.IsNullOrWhiteSpace(token)) - { - return lineups; - } - - using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/headends?country=" + country + "&postalcode=" + location); - options.Headers.TryAddWithoutValidation("token", token); - - try - { - using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false); - var root = await httpResponse.Content.ReadFromJsonAsync<IReadOnlyList<HeadendsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false); - if (root is not null) - { - foreach (HeadendsDto headend in root) - { - foreach (LineupDto lineup in headend.Lineups) - { - lineups.Add(new NameIdPair - { - Name = string.IsNullOrWhiteSpace(lineup.Name) ? lineup.Lineup : lineup.Name, - Id = lineup.Uri?[18..] - }); - } - } - } - else - { - _logger.LogInformation("No lineups available"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting headends"); - } - - return lineups; - } - - private async Task<string> GetToken(ListingsProviderInfo info, CancellationToken cancellationToken) - { - var username = info.Username; - - // Reset the token if there's no username - if (string.IsNullOrWhiteSpace(username)) - { - return null; - } - - var password = info.Password; - if (string.IsNullOrEmpty(password)) - { - return null; - } - - // Avoid hammering SD - if ((DateTime.UtcNow - _lastErrorResponse).TotalMinutes < 1) - { - return null; - } - - if (!_tokens.TryGetValue(username, out NameValuePair savedToken)) - { - savedToken = new NameValuePair(); - _tokens.TryAdd(username, savedToken); - } - - if (!string.IsNullOrEmpty(savedToken.Name) - && long.TryParse(savedToken.Value, CultureInfo.InvariantCulture, out long ticks)) - { - // If it's under 24 hours old we can still use it - if (DateTime.UtcNow.Ticks - ticks < TimeSpan.FromHours(20).Ticks) - { - return savedToken.Name; - } - } - - await _tokenSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - try - { - var result = await GetTokenInternal(username, password, cancellationToken).ConfigureAwait(false); - savedToken.Name = result; - savedToken.Value = DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture); - return result; - } - catch (HttpRequestException ex) - { - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest) - { - _tokens.Clear(); - _lastErrorResponse = DateTime.UtcNow; - } - - throw; - } - finally - { - _tokenSemaphore.Release(); - } - } - - private async Task<HttpResponseMessage> Send( - HttpRequestMessage options, - bool enableRetry, - ListingsProviderInfo providerInfo, - CancellationToken cancellationToken, - HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) - { - var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false); - if (response.IsSuccessStatusCode) - { - return response; - } - - // Response is automatically disposed in the calling function, - // so dispose manually if not returning. - response.Dispose(); - if (!enableRetry || (int)response.StatusCode >= 500) - { - throw new HttpRequestException( - string.Format(CultureInfo.InvariantCulture, "Request failed: {0}", response.ReasonPhrase), - null, - response.StatusCode); - } - - _tokens.Clear(); - options.Headers.TryAddWithoutValidation("token", await GetToken(providerInfo, cancellationToken).ConfigureAwait(false)); - return await Send(options, false, providerInfo, cancellationToken).ConfigureAwait(false); - } - - private async Task<string> GetTokenInternal( - string username, - string password, - CancellationToken cancellationToken) - { - using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token"); -#pragma warning disable CA5350 // SchedulesDirect is always SHA1. - var hashedPasswordBytes = SHA1.HashData(Encoding.ASCII.GetBytes(password)); -#pragma warning restore CA5350 - // TODO: remove ToLower when Convert.ToHexString supports lowercase - // Schedules Direct requires the hex to be lowercase - string hashedPassword = Convert.ToHexString(hashedPasswordBytes).ToLowerInvariant(); - options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json); - - using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - var root = await response.Content.ReadFromJsonAsync<TokenDto>(_jsonOptions, cancellationToken).ConfigureAwait(false); - if (string.Equals(root?.Message, "OK", StringComparison.Ordinal)) - { - _logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token); - return root.Token; - } - - throw new AuthenticationException("Could not authenticate with Schedules Direct Error: " + root.Message); - } - - private async Task AddLineupToAccount(ListingsProviderInfo info, CancellationToken cancellationToken) - { - var token = await GetToken(info, cancellationToken).ConfigureAwait(false); - - ArgumentException.ThrowIfNullOrEmpty(token); - ArgumentException.ThrowIfNullOrEmpty(info.ListingsId); - - _logger.LogInformation("Adding new LineUp "); - - using var options = new HttpRequestMessage(HttpMethod.Put, ApiUrl + "/lineups/" + info.ListingsId); - options.Headers.TryAddWithoutValidation("token", token); - using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - } - - private async Task<bool> HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken) - { - ArgumentException.ThrowIfNullOrEmpty(info.ListingsId); - - var token = await GetToken(info, cancellationToken).ConfigureAwait(false); - - ArgumentException.ThrowIfNullOrEmpty(token); - - _logger.LogInformation("Headends on account "); - - using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups"); - options.Headers.TryAddWithoutValidation("token", token); - - try - { - using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false); - httpResponse.EnsureSuccessStatusCode(); - var root = await httpResponse.Content.ReadFromJsonAsync<LineupsDto>(_jsonOptions, cancellationToken).ConfigureAwait(false); - return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false; - } - catch (HttpRequestException ex) - { - // SchedulesDirect returns 400 if no lineups are configured. - if (ex.StatusCode is HttpStatusCode.BadRequest) - { - return false; - } - - throw; - } - } - - public async Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings) - { - if (validateLogin) - { - ArgumentException.ThrowIfNullOrEmpty(info.Username); - ArgumentException.ThrowIfNullOrEmpty(info.Password); - } - - if (validateListings) - { - ArgumentException.ThrowIfNullOrEmpty(info.ListingsId); - - var hasLineup = await HasLineup(info, CancellationToken.None).ConfigureAwait(false); - - if (!hasLineup) - { - await AddLineupToAccount(info, CancellationToken.None).ConfigureAwait(false); - } - } - } - - public Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location) - { - return GetHeadends(info, country, location, CancellationToken.None); - } - - public async Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken) - { - var listingsId = info.ListingsId; - ArgumentException.ThrowIfNullOrEmpty(listingsId); - - var token = await GetToken(info, cancellationToken).ConfigureAwait(false); - - ArgumentException.ThrowIfNullOrEmpty(token); - - using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups/" + listingsId); - options.Headers.TryAddWithoutValidation("token", token); - - using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false); - var root = await httpResponse.Content.ReadFromJsonAsync<ChannelDto>(_jsonOptions, cancellationToken).ConfigureAwait(false); - if (root is null) - { - return new List<ChannelInfo>(); - } - - _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.Map.Count); - _logger.LogInformation("Mapping Stations to Channel"); - - var allStations = root.Stations; - - var map = root.Map; - var list = new List<ChannelInfo>(map.Count); - foreach (var channel in map) - { - var channelNumber = GetChannelNumber(channel); - - var stationIndex = allStations.FindIndex(item => string.Equals(item.StationId, channel.StationId, StringComparison.OrdinalIgnoreCase)); - var station = stationIndex == -1 - ? new StationDto { StationId = channel.StationId } - : allStations[stationIndex]; - - var channelInfo = new ChannelInfo - { - Id = station.StationId, - CallSign = station.Callsign, - Number = channelNumber, - Name = string.IsNullOrWhiteSpace(station.Name) ? channelNumber : station.Name - }; - - if (station.Logo is not null) - { - channelInfo.ImageUrl = station.Logo.Url; - } - - list.Add(channelInfo); - } - - return list; - } - - /// <inheritdoc /> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Releases unmanaged and optionally managed resources. - /// </summary> - /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - _tokenSemaphore?.Dispose(); - } - - _disposed = true; - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs deleted file mode 100644 index 95ac996e0..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Broadcaster dto. - /// </summary> - public class BroadcasterDto - { - /// <summary> - /// Gets or sets the city. - /// </summary> - [JsonPropertyName("city")] - public string? City { get; set; } - - /// <summary> - /// Gets or sets the state. - /// </summary> - [JsonPropertyName("state")] - public string? State { get; set; } - - /// <summary> - /// Gets or sets the postal code. - /// </summary> - [JsonPropertyName("postalCode")] - public string? Postalcode { get; set; } - - /// <summary> - /// Gets or sets the country. - /// </summary> - [JsonPropertyName("country")] - public string? Country { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs deleted file mode 100644 index f6251b9ad..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Caption dto. - /// </summary> - public class CaptionDto - { - /// <summary> - /// Gets or sets the content. - /// </summary> - [JsonPropertyName("content")] - public string? Content { get; set; } - - /// <summary> - /// Gets or sets the lang. - /// </summary> - [JsonPropertyName("lang")] - public string? Lang { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs deleted file mode 100644 index 0b7a2c63a..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Cast dto. - /// </summary> - public class CastDto - { - /// <summary> - /// Gets or sets the billing order. - /// </summary> - [JsonPropertyName("billingOrder")] - public string? BillingOrder { get; set; } - - /// <summary> - /// Gets or sets the role. - /// </summary> - [JsonPropertyName("role")] - public string? Role { get; set; } - - /// <summary> - /// Gets or sets the name id. - /// </summary> - [JsonPropertyName("nameId")] - public string? NameId { get; set; } - - /// <summary> - /// Gets or sets the person id. - /// </summary> - [JsonPropertyName("personId")] - public string? PersonId { get; set; } - - /// <summary> - /// Gets or sets the name. - /// </summary> - [JsonPropertyName("name")] - public string? Name { get; set; } - - /// <summary> - /// Gets or sets the character name. - /// </summary> - [JsonPropertyName("characterName")] - public string? CharacterName { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs deleted file mode 100644 index 87c327ed8..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Channel dto. - /// </summary> - public class ChannelDto - { - /// <summary> - /// Gets or sets the list of maps. - /// </summary> - [JsonPropertyName("map")] - public IReadOnlyList<MapDto> Map { get; set; } = Array.Empty<MapDto>(); - - /// <summary> - /// Gets or sets the list of stations. - /// </summary> - [JsonPropertyName("stations")] - public IReadOnlyList<StationDto> Stations { get; set; } = Array.Empty<StationDto>(); - - /// <summary> - /// Gets or sets the metadata. - /// </summary> - [JsonPropertyName("metadata")] - public MetadataDto? Metadata { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs deleted file mode 100644 index c19cd2e48..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Content rating dto. - /// </summary> - public class ContentRatingDto - { - /// <summary> - /// Gets or sets the body. - /// </summary> - [JsonPropertyName("body")] - public string? Body { get; set; } - - /// <summary> - /// Gets or sets the code. - /// </summary> - [JsonPropertyName("code")] - public string? Code { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs deleted file mode 100644 index f00c9accd..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Crew dto. - /// </summary> - public class CrewDto - { - /// <summary> - /// Gets or sets the billing order. - /// </summary> - [JsonPropertyName("billingOrder")] - public string? BillingOrder { get; set; } - - /// <summary> - /// Gets or sets the role. - /// </summary> - [JsonPropertyName("role")] - public string? Role { get; set; } - - /// <summary> - /// Gets or sets the name id. - /// </summary> - [JsonPropertyName("nameId")] - public string? NameId { get; set; } - - /// <summary> - /// Gets or sets the person id. - /// </summary> - [JsonPropertyName("personId")] - public string? PersonId { get; set; } - - /// <summary> - /// Gets or sets the name. - /// </summary> - [JsonPropertyName("name")] - public string? Name { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs deleted file mode 100644 index 1a371965c..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Day dto. - /// </summary> - public class DayDto - { - /// <summary> - /// Gets or sets the station id. - /// </summary> - [JsonPropertyName("stationID")] - public string? StationId { get; set; } - - /// <summary> - /// Gets or sets the list of programs. - /// </summary> - [JsonPropertyName("programs")] - public IReadOnlyList<ProgramDto> Programs { get; set; } = Array.Empty<ProgramDto>(); - - /// <summary> - /// Gets or sets the metadata schedule. - /// </summary> - [JsonPropertyName("metadata")] - public MetadataScheduleDto? Metadata { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs deleted file mode 100644 index ca6ae7fb1..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Description 1_000 dto. - /// </summary> - public class Description1000Dto - { - /// <summary> - /// Gets or sets the description language. - /// </summary> - [JsonPropertyName("descriptionLanguage")] - public string? DescriptionLanguage { get; set; } - - /// <summary> - /// Gets or sets the description. - /// </summary> - [JsonPropertyName("description")] - public string? Description { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs deleted file mode 100644 index 1577219ed..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Description 100 dto. - /// </summary> - public class Description100Dto - { - /// <summary> - /// Gets or sets the description language. - /// </summary> - [JsonPropertyName("descriptionLanguage")] - public string? DescriptionLanguage { get; set; } - - /// <summary> - /// Gets or sets the description. - /// </summary> - [JsonPropertyName("description")] - public string? Description { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs deleted file mode 100644 index eaf4a340b..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Descriptions program dto. - /// </summary> - public class DescriptionsProgramDto - { - /// <summary> - /// Gets or sets the list of description 100. - /// </summary> - [JsonPropertyName("description100")] - public IReadOnlyList<Description100Dto> Description100 { get; set; } = Array.Empty<Description100Dto>(); - - /// <summary> - /// Gets or sets the list of description1000. - /// </summary> - [JsonPropertyName("description1000")] - public IReadOnlyList<Description1000Dto> Description1000 { get; set; } = Array.Empty<Description1000Dto>(); - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs deleted file mode 100644 index fbdfb1f71..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Event details dto. - /// </summary> - public class EventDetailsDto - { - /// <summary> - /// Gets or sets the sub type. - /// </summary> - [JsonPropertyName("subType")] - public string? SubType { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs deleted file mode 100644 index 6852d89d7..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Gracenote dto. - /// </summary> - public class GracenoteDto - { - /// <summary> - /// Gets or sets the season. - /// </summary> - [JsonPropertyName("season")] - public int Season { get; set; } - - /// <summary> - /// Gets or sets the episode. - /// </summary> - [JsonPropertyName("episode")] - public int Episode { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs deleted file mode 100644 index b9844562f..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Headends dto. - /// </summary> - public class HeadendsDto - { - /// <summary> - /// Gets or sets the headend. - /// </summary> - [JsonPropertyName("headend")] - public string? Headend { get; set; } - - /// <summary> - /// Gets or sets the transport. - /// </summary> - [JsonPropertyName("transport")] - public string? Transport { get; set; } - - /// <summary> - /// Gets or sets the location. - /// </summary> - [JsonPropertyName("location")] - public string? Location { get; set; } - - /// <summary> - /// Gets or sets the list of lineups. - /// </summary> - [JsonPropertyName("lineups")] - public IReadOnlyList<LineupDto> Lineups { get; set; } = Array.Empty<LineupDto>(); - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs deleted file mode 100644 index a1ae3ca6d..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Image data dto. - /// </summary> - public class ImageDataDto - { - /// <summary> - /// Gets or sets the width. - /// </summary> - [JsonPropertyName("width")] - public string? Width { get; set; } - - /// <summary> - /// Gets or sets the height. - /// </summary> - [JsonPropertyName("height")] - public string? Height { get; set; } - - /// <summary> - /// Gets or sets the uri. - /// </summary> - [JsonPropertyName("uri")] - public string? Uri { get; set; } - - /// <summary> - /// Gets or sets the size. - /// </summary> - [JsonPropertyName("size")] - public string? Size { get; set; } - - /// <summary> - /// Gets or sets the aspect. - /// </summary> - [JsonPropertyName("aspect")] - public string? Aspect { get; set; } - - /// <summary> - /// Gets or sets the category. - /// </summary> - [JsonPropertyName("category")] - public string? Category { get; set; } - - /// <summary> - /// Gets or sets the text. - /// </summary> - [JsonPropertyName("text")] - public string? Text { get; set; } - - /// <summary> - /// Gets or sets the primary. - /// </summary> - [JsonPropertyName("primary")] - public string? Primary { get; set; } - - /// <summary> - /// Gets or sets the tier. - /// </summary> - [JsonPropertyName("tier")] - public string? Tier { get; set; } - - /// <summary> - /// Gets or sets the caption. - /// </summary> - [JsonPropertyName("caption")] - public CaptionDto? Caption { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs deleted file mode 100644 index 3dc64e5d8..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// The lineup dto. - /// </summary> - public class LineupDto - { - /// <summary> - /// Gets or sets the linup. - /// </summary> - [JsonPropertyName("lineup")] - public string? Lineup { get; set; } - - /// <summary> - /// Gets or sets the lineup name. - /// </summary> - [JsonPropertyName("name")] - public string? Name { get; set; } - - /// <summary> - /// Gets or sets the transport. - /// </summary> - [JsonPropertyName("transport")] - public string? Transport { get; set; } - - /// <summary> - /// Gets or sets the location. - /// </summary> - [JsonPropertyName("location")] - public string? Location { get; set; } - - /// <summary> - /// Gets or sets the uri. - /// </summary> - [JsonPropertyName("uri")] - public string? Uri { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this lineup was deleted. - /// </summary> - [JsonPropertyName("isDeleted")] - public bool? IsDeleted { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs deleted file mode 100644 index f19081781..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Lineups dto. - /// </summary> - public class LineupsDto - { - /// <summary> - /// Gets or sets the response code. - /// </summary> - [JsonPropertyName("code")] - public int Code { get; set; } - - /// <summary> - /// Gets or sets the server id. - /// </summary> - [JsonPropertyName("serverID")] - public string? ServerId { get; set; } - - /// <summary> - /// Gets or sets the datetime. - /// </summary> - [JsonPropertyName("datetime")] - public DateTime? LineupTimestamp { get; set; } - - /// <summary> - /// Gets or sets the list of lineups. - /// </summary> - [JsonPropertyName("lineups")] - public IReadOnlyList<LineupDto> Lineups { get; set; } = Array.Empty<LineupDto>(); - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs deleted file mode 100644 index fecc55e03..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Logo dto. - /// </summary> - public class LogoDto - { - /// <summary> - /// Gets or sets the url. - /// </summary> - [JsonPropertyName("URL")] - public string? Url { get; set; } - - /// <summary> - /// Gets or sets the height. - /// </summary> - [JsonPropertyName("height")] - public int Height { get; set; } - - /// <summary> - /// Gets or sets the width. - /// </summary> - [JsonPropertyName("width")] - public int Width { get; set; } - - /// <summary> - /// Gets or sets the md5. - /// </summary> - [JsonPropertyName("md5")] - public string? Md5 { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs deleted file mode 100644 index ffd02d474..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Map dto. - /// </summary> - public class MapDto - { - /// <summary> - /// Gets or sets the station id. - /// </summary> - [JsonPropertyName("stationID")] - public string? StationId { get; set; } - - /// <summary> - /// Gets or sets the channel. - /// </summary> - [JsonPropertyName("channel")] - public string? Channel { get; set; } - - /// <summary> - /// Gets or sets the provider callsign. - /// </summary> - [JsonPropertyName("providerCallsign")] - public string? ProvderCallsign { get; set; } - - /// <summary> - /// Gets or sets the logical channel number. - /// </summary> - [JsonPropertyName("logicalChannelNumber")] - public string? LogicalChannelNumber { get; set; } - - /// <summary> - /// Gets or sets the uhfvhf. - /// </summary> - [JsonPropertyName("uhfVhf")] - public int UhfVhf { get; set; } - - /// <summary> - /// Gets or sets the atsc major. - /// </summary> - [JsonPropertyName("atscMajor")] - public int AtscMajor { get; set; } - - /// <summary> - /// Gets or sets the atsc minor. - /// </summary> - [JsonPropertyName("atscMinor")] - public int AtscMinor { get; set; } - - /// <summary> - /// Gets or sets the match type. - /// </summary> - [JsonPropertyName("matchType")] - public string? MatchType { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs deleted file mode 100644 index 40faa493c..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Metadata dto. - /// </summary> - public class MetadataDto - { - /// <summary> - /// Gets or sets the linup. - /// </summary> - [JsonPropertyName("lineup")] - public string? Lineup { get; set; } - - /// <summary> - /// Gets or sets the modified timestamp. - /// </summary> - [JsonPropertyName("modified")] - public string? Modified { get; set; } - - /// <summary> - /// Gets or sets the transport. - /// </summary> - [JsonPropertyName("transport")] - public string? Transport { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs deleted file mode 100644 index 43f290156..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Metadata programs dto. - /// </summary> - public class MetadataProgramsDto - { - /// <summary> - /// Gets or sets the gracenote object. - /// </summary> - [JsonPropertyName("Gracenote")] - public GracenoteDto? Gracenote { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs deleted file mode 100644 index 04560ab55..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Metadata schedule dto. - /// </summary> - public class MetadataScheduleDto - { - /// <summary> - /// Gets or sets the modified timestamp. - /// </summary> - [JsonPropertyName("modified")] - public string? Modified { get; set; } - - /// <summary> - /// Gets or sets the md5. - /// </summary> - [JsonPropertyName("md5")] - public string? Md5 { get; set; } - - /// <summary> - /// Gets or sets the start date. - /// </summary> - [JsonPropertyName("startDate")] - public DateTime? StartDate { get; set; } - - /// <summary> - /// Gets or sets the end date. - /// </summary> - [JsonPropertyName("endDate")] - public DateTime? EndDate { get; set; } - - /// <summary> - /// Gets or sets the days count. - /// </summary> - [JsonPropertyName("days")] - public int Days { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs deleted file mode 100644 index 31bef423b..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Movie dto. - /// </summary> - public class MovieDto - { - /// <summary> - /// Gets or sets the year. - /// </summary> - [JsonPropertyName("year")] - public string? Year { get; set; } - - /// <summary> - /// Gets or sets the duration. - /// </summary> - [JsonPropertyName("duration")] - public int Duration { get; set; } - - /// <summary> - /// Gets or sets the list of quality rating. - /// </summary> - [JsonPropertyName("qualityRating")] - public IReadOnlyList<QualityRatingDto> QualityRating { get; set; } = Array.Empty<QualityRatingDto>(); - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs deleted file mode 100644 index e8b15dc07..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Multipart dto. - /// </summary> - public class MultipartDto - { - /// <summary> - /// Gets or sets the part number. - /// </summary> - [JsonPropertyName("partNumber")] - public int PartNumber { get; set; } - - /// <summary> - /// Gets or sets the total parts. - /// </summary> - [JsonPropertyName("totalParts")] - public int TotalParts { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs deleted file mode 100644 index 84c48f67f..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Program details dto. - /// </summary> - public class ProgramDetailsDto - { - /// <summary> - /// Gets or sets the audience. - /// </summary> - [JsonPropertyName("audience")] - public string? Audience { get; set; } - - /// <summary> - /// Gets or sets the program id. - /// </summary> - [JsonPropertyName("programID")] - public string? ProgramId { get; set; } - - /// <summary> - /// Gets or sets the list of titles. - /// </summary> - [JsonPropertyName("titles")] - public IReadOnlyList<TitleDto> Titles { get; set; } = Array.Empty<TitleDto>(); - - /// <summary> - /// Gets or sets the event details object. - /// </summary> - [JsonPropertyName("eventDetails")] - public EventDetailsDto? EventDetails { get; set; } - - /// <summary> - /// Gets or sets the descriptions. - /// </summary> - [JsonPropertyName("descriptions")] - public DescriptionsProgramDto? Descriptions { get; set; } - - /// <summary> - /// Gets or sets the original air date. - /// </summary> - [JsonPropertyName("originalAirDate")] - public DateTime? OriginalAirDate { get; set; } - - /// <summary> - /// Gets or sets the list of genres. - /// </summary> - [JsonPropertyName("genres")] - public IReadOnlyList<string> Genres { get; set; } = Array.Empty<string>(); - - /// <summary> - /// Gets or sets the episode title. - /// </summary> - [JsonPropertyName("episodeTitle150")] - public string? EpisodeTitle150 { get; set; } - - /// <summary> - /// Gets or sets the list of metadata. - /// </summary> - [JsonPropertyName("metadata")] - public IReadOnlyList<MetadataProgramsDto> Metadata { get; set; } = Array.Empty<MetadataProgramsDto>(); - - /// <summary> - /// Gets or sets the list of content raitings. - /// </summary> - [JsonPropertyName("contentRating")] - public IReadOnlyList<ContentRatingDto> ContentRating { get; set; } = Array.Empty<ContentRatingDto>(); - - /// <summary> - /// Gets or sets the list of cast. - /// </summary> - [JsonPropertyName("cast")] - public IReadOnlyList<CastDto> Cast { get; set; } = Array.Empty<CastDto>(); - - /// <summary> - /// Gets or sets the list of crew. - /// </summary> - [JsonPropertyName("crew")] - public IReadOnlyList<CrewDto> Crew { get; set; } = Array.Empty<CrewDto>(); - - /// <summary> - /// Gets or sets the entity type. - /// </summary> - [JsonPropertyName("entityType")] - public string? EntityType { get; set; } - - /// <summary> - /// Gets or sets the show type. - /// </summary> - [JsonPropertyName("showType")] - public string? ShowType { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether there is image artwork. - /// </summary> - [JsonPropertyName("hasImageArtwork")] - public bool HasImageArtwork { get; set; } - - /// <summary> - /// Gets or sets the primary image. - /// </summary> - [JsonPropertyName("primaryImage")] - public string? PrimaryImage { get; set; } - - /// <summary> - /// Gets or sets the thumb image. - /// </summary> - [JsonPropertyName("thumbImage")] - public string? ThumbImage { get; set; } - - /// <summary> - /// Gets or sets the backdrop image. - /// </summary> - [JsonPropertyName("backdropImage")] - public string? BackdropImage { get; set; } - - /// <summary> - /// Gets or sets the banner image. - /// </summary> - [JsonPropertyName("bannerImage")] - public string? BannerImage { get; set; } - - /// <summary> - /// Gets or sets the image id. - /// </summary> - [JsonPropertyName("imageID")] - public string? ImageId { get; set; } - - /// <summary> - /// Gets or sets the md5. - /// </summary> - [JsonPropertyName("md5")] - public string? Md5 { get; set; } - - /// <summary> - /// Gets or sets the list of content advisory. - /// </summary> - [JsonPropertyName("contentAdvisory")] - public IReadOnlyList<string> ContentAdvisory { get; set; } = Array.Empty<string>(); - - /// <summary> - /// Gets or sets the movie object. - /// </summary> - [JsonPropertyName("movie")] - public MovieDto? Movie { get; set; } - - /// <summary> - /// Gets or sets the list of recommendations. - /// </summary> - [JsonPropertyName("recommendations")] - public IReadOnlyList<RecommendationDto> Recommendations { get; set; } = Array.Empty<RecommendationDto>(); - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs deleted file mode 100644 index 60389b45b..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Program dto. - /// </summary> - public class ProgramDto - { - /// <summary> - /// Gets or sets the program id. - /// </summary> - [JsonPropertyName("programID")] - public string? ProgramId { get; set; } - - /// <summary> - /// Gets or sets the air date time. - /// </summary> - [JsonPropertyName("airDateTime")] - public DateTime? AirDateTime { get; set; } - - /// <summary> - /// Gets or sets the duration. - /// </summary> - [JsonPropertyName("duration")] - public int Duration { get; set; } - - /// <summary> - /// Gets or sets the md5. - /// </summary> - [JsonPropertyName("md5")] - public string? Md5 { get; set; } - - /// <summary> - /// Gets or sets the list of audio properties. - /// </summary> - [JsonPropertyName("audioProperties")] - public IReadOnlyList<string> AudioProperties { get; set; } = Array.Empty<string>(); - - /// <summary> - /// Gets or sets the list of video properties. - /// </summary> - [JsonPropertyName("videoProperties")] - public IReadOnlyList<string> VideoProperties { get; set; } = Array.Empty<string>(); - - /// <summary> - /// Gets or sets the list of ratings. - /// </summary> - [JsonPropertyName("ratings")] - public IReadOnlyList<RatingDto> Ratings { get; set; } = Array.Empty<RatingDto>(); - - /// <summary> - /// Gets or sets a value indicating whether this program is new. - /// </summary> - [JsonPropertyName("new")] - public bool? New { get; set; } - - /// <summary> - /// Gets or sets the multipart object. - /// </summary> - [JsonPropertyName("multipart")] - public MultipartDto? Multipart { get; set; } - - /// <summary> - /// Gets or sets the live tape delay. - /// </summary> - [JsonPropertyName("liveTapeDelay")] - public string? LiveTapeDelay { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this is the premiere. - /// </summary> - [JsonPropertyName("premiere")] - public bool Premiere { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this is a repeat. - /// </summary> - [JsonPropertyName("repeat")] - public bool Repeat { get; set; } - - /// <summary> - /// Gets or sets the premiere or finale. - /// </summary> - [JsonPropertyName("isPremiereOrFinale")] - public string? IsPremiereOrFinale { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs deleted file mode 100644 index c5ddcf7c5..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Quality rating dto. - /// </summary> - public class QualityRatingDto - { - /// <summary> - /// Gets or sets the ratings body. - /// </summary> - [JsonPropertyName("ratingsBody")] - public string? RatingsBody { get; set; } - - /// <summary> - /// Gets or sets the rating. - /// </summary> - [JsonPropertyName("rating")] - public string? Rating { get; set; } - - /// <summary> - /// Gets or sets the min rating. - /// </summary> - [JsonPropertyName("minRating")] - public string? MinRating { get; set; } - - /// <summary> - /// Gets or sets the max rating. - /// </summary> - [JsonPropertyName("maxRating")] - public string? MaxRating { get; set; } - - /// <summary> - /// Gets or sets the increment. - /// </summary> - [JsonPropertyName("increment")] - public string? Increment { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs deleted file mode 100644 index e04b619a4..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Rating dto. - /// </summary> - public class RatingDto - { - /// <summary> - /// Gets or sets the body. - /// </summary> - [JsonPropertyName("body")] - public string? Body { get; set; } - - /// <summary> - /// Gets or sets the code. - /// </summary> - [JsonPropertyName("code")] - public string? Code { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs deleted file mode 100644 index c8f79fd1c..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Recommendation dto. - /// </summary> - public class RecommendationDto - { - /// <summary> - /// Gets or sets the program id. - /// </summary> - [JsonPropertyName("programID")] - public string? ProgramId { get; set; } - - /// <summary> - /// Gets or sets the title. - /// </summary> - [JsonPropertyName("title120")] - public string? Title120 { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs deleted file mode 100644 index 0cd05709b..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Request schedule for channel dto. - /// </summary> - public class RequestScheduleForChannelDto - { - /// <summary> - /// Gets or sets the station id. - /// </summary> - [JsonPropertyName("stationID")] - public string? StationId { get; set; } - - /// <summary> - /// Gets or sets the list of dates. - /// </summary> - [JsonPropertyName("date")] - public IReadOnlyList<string> Date { get; set; } = Array.Empty<string>(); - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs deleted file mode 100644 index 84e224b71..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Show image dto. - /// </summary> - public class ShowImagesDto - { - /// <summary> - /// Gets or sets the program id. - /// </summary> - [JsonPropertyName("programID")] - public string? ProgramId { get; set; } - - /// <summary> - /// Gets or sets the list of data. - /// </summary> - [JsonPropertyName("data")] - public IReadOnlyList<ImageDataDto> Data { get; set; } = Array.Empty<ImageDataDto>(); - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs deleted file mode 100644 index d797fd49b..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Station dto. - /// </summary> - public class StationDto - { - /// <summary> - /// Gets or sets the station id. - /// </summary> - [JsonPropertyName("stationID")] - public string? StationId { get; set; } - - /// <summary> - /// Gets or sets the name. - /// </summary> - [JsonPropertyName("name")] - public string? Name { get; set; } - - /// <summary> - /// Gets or sets the callsign. - /// </summary> - [JsonPropertyName("callsign")] - public string? Callsign { get; set; } - - /// <summary> - /// Gets or sets the broadcast language. - /// </summary> - [JsonPropertyName("broadcastLanguage")] - public IReadOnlyList<string> BroadcastLanguage { get; set; } = Array.Empty<string>(); - - /// <summary> - /// Gets or sets the description language. - /// </summary> - [JsonPropertyName("descriptionLanguage")] - public IReadOnlyList<string> DescriptionLanguage { get; set; } = Array.Empty<string>(); - - /// <summary> - /// Gets or sets the broadcaster. - /// </summary> - [JsonPropertyName("broadcaster")] - public BroadcasterDto? Broadcaster { get; set; } - - /// <summary> - /// Gets or sets the affiliate. - /// </summary> - [JsonPropertyName("affiliate")] - public string? Affiliate { get; set; } - - /// <summary> - /// Gets or sets the logo. - /// </summary> - [JsonPropertyName("logo")] - public LogoDto? Logo { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether it is commercial free. - /// </summary> - [JsonPropertyName("isCommercialFree")] - public bool? IsCommercialFree { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs deleted file mode 100644 index 61cd4a9b0..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// Title dto. - /// </summary> - public class TitleDto - { - /// <summary> - /// Gets or sets the title. - /// </summary> - [JsonPropertyName("title120")] - public string? Title120 { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs deleted file mode 100644 index afb999486..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos -{ - /// <summary> - /// The token dto. - /// </summary> - public class TokenDto - { - /// <summary> - /// Gets or sets the response code. - /// </summary> - [JsonPropertyName("code")] - public int Code { get; set; } - - /// <summary> - /// Gets or sets the response message. - /// </summary> - [JsonPropertyName("message")] - public string? Message { get; set; } - - /// <summary> - /// Gets or sets the server id. - /// </summary> - [JsonPropertyName("serverID")] - public string? ServerId { get; set; } - - /// <summary> - /// Gets or sets the token. - /// </summary> - [JsonPropertyName("token")] - public string? Token { get; set; } - - /// <summary> - /// Gets or sets the current datetime. - /// </summary> - [JsonPropertyName("datetime")] - public DateTime? TokenTimestamp { get; set; } - - /// <summary> - /// Gets or sets the response message. - /// </summary> - [JsonPropertyName("response")] - public string? Response { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs deleted file mode 100644 index 066afb956..000000000 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ /dev/null @@ -1,252 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Extensions; -using Jellyfin.XmlTv; -using Jellyfin.XmlTv.Entities; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.LiveTv; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.LiveTv.Listings -{ - public class XmlTvListingsProvider : IListingsProvider - { - private static readonly TimeSpan _maxCacheAge = TimeSpan.FromHours(1); - - private readonly IServerConfigurationManager _config; - private readonly IHttpClientFactory _httpClientFactory; - private readonly ILogger<XmlTvListingsProvider> _logger; - - public XmlTvListingsProvider( - IServerConfigurationManager config, - IHttpClientFactory httpClientFactory, - ILogger<XmlTvListingsProvider> logger) - { - _config = config; - _httpClientFactory = httpClientFactory; - _logger = logger; - } - - public string Name => "XmlTV"; - - public string Type => "xmltv"; - - private string GetLanguage(ListingsProviderInfo info) - { - if (!string.IsNullOrWhiteSpace(info.PreferredLanguage)) - { - return info.PreferredLanguage; - } - - return _config.Configuration.PreferredMetadataLanguage; - } - - private async Task<string> GetXml(ListingsProviderInfo info, CancellationToken cancellationToken) - { - _logger.LogInformation("xmltv path: {Path}", info.Path); - - string cacheFilename = info.Id + ".xml"; - string cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename); - - if (File.Exists(cacheFile) && File.GetLastWriteTimeUtc(cacheFile) >= DateTime.UtcNow.Subtract(_maxCacheAge)) - { - return cacheFile; - } - - // Must check if file exists as parent directory may not exist. - if (File.Exists(cacheFile)) - { - File.Delete(cacheFile); - } - else - { - Directory.CreateDirectory(Path.GetDirectoryName(cacheFile)); - } - - if (info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - _logger.LogInformation("Downloading xmltv listings from {Path}", info.Path); - - using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(info.Path, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false); - } - else - { - await using var stream = AsyncFile.OpenRead(info.Path); - return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false); - } - } - - private async Task<string> UnzipIfNeededAndCopy(string originalUrl, Stream stream, string file, CancellationToken cancellationToken) - { - await using var fileStream = new FileStream(file, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); - - if (Path.GetExtension(originalUrl.AsSpan().LeftPart('?')).Equals(".gz", StringComparison.OrdinalIgnoreCase)) - { - try - { - using var reader = new GZipStream(stream, CompressionMode.Decompress); - await reader.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error extracting from gz file {File}", originalUrl); - } - } - else - { - await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); - } - - return file; - } - - public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(channelId)) - { - throw new ArgumentNullException(nameof(channelId)); - } - - _logger.LogDebug("Getting xmltv programs for channel {Id}", channelId); - - string path = await GetXml(info, cancellationToken).ConfigureAwait(false); - _logger.LogDebug("Opening XmlTvReader for {Path}", path); - var reader = new XmlTvReader(path, GetLanguage(info)); - - return reader.GetProgrammes(channelId, startDateUtc, endDateUtc, cancellationToken) - .Select(p => GetProgramInfo(p, info)); - } - - private static ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info) - { - string episodeTitle = program.Episode.Title; - var programCategories = program.Categories.Where(c => !string.IsNullOrWhiteSpace(c)).ToList(); - - var programInfo = new ProgramInfo - { - ChannelId = program.ChannelId, - EndDate = program.EndDate.UtcDateTime, - EpisodeNumber = program.Episode.Episode, - EpisodeTitle = episodeTitle, - Genres = programCategories, - StartDate = program.StartDate.UtcDateTime, - Name = program.Title, - Overview = program.Description, - ProductionYear = program.CopyrightDate?.Year, - SeasonNumber = program.Episode.Series, - IsSeries = program.Episode.Series is not null, - IsRepeat = program.IsPreviouslyShown && !program.IsNew, - IsPremiere = program.Premiere is not null, - IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), - IsMovie = programCategories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), - IsNews = programCategories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), - IsSports = programCategories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)), - ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source, - HasImage = !string.IsNullOrEmpty(program.Icon?.Source), - OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value, - CommunityRating = program.StarRating, - SeriesId = program.Episode.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture) - }; - - if (string.IsNullOrWhiteSpace(program.ProgramId)) - { - string uniqueString = (program.Title ?? string.Empty) + (episodeTitle ?? string.Empty); - - if (programInfo.SeasonNumber.HasValue) - { - uniqueString = "-" + programInfo.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture); - } - - if (programInfo.EpisodeNumber.HasValue) - { - uniqueString = "-" + programInfo.EpisodeNumber.Value.ToString(CultureInfo.InvariantCulture); - } - - programInfo.ShowId = uniqueString.GetMD5().ToString("N", CultureInfo.InvariantCulture); - - // If we don't have valid episode info, assume it's a unique program, otherwise recordings might be skipped - if (programInfo.IsSeries - && !programInfo.IsRepeat - && (programInfo.EpisodeNumber ?? 0) == 0) - { - programInfo.ShowId += programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture); - } - } - else - { - programInfo.ShowId = program.ProgramId; - } - - // Construct an id from the channel and start date - programInfo.Id = string.Format(CultureInfo.InvariantCulture, "{0}_{1:O}", program.ChannelId, program.StartDate); - - if (programInfo.IsMovie) - { - programInfo.IsSeries = false; - programInfo.EpisodeNumber = null; - programInfo.EpisodeTitle = null; - } - - return programInfo; - } - - public Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings) - { - // Assume all urls are valid. check files for existence - if (!info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase) && !File.Exists(info.Path)) - { - throw new FileNotFoundException("Could not find the XmlTv file specified:", info.Path); - } - - return Task.CompletedTask; - } - - public async Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location) - { - // In theory this should never be called because there is always only one lineup - string path = await GetXml(info, CancellationToken.None).ConfigureAwait(false); - _logger.LogDebug("Opening XmlTvReader for {Path}", path); - var reader = new XmlTvReader(path, GetLanguage(info)); - IEnumerable<XmlTvChannel> results = reader.GetChannels(); - - // Should this method be async? - return results.Select(c => new NameIdPair() { Id = c.Id, Name = c.DisplayName }).ToList(); - } - - public async Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken) - { - // In theory this should never be called because there is always only one lineup - string path = await GetXml(info, cancellationToken).ConfigureAwait(false); - _logger.LogDebug("Opening XmlTvReader for {Path}", path); - var reader = new XmlTvReader(path, GetLanguage(info)); - var results = reader.GetChannels(); - - // Should this method be async? - return results.Select(c => new ChannelInfo - { - Id = c.Id, - Name = c.DisplayName, - ImageUrl = string.IsNullOrEmpty(c.Icon?.Source) ? null : c.Icon.Source, - Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number - }).ToList(); - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs deleted file mode 100644 index 098f193fb..000000000 --- a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.LiveTv; - -namespace Emby.Server.Implementations.LiveTv -{ - /// <summary> - /// <see cref="IConfigurationFactory" /> implementation for <see cref="LiveTvOptions" />. - /// </summary> - public class LiveTvConfigurationFactory : IConfigurationFactory - { - /// <inheritdoc /> - public IEnumerable<ConfigurationStore> GetConfigurations() - { - return new ConfigurationStore[] - { - new ConfigurationStore - { - ConfigurationType = typeof(LiveTvOptions), - Key = "livetv" - } - }; - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs deleted file mode 100644 index 9326fbd5c..000000000 --- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ /dev/null @@ -1,548 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Data.Enums; -using MediaBrowser.Common; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Drawing; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.LiveTv; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.LiveTv -{ - public class LiveTvDtoService - { - private const string InternalVersionNumber = "4"; - - private const string ServiceName = "Emby"; - - private readonly ILogger<LiveTvDtoService> _logger; - private readonly IImageProcessor _imageProcessor; - private readonly IDtoService _dtoService; - private readonly IApplicationHost _appHost; - private readonly ILibraryManager _libraryManager; - - public LiveTvDtoService( - IDtoService dtoService, - IImageProcessor imageProcessor, - ILogger<LiveTvDtoService> logger, - IApplicationHost appHost, - ILibraryManager libraryManager) - { - _dtoService = dtoService; - _imageProcessor = imageProcessor; - _logger = logger; - _appHost = appHost; - _libraryManager = libraryManager; - } - - public TimerInfoDto GetTimerInfoDto(TimerInfo info, ILiveTvService service, LiveTvProgram program, BaseItem channel) - { - var dto = new TimerInfoDto - { - Id = GetInternalTimerId(info.Id), - Overview = info.Overview, - EndDate = info.EndDate, - Name = info.Name, - StartDate = info.StartDate, - ExternalId = info.Id, - ChannelId = GetInternalChannelId(service.Name, info.ChannelId), - Status = info.Status, - SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId) ? null : GetInternalSeriesTimerId(info.SeriesTimerId).ToString("N", CultureInfo.InvariantCulture), - PrePaddingSeconds = info.PrePaddingSeconds, - PostPaddingSeconds = info.PostPaddingSeconds, - IsPostPaddingRequired = info.IsPostPaddingRequired, - IsPrePaddingRequired = info.IsPrePaddingRequired, - KeepUntil = info.KeepUntil, - ExternalChannelId = info.ChannelId, - ExternalSeriesTimerId = info.SeriesTimerId, - ServiceName = service.Name, - ExternalProgramId = info.ProgramId, - Priority = info.Priority, - RunTimeTicks = (info.EndDate - info.StartDate).Ticks, - ServerId = _appHost.SystemId - }; - - if (!string.IsNullOrEmpty(info.ProgramId)) - { - dto.ProgramId = GetInternalProgramId(info.ProgramId).ToString("N", CultureInfo.InvariantCulture); - } - - if (program is not null) - { - dto.ProgramInfo = _dtoService.GetBaseItemDto(program, new DtoOptions()); - - if (info.Status != RecordingStatus.Cancelled && info.Status != RecordingStatus.Error) - { - dto.ProgramInfo.TimerId = dto.Id; - dto.ProgramInfo.Status = info.Status.ToString(); - } - - dto.ProgramInfo.SeriesTimerId = dto.SeriesTimerId; - - if (!string.IsNullOrEmpty(info.SeriesTimerId)) - { - FillImages(dto.ProgramInfo, info.Name, info.SeriesId); - } - } - - if (channel is not null) - { - dto.ChannelName = channel.Name; - - if (channel.HasImage(ImageType.Primary)) - { - dto.ChannelPrimaryImageTag = GetImageTag(channel); - } - } - - return dto; - } - - public SeriesTimerInfoDto GetSeriesTimerInfoDto(SeriesTimerInfo info, ILiveTvService service, string channelName) - { - var dto = new SeriesTimerInfoDto - { - Id = GetInternalSeriesTimerId(info.Id).ToString("N", CultureInfo.InvariantCulture), - Overview = info.Overview, - EndDate = info.EndDate, - Name = info.Name, - StartDate = info.StartDate, - ExternalId = info.Id, - PrePaddingSeconds = info.PrePaddingSeconds, - PostPaddingSeconds = info.PostPaddingSeconds, - IsPostPaddingRequired = info.IsPostPaddingRequired, - IsPrePaddingRequired = info.IsPrePaddingRequired, - Days = info.Days.ToArray(), - Priority = info.Priority, - RecordAnyChannel = info.RecordAnyChannel, - RecordAnyTime = info.RecordAnyTime, - SkipEpisodesInLibrary = info.SkipEpisodesInLibrary, - KeepUpTo = info.KeepUpTo, - KeepUntil = info.KeepUntil, - RecordNewOnly = info.RecordNewOnly, - ExternalChannelId = info.ChannelId, - ExternalProgramId = info.ProgramId, - ServiceName = service.Name, - ChannelName = channelName, - ServerId = _appHost.SystemId - }; - - if (!string.IsNullOrEmpty(info.ChannelId)) - { - dto.ChannelId = GetInternalChannelId(service.Name, info.ChannelId); - } - - if (!string.IsNullOrEmpty(info.ProgramId)) - { - dto.ProgramId = GetInternalProgramId(info.ProgramId).ToString("N", CultureInfo.InvariantCulture); - } - - dto.DayPattern = info.Days is null ? null : GetDayPattern(info.Days.ToArray()); - - FillImages(dto, info.Name, info.SeriesId); - - return dto; - } - - private void FillImages(BaseItemDto dto, string seriesName, string programSeriesId) - { - var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.Series }, - Name = seriesName, - Limit = 1, - ImageTypes = new ImageType[] { ImageType.Thumb }, - DtoOptions = new DtoOptions(false) - }).FirstOrDefault(); - - if (librarySeries is not null) - { - var image = librarySeries.GetImageInfo(ImageType.Thumb, 0); - if (image is not null) - { - try - { - dto.ParentThumbImageTag = _imageProcessor.GetImageCacheTag(librarySeries, image); - dto.ParentThumbItemId = librarySeries.Id; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error"); - } - } - - image = librarySeries.GetImageInfo(ImageType.Backdrop, 0); - if (image is not null) - { - try - { - dto.ParentBackdropImageTags = new string[] - { - _imageProcessor.GetImageCacheTag(librarySeries, image) - }; - dto.ParentBackdropItemId = librarySeries.Id; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error"); - } - } - } - - var program = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, - ExternalSeriesId = programSeriesId, - Limit = 1, - ImageTypes = new ImageType[] { ImageType.Primary }, - DtoOptions = new DtoOptions(false), - Name = string.IsNullOrEmpty(programSeriesId) ? seriesName : null - }).FirstOrDefault(); - - if (program is not null) - { - var image = program.GetImageInfo(ImageType.Primary, 0); - if (image is not null) - { - try - { - dto.ParentPrimaryImageTag = _imageProcessor.GetImageCacheTag(program, image); - dto.ParentPrimaryImageItemId = program.Id.ToString("N", CultureInfo.InvariantCulture); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error"); - } - } - - if (dto.ParentBackdropImageTags is null || dto.ParentBackdropImageTags.Length == 0) - { - image = program.GetImageInfo(ImageType.Backdrop, 0); - if (image is not null) - { - try - { - dto.ParentBackdropImageTags = new string[] - { - _imageProcessor.GetImageCacheTag(program, image) - }; - - dto.ParentBackdropItemId = program.Id; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error"); - } - } - } - } - } - - private void FillImages(SeriesTimerInfoDto dto, string seriesName, string programSeriesId) - { - var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.Series }, - Name = seriesName, - Limit = 1, - ImageTypes = new ImageType[] { ImageType.Thumb }, - DtoOptions = new DtoOptions(false) - }).FirstOrDefault(); - - if (librarySeries is not null) - { - var image = librarySeries.GetImageInfo(ImageType.Thumb, 0); - if (image is not null) - { - try - { - dto.ParentThumbImageTag = _imageProcessor.GetImageCacheTag(librarySeries, image); - dto.ParentThumbItemId = librarySeries.Id.ToString("N", CultureInfo.InvariantCulture); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error"); - } - } - - image = librarySeries.GetImageInfo(ImageType.Backdrop, 0); - if (image is not null) - { - try - { - dto.ParentBackdropImageTags = new string[] - { - _imageProcessor.GetImageCacheTag(librarySeries, image) - }; - dto.ParentBackdropItemId = librarySeries.Id.ToString("N", CultureInfo.InvariantCulture); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error"); - } - } - } - - var program = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.Series }, - Name = seriesName, - Limit = 1, - ImageTypes = new ImageType[] { ImageType.Primary }, - DtoOptions = new DtoOptions(false) - }).FirstOrDefault(); - - if (program is null) - { - program = _libraryManager.GetItemList(new InternalItemsQuery - { - IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, - ExternalSeriesId = programSeriesId, - Limit = 1, - ImageTypes = new ImageType[] { ImageType.Primary }, - DtoOptions = new DtoOptions(false), - Name = string.IsNullOrEmpty(programSeriesId) ? seriesName : null - }).FirstOrDefault(); - } - - if (program is not null) - { - var image = program.GetImageInfo(ImageType.Primary, 0); - if (image is not null) - { - try - { - dto.ParentPrimaryImageTag = _imageProcessor.GetImageCacheTag(program, image); - dto.ParentPrimaryImageItemId = program.Id.ToString("N", CultureInfo.InvariantCulture); - } - catch (Exception ex) - { - _logger.LogDebug(ex, "GetImageCacheTag raised an exception in LiveTvDtoService.FillImages."); - } - } - - if (dto.ParentBackdropImageTags is null || dto.ParentBackdropImageTags.Length == 0) - { - image = program.GetImageInfo(ImageType.Backdrop, 0); - if (image is not null) - { - try - { - dto.ParentBackdropImageTags = new[] - { - _imageProcessor.GetImageCacheTag(program, image) - }; - dto.ParentBackdropItemId = program.Id.ToString("N", CultureInfo.InvariantCulture); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error"); - } - } - } - } - } - - public DayPattern? GetDayPattern(DayOfWeek[] days) - { - DayPattern? pattern = null; - - if (days.Length > 0) - { - if (days.Length == 7) - { - pattern = DayPattern.Daily; - } - else if (days.Length == 2) - { - if (days.Contains(DayOfWeek.Saturday) && days.Contains(DayOfWeek.Sunday)) - { - pattern = DayPattern.Weekends; - } - } - else if (days.Length == 5) - { - if (days.Contains(DayOfWeek.Monday) && days.Contains(DayOfWeek.Tuesday) && days.Contains(DayOfWeek.Wednesday) && days.Contains(DayOfWeek.Thursday) && days.Contains(DayOfWeek.Friday)) - { - pattern = DayPattern.Weekdays; - } - } - } - - return pattern; - } - - internal string GetImageTag(BaseItem info) - { - try - { - return _imageProcessor.GetImageCacheTag(info, ImageType.Primary); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting image info for {Name}", info.Name); - } - - return null; - } - - public Guid GetInternalChannelId(string serviceName, string externalId) - { - var name = serviceName + externalId + InternalVersionNumber; - - return _libraryManager.GetNewItemId(name.ToLowerInvariant(), typeof(LiveTvChannel)); - } - - public string GetInternalTimerId(string externalId) - { - var name = ServiceName + externalId + InternalVersionNumber; - - return name.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture); - } - - public Guid GetInternalSeriesTimerId(string externalId) - { - var name = ServiceName + externalId + InternalVersionNumber; - - return name.ToLowerInvariant().GetMD5(); - } - - public Guid GetInternalProgramId(string externalId) - { - var name = ServiceName + externalId + InternalVersionNumber; - - return _libraryManager.GetNewItemId(name.ToLowerInvariant(), typeof(LiveTvProgram)); - } - - public async Task<TimerInfo> GetTimerInfo(TimerInfoDto dto, bool isNew, LiveTvManager liveTv, CancellationToken cancellationToken) - { - var info = new TimerInfo - { - Overview = dto.Overview, - EndDate = dto.EndDate, - Name = dto.Name, - StartDate = dto.StartDate, - Status = dto.Status, - PrePaddingSeconds = dto.PrePaddingSeconds, - PostPaddingSeconds = dto.PostPaddingSeconds, - IsPostPaddingRequired = dto.IsPostPaddingRequired, - IsPrePaddingRequired = dto.IsPrePaddingRequired, - KeepUntil = dto.KeepUntil, - Priority = dto.Priority, - SeriesTimerId = dto.ExternalSeriesTimerId, - ProgramId = dto.ExternalProgramId, - ChannelId = dto.ExternalChannelId, - Id = dto.ExternalId - }; - - // Convert internal server id's to external tv provider id's - if (!isNew && !string.IsNullOrEmpty(dto.Id) && string.IsNullOrEmpty(info.Id)) - { - var timer = await liveTv.GetSeriesTimer(dto.Id, cancellationToken).ConfigureAwait(false); - - info.Id = timer.ExternalId; - } - - if (!dto.ChannelId.Equals(default) && string.IsNullOrEmpty(info.ChannelId)) - { - var channel = _libraryManager.GetItemById(dto.ChannelId); - - if (channel is not null) - { - info.ChannelId = channel.ExternalId; - } - } - - if (!string.IsNullOrEmpty(dto.ProgramId) && string.IsNullOrEmpty(info.ProgramId)) - { - var program = _libraryManager.GetItemById(dto.ProgramId); - - if (program is not null) - { - info.ProgramId = program.ExternalId; - } - } - - if (!string.IsNullOrEmpty(dto.SeriesTimerId) && string.IsNullOrEmpty(info.SeriesTimerId)) - { - var timer = await liveTv.GetSeriesTimer(dto.SeriesTimerId, cancellationToken).ConfigureAwait(false); - - if (timer is not null) - { - info.SeriesTimerId = timer.ExternalId; - } - } - - return info; - } - - public async Task<SeriesTimerInfo> GetSeriesTimerInfo(SeriesTimerInfoDto dto, bool isNew, LiveTvManager liveTv, CancellationToken cancellationToken) - { - var info = new SeriesTimerInfo - { - Overview = dto.Overview, - EndDate = dto.EndDate, - Name = dto.Name, - StartDate = dto.StartDate, - PrePaddingSeconds = dto.PrePaddingSeconds, - PostPaddingSeconds = dto.PostPaddingSeconds, - IsPostPaddingRequired = dto.IsPostPaddingRequired, - IsPrePaddingRequired = dto.IsPrePaddingRequired, - Days = dto.Days.ToList(), - Priority = dto.Priority, - RecordAnyChannel = dto.RecordAnyChannel, - RecordAnyTime = dto.RecordAnyTime, - SkipEpisodesInLibrary = dto.SkipEpisodesInLibrary, - KeepUpTo = dto.KeepUpTo, - KeepUntil = dto.KeepUntil, - RecordNewOnly = dto.RecordNewOnly, - ProgramId = dto.ExternalProgramId, - ChannelId = dto.ExternalChannelId, - Id = dto.ExternalId - }; - - // Convert internal server id's to external tv provider id's - if (!isNew && !string.IsNullOrEmpty(dto.Id) && string.IsNullOrEmpty(info.Id)) - { - var timer = await liveTv.GetSeriesTimer(dto.Id, cancellationToken).ConfigureAwait(false); - - info.Id = timer.ExternalId; - } - - if (!dto.ChannelId.Equals(default) && string.IsNullOrEmpty(info.ChannelId)) - { - var channel = _libraryManager.GetItemById(dto.ChannelId); - - if (channel is not null) - { - info.ChannelId = channel.ExternalId; - } - } - - if (!string.IsNullOrEmpty(dto.ProgramId) && string.IsNullOrEmpty(info.ProgramId)) - { - var program = _libraryManager.GetItemById(dto.ProgramId); - - if (program is not null) - { - info.ProgramId = program.ExternalId; - } - } - - return info; - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs deleted file mode 100644 index dd427c736..000000000 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ /dev/null @@ -1,2409 +0,0 @@ -#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; - -namespace Emby.Server.Implementations.LiveTv -{ - /// <summary> - /// Class LiveTvManager. - /// </summary> - public class LiveTvManager : ILiveTvManager - { - private const int MaxGuideDays = 14; - private const string ExternalServiceTag = "ExternalServiceId"; - - private const string EtagKey = "ProgramEtag"; - - private readonly IServerConfigurationManager _config; - private readonly ILogger<LiveTvManager> _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<ILiveTvService>(); - private ITunerHost[] _tunerHosts = Array.Empty<ITunerHost>(); - private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>(); - - public LiveTvManager( - IServerConfigurationManager config, - ILogger<LiveTvManager> 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<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled; - - public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled; - - public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated; - - public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated; - - /// <summary> - /// Gets the services. - /// </summary> - /// <value>The services.</value> - public IReadOnlyList<ILiveTvService> Services => _services; - - public ITunerHost[] TunerHosts => _tunerHosts; - - public IListingsProvider[] ListingProviders => _listingProviders; - - private LiveTvOptions GetConfiguration() - { - return _config.GetConfiguration<LiveTvOptions>("livetv"); - } - - public string GetEmbyTvActiveRecordingPath(string id) - { - return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id); - } - - /// <summary> - /// Adds the parts. - /// </summary> - /// <param name="services">The services.</param> - /// <param name="tunerHosts">The tuner hosts.</param> - /// <param name="listingProviders">The listing providers.</param> - public void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<ITunerHost> tunerHosts, IEnumerable<IListingsProvider> 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<string> e) - { - var timerId = e.Argument; - - TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(timerId))); - } - - private void OnEmbyTvTimerCreated(object sender, GenericEventArgs<TimerInfo> e) - { - var timer = e.Argument; - - TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>( - new TimerEventInfo(timer.Id) - { - ProgramId = _tvDtoService.GetInternalProgramId(timer.ProgramId) - })); - } - - public List<NameIdPair> GetTunerHostTypes() - { - return _tunerHosts.OrderBy(i => i.Name).Select(i => new NameIdPair - { - Name = i.Name, - Id = i.Type - }).ToList(); - } - - public Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken) - { - return EmbyTV.EmbyTV.Current.DiscoverTuners(newDevicesOnly, cancellationToken); - } - - public QueryResult<BaseItem> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken) - { - var user = query.UserId.Equals(default) - ? 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[] { BaseItemKind.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.All(i => i.OrderBy != ItemSortBy.SortName)) - { - orderBy.Add((ItemSortBy.SortName, SortOrder.Ascending)); - } - - internalQuery.OrderBy = orderBy.ToArray(); - - return _libraryManager.GetItemsResult(internalQuery); - } - - public async Task<Tuple<MediaSourceInfo, ILiveStream>> GetChannelStream(string id, string mediaSourceId, List<ILiveStream> 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<Task> 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<MediaSourceInfo, ILiveStream>(info, liveStream); - } - - public async Task<IEnumerable<MediaSourceInfo>> 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 = new MediaStream[] - { - 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 = new MediaStream[] - { - 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<LiveTvChannel> 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 is null) - { - 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 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<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; - - 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<BaseItemDto> 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<(BaseItemDto ItemDto, string ExternalId, string ExternalSeriesId)> - { - (dto, program.ExternalId, program.ExternalSeriesId) - }; - - await AddRecordingInfo(list, cancellationToken).ConfigureAwait(false); - - return dto; - } - - public async Task<QueryResult<BaseItemDto>> 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[] { BaseItemKind.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 is not null) - { - internalQuery.ExternalSeriesId = seriesTimer.SeriesId; - - if (string.IsNullOrWhiteSpace(seriesTimer.SeriesId)) - { - // Better to return nothing than every program in the database - return new QueryResult<BaseItemDto>(); - } - } - else - { - // Better to return nothing than every program in the database - return new QueryResult<BaseItemDto>(); - } - } - - var queryResult = _libraryManager.QueryItems(internalQuery); - - var returnArray = _dtoService.GetBaseItemDtos(queryResult.Items, options, user); - - return new QueryResult<BaseItemDto>( - query.StartIndex, - queryResult.TotalRecordCount, - returnArray); - } - - public QueryResult<BaseItem> GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken) - { - var user = query.User; - - var topFolder = GetInternalLiveTvFolder(cancellationToken); - - var internalQuery = new InternalItemsQuery(user) - { - IncludeItemTypes = new[] { BaseItemKind.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<LiveTvProgram>().OrderBy(i => i.StartDate.Date); - - if (query.IsAiring ?? false) - { - orderedPrograms = orderedPrograms - .ThenByDescending(i => GetRecommendationScore(i, user, true)); - } - - IEnumerable<BaseItem> programs = orderedPrograms; - - if (query.Limit.HasValue) - { - programs = programs.Take(query.Limit.Value); - } - - return new QueryResult<BaseItem>( - query.StartIndex, - totalCount, - programs.ToArray()); - } - - public Task<QueryResult<BaseItemDto>> GetRecommendedProgramsAsync(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken) - { - if (!(query.IsAiring ?? false)) - { - return GetPrograms(query, options, cancellationToken); - } - - RemoveFields(options); - - var internalResult = GetRecommendedProgramsInternal(query, options, cancellationToken); - - return Task.FromResult(new QueryResult<BaseItemDto>( - query.StartIndex, - internalResult.TotalRecordCount, - _dtoService.GetBaseItemDtos(internalResult.Items, options, query.User))); - } - - 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 is 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<(BaseItemDto ItemDto, string ExternalId, string ExternalSeriesId)> programs, CancellationToken cancellationToken) - { - IReadOnlyList<TimerInfo> timerList = null; - IReadOnlyList<SeriesTimerInfo> seriesTimerList = null; - - foreach (var programTuple in programs) - { - var program = programTuple.ItemDto; - var externalProgramId = programTuple.ExternalId; - string externalSeriesId = programTuple.ExternalSeriesId; - - 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 is not 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 is not null) - { - program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(seriesTimer.Id) - .ToString("N", CultureInfo.InvariantCulture); - } - } - } - - internal Task RefreshChannels(IProgress<double> progress, CancellationToken cancellationToken) - { - return RefreshChannelsInternal(progress, cancellationToken); - } - - private async Task RefreshChannelsInternal(IProgress<double> 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<Guid>(); - var newProgramIdList = new List<Guid>(); - - var cleanDatabase = true; - - foreach (var service in _services) - { - cancellationToken.ThrowIfCancellationRequested(); - - _logger.LogDebug("Refreshing guide from {Name}", service.Name); - - try - { - var innerProgress = new ActionableProgress<double>(); - 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[] { BaseItemKind.LiveTvChannel }, progress, cancellationToken); - CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { BaseItemKind.LiveTvProgram }, progress, cancellationToken); - } - - var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault(); - - if (coreService is not 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<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 = 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<Guid>(); - var channels = new List<Guid>(); - - 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[] { BaseItemKind.LiveTvProgram }, - ChannelIds = new Guid[] { currentChannel.Id }, - DtoOptions = new DtoOptions(true) - }).Cast<LiveTvProgram>().ToDictionary(i => i.Id); - - var newPrograms = new List<LiveTvProgram>(); - var updatedPrograms = new List<BaseItem>(); - - 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<Guid>, List<Guid>>(channels, programs); - } - - private void CleanDatabaseInternal(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.Equals(default)) - { - // 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 double GetGuideDays() - { - var config = GetConfiguration(); - - if (config.GuideDays.HasValue) - { - return Math.Max(1, Math.Min(config.GuideDays.Value, MaxGuideDays)); - } - - return 7; - } - - private async Task<QueryResult<BaseItem>> GetEmbyRecordingsAsync(RecordingQuery query, DtoOptions dtoOptions, User user) - { - if (user is null) - { - return new QueryResult<BaseItem>(); - } - - var folders = await GetRecordingFoldersAsync(user, true).ConfigureAwait(false); - var folderIds = Array.ConvertAll(folders, x => x.Id); - - var excludeItemTypes = new List<BaseItemKind>(); - - if (folderIds.Length == 0) - { - return new QueryResult<BaseItem>(); - } - - var includeItemTypes = new List<BaseItemKind>(); - var genres = new List<string>(); - - if (query.IsMovie.HasValue) - { - if (query.IsMovie.Value) - { - includeItemTypes.Add(BaseItemKind.Movie); - } - else - { - excludeItemTypes.Add(BaseItemKind.Movie); - } - } - - if (query.IsSeries.HasValue) - { - if (query.IsSeries.Value) - { - includeItemTypes.Add(BaseItemKind.Episode); - } - else - { - excludeItemTypes.Add(BaseItemKind.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 is not null).ToArray(); - - // return new QueryResult<BaseItem> - // { - // 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, - 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<Video>() - .Where(i => !i.IsCompleteMedia) - .ToArray(); - - result.TotalRecordCount = result.Items.Count; - } - - return result; - } - - public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem Item, BaseItemDto ItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null) - { - var programTuples = new List<(BaseItemDto Dto, string ExternalId, string ExternalSeriesId)>(); - var hasChannelImage = fields.Contains(ItemFields.ChannelImage); - var hasChannelInfo = fields.Contains(ItemFields.ChannelInfo); - - foreach (var (item, dto) in programs) - { - var program = (LiveTvProgram)item; - - dto.StartDate = program.StartDate; - dto.EpisodeTitle = program.EpisodeTitle; - dto.IsRepeat |= program.IsRepeat; - dto.IsMovie |= program.IsMovie; - dto.IsSeries |= program.IsSeries; - dto.IsSports |= program.IsSports; - dto.IsLive |= program.IsLive; - dto.IsNews |= program.IsNews; - dto.IsKids |= program.IsKids; - dto.IsPremiere |= program.IsPremiere; - - if (hasChannelInfo || hasChannelImage) - { - var channel = _libraryManager.GetItemById(program.ChannelId); - - if (channel is LiveTvChannel liveChannel) - { - dto.ChannelName = liveChannel.Name; - dto.MediaType = liveChannel.MediaType; - dto.ChannelNumber = liveChannel.Number; - - if (hasChannelImage && liveChannel.HasImage(ImageType.Primary)) - { - dto.ChannelPrimaryImageTag = _tvDtoService.GetImageTag(liveChannel); - } - } - } - - programTuples.Add((dto, program.ExternalId, program.ExternalSeriesId)); - } - - return AddRecordingInfo(programTuples, CancellationToken.None); - } - - public ActiveRecordingInfo GetActiveRecordingInfo(string path) - { - return EmbyTV.EmbyTV.Current.GetActiveRecordingInfo(path); - } - - public void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, ActiveRecordingInfo activeRecordingInfo, User user = null) - { - var service = EmbyTV.EmbyTV.Current; - - var info = activeRecordingInfo.Timer; - - var channel = string.IsNullOrWhiteSpace(info.ChannelId) ? null : _libraryManager.GetItemById(_tvDtoService.GetInternalChannelId(service.Name, info.ChannelId)); - - dto.SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId) - ? null - : _tvDtoService.GetInternalSeriesTimerId(info.SeriesTimerId).ToString("N", CultureInfo.InvariantCulture); - - dto.TimerId = string.IsNullOrEmpty(info.Id) - ? null - : _tvDtoService.GetInternalTimerId(info.Id); - - var startDate = info.StartDate; - var endDate = info.EndDate; - - dto.StartDate = startDate; - dto.EndDate = endDate; - dto.Status = info.Status.ToString(); - dto.IsRepeat = info.IsRepeat; - dto.EpisodeTitle = info.EpisodeTitle; - dto.IsMovie = info.IsMovie; - dto.IsSeries = info.IsSeries; - dto.IsSports = info.IsSports; - dto.IsLive = info.IsLive; - dto.IsNews = info.IsNews; - dto.IsKids = info.IsKids; - dto.IsPremiere = info.IsPremiere; - - if (info.Status == RecordingStatus.InProgress) - { - startDate = info.StartDate.AddSeconds(0 - info.PrePaddingSeconds); - endDate = info.EndDate.AddSeconds(info.PostPaddingSeconds); - - var now = DateTime.UtcNow.Ticks; - var start = startDate.Ticks; - var end = endDate.Ticks; - - var pct = now - start; - - pct /= end; - pct *= 100; - dto.CompletionPercentage = pct; - } - - if (channel is not null) - { - dto.ChannelName = channel.Name; - - if (channel.HasImage(ImageType.Primary)) - { - dto.ChannelPrimaryImageTag = _tvDtoService.GetImageTag(channel); - } - } - } - - public async Task<QueryResult<BaseItemDto>> GetRecordingsAsync(RecordingQuery query, DtoOptions options) - { - var user = query.UserId.Equals(default) - ? null - : _userManager.GetUserById(query.UserId); - - RemoveFields(options); - - var internalResult = await GetEmbyRecordingsAsync(query, options, user).ConfigureAwait(false); - - var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user); - - return new QueryResult<BaseItemDto>( - query.StartIndex, - internalResult.TotalRecordCount, - returnArray); - } - - private async Task<QueryResult<TimerInfo>> GetTimersInternal(TimerQuery query, CancellationToken cancellationToken) - { - var tasks = _services.Select(async i => - { - try - { - var recs = await i.GetTimersAsync(cancellationToken).ConfigureAwait(false); - return recs.Select(r => new Tuple<TimerInfo, ILiveTvService>(r, i)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting recordings"); - return new List<Tuple<TimerInfo, ILiveTvService>>(); - } - }); - var results = await Task.WhenAll(tasks).ConfigureAwait(false); - var timers = results.SelectMany(i => i.ToList()); - - if (query.IsActive.HasValue) - { - if (query.IsActive.Value) - { - timers = timers.Where(i => i.Item1.Status == RecordingStatus.InProgress); - } - else - { - timers = timers.Where(i => i.Item1.Status != RecordingStatus.InProgress); - } - } - - if (query.IsScheduled.HasValue) - { - if (query.IsScheduled.Value) - { - timers = timers.Where(i => i.Item1.Status == RecordingStatus.New); - } - else - { - timers = timers.Where(i => i.Item1.Status != RecordingStatus.New); - } - } - - if (!string.IsNullOrEmpty(query.ChannelId)) - { - var guid = new Guid(query.ChannelId); - timers = timers.Where(i => _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId).Equals(guid)); - } - - if (!string.IsNullOrEmpty(query.SeriesTimerId)) - { - var guid = new Guid(query.SeriesTimerId); - - timers = timers - .Where(i => _tvDtoService.GetInternalSeriesTimerId(i.Item1.SeriesTimerId).Equals(guid)); - } - - if (!string.IsNullOrEmpty(query.Id)) - { - timers = timers - .Where(i => string.Equals(_tvDtoService.GetInternalTimerId(i.Item1.Id), query.Id, StringComparison.OrdinalIgnoreCase)); - } - - var returnArray = timers - .Select(i => i.Item1) - .OrderBy(i => i.StartDate) - .ToArray(); - - return new QueryResult<TimerInfo>(returnArray); - } - - public async Task<QueryResult<TimerInfoDto>> GetTimers(TimerQuery query, CancellationToken cancellationToken) - { - var tasks = _services.Select(async i => - { - try - { - var recs = await i.GetTimersAsync(cancellationToken).ConfigureAwait(false); - return recs.Select(r => new Tuple<TimerInfo, ILiveTvService>(r, i)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting recordings"); - return new List<Tuple<TimerInfo, ILiveTvService>>(); - } - }); - var results = await Task.WhenAll(tasks).ConfigureAwait(false); - var timers = results.SelectMany(i => i.ToList()); - - if (query.IsActive.HasValue) - { - if (query.IsActive.Value) - { - timers = timers.Where(i => i.Item1.Status == RecordingStatus.InProgress); - } - else - { - timers = timers.Where(i => i.Item1.Status != RecordingStatus.InProgress); - } - } - - if (query.IsScheduled.HasValue) - { - if (query.IsScheduled.Value) - { - timers = timers.Where(i => i.Item1.Status == RecordingStatus.New); - } - else - { - timers = timers.Where(i => i.Item1.Status != RecordingStatus.New); - } - } - - if (!string.IsNullOrEmpty(query.ChannelId)) - { - var guid = new Guid(query.ChannelId); - timers = timers.Where(i => _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId).Equals(guid)); - } - - if (!string.IsNullOrEmpty(query.SeriesTimerId)) - { - var guid = new Guid(query.SeriesTimerId); - - timers = timers - .Where(i => _tvDtoService.GetInternalSeriesTimerId(i.Item1.SeriesTimerId).Equals(guid)); - } - - if (!string.IsNullOrEmpty(query.Id)) - { - timers = timers - .Where(i => string.Equals(_tvDtoService.GetInternalTimerId(i.Item1.Id), query.Id, StringComparison.OrdinalIgnoreCase)); - } - - var returnList = new List<TimerInfoDto>(); - - foreach (var i in timers) - { - var program = string.IsNullOrEmpty(i.Item1.ProgramId) ? - null : - _libraryManager.GetItemById(_tvDtoService.GetInternalProgramId(i.Item1.ProgramId)) as LiveTvProgram; - - var channel = string.IsNullOrEmpty(i.Item1.ChannelId) ? null : _libraryManager.GetItemById(_tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId)); - - returnList.Add(_tvDtoService.GetTimerInfoDto(i.Item1, i.Item2, program, channel)); - } - - var returnArray = returnList - .OrderBy(i => i.StartDate) - .ToArray(); - - return new QueryResult<TimerInfoDto>(returnArray); - } - - public async Task CancelTimer(string id) - { - var timer = await GetTimer(id, CancellationToken.None).ConfigureAwait(false); - - if (timer is null) - { - throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "Timer with Id {0} not found", id)); - } - - var service = GetService(timer.ServiceName); - - await service.CancelTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false); - - if (service is not EmbyTV.EmbyTV) - { - TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(id))); - } - } - - public async Task CancelSeriesTimer(string id) - { - var timer = await GetSeriesTimer(id, CancellationToken.None).ConfigureAwait(false); - - if (timer is null) - { - throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "SeriesTimer with Id {0} not found", id)); - } - - var service = GetService(timer.ServiceName); - - await service.CancelSeriesTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false); - - SeriesTimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(id))); - } - - public async Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken) - { - var results = await GetTimers( - new TimerQuery - { - Id = id - }, - cancellationToken).ConfigureAwait(false); - - return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); - } - - public async Task<SeriesTimerInfoDto> GetSeriesTimer(string id, CancellationToken cancellationToken) - { - var results = await GetSeriesTimers(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false); - - return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); - } - - private async Task<QueryResult<SeriesTimerInfo>> GetSeriesTimersInternal(SeriesTimerQuery query, CancellationToken cancellationToken) - { - var tasks = _services.Select(async i => - { - try - { - var recs = await i.GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false); - return recs.Select(r => - { - r.ServiceName = i.Name; - return new Tuple<SeriesTimerInfo, ILiveTvService>(r, i); - }); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting recordings"); - return new List<Tuple<SeriesTimerInfo, ILiveTvService>>(); - } - }); - var results = await Task.WhenAll(tasks).ConfigureAwait(false); - var timers = results.SelectMany(i => i.ToList()); - - if (string.Equals(query.SortBy, "Priority", StringComparison.OrdinalIgnoreCase)) - { - timers = query.SortOrder == SortOrder.Descending ? - timers.OrderBy(i => i.Item1.Priority).ThenByStringDescending(i => i.Item1.Name) : - timers.OrderByDescending(i => i.Item1.Priority).ThenByString(i => i.Item1.Name); - } - else - { - timers = query.SortOrder == SortOrder.Descending ? - timers.OrderByStringDescending(i => i.Item1.Name) : - timers.OrderByString(i => i.Item1.Name); - } - - var returnArray = timers - .Select(i => i.Item1) - .ToArray(); - - return new QueryResult<SeriesTimerInfo>(returnArray); - } - - public async Task<QueryResult<SeriesTimerInfoDto>> GetSeriesTimers(SeriesTimerQuery query, CancellationToken cancellationToken) - { - var tasks = _services.Select(async i => - { - try - { - var recs = await i.GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false); - return recs.Select(r => new Tuple<SeriesTimerInfo, ILiveTvService>(r, i)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting recordings"); - return new List<Tuple<SeriesTimerInfo, ILiveTvService>>(); - } - }); - var results = await Task.WhenAll(tasks).ConfigureAwait(false); - var timers = results.SelectMany(i => i.ToList()); - - if (string.Equals(query.SortBy, "Priority", StringComparison.OrdinalIgnoreCase)) - { - timers = query.SortOrder == SortOrder.Descending ? - timers.OrderBy(i => i.Item1.Priority).ThenByStringDescending(i => i.Item1.Name) : - timers.OrderByDescending(i => i.Item1.Priority).ThenByString(i => i.Item1.Name); - } - else - { - timers = query.SortOrder == SortOrder.Descending ? - timers.OrderByStringDescending(i => i.Item1.Name) : - timers.OrderByString(i => i.Item1.Name); - } - - var returnArray = timers - .Select(i => - { - string channelName = null; - - if (!string.IsNullOrEmpty(i.Item1.ChannelId)) - { - var internalChannelId = _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId); - var channel = _libraryManager.GetItemById(internalChannelId); - channelName = channel is null ? null : channel.Name; - } - - return _tvDtoService.GetSeriesTimerInfoDto(i.Item1, i.Item2, channelName); - }) - .ToArray(); - - return new QueryResult<SeriesTimerInfoDto>(returnArray); - } - - public BaseItem GetLiveTvChannel(TimerInfo timer, ILiveTvService service) - { - var internalChannelId = _tvDtoService.GetInternalChannelId(service.Name, timer.ChannelId); - return _libraryManager.GetItemById(internalChannelId); - } - - public void AddChannelInfo(IReadOnlyCollection<(BaseItemDto ItemDto, LiveTvChannel Channel)> items, DtoOptions options, User user) - { - var now = DateTime.UtcNow; - - var channelIds = items.Select(i => i.Channel.Id).Distinct().ToArray(); - - var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user) - { - IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram }, - ChannelIds = channelIds, - MaxStartDate = now, - MinEndDate = now, - Limit = channelIds.Length, - OrderBy = new[] { (ItemSortBy.StartDate, SortOrder.Ascending) }, - TopParentIds = new[] { GetInternalLiveTvFolder(CancellationToken.None).Id }, - DtoOptions = options - }) : new List<BaseItem>(); - - RemoveFields(options); - - var currentProgramsList = new List<BaseItem>(); - var currentChannelsDict = new Dictionary<Guid, BaseItemDto>(); - - var addCurrentProgram = options.AddCurrentProgram; - - foreach (var (dto, channel) in items) - { - dto.Number = channel.Number; - dto.ChannelNumber = channel.Number; - dto.ChannelType = channel.ChannelType; - - currentChannelsDict[dto.Id] = dto; - - if (addCurrentProgram) - { - var currentProgram = programs.FirstOrDefault(i => channel.Id.Equals(i.ChannelId)); - - if (currentProgram is not null) - { - currentProgramsList.Add(currentProgram); - } - } - } - - if (addCurrentProgram) - { - var currentProgramDtos = _dtoService.GetBaseItemDtos(currentProgramsList, options, user); - - foreach (var programDto in currentProgramDtos) - { - if (programDto.ChannelId.HasValue && currentChannelsDict.TryGetValue(programDto.ChannelId.Value, out BaseItemDto channelDto)) - { - channelDto.CurrentProgram = programDto; - } - } - } - } - - private async Task<Tuple<SeriesTimerInfo, ILiveTvService>> GetNewTimerDefaultsInternal(CancellationToken cancellationToken, LiveTvProgram program = null) - { - ILiveTvService service = null; - ProgramInfo programInfo = null; - - if (program is not null) - { - service = GetService(program); - - var channel = _libraryManager.GetItemById(program.ChannelId); - - programInfo = new ProgramInfo - { - Audio = program.Audio, - ChannelId = channel.ExternalId, - CommunityRating = program.CommunityRating, - EndDate = program.EndDate ?? DateTime.MinValue, - EpisodeTitle = program.EpisodeTitle, - Genres = program.Genres.ToList(), - Id = program.ExternalId, - IsHD = program.IsHD, - IsKids = program.IsKids, - IsLive = program.IsLive, - IsMovie = program.IsMovie, - IsNews = program.IsNews, - IsPremiere = program.IsPremiere, - IsRepeat = program.IsRepeat, - IsSeries = program.IsSeries, - IsSports = program.IsSports, - OriginalAirDate = program.PremiereDate, - Overview = program.Overview, - StartDate = program.StartDate, - // ImagePath = program.ExternalImagePath, - Name = program.Name, - OfficialRating = program.OfficialRating - }; - } - - service ??= _services[0]; - - var info = await service.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false); - - info.RecordAnyTime = true; - info.Days = new List<DayOfWeek> - { - DayOfWeek.Sunday, - DayOfWeek.Monday, - DayOfWeek.Tuesday, - DayOfWeek.Wednesday, - DayOfWeek.Thursday, - DayOfWeek.Friday, - DayOfWeek.Saturday - }; - - info.Id = null; - - return new Tuple<SeriesTimerInfo, ILiveTvService>(info, service); - } - - public async Task<SeriesTimerInfoDto> GetNewTimerDefaults(CancellationToken cancellationToken) - { - var info = await GetNewTimerDefaultsInternal(cancellationToken).ConfigureAwait(false); - - return _tvDtoService.GetSeriesTimerInfoDto(info.Item1, info.Item2, null); - } - - public async Task<SeriesTimerInfoDto> GetNewTimerDefaults(string programId, CancellationToken cancellationToken) - { - var program = (LiveTvProgram)_libraryManager.GetItemById(programId); - var programDto = await GetProgram(programId, cancellationToken).ConfigureAwait(false); - - var defaults = await GetNewTimerDefaultsInternal(cancellationToken, program).ConfigureAwait(false); - var info = _tvDtoService.GetSeriesTimerInfoDto(defaults.Item1, defaults.Item2, null); - - info.Days = defaults.Item1.Days.ToArray(); - - info.DayPattern = _tvDtoService.GetDayPattern(info.Days); - - info.Name = program.Name; - info.ChannelId = programDto.ChannelId ?? Guid.Empty; - info.ChannelName = programDto.ChannelName; - info.StartDate = program.StartDate; - info.Name = program.Name; - info.Overview = program.Overview; - info.ProgramId = programDto.Id.ToString("N", CultureInfo.InvariantCulture); - info.ExternalProgramId = program.ExternalId; - - if (program.EndDate.HasValue) - { - info.EndDate = program.EndDate.Value; - } - - return info; - } - - public async Task CreateTimer(TimerInfoDto timer, CancellationToken cancellationToken) - { - var service = GetService(timer.ServiceName); - - var info = await _tvDtoService.GetTimerInfo(timer, true, this, cancellationToken).ConfigureAwait(false); - - // Set priority from default values - var defaultValues = await service.GetNewTimerDefaultsAsync(cancellationToken).ConfigureAwait(false); - info.Priority = defaultValues.Priority; - - string newTimerId = null; - if (service is ISupportsNewTimerIds supportsNewTimerIds) - { - newTimerId = await supportsNewTimerIds.CreateTimer(info, cancellationToken).ConfigureAwait(false); - newTimerId = _tvDtoService.GetInternalTimerId(newTimerId); - } - else - { - await service.CreateTimerAsync(info, cancellationToken).ConfigureAwait(false); - } - - _logger.LogInformation("New recording scheduled"); - - if (service is not EmbyTV.EmbyTV) - { - TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>( - new TimerEventInfo(newTimerId) - { - ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId) - })); - } - } - - public async Task CreateSeriesTimer(SeriesTimerInfoDto timer, CancellationToken cancellationToken) - { - var service = GetService(timer.ServiceName); - - var info = await _tvDtoService.GetSeriesTimerInfo(timer, true, this, cancellationToken).ConfigureAwait(false); - - // Set priority from default values - var defaultValues = await service.GetNewTimerDefaultsAsync(cancellationToken).ConfigureAwait(false); - info.Priority = defaultValues.Priority; - - string newTimerId = null; - if (service is ISupportsNewTimerIds supportsNewTimerIds) - { - newTimerId = await supportsNewTimerIds.CreateSeriesTimer(info, cancellationToken).ConfigureAwait(false); - newTimerId = _tvDtoService.GetInternalSeriesTimerId(newTimerId).ToString("N", CultureInfo.InvariantCulture); - } - else - { - await service.CreateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false); - } - - SeriesTimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>( - new TimerEventInfo(newTimerId) - { - ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId) - })); - } - - public async Task UpdateTimer(TimerInfoDto timer, CancellationToken cancellationToken) - { - var info = await _tvDtoService.GetTimerInfo(timer, false, this, cancellationToken).ConfigureAwait(false); - - var service = GetService(timer.ServiceName); - - await service.UpdateTimerAsync(info, cancellationToken).ConfigureAwait(false); - } - - public async Task UpdateSeriesTimer(SeriesTimerInfoDto timer, CancellationToken cancellationToken) - { - var info = await _tvDtoService.GetSeriesTimerInfo(timer, false, this, cancellationToken).ConfigureAwait(false); - - var service = GetService(timer.ServiceName); - - await service.UpdateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false); - } - - public GuideInfo GetGuideInfo() - { - var startDate = DateTime.UtcNow; - var endDate = startDate.AddDays(GetGuideDays()); - - return new GuideInfo - { - StartDate = startDate, - EndDate = endDate - }; - } - - private LiveTvServiceInfo[] GetServiceInfos() - { - return Services.Select(GetServiceInfo).ToArray(); - } - - private static LiveTvServiceInfo GetServiceInfo(ILiveTvService service) - { - return new LiveTvServiceInfo - { - Name = service.Name - }; - } - - public LiveTvInfo GetLiveTvInfo(CancellationToken cancellationToken) - { - var services = GetServiceInfos(); - - var info = new LiveTvInfo - { - Services = services, - IsEnabled = services.Length > 0, - EnabledUsers = _userManager.Users - .Where(IsLiveTvEnabled) - .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) - .ToArray() - }; - - return info; - } - - private bool IsLiveTvEnabled(User user) - { - return user.HasPermission(PermissionKind.EnableLiveTvAccess) && (Services.Count > 1 || GetConfiguration().TunerHosts.Length > 0); - } - - public IEnumerable<User> GetEnabledUsers() - { - return _userManager.Users - .Where(IsLiveTvEnabled); - } - - /// <summary> - /// Resets the tuner. - /// </summary> - /// <param name="id">The identifier.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - public Task ResetTuner(string id, CancellationToken cancellationToken) - { - var parts = id.Split('_', 2); - - var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), parts[0], StringComparison.OrdinalIgnoreCase)); - - if (service is null) - { - throw new ArgumentException("Service not found."); - } - - return service.ResetTuner(parts[1], cancellationToken); - } - - private static void RemoveFields(DtoOptions options) - { - var fields = options.Fields.ToList(); - - fields.Remove(ItemFields.CanDelete); - fields.Remove(ItemFields.CanDownload); - fields.Remove(ItemFields.DisplayPreferencesId); - fields.Remove(ItemFields.Etag); - options.Fields = fields.ToArray(); - } - - public Folder GetInternalLiveTvFolder(CancellationToken cancellationToken) - { - var name = _localization.GetLocalizedString("HeaderLiveTV"); - return _libraryManager.GetNamedView(name, CollectionType.LiveTv, name); - } - - public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true) - { - info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.SerializeToUtf8Bytes(info)); - - var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); - - if (provider is null) - { - throw new ResourceNotFoundException(); - } - - if (provider is IConfigurableTunerHost configurable) - { - await configurable.Validate(info).ConfigureAwait(false); - } - - var config = GetConfiguration(); - - var list = config.TunerHosts.ToList(); - var index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase)); - - if (index == -1 || string.IsNullOrWhiteSpace(info.Id)) - { - info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - list.Add(info); - config.TunerHosts = list.ToArray(); - } - else - { - config.TunerHosts[index] = info; - } - - _config.SaveConfiguration("livetv", config); - - if (dataSourceChanged) - { - _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>(); - } - - return info; - } - - public async Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings) - { - // Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider - // ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider - info = JsonSerializer.Deserialize<ListingsProviderInfo>(JsonSerializer.SerializeToUtf8Bytes(info)); - - var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); - - if (provider is null) - { - throw new ResourceNotFoundException( - string.Format( - CultureInfo.InvariantCulture, - "Couldn't find provider of type: '{0}'", - info.Type)); - } - - await provider.Validate(info, validateLogin, validateListings).ConfigureAwait(false); - - LiveTvOptions config = GetConfiguration(); - - var list = config.ListingProviders.ToList(); - int index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase)); - - if (index == -1 || string.IsNullOrWhiteSpace(info.Id)) - { - info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - list.Add(info); - config.ListingProviders = list.ToArray(); - } - else - { - config.ListingProviders[index] = info; - } - - _config.SaveConfiguration("livetv", config); - - _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>(); - - return info; - } - - public void DeleteListingsProvider(string id) - { - var config = GetConfiguration(); - - config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray(); - - _config.SaveConfiguration("livetv", config); - _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>(); - } - - public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber) - { - var config = GetConfiguration(); - - var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase)); - listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray(); - - if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase)) - { - var list = listingsProviderInfo.ChannelMappings.ToList(); - list.Add(new NameValuePair - { - Name = tunerChannelNumber, - Value = providerChannelNumber - }); - listingsProviderInfo.ChannelMappings = list.ToArray(); - } - - _config.SaveConfiguration("livetv", config); - - var tunerChannels = await GetChannelsForListingsProvider(providerId, CancellationToken.None) - .ConfigureAwait(false); - - var providerChannels = await GetChannelsFromListingsProviderData(providerId, CancellationToken.None) - .ConfigureAwait(false); - - var mappings = listingsProviderInfo.ChannelMappings; - - var tunerChannelMappings = - tunerChannels.Select(i => GetTunerChannelMapping(i, mappings, providerChannels)).ToList(); - - _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>(); - - return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)); - } - - public TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> providerChannels) - { - var result = new TunerChannelMapping - { - Name = tunerChannel.Name, - Id = tunerChannel.Id - }; - - if (!string.IsNullOrWhiteSpace(tunerChannel.Number)) - { - result.Name = tunerChannel.Number + " " + result.Name; - } - - var providerChannel = EmbyTV.EmbyTV.Current.GetEpgChannelFromTunerChannel(mappings, tunerChannel, providerChannels); - - if (providerChannel is not null) - { - result.ProviderChannelName = providerChannel.Name; - result.ProviderChannelId = providerChannel.Id; - } - - return result; - } - - public Task<List<NameIdPair>> GetLineups(string providerType, string providerId, string country, string location) - { - var config = GetConfiguration(); - - if (string.IsNullOrWhiteSpace(providerId)) - { - var provider = _listingProviders.FirstOrDefault(i => string.Equals(providerType, i.Type, StringComparison.OrdinalIgnoreCase)); - - if (provider is null) - { - throw new ResourceNotFoundException(); - } - - return provider.GetLineups(null, country, location); - } - else - { - var info = config.ListingProviders.FirstOrDefault(i => string.Equals(i.Id, providerId, StringComparison.OrdinalIgnoreCase)); - - var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); - - if (provider is null) - { - throw new ResourceNotFoundException(); - } - - return provider.GetLineups(info, country, location); - } - } - - public Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken) - { - var info = GetConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); - return EmbyTV.EmbyTV.Current.GetChannelsForListingsProvider(info, cancellationToken); - } - - public Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken) - { - var info = GetConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); - var provider = _listingProviders.First(i => string.Equals(i.Type, info.Type, StringComparison.OrdinalIgnoreCase)); - return provider.GetChannels(info, cancellationToken); - } - - public Guid GetInternalChannelId(string serviceName, string externalId) - { - return _tvDtoService.GetInternalChannelId(serviceName, externalId); - } - - public Guid GetInternalProgramId(string externalId) - { - return _tvDtoService.GetInternalProgramId(externalId); - } - - /// <inheritdoc /> - public Task<BaseItem[]> GetRecordingFoldersAsync(User user) - => GetRecordingFoldersAsync(user, false); - - private async Task<BaseItem[]> GetRecordingFoldersAsync(User user, bool refreshChannels) - { - var folders = EmbyTV.EmbyTV.Current.GetRecordingFolders() - .SelectMany(i => i.Locations) - .Distinct(StringComparer.OrdinalIgnoreCase) - .Select(i => _libraryManager.FindByPath(i, true)) - .Where(i => i is not null && i.IsVisibleStandalone(user)) - .SelectMany(i => _libraryManager.GetCollectionFolders(i)) - .DistinctBy(x => x.Id) - .OrderBy(i => i.SortName) - .ToList(); - - var channels = await _channelManager.GetChannelsInternalAsync(new MediaBrowser.Model.Channels.ChannelQuery - { - UserId = user.Id, - IsRecordingsFolder = true, - RefreshLatestChannelItems = refreshChannels - }).ConfigureAwait(false); - - folders.AddRange(channels.Items); - - return folders.Cast<BaseItem>().ToArray(); - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs deleted file mode 100644 index 6a92fc599..000000000 --- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ /dev/null @@ -1,128 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.MediaInfo; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.LiveTv -{ - public class LiveTvMediaSourceProvider : IMediaSourceProvider - { - // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message. - private const char StreamIdDelimiter = '_'; - - private readonly ILiveTvManager _liveTvManager; - private readonly ILogger<LiveTvMediaSourceProvider> _logger; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IServerApplicationHost _appHost; - - public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, ILogger<LiveTvMediaSourceProvider> logger, IMediaSourceManager mediaSourceManager, IServerApplicationHost appHost) - { - _liveTvManager = liveTvManager; - _logger = logger; - _mediaSourceManager = mediaSourceManager; - _appHost = appHost; - } - - public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(BaseItem item, CancellationToken cancellationToken) - { - if (item.SourceType == SourceType.LiveTV) - { - var activeRecordingInfo = _liveTvManager.GetActiveRecordingInfo(item.Path); - - if (string.IsNullOrEmpty(item.Path) || activeRecordingInfo is not null) - { - return GetMediaSourcesInternal(item, activeRecordingInfo, cancellationToken); - } - } - - return Task.FromResult(Enumerable.Empty<MediaSourceInfo>()); - } - - private async Task<IEnumerable<MediaSourceInfo>> GetMediaSourcesInternal(BaseItem item, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken) - { - IEnumerable<MediaSourceInfo> sources; - - var forceRequireOpening = false; - - try - { - if (activeRecordingInfo is not null) - { - sources = await EmbyTV.EmbyTV.Current.GetRecordingStreamMediaSources(activeRecordingInfo, cancellationToken) - .ConfigureAwait(false); - } - else - { - sources = await _liveTvManager.GetChannelMediaSources(item, cancellationToken) - .ConfigureAwait(false); - } - } - catch (NotImplementedException) - { - sources = _mediaSourceManager.GetStaticMediaSources(item, false); - - forceRequireOpening = true; - } - - var list = sources.ToList(); - - foreach (var source in list) - { - source.Type = MediaSourceType.Default; - source.BufferMs ??= 1500; - - if (source.RequiresOpening || forceRequireOpening) - { - source.RequiresOpening = true; - } - - if (source.RequiresOpening) - { - var openKeys = new List<string> - { - item.GetType().Name, - item.Id.ToString("N", CultureInfo.InvariantCulture), - source.Id ?? string.Empty - }; - - source.OpenToken = string.Join(StreamIdDelimiter, openKeys); - } - - // Dummy this up so that direct play checks can still run - if (string.IsNullOrEmpty(source.Path) && source.Protocol == MediaProtocol.Http) - { - source.Path = _appHost.GetApiUrlForLocalAccess(); - } - } - - _logger.LogDebug("MediaSources: {@MediaSources}", list); - - return list; - } - - /// <inheritdoc /> - public async Task<ILiveStream> OpenMediaSource(string openToken, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) - { - var keys = openToken.Split(StreamIdDelimiter, 3); - var mediaSourceId = keys.Length >= 3 ? keys[2] : null; - - var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false); - var liveStream = info.Item2; - - return liveStream; - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs b/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs deleted file mode 100644 index 72bbdd14a..000000000 --- a/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.LiveTv; -using MediaBrowser.Model.Tasks; - -namespace Emby.Server.Implementations.LiveTv -{ - /// <summary> - /// The "Refresh Guide" scheduled task. - /// </summary> - public class RefreshGuideScheduledTask : IScheduledTask, IConfigurableScheduledTask - { - private readonly ILiveTvManager _liveTvManager; - private readonly IConfigurationManager _config; - - /// <summary> - /// Initializes a new instance of the <see cref="RefreshGuideScheduledTask"/> class. - /// </summary> - /// <param name="liveTvManager">The live tv manager.</param> - /// <param name="config">The configuration manager.</param> - public RefreshGuideScheduledTask(ILiveTvManager liveTvManager, IConfigurationManager config) - { - _liveTvManager = liveTvManager; - _config = config; - } - - /// <inheritdoc /> - public string Name => "Refresh Guide"; - - /// <inheritdoc /> - public string Description => "Downloads channel information from live tv services."; - - /// <inheritdoc /> - public string Category => "Live TV"; - - /// <inheritdoc /> - public bool IsHidden => _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Length == 0; - - /// <inheritdoc /> - public bool IsEnabled => true; - - /// <inheritdoc /> - public bool IsLogged => true; - - /// <inheritdoc /> - public string Key => "RefreshGuide"; - - /// <inheritdoc /> - public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) - { - var manager = (LiveTvManager)_liveTvManager; - - return manager.RefreshChannels(progress, cancellationToken); - } - - /// <inheritdoc /> - public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() - { - return new[] - { - // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks } - }; - } - - private LiveTvOptions GetConfiguration() - { - return _config.GetConfiguration<LiveTvOptions>("livetv"); - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs deleted file mode 100644 index ff25ee585..000000000 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ /dev/null @@ -1,230 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.LiveTv; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.LiveTv.TunerHosts -{ - public abstract class BaseTunerHost - { - private readonly ConcurrentDictionary<string, List<ChannelInfo>> _cache; - - protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem) - { - Config = config; - Logger = logger; - FileSystem = fileSystem; - _cache = new ConcurrentDictionary<string, List<ChannelInfo>>(); - } - - protected IServerConfigurationManager Config { get; } - - protected ILogger<BaseTunerHost> Logger { get; } - - protected IFileSystem FileSystem { get; } - - public virtual bool IsSupported => true; - - public abstract string Type { get; } - - protected virtual string ChannelIdPrefix => Type + "_"; - - protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken); - - public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken) - { - var key = tuner.Id; - - if (enableCache && !string.IsNullOrEmpty(key) && _cache.TryGetValue(key, out List<ChannelInfo> cache)) - { - return cache; - } - - var list = await GetChannelsInternal(tuner, cancellationToken).ConfigureAwait(false); - // logger.LogInformation("Channels from {0}: {1}", tuner.Url, JsonSerializer.SerializeToString(list)); - - if (!string.IsNullOrEmpty(key) && list.Count > 0) - { - _cache[key] = list; - } - - return list; - } - - protected virtual List<TunerHostInfo> GetTunerHosts() - { - return GetConfiguration().TunerHosts - .Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase)) - .ToList(); - } - - public async Task<List<ChannelInfo>> GetChannels(bool enableCache, CancellationToken cancellationToken) - { - var list = new List<ChannelInfo>(); - - var hosts = GetTunerHosts(); - - foreach (var host in hosts) - { - var channelCacheFile = Path.Combine(Config.ApplicationPaths.CachePath, host.Id + "_channels"); - - try - { - var channels = await GetChannels(host, enableCache, cancellationToken).ConfigureAwait(false); - var newChannels = channels.Where(i => !list.Any(l => string.Equals(i.Id, l.Id, StringComparison.OrdinalIgnoreCase))).ToList(); - - list.AddRange(newChannels); - - if (!enableCache) - { - try - { - Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile)); - await using var writeStream = AsyncFile.OpenWrite(channelCacheFile); - await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false); - } - catch (IOException) - { - } - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Error getting channel list"); - - if (enableCache) - { - try - { - await using var readStream = AsyncFile.OpenRead(channelCacheFile); - var channels = await JsonSerializer.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken) - .ConfigureAwait(false); - list.AddRange(channels); - } - catch (IOException) - { - } - } - } - } - - return list; - } - - protected abstract Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, ChannelInfo channel, CancellationToken cancellationToken); - - public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken) - { - ArgumentException.ThrowIfNullOrEmpty(channelId); - - if (IsValidChannelId(channelId)) - { - var hosts = GetTunerHosts(); - - foreach (var host in hosts) - { - try - { - var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false); - var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)); - - if (channelInfo is not null) - { - return await GetChannelStreamMediaSources(host, channelInfo, cancellationToken).ConfigureAwait(false); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Error getting channels"); - } - } - } - - return new List<MediaSourceInfo>(); - } - - protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); - - public async Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) - { - ArgumentException.ThrowIfNullOrEmpty(channelId); - - if (!IsValidChannelId(channelId)) - { - throw new FileNotFoundException(); - } - - var hosts = GetTunerHosts(); - - var hostsWithChannel = new List<Tuple<TunerHostInfo, ChannelInfo>>(); - - foreach (var host in hosts) - { - try - { - var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false); - var channelInfo = channels.FirstOrDefault(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)); - - if (channelInfo is not null) - { - hostsWithChannel.Add(new Tuple<TunerHostInfo, ChannelInfo>(host, channelInfo)); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Error getting channels"); - } - } - - foreach (var hostTuple in hostsWithChannel) - { - var host = hostTuple.Item1; - var channelInfo = hostTuple.Item2; - - try - { - var liveStream = await GetChannelStream(host, channelInfo, streamId, currentLiveStreams, cancellationToken).ConfigureAwait(false); - 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); - return liveStream; - } - catch (Exception ex) - { - Logger.LogError(ex, "Error opening tuner"); - } - } - - throw new LiveTvConflictException(); - } - - protected virtual bool IsValidChannelId(string channelId) - { - ArgumentException.ThrowIfNullOrEmpty(channelId); - - return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase); - } - - protected LiveTvOptions GetConfiguration() - { - return Config.GetConfiguration<LiveTvOptions>("livetv"); - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs deleted file mode 100644 index 0f0453189..000000000 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs +++ /dev/null @@ -1,23 +0,0 @@ -#nullable disable - -namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun -{ - internal class Channels - { - public string GuideNumber { get; set; } - - public string GuideName { get; set; } - - public string VideoCodec { get; set; } - - public string AudioCodec { get; set; } - - public string URL { get; set; } - - public bool Favorite { get; set; } - - public bool DRM { get; set; } - - public bool HD { get; set; } - } -} diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs deleted file mode 100644 index 42068cd34..000000000 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs +++ /dev/null @@ -1,42 +0,0 @@ -#nullable disable - -using System; - -namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun -{ - internal class DiscoverResponse - { - public string FriendlyName { get; set; } - - public string ModelNumber { get; set; } - - public string FirmwareName { get; set; } - - public string FirmwareVersion { get; set; } - - public string DeviceID { get; set; } - - public string DeviceAuth { get; set; } - - public string BaseURL { get; set; } - - public string LineupURL { get; set; } - - public int TunerCount { get; set; } - - public bool SupportsTranscoding - { - get - { - var model = ModelNumber ?? string.Empty; - - if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1) - { - return true; - } - - return false; - } - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs deleted file mode 100644 index aae33503f..000000000 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs +++ /dev/null @@ -1,35 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; - -namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun -{ - public class HdHomerunChannelCommands : IHdHomerunChannelCommands - { - private string? _channel; - private string? _profile; - - public HdHomerunChannelCommands(string? channel, string? profile) - { - _channel = channel; - _profile = profile; - } - - public IEnumerable<(string CommandName, string CommandValue)> GetCommands() - { - if (!string.IsNullOrEmpty(_channel)) - { - if (!string.IsNullOrEmpty(_profile) - && !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase)) - { - yield return ("vchannel", $"{_channel} transcode={_profile}"); - } - else - { - yield return ("vchannel", _channel); - } - } - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs deleted file mode 100644 index 8cd0c4ffb..000000000 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ /dev/null @@ -1,718 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Json; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Extensions; -using Jellyfin.Extensions.Json; -using Jellyfin.Extensions.Json.Converters; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.LiveTv; -using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Net; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun -{ - public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost - { - private readonly IHttpClientFactory _httpClientFactory; - private readonly IServerApplicationHost _appHost; - private readonly ISocketFactory _socketFactory; - private readonly IStreamHelper _streamHelper; - - private readonly JsonSerializerOptions _jsonOptions; - - private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>(); - - public HdHomerunHost( - IServerConfigurationManager config, - ILogger<HdHomerunHost> logger, - IFileSystem fileSystem, - IHttpClientFactory httpClientFactory, - IServerApplicationHost appHost, - ISocketFactory socketFactory, - IStreamHelper streamHelper) - : base(config, logger, fileSystem) - { - _httpClientFactory = httpClientFactory; - _appHost = appHost; - _socketFactory = socketFactory; - _streamHelper = streamHelper; - - _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options); - _jsonOptions.Converters.Add(new JsonBoolNumberConverter()); - } - - public string Name => "HD Homerun"; - - public override string Type => "hdhomerun"; - - protected override string ChannelIdPrefix => "hdhr_"; - - private string GetChannelId(Channels i) - => ChannelIdPrefix + i.GuideNumber; - - internal async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken) - { - var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - - using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - var lineup = await response.Content.ReadFromJsonAsync<IEnumerable<Channels>>(_jsonOptions, cancellationToken).ConfigureAwait(false) ?? Enumerable.Empty<Channels>(); - if (info.ImportFavoritesOnly) - { - lineup = lineup.Where(i => i.Favorite); - } - - return lineup.Where(i => !i.DRM).ToList(); - } - - protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken) - { - var lineup = await GetLineup(tuner, cancellationToken).ConfigureAwait(false); - - return lineup.Select(i => new HdHomerunChannelInfo - { - Name = i.GuideName, - Number = i.GuideNumber, - Id = GetChannelId(i), - IsFavorite = i.Favorite, - TunerHostId = tuner.Id, - IsHD = i.HD, - AudioCodec = i.AudioCodec, - VideoCodec = i.VideoCodec, - ChannelType = ChannelType.TV, - IsLegacyTuner = (i.URL ?? string.Empty).StartsWith("hdhomerun", StringComparison.OrdinalIgnoreCase), - Path = i.URL - }).Cast<ChannelInfo>().ToList(); - } - - internal async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken) - { - var cacheKey = info.Id; - - lock (_modelCache) - { - if (!string.IsNullOrEmpty(cacheKey)) - { - if (_modelCache.TryGetValue(cacheKey, out DiscoverResponse response)) - { - return response; - } - } - } - - try - { - using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken) - .ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - var discoverResponse = await response.Content.ReadFromJsonAsync<DiscoverResponse>(_jsonOptions, cancellationToken).ConfigureAwait(false); - - if (!string.IsNullOrEmpty(cacheKey)) - { - lock (_modelCache) - { - _modelCache[cacheKey] = discoverResponse; - } - } - - return discoverResponse; - } - catch (HttpRequestException ex) - { - if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) - { - const string DefaultValue = "HDHR"; - var response = new DiscoverResponse - { - ModelNumber = DefaultValue - }; - if (!string.IsNullOrEmpty(cacheKey)) - { - // HDHR4 doesn't have this api - lock (_modelCache) - { - _modelCache[cacheKey] = response; - } - } - - return response; - } - - throw; - } - } - - private async Task<List<LiveTvTunerInfo>> GetTunerInfosHttp(TunerHostInfo info, CancellationToken cancellationToken) - { - var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - - using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken) - .ConfigureAwait(false); - var tuners = new List<LiveTvTunerInfo>(); - var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using (stream.ConfigureAwait(false)) - { - using var sr = new StreamReader(stream, System.Text.Encoding.UTF8); - await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false)) - { - string stripedLine = StripXML(line); - if (stripedLine.Contains("Channel", StringComparison.Ordinal)) - { - LiveTvTunerStatus status; - var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); - var name = stripedLine.Substring(0, index - 1); - var currentChannel = stripedLine.Substring(index + 7); - if (string.Equals(currentChannel, "none", StringComparison.Ordinal)) - { - status = LiveTvTunerStatus.LiveTv; - } - else - { - status = LiveTvTunerStatus.Available; - } - - tuners.Add(new LiveTvTunerInfo - { - Name = name, - SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, - ProgramName = currentChannel, - Status = status - }); - } - } - } - - return tuners; - } - - private static string StripXML(string source) - { - if (string.IsNullOrEmpty(source)) - { - return string.Empty; - } - - char[] buffer = new char[source.Length]; - int bufferIndex = 0; - bool inside = false; - - for (int i = 0; i < source.Length; i++) - { - char let = source[i]; - if (let == '<') - { - inside = true; - continue; - } - - if (let == '>') - { - inside = false; - continue; - } - - if (!inside) - { - buffer[bufferIndex++] = let; - } - } - - return new string(buffer, 0, bufferIndex); - } - - private async Task<List<LiveTvTunerInfo>> GetTunerInfosUdp(TunerHostInfo info, CancellationToken cancellationToken) - { - var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - - var tuners = new List<LiveTvTunerInfo>(model.TunerCount); - - var uri = new Uri(GetApiUrl(info)); - - using (var manager = new HdHomerunManager()) - { - // Legacy HdHomeruns are IPv4 only - var ipInfo = IPAddress.Parse(uri.Host); - - for (int i = 0; i < model.TunerCount; i++) - { - var name = string.Format(CultureInfo.InvariantCulture, "Tuner {0}", i + 1); - var currentChannel = "none"; // TODO: Get current channel and map back to Station Id - var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false); - var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv; - tuners.Add(new LiveTvTunerInfo - { - Name = name, - SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, - ProgramName = currentChannel, - Status = status - }); - } - } - - return tuners; - } - - public async Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken) - { - var list = new List<LiveTvTunerInfo>(); - - foreach (var host in GetConfiguration().TunerHosts - .Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))) - { - try - { - list.AddRange(await GetTunerInfos(host, cancellationToken).ConfigureAwait(false)); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error getting tuner info"); - } - } - - return list; - } - - public async Task<List<LiveTvTunerInfo>> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken) - { - // TODO Need faster way to determine UDP vs HTTP - var channels = await GetChannels(info, true, cancellationToken).ConfigureAwait(false); - - var hdHomerunChannelInfo = channels.FirstOrDefault() as HdHomerunChannelInfo; - - if (hdHomerunChannelInfo is null || hdHomerunChannelInfo.IsLegacyTuner) - { - return await GetTunerInfosUdp(info, cancellationToken).ConfigureAwait(false); - } - - return await GetTunerInfosHttp(info, cancellationToken).ConfigureAwait(false); - } - - private static string GetApiUrl(TunerHostInfo info) - { - var url = info.Url; - - if (string.IsNullOrWhiteSpace(url)) - { - throw new ArgumentException("Invalid tuner info"); - } - - if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - url = "http://" + url; - } - - return new Uri(url).AbsoluteUri.TrimEnd('/'); - } - - private static string GetHdHrIdFromChannelId(string channelId) - { - return channelId.Split('_')[1]; - } - - private MediaSourceInfo GetMediaSource(TunerHostInfo info, string channelId, ChannelInfo channelInfo, string profile) - { - int? width = null; - int? height = null; - bool isInterlaced = true; - string videoCodec = null; - - int? videoBitrate = null; - - var isHd = channelInfo.IsHD ?? true; - - if (string.Equals(profile, "mobile", StringComparison.OrdinalIgnoreCase)) - { - width = 1280; - height = 720; - isInterlaced = false; - videoCodec = "h264"; - videoBitrate = 2000000; - } - else if (string.Equals(profile, "heavy", StringComparison.OrdinalIgnoreCase)) - { - width = 1920; - height = 1080; - isInterlaced = false; - videoCodec = "h264"; - videoBitrate = 15000000; - } - else if (string.Equals(profile, "internet720", StringComparison.OrdinalIgnoreCase)) - { - width = 1280; - height = 720; - isInterlaced = false; - videoCodec = "h264"; - videoBitrate = 8000000; - } - else if (string.Equals(profile, "internet540", StringComparison.OrdinalIgnoreCase)) - { - width = 960; - height = 540; - isInterlaced = false; - videoCodec = "h264"; - videoBitrate = 2500000; - } - else if (string.Equals(profile, "internet480", StringComparison.OrdinalIgnoreCase)) - { - width = 848; - height = 480; - isInterlaced = false; - videoCodec = "h264"; - videoBitrate = 2000000; - } - else if (string.Equals(profile, "internet360", StringComparison.OrdinalIgnoreCase)) - { - width = 640; - height = 360; - isInterlaced = false; - videoCodec = "h264"; - videoBitrate = 1500000; - } - else if (string.Equals(profile, "internet240", StringComparison.OrdinalIgnoreCase)) - { - width = 432; - height = 240; - isInterlaced = false; - videoCodec = "h264"; - videoBitrate = 1000000; - } - else - { - // This is for android tv's 1200 condition. Remove once not needed anymore so that we can avoid possible side effects of dummying up this data - if (isHd) - { - width = 1920; - height = 1080; - } - } - - if (string.IsNullOrWhiteSpace(videoCodec)) - { - videoCodec = channelInfo.VideoCodec; - } - - string audioCodec = channelInfo.AudioCodec; - - videoBitrate ??= isHd ? 15000000 : 2000000; - - int? audioBitrate = isHd ? 448000 : 192000; - - // normalize - if (string.Equals(videoCodec, "mpeg2", StringComparison.OrdinalIgnoreCase)) - { - videoCodec = "mpeg2video"; - } - - string nal = null; - if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)) - { - nal = "0"; - } - - var url = GetApiUrl(info); - - var id = profile; - if (string.IsNullOrWhiteSpace(id)) - { - id = "native"; - } - - id += "_" + channelId.GetMD5().ToString("N", CultureInfo.InvariantCulture) + "_" + url.GetMD5().ToString("N", CultureInfo.InvariantCulture); - - var mediaSource = new MediaSourceInfo - { - Path = url, - Protocol = MediaProtocol.Udp, - MediaStreams = new MediaStream[] - { - 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, - IsInterlaced = isInterlaced, - Codec = videoCodec, - Width = width, - Height = height, - BitRate = videoBitrate, - NalLengthSize = nal - }, - 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, - Codec = audioCodec, - BitRate = audioBitrate - } - }, - RequiresOpening = true, - RequiresClosing = true, - BufferMs = 0, - Container = "ts", - Id = id, - SupportsDirectPlay = false, - SupportsDirectStream = true, - SupportsTranscoding = true, - IsInfiniteStream = true, - IgnoreDts = true, - // IgnoreIndex = true, - // ReadAtNativeFramerate = true - }; - - mediaSource.InferTotalBitrate(); - - return mediaSource; - } - - protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, ChannelInfo channel, CancellationToken cancellationToken) - { - var list = new List<MediaSourceInfo>(); - - var channelId = channel.Id; - var hdhrId = GetHdHrIdFromChannelId(channelId); - - if (channel is HdHomerunChannelInfo hdHomerunChannelInfo && hdHomerunChannelInfo.IsLegacyTuner) - { - list.Add(GetMediaSource(tuner, hdhrId, channel, "native")); - } - else - { - var modelInfo = await GetModelInfo(tuner, false, cancellationToken).ConfigureAwait(false); - - if (modelInfo is not null && modelInfo.SupportsTranscoding) - { - if (tuner.AllowHWTranscoding) - { - list.Add(GetMediaSource(tuner, hdhrId, channel, "heavy")); - - list.Add(GetMediaSource(tuner, hdhrId, channel, "internet540")); - list.Add(GetMediaSource(tuner, hdhrId, channel, "internet480")); - list.Add(GetMediaSource(tuner, hdhrId, channel, "internet360")); - list.Add(GetMediaSource(tuner, hdhrId, channel, "internet240")); - list.Add(GetMediaSource(tuner, hdhrId, channel, "mobile")); - } - - list.Add(GetMediaSource(tuner, hdhrId, channel, "native")); - } - - if (list.Count == 0) - { - list.Add(GetMediaSource(tuner, hdhrId, channel, "native")); - } - } - - return list; - } - - protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) - { - var tunerCount = tunerHost.TunerCount; - - if (tunerCount > 0) - { - var tunerHostId = tunerHost.Id; - var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase)); - - if (liveStreams.Count() >= tunerCount) - { - throw new LiveTvConflictException("HDHomeRun simultaneous stream limit has been reached."); - } - } - - var profile = streamId.AsSpan().LeftPart('_').ToString(); - - Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channel.Id, streamId, profile); - - var hdhrId = GetHdHrIdFromChannelId(channel.Id); - - var hdhomerunChannel = channel as HdHomerunChannelInfo; - - var modelInfo = await GetModelInfo(tunerHost, false, cancellationToken).ConfigureAwait(false); - - if (!modelInfo.SupportsTranscoding) - { - profile = "native"; - } - - var mediaSource = GetMediaSource(tunerHost, hdhrId, channel, profile); - - if (hdhomerunChannel is not null && hdhomerunChannel.IsLegacyTuner) - { - return new HdHomerunUdpStream( - mediaSource, - tunerHost, - streamId, - new LegacyHdHomerunChannelCommands(hdhomerunChannel.Path), - modelInfo.TunerCount, - FileSystem, - Logger, - Config, - _appHost, - _streamHelper); - } - - var enableHttpStream = true; - if (enableHttpStream) - { - mediaSource.Protocol = MediaProtocol.Http; - - var httpUrl = channel.Path; - - // If raw was used, the tuner doesn't support params - if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase)) - { - httpUrl += "?transcode=" + profile; - } - - mediaSource.Path = httpUrl; - - return new SharedHttpStream( - mediaSource, - tunerHost, - streamId, - FileSystem, - _httpClientFactory, - Logger, - Config, - _appHost, - _streamHelper); - } - - return new HdHomerunUdpStream( - mediaSource, - tunerHost, - streamId, - new HdHomerunChannelCommands(hdhomerunChannel.Number, profile), - modelInfo.TunerCount, - FileSystem, - Logger, - Config, - _appHost, - _streamHelper); - } - - public async Task Validate(TunerHostInfo info) - { - lock (_modelCache) - { - _modelCache.Clear(); - } - - try - { - // Test it by pulling down the lineup - var modelInfo = await GetModelInfo(info, true, CancellationToken.None).ConfigureAwait(false); - info.DeviceId = modelInfo.DeviceID; - } - catch (HttpRequestException ex) - { - if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) - { - // HDHR4 doesn't have this api - return; - } - - throw; - } - } - - public async Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken) - { - lock (_modelCache) - { - _modelCache.Clear(); - } - - using var timedCancellationToken = new CancellationTokenSource(discoveryDurationMs); - using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timedCancellationToken.Token, cancellationToken); - cancellationToken = linkedCancellationTokenSource.Token; - var list = new List<TunerHostInfo>(); - - // Create udp broadcast discovery message - byte[] discBytes = { 0, 2, 0, 12, 1, 4, 255, 255, 255, 255, 2, 4, 255, 255, 255, 255, 115, 204, 125, 143 }; - using (var udpClient = _socketFactory.CreateUdpBroadcastSocket(0)) - { - // Need a way to set the Receive timeout on the socket otherwise this might never timeout? - try - { - await udpClient.SendToAsync(discBytes, new IPEndPoint(IPAddress.Broadcast, 65001), cancellationToken).ConfigureAwait(false); - var receiveBuffer = new byte[8192]; - - while (!cancellationToken.IsCancellationRequested) - { - var response = await udpClient.ReceiveMessageFromAsync(receiveBuffer, new IPEndPoint(IPAddress.Any, 0), cancellationToken).ConfigureAwait(false); - var deviceIP = ((IPEndPoint)response.RemoteEndPoint).Address.ToString(); - - // Check to make sure we have enough bytes received to be a valid message and make sure the 2nd byte is the discover reply byte - if (response.ReceivedBytes > 13 && receiveBuffer[1] == 3) - { - var deviceAddress = "http://" + deviceIP; - - var info = await TryGetTunerHostInfo(deviceAddress, cancellationToken).ConfigureAwait(false); - - if (info is not null) - { - list.Add(info); - } - } - } - } - catch (OperationCanceledException) - { - } - catch (Exception ex) - { - // Socket timeout indicates all messages have been received. - Logger.LogError(ex, "Error while sending discovery message"); - } - } - - return list; - } - - internal async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken) - { - var hostInfo = new TunerHostInfo - { - Type = Type, - Url = url - }; - - var modelInfo = await GetModelInfo(hostInfo, false, cancellationToken).ConfigureAwait(false); - - hostInfo.DeviceId = modelInfo.DeviceID; - hostInfo.FriendlyName = modelInfo.FriendlyName; - hostInfo.TunerCount = modelInfo.TunerCount; - - return hostInfo; - } - - private class HdHomerunChannelInfo : ChannelInfo - { - public bool IsLegacyTuner { get; set; } - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs deleted file mode 100644 index 68383a554..000000000 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ /dev/null @@ -1,351 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Buffers; -using System.Buffers.Binary; -using System.Globalization; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common; -using MediaBrowser.Controller.LiveTv; - -namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun -{ - public sealed class HdHomerunManager : IDisposable - { - public const int HdHomeRunPort = 65001; - - // Message constants - private const byte GetSetName = 3; - private const byte GetSetValue = 4; - private const byte GetSetLockkey = 21; - private const ushort GetSetRequest = 4; - private const ushort GetSetReply = 5; - - private uint? _lockkey = null; - private int _activeTuner = -1; - private IPEndPoint _remoteEndPoint; - - private TcpClient _tcpClient; - - public void Dispose() - { - using (var socket = _tcpClient) - { - if (socket is not null) - { - _tcpClient = null; - - StopStreaming(socket).GetAwaiter().GetResult(); - } - } - } - - public async Task<bool> CheckTunerAvailability(IPAddress remoteIP, int tuner, CancellationToken cancellationToken) - { - using var client = new TcpClient(); - await client.ConnectAsync(remoteIP, HdHomeRunPort, cancellationToken).ConfigureAwait(false); - - using var stream = client.GetStream(); - return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false); - } - - private static async Task<bool> CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken) - { - byte[] buffer = ArrayPool<byte>.Shared.Rent(8192); - try - { - var msgLen = WriteGetMessage(buffer, tuner, "lockkey"); - await stream.WriteAsync(buffer.AsMemory(0, msgLen), cancellationToken).ConfigureAwait(false); - - int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - - return VerifyReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), "none"); - } - finally - { - ArrayPool<byte>.Shared.Return(buffer); - } - } - - public async Task StartStreaming(IPAddress remoteIP, IPAddress localIP, int localPort, IHdHomerunChannelCommands commands, int numTuners, CancellationToken cancellationToken) - { - _remoteEndPoint = new IPEndPoint(remoteIP, HdHomeRunPort); - - _tcpClient = new TcpClient(); - await _tcpClient.ConnectAsync(_remoteEndPoint, cancellationToken).ConfigureAwait(false); - - if (!_lockkey.HasValue) - { - _lockkey = (uint)Random.Shared.Next(); - } - - var lockKeyValue = _lockkey.Value; - var stream = _tcpClient.GetStream(); - - byte[] buffer = ArrayPool<byte>.Shared.Rent(8192); - try - { - for (int i = 0; i < numTuners; ++i) - { - if (!await CheckTunerAvailability(stream, i, cancellationToken).ConfigureAwait(false)) - { - continue; - } - - _activeTuner = i; - var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue); - var lockkeyMsgLen = WriteSetMessage(buffer, i, "lockkey", lockKeyString, null); - await stream.WriteAsync(buffer.AsMemory(0, lockkeyMsgLen), cancellationToken).ConfigureAwait(false); - int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - - // parse response to make sure it worked - if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _)) - { - continue; - } - - foreach (var command in commands.GetCommands()) - { - var channelMsgLen = WriteSetMessage(buffer, i, command.CommandName, command.CommandValue, lockKeyValue); - await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false); - receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - - // parse response to make sure it worked - if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _)) - { - await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false); - } - } - - var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIP, localPort); - var targetMsgLen = WriteSetMessage(buffer, i, "target", targetValue, lockKeyValue); - - await stream.WriteAsync(buffer.AsMemory(0, targetMsgLen), cancellationToken).ConfigureAwait(false); - receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - - // parse response to make sure it worked - if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _)) - { - await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false); - continue; - } - - return; - } - } - finally - { - ArrayPool<byte>.Shared.Return(buffer); - } - - _activeTuner = -1; - throw new LiveTvConflictException(); - } - - public async Task ChangeChannel(IHdHomerunChannelCommands commands, CancellationToken cancellationToken) - { - if (!_lockkey.HasValue) - { - return; - } - - using var tcpClient = new TcpClient(); - await tcpClient.ConnectAsync(_remoteEndPoint, cancellationToken).ConfigureAwait(false); - - using var stream = tcpClient.GetStream(); - var commandList = commands.GetCommands(); - byte[] buffer = ArrayPool<byte>.Shared.Rent(8192); - try - { - foreach (var command in commandList) - { - var channelMsgLen = WriteSetMessage(buffer, _activeTuner, command.CommandName, command.CommandValue, _lockkey); - await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false); - int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); - - // parse response to make sure it worked - if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _)) - { - return; - } - } - } - finally - { - ArrayPool<byte>.Shared.Return(buffer); - } - } - - public Task StopStreaming(TcpClient client) - { - var lockKey = _lockkey; - - if (!lockKey.HasValue) - { - return Task.CompletedTask; - } - - return ReleaseLockkey(client, lockKey.Value); - } - - private async Task ReleaseLockkey(TcpClient client, uint lockKeyValue) - { - var stream = client.GetStream(); - - var buffer = ArrayPool<byte>.Shared.Rent(8192); - try - { - var releaseTargetLen = WriteSetMessage(buffer, _activeTuner, "target", "none", lockKeyValue); - await stream.WriteAsync(buffer.AsMemory(0, releaseTargetLen)).ConfigureAwait(false); - - await stream.ReadAsync(buffer).ConfigureAwait(false); - var releaseKeyMsgLen = WriteSetMessage(buffer, _activeTuner, "lockkey", "none", lockKeyValue); - _lockkey = null; - await stream.WriteAsync(buffer.AsMemory(0, releaseKeyMsgLen)).ConfigureAwait(false); - await stream.ReadAsync(buffer).ConfigureAwait(false); - } - finally - { - ArrayPool<byte>.Shared.Return(buffer); - } - } - - internal static int WriteGetMessage(Span<byte> buffer, int tuner, string name) - { - var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name); - int offset = WriteHeaderAndPayload(buffer, byteName); - return FinishPacket(buffer, offset); - } - - internal static int WriteSetMessage(Span<byte> buffer, int tuner, string name, string value, uint? lockkey) - { - var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name); - int offset = WriteHeaderAndPayload(buffer, byteName); - - buffer[offset++] = GetSetValue; - offset += WriteNullTerminatedString(buffer.Slice(offset), value); - - if (lockkey.HasValue) - { - buffer[offset++] = GetSetLockkey; - buffer[offset++] = 4; - BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(offset), lockkey.Value); - offset += 4; - } - - return FinishPacket(buffer, offset); - } - - internal static int WriteNullTerminatedString(Span<byte> buffer, ReadOnlySpan<char> payload) - { - int len = Encoding.UTF8.GetBytes(payload, buffer.Slice(1)) + 1; - - // TODO: variable length: this can be 2 bytes if len > 127 - // Write length in front of value - buffer[0] = Convert.ToByte(len); - - // null-terminate - buffer[len++] = 0; - - return len; - } - - private static int WriteHeaderAndPayload(Span<byte> buffer, ReadOnlySpan<char> payload) - { - // Packet type - BinaryPrimitives.WriteUInt16BigEndian(buffer, GetSetRequest); - - // We write the payload length at the end - int offset = 4; - - // Tag - buffer[offset++] = GetSetName; - - // Payload length + data - int strLen = WriteNullTerminatedString(buffer.Slice(offset), payload); - offset += strLen; - - return offset; - } - - private static int FinishPacket(Span<byte> buffer, int offset) - { - // Payload length - BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), (ushort)(offset - 4)); - - // calculate crc and insert at the end of the message - var crc = Crc32.Compute(buffer.Slice(0, offset)); - BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset), crc); - - return offset + 4; - } - - internal static bool VerifyReturnValueOfGetSet(ReadOnlySpan<byte> buffer, string expected) - { - return TryGetReturnValueOfGetSet(buffer, out var value) - && string.Equals(Encoding.UTF8.GetString(value), expected, StringComparison.OrdinalIgnoreCase); - } - - internal static bool TryGetReturnValueOfGetSet(ReadOnlySpan<byte> buffer, out ReadOnlySpan<byte> value) - { - value = ReadOnlySpan<byte>.Empty; - - if (buffer.Length < 8) - { - return false; - } - - uint crc = BinaryPrimitives.ReadUInt32LittleEndian(buffer[^4..]); - if (crc != Crc32.Compute(buffer[..^4])) - { - return false; - } - - if (BinaryPrimitives.ReadUInt16BigEndian(buffer) != GetSetReply) - { - return false; - } - - var msgLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2)); - if (buffer.Length != 2 + 2 + 4 + msgLength) - { - return false; - } - - var offset = 4; - if (buffer[offset++] != GetSetName) - { - return false; - } - - var nameLength = buffer[offset++]; - if (buffer.Length < 4 + 1 + offset + nameLength) - { - return false; - } - - offset += nameLength; - - if (buffer[offset++] != GetSetValue) - { - return false; - } - - var valueLength = buffer[offset++]; - if (buffer.Length < 4 + offset + valueLength) - { - return false; - } - - // remove null terminator - value = buffer.Slice(offset, valueLength - 1); - return true; - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs deleted file mode 100644 index 6195c7648..000000000 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ /dev/null @@ -1,218 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.NetworkInformation; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.LiveTv; -using MediaBrowser.Model.MediaInfo; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun -{ - public class HdHomerunUdpStream : LiveStream, IDirectStreamProvider - { - private const int RtpHeaderBytes = 12; - - private readonly IServerApplicationHost _appHost; - private readonly IHdHomerunChannelCommands _channelCommands; - private readonly int _numTuners; - - public HdHomerunUdpStream( - MediaSourceInfo mediaSource, - TunerHostInfo tunerHostInfo, - string originalStreamId, - IHdHomerunChannelCommands channelCommands, - int numTuners, - IFileSystem fileSystem, - ILogger logger, - IConfigurationManager configurationManager, - IServerApplicationHost appHost, - IStreamHelper streamHelper) - : base(mediaSource, tunerHostInfo, fileSystem, logger, configurationManager, streamHelper) - { - _appHost = appHost; - OriginalStreamId = originalStreamId; - _channelCommands = channelCommands; - _numTuners = numTuners; - EnableStreamSharing = true; - } - - /// <summary> - /// Returns an unused UDP port number in the range specified. - /// Temporarily placed here until future network PR merged. - /// </summary> - /// <param name="range">Upper and Lower boundary of ports to select.</param> - /// <returns>System.Int32.</returns> - private static int GetUdpPortFromRange((int Min, int Max) range) - { - var properties = IPGlobalProperties.GetIPGlobalProperties(); - - // Get active udp listeners. - var udpListenerPorts = properties.GetActiveUdpListeners() - .Where(n => n.Port >= range.Min && n.Port <= range.Max) - .Select(n => n.Port); - - return Enumerable - .Range(range.Min, range.Max) - .FirstOrDefault(i => !udpListenerPorts.Contains(i)); - } - - public override async Task Open(CancellationToken openCancellationToken) - { - LiveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested(); - - var mediaSource = OriginalMediaSource; - - var uri = new Uri(mediaSource.Path); - // Temporary code to reduce PR size. This will be updated by a future network pr. - var localPort = GetUdpPortFromRange((49152, 65535)); - - Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath)); - - Logger.LogInformation("Opening HDHR UDP Live stream from {Host}", uri.Host); - - var remoteAddress = IPAddress.Parse(uri.Host); - IPAddress localAddress; - using (var tcpClient = new TcpClient()) - { - try - { - await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort, openCancellationToken).ConfigureAwait(false); - localAddress = ((IPEndPoint)tcpClient.Client.LocalEndPoint).Address; - tcpClient.Close(); - } - catch (Exception ex) - { - Logger.LogError(ex, "Unable to determine local ip address for Legacy HDHomerun stream."); - return; - } - } - - if (localAddress.IsIPv4MappedToIPv6) - { - localAddress = localAddress.MapToIPv4(); - } - - var udpClient = new UdpClient(localPort, AddressFamily.InterNetwork); - var hdHomerunManager = new HdHomerunManager(); - - try - { - // send url to start streaming - await hdHomerunManager.StartStreaming( - remoteAddress, - localAddress, - localPort, - _channelCommands, - _numTuners, - openCancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - using (udpClient) - using (hdHomerunManager) - { - if (ex is not OperationCanceledException) - { - Logger.LogError(ex, "Error opening live stream:"); - } - - throw; - } - } - - var taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); - - _ = StartStreaming( - udpClient, - hdHomerunManager, - remoteAddress, - taskCompletionSource, - LiveStreamCancellationTokenSource.Token); - - // OpenedMediaSource.Protocol = MediaProtocol.File; - // OpenedMediaSource.Path = tempFile; - // OpenedMediaSource.ReadAtNativeFramerate = true; - - MediaSource.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; - MediaSource.Protocol = MediaProtocol.Http; - // OpenedMediaSource.SupportsDirectPlay = false; - // OpenedMediaSource.SupportsDirectStream = true; - // OpenedMediaSource.SupportsTranscoding = true; - - // await Task.Delay(5000).ConfigureAwait(false); - await taskCompletionSource.Task.ConfigureAwait(false); - } - - private async Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) - { - using (udpClient) - using (hdHomerunManager) - { - try - { - await CopyTo(udpClient, TempFilePath, openTaskCompletionSource, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) when (ex is OperationCanceledException || ex is TimeoutException) - { - Logger.LogInformation("HDHR UDP stream cancelled or timed out from {0}", remoteAddress); - openTaskCompletionSource.TrySetException(ex); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error opening live stream:"); - openTaskCompletionSource.TrySetException(ex); - } - - EnableStreamSharing = false; - } - - await DeleteTempFiles(TempFilePath).ConfigureAwait(false); - } - - private async Task CopyTo(UdpClient udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) - { - var resolved = false; - - var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read); - await using (fileStream.ConfigureAwait(false)) - { - while (true) - { - cancellationToken.ThrowIfCancellationRequested(); - var res = await udpClient.ReceiveAsync(cancellationToken) - .AsTask() - .WaitAsync(TimeSpan.FromMilliseconds(30000), CancellationToken.None) - .ConfigureAwait(false); - var buffer = res.Buffer; - - var read = buffer.Length - RtpHeaderBytes; - - if (read > 0) - { - await fileStream.WriteAsync(buffer.AsMemory(RtpHeaderBytes, read), cancellationToken).ConfigureAwait(false); - } - - if (!resolved) - { - resolved = true; - DateOpened = DateTime.UtcNow; - openTaskCompletionSource.TrySetResult(true); - } - } - } - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs deleted file mode 100644 index 11bd40ab1..000000000 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun -{ - public interface IHdHomerunChannelCommands - { - IEnumerable<(string CommandName, string CommandValue)> GetCommands(); - } -} diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs deleted file mode 100644 index 654474e97..000000000 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs +++ /dev/null @@ -1,40 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.Text.RegularExpressions; - -namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun -{ - public partial class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands - { - private string? _channel; - private string? _program; - - public LegacyHdHomerunChannelCommands(string url) - { - // parse url for channel and program - var match = ChannelAndProgramRegex().Match(url); - if (match.Success) - { - _channel = match.Groups[1].Value; - _program = match.Groups[2].Value; - } - } - - [GeneratedRegex(@"\/ch([0-9]+)-?([0-9]*)")] - private static partial Regex ChannelAndProgramRegex(); - - public IEnumerable<(string CommandName, string CommandValue)> GetCommands() - { - if (!string.IsNullOrEmpty(_channel)) - { - yield return ("channel", _channel); - } - - if (!string.IsNullOrEmpty(_program)) - { - yield return ("program", _program); - } - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs deleted file mode 100644 index 767b94136..000000000 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ /dev/null @@ -1,160 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.LiveTv; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.LiveTv.TunerHosts -{ - public class LiveStream : ILiveStream - { - private readonly IConfigurationManager _configurationManager; - - public LiveStream( - MediaSourceInfo mediaSource, - TunerHostInfo tuner, - IFileSystem fileSystem, - ILogger logger, - IConfigurationManager configurationManager, - IStreamHelper streamHelper) - { - OriginalMediaSource = mediaSource; - FileSystem = fileSystem; - MediaSource = mediaSource; - Logger = logger; - EnableStreamSharing = true; - UniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - - if (tuner is not null) - { - TunerHostId = tuner.Id; - } - - _configurationManager = configurationManager; - StreamHelper = streamHelper; - - ConsumerCount = 1; - SetTempFilePath("ts"); - } - - protected IFileSystem FileSystem { get; } - - protected IStreamHelper StreamHelper { get; } - - protected ILogger Logger { get; } - - protected CancellationTokenSource LiveStreamCancellationTokenSource { get; } = new CancellationTokenSource(); - - protected string TempFilePath { get; set; } - - public MediaSourceInfo OriginalMediaSource { get; set; } - - public MediaSourceInfo MediaSource { get; set; } - - public int ConsumerCount { get; set; } - - public string OriginalStreamId { get; set; } - - public bool EnableStreamSharing { get; set; } - - public string UniqueId { get; } - - public string TunerHostId { get; } - - public DateTime DateOpened { get; protected set; } - - protected void SetTempFilePath(string extension) - { - TempFilePath = Path.Combine(_configurationManager.GetTranscodePath(), UniqueId + "." + extension); - } - - public virtual Task Open(CancellationToken openCancellationToken) - { - DateOpened = DateTime.UtcNow; - return Task.CompletedTask; - } - - public async Task Close() - { - EnableStreamSharing = false; - - Logger.LogInformation("Closing {Type}", GetType().Name); - - await LiveStreamCancellationTokenSource.CancelAsync().ConfigureAwait(false); - } - - public Stream GetStream() - { - var stream = new FileStream( - TempFilePath, - FileMode.Open, - FileAccess.Read, - FileShare.ReadWrite, - IODefaults.FileStreamBufferSize, - FileOptions.SequentialScan | FileOptions.Asynchronous); - - bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10; - if (seekFile) - { - TrySeek(stream, -20000); - } - - return stream; - } - - protected async Task DeleteTempFiles(string path, int retryCount = 0) - { - if (retryCount == 0) - { - Logger.LogInformation("Deleting temp file {FilePath}", path); - } - - try - { - FileSystem.DeleteFile(path); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error deleting file {FilePath}", path); - if (retryCount <= 40) - { - await Task.Delay(500).ConfigureAwait(false); - await DeleteTempFiles(path, retryCount + 1).ConfigureAwait(false); - } - } - } - - private void TrySeek(Stream stream, long offset) - { - if (!stream.CanSeek) - { - return; - } - - try - { - stream.Seek(offset, SeekOrigin.End); - } - catch (IOException) - { - } - catch (ArgumentException) - { - } - catch (Exception ex) - { - Logger.LogError(ex, "Error seeking stream"); - } - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs deleted file mode 100644 index db5e81df5..000000000 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ /dev/null @@ -1,220 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Extensions; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.LiveTv; -using MediaBrowser.Model.MediaInfo; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.LiveTv.TunerHosts -{ - public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost - { - private static readonly string[] _disallowedMimeTypes = - { - "video/x-matroska", - "video/mp4", - "application/vnd.apple.mpegurl", - "application/mpegurl", - "application/x-mpegurl", - "video/vnd.mpeg.dash.mpd" - }; - - private readonly IHttpClientFactory _httpClientFactory; - private readonly IServerApplicationHost _appHost; - private readonly INetworkManager _networkManager; - private readonly IMediaSourceManager _mediaSourceManager; - private readonly IStreamHelper _streamHelper; - - public M3UTunerHost( - IServerConfigurationManager config, - IMediaSourceManager mediaSourceManager, - ILogger<M3UTunerHost> logger, - IFileSystem fileSystem, - IHttpClientFactory httpClientFactory, - IServerApplicationHost appHost, - INetworkManager networkManager, - IStreamHelper streamHelper) - : base(config, logger, fileSystem) - { - _httpClientFactory = httpClientFactory; - _appHost = appHost; - _networkManager = networkManager; - _mediaSourceManager = mediaSourceManager; - _streamHelper = streamHelper; - } - - public override string Type => "m3u"; - - public virtual string Name => "M3U Tuner"; - - private string GetFullChannelIdPrefix(TunerHostInfo info) - { - return ChannelIdPrefix + info.Url.GetMD5().ToString("N", CultureInfo.InvariantCulture); - } - - protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken) - { - var channelIdPrefix = GetFullChannelIdPrefix(tuner); - - return await new M3uParser(Logger, _httpClientFactory) - .Parse(tuner, channelIdPrefix, cancellationToken) - .ConfigureAwait(false); - } - - public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken) - { - var list = GetTunerHosts() - .Select(i => new LiveTvTunerInfo() - { - Name = Name, - SourceType = Type, - Status = LiveTvTunerStatus.Available, - Id = i.Url.GetMD5().ToString("N", CultureInfo.InvariantCulture), - Url = i.Url - }) - .ToList(); - - return Task.FromResult(list); - } - - protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) - { - var tunerCount = tunerHost.TunerCount; - - if (tunerCount > 0) - { - var tunerHostId = tunerHost.Id; - var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase)); - - if (liveStreams.Count() >= tunerCount) - { - throw new LiveTvConflictException("M3U simultaneous stream limit has been reached."); - } - } - - var sources = await GetChannelStreamMediaSources(tunerHost, channel, cancellationToken).ConfigureAwait(false); - - var mediaSource = sources[0]; - - if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping) - { - using var message = new HttpRequestMessage(HttpMethod.Head, mediaSource.Path); - using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .SendAsync(message, cancellationToken) - .ConfigureAwait(false); - - response.EnsureSuccessStatusCode(); - - if (!_disallowedMimeTypes.Contains(response.Content.Headers.ContentType?.ToString(), StringComparison.OrdinalIgnoreCase)) - { - return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper); - } - } - - return new LiveStream(mediaSource, tunerHost, FileSystem, Logger, Config, _streamHelper); - } - - public async Task Validate(TunerHostInfo info) - { - using (await new M3uParser(Logger, _httpClientFactory).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false)) - { - } - } - - protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, ChannelInfo channel, CancellationToken cancellationToken) - { - return Task.FromResult(new List<MediaSourceInfo> { CreateMediaSourceInfo(tuner, channel) }); - } - - protected virtual MediaSourceInfo CreateMediaSourceInfo(TunerHostInfo info, ChannelInfo channel) - { - var path = channel.Path; - - var supportsDirectPlay = !info.EnableStreamLooping && info.TunerCount == 0; - var supportsDirectStream = !info.EnableStreamLooping; - - var protocol = _mediaSourceManager.GetPathProtocol(path); - - var isRemote = true; - if (Uri.TryCreate(path, UriKind.Absolute, out var uri)) - { - isRemote = !_networkManager.IsInLocalNetwork(uri.Host); - } - - var httpHeaders = new Dictionary<string, string>(); - - if (protocol == MediaProtocol.Http) - { - // Use user-defined user-agent. If there isn't one, make it look like a browser. - httpHeaders[HeaderNames.UserAgent] = string.IsNullOrWhiteSpace(info.UserAgent) ? - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.85 Safari/537.36" : - info.UserAgent; - } - - var mediaSource = new MediaSourceInfo - { - Path = path, - Protocol = protocol, - MediaStreams = new MediaStream[] - { - 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, - 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 - } - }, - RequiresOpening = true, - RequiresClosing = true, - RequiresLooping = info.EnableStreamLooping, - - ReadAtNativeFramerate = false, - - Id = channel.Path.GetMD5().ToString("N", CultureInfo.InvariantCulture), - IsInfiniteStream = true, - IsRemote = isRemote, - - IgnoreDts = info.IgnoreDts, - SupportsDirectPlay = supportsDirectPlay, - SupportsDirectStream = supportsDirectStream, - - RequiredHttpHeaders = httpHeaders - }; - - mediaSource.InferTotalBitrate(); - - return mediaSource; - } - - public Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken) - { - return Task.FromResult(new List<TunerHostInfo>()); - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs deleted file mode 100644 index 341782d9d..000000000 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ /dev/null @@ -1,326 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Net.Http; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Extensions; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.LiveTv; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.LiveTv.TunerHosts -{ - public partial class M3uParser - { - private const string ExtInfPrefix = "#EXTINF:"; - - private readonly ILogger _logger; - private readonly IHttpClientFactory _httpClientFactory; - - public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory) - { - _logger = logger; - _httpClientFactory = httpClientFactory; - } - - [GeneratedRegex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase, "en-US")] - private static partial Regex KeyValueRegex(); - - public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken) - { - // Read the file and display it line by line. - using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false))) - { - return await GetChannelsAsync(reader, channelIdPrefix, info.Id).ConfigureAwait(false); - } - } - - public async Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(info); - - if (!info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - { - return AsyncFile.OpenRead(info.Url); - } - - using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url); - if (!string.IsNullOrEmpty(info.UserAgent)) - { - requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent); - } - - // Set HttpCompletionOption.ResponseHeadersRead to prevent timeouts on larger files - var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken) - .ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - - return await response.Content.ReadAsStreamAsync(cancellationToken); - } - - private async Task<List<ChannelInfo>> GetChannelsAsync(TextReader reader, string channelIdPrefix, string tunerHostId) - { - var channels = new List<ChannelInfo>(); - string extInf = string.Empty; - - await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) - { - var trimmedLine = line.Trim(); - if (string.IsNullOrWhiteSpace(trimmedLine)) - { - continue; - } - - if (trimmedLine.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - if (trimmedLine.StartsWith(ExtInfPrefix, StringComparison.OrdinalIgnoreCase)) - { - extInf = trimmedLine.Substring(ExtInfPrefix.Length).Trim(); - } - else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#')) - { - var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine); - channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture); - - channel.Path = trimmedLine; - channels.Add(channel); - _logger.LogInformation("Parsed channel: {ChannelName}", channel.Name); - extInf = string.Empty; - } - } - - return channels; - } - - private ChannelInfo GetChannelnfo(string extInf, string tunerHostId, string mediaUrl) - { - var channel = new ChannelInfo() - { - TunerHostId = tunerHostId - }; - - extInf = extInf.Trim(); - - var attributes = ParseExtInf(extInf, out string remaining); - extInf = remaining; - - if (attributes.TryGetValue("tvg-logo", out string tvgLogo)) - { - channel.ImageUrl = tvgLogo; - } - else if (attributes.TryGetValue("logo", out string logo)) - { - channel.ImageUrl = logo; - } - - if (attributes.TryGetValue("group-title", out string groupTitle)) - { - channel.ChannelGroup = groupTitle; - } - - channel.Name = GetChannelName(extInf, attributes); - channel.Number = GetChannelNumber(extInf, attributes, mediaUrl); - - attributes.TryGetValue("tvg-id", out string tvgId); - - attributes.TryGetValue("channel-id", out string channelId); - - channel.TunerChannelId = string.IsNullOrWhiteSpace(tvgId) ? channelId : tvgId; - - var channelIdValues = new List<string>(); - if (!string.IsNullOrWhiteSpace(channelId)) - { - channelIdValues.Add(channelId); - } - - if (!string.IsNullOrWhiteSpace(tvgId)) - { - channelIdValues.Add(tvgId); - } - - if (channelIdValues.Count > 0) - { - channel.Id = string.Join('_', channelIdValues); - } - - return channel; - } - - private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl) - { - var nameParts = extInf.Split(',', StringSplitOptions.RemoveEmptyEntries); - var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty; - - string numberString = null; - - if (attributes.TryGetValue("tvg-chno", out var attributeValue) - && double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _)) - { - numberString = attributeValue; - } - - if (!IsValidChannelNumber(numberString)) - { - if (attributes.TryGetValue("tvg-id", out attributeValue)) - { - if (double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _)) - { - numberString = attributeValue; - } - else if (attributes.TryGetValue("channel-id", out attributeValue) - && double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _)) - { - numberString = attributeValue; - } - } - - if (string.IsNullOrWhiteSpace(numberString)) - { - // Using this as a fallback now as this leads to Problems with channels like "5 USA" - // where 5 isn't meant to be the channel number - // Check for channel number with the format from SatIp - // #EXTINF:0,84. VOX Schweiz - // #EXTINF:0,84.0 - VOX Schweiz - if (!nameInExtInf.IsEmpty && !nameInExtInf.IsWhiteSpace()) - { - var numberIndex = nameInExtInf.IndexOf(' '); - if (numberIndex > 0) - { - var numberPart = nameInExtInf.Slice(0, numberIndex).Trim(new[] { ' ', '.' }); - - if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _)) - { - numberString = numberPart.ToString(); - } - } - } - } - } - - if (!IsValidChannelNumber(numberString)) - { - numberString = null; - } - - if (!string.IsNullOrWhiteSpace(numberString)) - { - numberString = numberString.Trim(); - } - else - { - if (string.IsNullOrWhiteSpace(mediaUrl)) - { - numberString = null; - } - else - { - try - { - numberString = Path.GetFileNameWithoutExtension(mediaUrl.AsSpan().RightPart('/')).ToString(); - - if (!IsValidChannelNumber(numberString)) - { - numberString = null; - } - } - catch - { - // Seeing occasional argument exception here - numberString = null; - } - } - } - - return numberString; - } - - private static bool IsValidChannelNumber(string numberString) - { - if (string.IsNullOrWhiteSpace(numberString) - || string.Equals(numberString, "-1", StringComparison.Ordinal) - || string.Equals(numberString, "0", StringComparison.Ordinal)) - { - return false; - } - - return double.TryParse(numberString, CultureInfo.InvariantCulture, out _); - } - - private static string GetChannelName(string extInf, Dictionary<string, string> attributes) - { - var nameParts = extInf.Split(',', StringSplitOptions.RemoveEmptyEntries); - var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].Trim() : null; - - // Check for channel number with the format from SatIp - // #EXTINF:0,84. VOX Schweiz - // #EXTINF:0,84.0 - VOX Schweiz - if (!string.IsNullOrWhiteSpace(nameInExtInf)) - { - var numberIndex = nameInExtInf.IndexOf(' ', StringComparison.Ordinal); - if (numberIndex > 0) - { - var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' }); - - if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _)) - { - // channel.Number = number.ToString(); - nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' }); - } - } - } - - string name = nameInExtInf; - - if (string.IsNullOrWhiteSpace(name)) - { - attributes.TryGetValue("tvg-name", out name); - } - - if (string.IsNullOrWhiteSpace(name)) - { - attributes.TryGetValue("tvg-id", out name); - } - - if (string.IsNullOrWhiteSpace(name)) - { - name = null; - } - - return name; - } - - private static Dictionary<string, string> ParseExtInf(string line, out string remaining) - { - var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - var matches = KeyValueRegex().Matches(line); - - remaining = line; - - foreach (Match match in matches) - { - var key = match.Groups[1].Value; - var value = match.Groups[2].Value; - - dict[key] = value; - remaining = remaining.Replace(key + "=\"" + value + "\"", string.Empty, StringComparison.OrdinalIgnoreCase); - } - - return dict; - } - } -} diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs deleted file mode 100644 index 51f46f4da..000000000 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ /dev/null @@ -1,121 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.IO; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.LiveTv; -using MediaBrowser.Model.MediaInfo; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.LiveTv.TunerHosts -{ - public class SharedHttpStream : LiveStream, IDirectStreamProvider - { - private readonly IHttpClientFactory _httpClientFactory; - private readonly IServerApplicationHost _appHost; - - public SharedHttpStream( - MediaSourceInfo mediaSource, - TunerHostInfo tunerHostInfo, - string originalStreamId, - IFileSystem fileSystem, - IHttpClientFactory httpClientFactory, - ILogger logger, - IConfigurationManager configurationManager, - IServerApplicationHost appHost, - IStreamHelper streamHelper) - : base(mediaSource, tunerHostInfo, fileSystem, logger, configurationManager, streamHelper) - { - _httpClientFactory = httpClientFactory; - _appHost = appHost; - OriginalStreamId = originalStreamId; - } - - public override async Task Open(CancellationToken openCancellationToken) - { - LiveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested(); - - var mediaSource = OriginalMediaSource; - - var url = mediaSource.Path; - - Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath) ?? throw new InvalidOperationException("Path can't be a root directory.")); - - var typeName = GetType().Name; - Logger.LogInformation("Opening {StreamType} Live stream from {Url}", typeName, url); - - // Response stream is disposed manually. - var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) - .ConfigureAwait(false); - - var taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); - - _ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token); - - MediaSource.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; - MediaSource.Protocol = MediaProtocol.Http; - - var res = await taskCompletionSource.Task.ConfigureAwait(false); - if (!res) - { - Logger.LogWarning("Zero bytes copied from stream {StreamType} to {FilePath} but no exception raised", GetType().Name, TempFilePath); - throw new EndOfStreamException(string.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name)); - } - } - - private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) - { - return Task.Run( - async () => - { - try - { - Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath); - using (response) - { - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); - await StreamHelper.CopyToAsync( - stream, - fileStream, - IODefaults.CopyToBufferSize, - () => Resolve(openTaskCompletionSource), - cancellationToken).ConfigureAwait(false); - } - } - catch (OperationCanceledException ex) - { - Logger.LogInformation("Copying of {StreamType} to {FilePath} was canceled", GetType().Name, TempFilePath); - openTaskCompletionSource.TrySetException(ex); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error copying live stream {StreamType} to {FilePath}", GetType().Name, TempFilePath); - openTaskCompletionSource.TrySetException(ex); - } - - openTaskCompletionSource.TrySetResult(false); - - EnableStreamSharing = false; - await DeleteTempFiles(TempFilePath).ConfigureAwait(false); - }, - CancellationToken.None); - } - - private void Resolve(TaskCompletionSource<bool> openTaskCompletionSource) - { - DateOpened = DateTime.UtcNow; - openTaskCompletionSource.TrySetResult(true); - } - } -} diff --git a/Emby.Server.Implementations/Localization/Core/ab.json b/Emby.Server.Implementations/Localization/Core/ab.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/ab.json @@ -0,0 +1 @@ +{} diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 26290df4d..c4d8c6947 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.", "TaskKeyframeExtractor": "Extractor de fotogrames clau", "External": "Extern", - "HearingImpaired": "Discapacitat auditiva" + "HearingImpaired": "Discapacitat auditiva", + "TaskRefreshTrickplayImages": "Generar miniatures de línia de temps", + "TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades." } diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 5da33febe..1c7bc75b5 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -83,8 +83,8 @@ "UserDeletedWithName": "Uživatel {0} byl smazán", "UserDownloadingItemWithValues": "{0} stahuje {1}", "UserLockedOutWithName": "Uživatel {0} byl odemčen", - "UserOfflineFromDevice": "{0} se odpojil od {1}", - "UserOnlineFromDevice": "{0} se připojil z {1}", + "UserOfflineFromDevice": "{0} se odpojil ze zařízení {1}", + "UserOnlineFromDevice": "{0} se připojil ze zařízení {1}", "UserPasswordChangedWithName": "Provedena změna hesla pro uživatele {0}", "UserPolicyUpdatedWithName": "Zásady uživatele pro {0} byly aktualizovány", "UserStartedPlayingItemWithValues": "{0} spustil přehrávání {1}", diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index f1dbf3c89..7a4c2067b 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -121,8 +121,8 @@ "Default": "Standard", "TaskOptimizeDatabaseDescription": "Komprimiert die Datenbank und trimmt den freien Speicherplatz. Die Ausführung dieser Aufgabe nach dem Scannen der Bibliothek oder nach anderen Änderungen, die Datenbankänderungen implizieren, kann die Leistung verbessern.", "TaskOptimizeDatabase": "Datenbank optimieren", - "TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Dieser Vorgang kann sehr lange dauern.", - "TaskKeyframeExtractor": "Keyframe Extraktor", + "TaskKeyframeExtractorDescription": "Extrahiert Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Dieser Vorgang kann sehr lange dauern.", + "TaskKeyframeExtractor": "Keyframe-Extraktor", "External": "Extern", "HearingImpaired": "Hörgeschädigt", "TaskRefreshTrickplayImages": "Trickplay-Bilder generieren", diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json index 3d5c04633..c6863ff36 100644 --- a/Emby.Server.Implementations/Localization/Core/es_419.json +++ b/Emby.Server.Implementations/Localization/Core/es_419.json @@ -123,5 +123,7 @@ "External": "Externo", "TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reproducción HLS más precisas. Esta tarea puede durar mucho tiempo.", "TaskKeyframeExtractor": "Extractor de Fotogramas Clave", - "HearingImpaired": "Discapacidad auditiva" + "HearingImpaired": "Discapacidad auditiva", + "TaskRefreshTrickplayImagesDescription": "Crea previsualizaciones para la barra de reproducción en las bibliotecas habilitadas.", + "TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción" } diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json index 8e4bba25b..8364ce236 100644 --- a/Emby.Server.Implementations/Localization/Core/fa.json +++ b/Emby.Server.Implementations/Localization/Core/fa.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "فریم های کلیدی را از فایل های ویدئویی استخراج می کند تا لیست های پخش HLS دقیق تری ایجاد کند. این کار ممکن است برای مدت طولانی اجرا شود.", "TaskKeyframeExtractor": "استخراج کننده فریم کلیدی", "External": "خارجی", - "HearingImpaired": "مشکل شنوایی" + "HearingImpaired": "مشکل شنوایی", + "TaskRefreshTrickplayImages": "تولید تصاویر Trickplay", + "TaskRefreshTrickplayImagesDescription": "تولید پیشنمایش های trickplay برای ویدیو های فعال شده در کتابخانه." } diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json index 88a4a358e..55ee1abaa 100644 --- a/Emby.Server.Implementations/Localization/Core/fil.json +++ b/Emby.Server.Implementations/Localization/Core/fil.json @@ -124,5 +124,6 @@ "TaskKeyframeExtractor": "Tagabunot ng Keyframe", "TaskKeyframeExtractorDescription": "Nagbubunot ng keyframe mula sa mga bidyo upang makabuo ng mas tumpak na HLS playlist. Maaaring matagal ito gawin.", "External": "External", - "TaskRefreshTrickplayImages": "Gumawa ng Trickplay na Imahe" + "TaskRefreshTrickplayImages": "Gumawa ng Trickplay na Imahe", + "TaskRefreshTrickplayImagesDescription": "Nanggagawa ng mga trickplay prebiyu para sa mga bidyo sa pinaganang mga aklatan." } diff --git a/Emby.Server.Implementations/Localization/Core/hy.json b/Emby.Server.Implementations/Localization/Core/hy.json new file mode 100644 index 000000000..563f84292 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/hy.json @@ -0,0 +1,39 @@ +{ + "TasksLibraryCategory": "Գրադարան", + "TasksApplicationCategory": "Հավելված", + "TaskCleanActivityLog": "Մաքրել ակտիվության մատյանը", + "Application": "Հավելված", + "AuthenticationSucceededWithUserName": "{0} հաջողությամբ վավերականացվել են", + "Books": "Գրքեր", + "CameraImageUploadedFrom": "Նոր լուսանկար է վերբեռնվել {0}-ի կողմից", + "Channels": "Ալիքներ", + "DeviceOfflineWithName": "{0}ը անջատվեց", + "External": "Արտաքին", + "FailedLoginAttemptWithUserName": "Ձախողված մուտքի փործ {0}-ի կողմից", + "Folders": "Պանակներ", + "HeaderContinueWatching": "Շարունակել դիտումը", + "Inherit": "Ժառանգել", + "ItemAddedWithName": "{0}ը ավացված է գրադարանի մեջ", + "ItemRemovedWithName": "{0}ը հեռացված է գրադարանից", + "LabelIpAddressValue": "IP հասցե` {0}", + "Movies": "Ֆիլմեր", + "Music": "Երաժշտություն", + "NameSeasonNumber": "Սեզոն {0}", + "Photos": "Լուսանկարներ", + "PluginInstalledWithName": "{0}ն տեղադրված է", + "Songs": "Երգեր", + "System": "Համակարգ", + "TvShows": "Հեռուստասերիալներ", + "User": "Օգտատեր", + "VersionNumber": "Տարբերակ {0}", + "TasksMaintenanceCategory": "Սպասարկում", + "TasksChannelsCategory": "Ինտերնետային ալիքներ", + "TaskRefreshPeople": "Թարմացնել մարդկանց", + "TaskRefreshChannels": "Թարմացնել ալիքները", + "TaskDownloadMissingSubtitles": "Ներբեռնել պակասող ենթագրերը", + "Albums": "Ալբոմներ", + "AppDeviceValues": "Հավելված` {0}, Սարք `{1}", + "ChapterNameValue": "Գլուխ {0}", + "Collections": "Հավաքածուներ", + "DeviceOnlineWithName": "{0}-ն միացված է" +} diff --git a/Emby.Server.Implementations/Localization/Core/ka.json b/Emby.Server.Implementations/Localization/Core/ka.json index dbbc81eeb..2d02522fe 100644 --- a/Emby.Server.Implementations/Localization/Core/ka.json +++ b/Emby.Server.Implementations/Localization/Core/ka.json @@ -4,9 +4,9 @@ "HeaderFavoriteAlbums": "რჩეული ალბომები", "TasksApplicationCategory": "აპლიკაცია", "Albums": "ალბომები", - "AppDeviceValues": "აპი: {0}, მოწყობილობა: {1}", + "AppDeviceValues": "აპლიკაცია: {0}, მოწყობილობა: {1}", "Application": "აპლიკაცია", - "Artists": "შემსრულებლები", + "Artists": "არტისტი", "AuthenticationSucceededWithUserName": "{0} -ის ავთენტიკაცია წარმატებულია", "Books": "წიგნები", "Forced": "ძალით", @@ -123,5 +123,7 @@ "TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული დამატებების განახლებების გადმოწერა და დაყენება.", "TaskCleanTranscodeDescription": "ერთ დღეზე უფრო ძველი ტრანსკოდირების ფაილების წაშლა.", "TaskDownloadMissingSubtitlesDescription": "მეტამონაცემებზე დაყრდნობით ინტერნეტში ნაკლული სუბტიტრების ძებნა.", - "TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლება. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს." + "TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლება. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს.", + "TaskRefreshTrickplayImagesDescription": "ქმნის trickplay წინასწარ ხედებს ვიდეოებისთვის ჩართულ ბიბლიოთეკებში.", + "TaskRefreshTrickplayImages": "Trickplay სურათების გენერირება" } diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json index 82a071309..6e58ef834 100644 --- a/Emby.Server.Implementations/Localization/Core/lv.json +++ b/Emby.Server.Implementations/Localization/Core/lv.json @@ -123,5 +123,7 @@ "External": "Ārējais", "HearingImpaired": "Ar dzirdes traucējumiem", "TaskKeyframeExtractor": "Atslēgkadru ekstraktors", - "TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs." + "TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs.", + "TaskRefreshTrickplayImages": "Ģenerēt partīšanas attēlus", + "TaskRefreshTrickplayImagesDescription": "Izveido priekšskatījumus videoklipu pārtīšanai iespējotajās bibliotēkās." } diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index 5c7dec7ef..b6c15d871 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -5,7 +5,7 @@ "Artists": "Artister", "AuthenticationSucceededWithUserName": "{0} har logget inn", "Books": "Bøker", - "CameraImageUploadedFrom": "Et nytt kamerabilde er lastet opp fra {0}", + "CameraImageUploadedFrom": "Et nytt kamerabilde har blitt lastet opp fra {0}", "Channels": "Kanaler", "ChapterNameValue": "Kapittel {0}", "Collections": "Samlinger", @@ -32,10 +32,10 @@ "LabelIpAddressValue": "IP-adresse: {0}", "LabelRunningTimeValue": "Spilletid {0}", "Latest": "Siste", - "MessageApplicationUpdated": "Jellyfin-tjeneren har blitt oppdatert", - "MessageApplicationUpdatedTo": "Jellyfin-tjeneren ble oppdatert til {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Tjenerkonfigurasjonsseksjon {0} har blitt oppdatert", - "MessageServerConfigurationUpdated": "Tjenerkonfigurasjon er oppdatert", + "MessageApplicationUpdated": "Jellyfin-serveren har blitt oppdatert", + "MessageApplicationUpdatedTo": "Jellyfin-serveren ble oppdatert til {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfigurasjonsseksjon {0} har blitt oppdatert", + "MessageServerConfigurationUpdated": "Serverkonfigurasjon har blitt oppdatert", "MixedContent": "Blandet innhold", "Movies": "Filmer", "Music": "Musikk", @@ -43,19 +43,19 @@ "NameInstallFailed": "Installasjonen av {0} mislyktes", "NameSeasonNumber": "Sesong {0}", "NameSeasonUnknown": "Ukjent sesong", - "NewVersionIsAvailable": "En ny versjon av Jellyfin-tjeneren er tilgjengelig for nedlasting.", + "NewVersionIsAvailable": "En ny versjon av Jellyfin Server er tilgjengelig for nedlasting.", "NotificationOptionApplicationUpdateAvailable": "En programvareoppdatering er tilgjengelig", "NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering installert", "NotificationOptionAudioPlayback": "Lydavspilling startet", "NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppet", "NotificationOptionCameraImageUploaded": "Kamerabilde lastet opp", - "NotificationOptionInstallationFailed": "Installasjonen feilet", + "NotificationOptionInstallationFailed": "Installasjonsfeil", "NotificationOptionNewLibraryContent": "Nytt innhold lagt til", "NotificationOptionPluginError": "Programvareutvidelsesfeil", "NotificationOptionPluginInstalled": "Programvareutvidelse installert", "NotificationOptionPluginUninstalled": "Programvareutvidelse avinstallert", "NotificationOptionPluginUpdateInstalled": "Programvareutvidelsesoppdatering installert", - "NotificationOptionServerRestartRequired": "Tjeneromstart er nødvendig", + "NotificationOptionServerRestartRequired": "Serveromstart er nødvendig", "NotificationOptionTaskFailed": "Feil under utføring av planlagt oppgave", "NotificationOptionUserLockedOut": "Bruker er utestengt", "NotificationOptionVideoPlayback": "Videoavspilling startet", @@ -70,9 +70,9 @@ "ScheduledTaskFailedWithName": "{0} mislykkes", "ScheduledTaskStartedWithName": "{0} startet", "ServerNameNeedsToBeRestarted": "{0} må startes på nytt", - "Shows": "Program", + "Shows": "Serier", "Songs": "Sanger", - "StartupEmbyServerIsLoading": "Jellyfin-tjener laster. Prøv igjen snart.", + "StartupEmbyServerIsLoading": "Jellyfin Server laster. Prøv igjen snart.", "SubtitleDownloadFailureForItem": "En feil oppstå under nedlasting av undertekster for {0}", "SubtitleDownloadFailureFromForItem": "Kunne ikke laste ned undertekster fra {0} for {1}", "Sync": "Synkroniser", @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "Trekker ut nøkkelbilder fra videofiler for å skape mere nøyaktige HLS-spillelister. Denne oppgaven kan ta lang tid.", "TaskKeyframeExtractor": "Nøkkelbilde-uttrekker", "External": "Ekstern", - "HearingImpaired": "Hørselshemmet" + "HearingImpaired": "Hørselshemmet", + "TaskRefreshTrickplayImages": "Generer Trickplay bilder", + "TaskRefreshTrickplayImagesDescription": "Oppretter trickplay-forhåndsvisninger for videoer i aktiverte biblioteker." } diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json index 32d4f3a8b..d0c914de3 100644 --- a/Emby.Server.Implementations/Localization/Core/nn.json +++ b/Emby.Server.Implementations/Localization/Core/nn.json @@ -117,5 +117,6 @@ "TaskCleanActivityLog": "Slett aktivitetslogg", "Undefined": "Udefinert", "Forced": "Tvungen", - "Default": "Standard" + "Default": "Standard", + "External": "Ekstern" } diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index 97062deec..1fc3cdbaa 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -43,7 +43,7 @@ "NameInstallFailed": "{0} installationen misslyckades", "NameSeasonNumber": "Säsong {0}", "NameSeasonUnknown": "Okänd säsong", - "NewVersionIsAvailable": "En ny version av Jellyfin Server är tillgänglig att hämta.", + "NewVersionIsAvailable": "En ny version av Jellyfin Server är tillgänglig för nedladdning.", "NotificationOptionApplicationUpdateAvailable": "Ny programversion tillgänglig", "NotificationOptionApplicationUpdateInstalled": "Programuppdatering installerad", "NotificationOptionAudioPlayback": "Ljuduppspelning har påbörjats", @@ -74,7 +74,7 @@ "Songs": "Låtar", "StartupEmbyServerIsLoading": "Jellyfin Server arbetar. Pröva igen snart.", "SubtitleDownloadFailureForItem": "Nerladdning av undertexter för {0} misslyckades", - "SubtitleDownloadFailureFromForItem": "Undertexter kunde inte laddas ner från {0} för {1}", + "SubtitleDownloadFailureFromForItem": "Undertexter kunde inte laddas ner från {0} till {1}", "Sync": "Synk", "System": "System", "TvShows": "TV-serier", diff --git a/Emby.Server.Implementations/Localization/Core/te.json b/Emby.Server.Implementations/Localization/Core/te.json index 24168b611..7d4422d62 100644 --- a/Emby.Server.Implementations/Localization/Core/te.json +++ b/Emby.Server.Implementations/Localization/Core/te.json @@ -38,5 +38,18 @@ "HeaderFavoriteSongs": "ఇష్టమైన పాటలు", "HeaderLiveTV": "ప్రత్యక్ష TV", "HeaderNextUp": "తదుపరి", - "HeaderRecordingGroups": "రికార్డింగ్ గుంపులు" + "HeaderRecordingGroups": "రికార్డింగ్ గుంపులు", + "MessageApplicationUpdated": "జెల్లీఫిన్ సర్వర్ అప్డేట్ చేయడం పూర్తి అయ్యింది", + "MessageApplicationUpdatedTo": "జెల్లీఫిన్ సర్వర్ {0} వెర్షన్ కి అప్డేట్ చెయ్యబడింది", + "MessageServerConfigurationUpdated": "సర్వర్ కన్ఫిగరేషన్ అప్డేట్ చేయబడింది", + "NewVersionIsAvailable": "జెల్లీఫిన్ సర్వర్ యొక్క కొత్త వెర్షన్ డౌన్లోడ్ చేసుకోవడానికి అందుబాటులో ఉంది.", + "NotificationOptionApplicationUpdateInstalled": "అప్లికేషన్ అప్డేట్ ఇన్స్టాల్ చేయబడింది", + "ItemAddedWithName": "{0} లైబ్రరీకి జోడించబడింది", + "ItemRemovedWithName": "లైబ్రరీ నుండి {0} తీసివేయబడింది", + "LabelIpAddressValue": "ఐపీ చిరునామా: {0}", + "LabelRunningTimeValue": "నడుస్తున్న సమయం: {0}", + "Latest": "తాజా", + "NameInstallFailed": "{0} ఇన్స్టాలేషన్ విఫలమైంది", + "NameSeasonUnknown": "భాగం తెలియదు", + "NotificationOptionApplicationUpdateAvailable": "అప్లికేషన్ అప్డేట్ అందుబాటులో ఉంది" } diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index a4877f4b5..d7a627d12 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -89,7 +89,7 @@ "UserPolicyUpdatedWithName": "{0} için kullanıcı politikası güncellendi", "UserStartedPlayingItemWithValues": "{0}, {2} cihazında {1} izliyor", "UserStoppedPlayingItemWithValues": "{0}, {2} cihazında {1} izlemeyi bitirdi", - "ValueHasBeenAddedToLibrary": "Medya kütüphanenize {0} eklendi", + "ValueHasBeenAddedToLibrary": "{0} medya kütüphanenize eklendi", "ValueSpecialEpisodeName": "Özel - {0}", "VersionNumber": "Sürüm {0}", "TaskCleanCache": "Önbellek Dizinini Temizle", @@ -111,7 +111,7 @@ "TaskCleanLogs": "Günlük Dizinini Temizle", "TaskRefreshLibraryDescription": "Medya kütüphanenize eklenen yeni dosyaları arar ve ortam bilgilerini yeniler.", "TaskRefreshLibrary": "Medya Kütüphanesini Tara", - "TaskRefreshChapterImagesDescription": "Sahnelere ayrılmış videolar için küçük resimler oluştur.", + "TaskRefreshChapterImagesDescription": "Bölümlere ayrılmış videolar için küçük resimler oluştur.", "TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar", "TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.", "TaskCleanActivityLog": "Etkinlik Günlüğünü Temizle", diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index bd5398f08..6f0dcfbe3 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -18,16 +18,16 @@ "HeaderContinueWatching": "Продовжити перегляд", "HeaderAlbumArtists": "Виконавці альбому", "Genres": "Жанри", - "Folders": "Каталоги", + "Folders": "Теки", "Favorites": "Обрані", "DeviceOnlineWithName": "Пристрій {0} підключився", "DeviceOfflineWithName": "Пристрій {0} відключився", - "Collections": "Добірки", - "ChapterNameValue": "Розділ {0}", + "Collections": "Колекції", + "ChapterNameValue": "Сцена {0}", "Channels": "Канали", - "CameraImageUploadedFrom": "Нова фотографія завантажена з {0}", + "CameraImageUploadedFrom": "Нову фотографію завантажено з {0}", "Books": "Книги", - "AuthenticationSucceededWithUserName": "{0} успішно автентифіковано", + "AuthenticationSucceededWithUserName": "{0} успішно авторизовано", "Artists": "Виконавці", "Application": "Додаток", "AppDeviceValues": "Додаток: {0}, Пристрій: {1}", @@ -83,7 +83,7 @@ "SubtitleDownloadFailureFromForItem": "Не вдалося завантажити субтитри з {0} для {1}", "StartupEmbyServerIsLoading": "Jellyfin Server завантажується. Будь ласка, спробуйте трішки пізніше.", "Songs": "Пісні", - "Shows": "Шоу", + "Shows": "Телепередачі", "ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити", "ScheduledTaskStartedWithName": "{0} розпочато", "ScheduledTaskFailedWithName": "{0} незавершено, збій", diff --git a/Emby.Server.Implementations/Localization/Core/ur.json b/Emby.Server.Implementations/Localization/Core/ur.json new file mode 100644 index 000000000..376683041 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/ur.json @@ -0,0 +1,3 @@ +{ + "Books": "کتابیں" +} diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index e8b8c2c5f..3ab9774c2 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -1,15 +1,15 @@ { "Albums": "專輯", - "AppDeviceValues": "程式: {0}, 設備: {1}", + "AppDeviceValues": "程式:{0},設備:{1}", "Application": "應用程式", "Artists": "藝人", - "AuthenticationSucceededWithUserName": "{0} 授權成功", + "AuthenticationSucceededWithUserName": "成功授權 {0}", "Books": "書籍", "CameraImageUploadedFrom": "{0} 成功上傳一張新照片", "Channels": "頻道", "ChapterNameValue": "第 {0} 章", "Collections": "系列", - "DeviceOfflineWithName": "{0} 已斷開連接", + "DeviceOfflineWithName": "{0} 已中斷連接", "DeviceOnlineWithName": "{0} 已連接", "FailedLoginAttemptWithUserName": "{0} 登入失敗", "Favorites": "我的最愛", @@ -27,15 +27,15 @@ "HeaderRecordingGroups": "錄製組", "HomeVideos": "家庭影片", "Inherit": "繼承", - "ItemAddedWithName": "{0} 已被添加至媒體庫", + "ItemAddedWithName": "{0} 已被加入至媒體庫", "ItemRemovedWithName": "{0} 已從媒體庫移除", - "LabelIpAddressValue": "IP 地址: {0}", - "LabelRunningTimeValue": "運行時間: {0}", + "LabelIpAddressValue": "IP 地址:{0}", + "LabelRunningTimeValue": "運作時間:{0}", "Latest": "最新", "MessageApplicationUpdated": "Jellyfin 已被更新", "MessageApplicationUpdatedTo": "Jellyfin 已被更新至 {0}", "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 已被更新", - "MessageServerConfigurationUpdated": "伺服器設定已經被更新", + "MessageServerConfigurationUpdated": "已更新伺服器設定", "MixedContent": "混合內容", "Movies": "電影", "Music": "音樂", @@ -43,23 +43,23 @@ "NameInstallFailed": "{0} 安裝失敗", "NameSeasonNumber": "第 {0} 季", "NameSeasonUnknown": "未知的季度", - "NewVersionIsAvailable": "有較新版本的 Jellyfin 可供下載。", + "NewVersionIsAvailable": "有新版本的 Jellyfin 可供下載。", "NotificationOptionApplicationUpdateAvailable": "有可用的更新", - "NotificationOptionApplicationUpdateInstalled": "應用程式已被更新", - "NotificationOptionAudioPlayback": "開始播放音訊", + "NotificationOptionApplicationUpdateInstalled": "完成更新應用程式", + "NotificationOptionAudioPlayback": "播放音訊", "NotificationOptionAudioPlaybackStopped": "停止播放音訊", - "NotificationOptionCameraImageUploaded": "相片已被上傳", + "NotificationOptionCameraImageUploaded": "相片上傳", "NotificationOptionInstallationFailed": "安裝失敗", - "NotificationOptionNewLibraryContent": "已添加新内容", - "NotificationOptionPluginError": "插件出現錯誤", - "NotificationOptionPluginInstalled": "插件已被安裝", - "NotificationOptionPluginUninstalled": "插件已被移除", - "NotificationOptionPluginUpdateInstalled": "插件已被更新", + "NotificationOptionNewLibraryContent": "新增媒體", + "NotificationOptionPluginError": "插件錯誤", + "NotificationOptionPluginInstalled": "安裝插件", + "NotificationOptionPluginUninstalled": "解除安裝插件", + "NotificationOptionPluginUpdateInstalled": "完成更新插件", "NotificationOptionServerRestartRequired": "伺服器需要重啟", - "NotificationOptionTaskFailed": "排程任務執行失敗", - "NotificationOptionUserLockedOut": "用戶已被鎖定", - "NotificationOptionVideoPlayback": "開始播放影片", - "NotificationOptionVideoPlaybackStopped": "已停止播放影片", + "NotificationOptionTaskFailed": "排程工作執行失敗", + "NotificationOptionUserLockedOut": "封鎖用戶", + "NotificationOptionVideoPlayback": "播放影片", + "NotificationOptionVideoPlaybackStopped": "停止播放影片", "Photos": "相片", "Playlists": "播放清單", "Plugin": "插件", @@ -68,7 +68,7 @@ "PluginUpdatedWithName": "已更新 {0}", "ProviderValue": "提供者:{0}", "ScheduledTaskFailedWithName": "{0} 執行失敗", - "ScheduledTaskStartedWithName": "{0} 開始執行", + "ScheduledTaskStartedWithName": "開始執行 {0}", "ServerNameNeedsToBeRestarted": "{0} 需要重啟", "Shows": "節目", "Songs": "歌曲", @@ -79,50 +79,52 @@ "System": "系統", "TvShows": "電視節目", "User": "用戶", - "UserCreatedWithName": "用戶 {0} 已被建立", + "UserCreatedWithName": "建立新用戶 {0}", "UserDeletedWithName": "用戶 {0} 已被移除", "UserDownloadingItemWithValues": "{0} 正在下載 {1}", - "UserLockedOutWithName": "使用者 {0} 已被鎖定", - "UserOfflineFromDevice": "{0} 從 {1} 斷開連接", + "UserLockedOutWithName": "用戶 {0} 已被封鎖", + "UserOfflineFromDevice": "{0} 終止了 {1} 的連接", "UserOnlineFromDevice": "{0} 從 {1} 連線", - "UserPasswordChangedWithName": "{0} 的密碼已被變改", - "UserPolicyUpdatedWithName": "使用者協議已更新為 {0}", - "UserStartedPlayingItemWithValues": "{0} 正在 {2} 上播放 {1}", - "UserStoppedPlayingItemWithValues": "{0} 已停止在 {2} 上播放 {1}", - "ValueHasBeenAddedToLibrary": "已添加 {0} 到你的媒體庫", + "UserPasswordChangedWithName": "{0} 的密碼已被更改", + "UserPolicyUpdatedWithName": "使用條款已更新為 {0}", + "UserStartedPlayingItemWithValues": "{0} 在 {2} 上播放 {1}", + "UserStoppedPlayingItemWithValues": "{0} 停止在 {2} 上播放 {1}", + "ValueHasBeenAddedToLibrary": "{0} 已被加入至你的媒體庫", "ValueSpecialEpisodeName": "特典 - {0}", "VersionNumber": "版本 {0}", - "TaskDownloadMissingSubtitles": "下載缺少的字幕", + "TaskDownloadMissingSubtitles": "下載欠缺字幕", "TaskUpdatePlugins": "更新插件", "TasksApplicationCategory": "應用程式", - "TaskRefreshLibraryDescription": "掃描媒體庫以加入新增檔案及重新載入 metadata。", + "TaskRefreshLibraryDescription": "掃描媒體庫以加入新增的檔案及重新載入元數據。", "TasksMaintenanceCategory": "維護", - "TaskDownloadMissingSubtitlesDescription": "根據元數據中的設定,在互聯網上搜索缺少的字幕。", + "TaskDownloadMissingSubtitlesDescription": "根據元數據中的設定,在網上搜尋欠缺的字幕。", "TaskRefreshChannelsDescription": "重新載入網絡頻道的資訊。", "TaskRefreshChannels": "重新載入頻道", - "TaskCleanTranscodeDescription": "刪除超過一天的轉碼文件。", - "TaskCleanTranscode": "清理轉碼目錄", + "TaskCleanTranscodeDescription": "刪除超過一天的轉碼檔案。", + "TaskCleanTranscode": "清理轉碼檔資料夾", "TaskUpdatePluginsDescription": "下載並更新能夠被自動更新的插件。", - "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的元數據。", + "TaskRefreshPeopleDescription": "更新你的媒體中有關的演員和導演的元數據。", "TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔。", - "TaskCleanLogs": "清理紀錄檔目錄", + "TaskCleanLogs": "清理紀錄檔資料夾", "TaskRefreshLibrary": "掃描媒體庫", "TaskRefreshChapterImagesDescription": "為帶有章節的影片建立縮圖。", "TaskRefreshChapterImages": "提取章節圖像", "TaskCleanCacheDescription": "刪除系統不再需要的緩存文件。", - "TaskCleanCache": "清理緩存目錄", + "TaskCleanCache": "清理緩存資料夾", "TasksChannelsCategory": "網絡頻道", - "TasksLibraryCategory": "庫", + "TasksLibraryCategory": "媒體庫", "TaskRefreshPeople": "重新載入人物", "TaskCleanActivityLog": "清理活動記錄", "Undefined": "未定義", "Forced": "強制", "Default": "預設", - "TaskOptimizeDatabaseDescription": "壓縮數據庫並截斷可用空間。在掃描媒體庫或執行其他數據庫的修改後運行此任務可能會提高性能。", + "TaskOptimizeDatabaseDescription": "壓縮數據庫及釋放可用空間。完成任何會修改數據庫的工作(例如掃描媒體庫)後,執行此工作或可提升伺服器速度。", "TaskOptimizeDatabase": "最佳化數據庫", "TaskCleanActivityLogDescription": "刪除早於設定時間的活動記錄。", - "TaskKeyframeExtractorDescription": "提取關鍵幀以建立更準確的 HLS 播放列表。此工作或需要使用較長時間來完成。", - "TaskKeyframeExtractor": "關鍵幀提取器", + "TaskKeyframeExtractorDescription": "提取關鍵影格(Keyframe)以建立更準確的 HLS playlist。此工作可能需要使用較長時間來完成。", + "TaskKeyframeExtractor": "關鍵影格提取器", "External": "外部", - "HearingImpaired": "聽力障礙" + "HearingImpaired": "聽力障礙", + "TaskRefreshTrickplayImages": "建立 Trickplay 圖像", + "TaskRefreshTrickplayImagesDescription": "為已啟用 Trickplay 的媒體庫內的影片建立 Trickplay 預覽圖。" } diff --git a/Emby.Server.Implementations/Localization/countries.json b/Emby.Server.Implementations/Localization/countries.json index 22ffc5e09..0a11b3e45 100644 --- a/Emby.Server.Implementations/Localization/countries.json +++ b/Emby.Server.Implementations/Localization/countries.json @@ -696,7 +696,7 @@ "TwoLetterISORegionName": "SI" }, { - "DisplayName": "Soomaaliya", + "DisplayName": "Somalia", "Name": "SO", "ThreeLetterISORegionName": "SOM", "TwoLetterISORegionName": "SO" diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index d2e2fd7d5..aea8d6532 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -178,7 +179,7 @@ namespace Emby.Server.Implementations.Playlists public Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId) { - var user = userId.Equals(default) ? null : _userManager.GetUserById(userId); + var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId); return AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false) { diff --git a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs index 5c616d534..f65d609c7 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs @@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Playlists public override bool SupportsInheritedParentImages => false; [JsonIgnore] - public override CollectionType? CollectionType => Jellyfin.Data.Enums.CollectionType.Playlists; + public override CollectionType? CollectionType => Jellyfin.Data.Enums.CollectionType.playlists; protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) { diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs index acd4bf905..812df8192 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs @@ -115,9 +115,10 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask List<LinkedChild>? itemsToRemove = null; foreach (var linkedChild in folder.LinkedChildren) { - if (!File.Exists(folder.Path)) + var path = linkedChild.Path; + if (!File.Exists(path)) { - _logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, linkedChild.Path); + _logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, path); (itemsToRemove ??= new List<LinkedChild>()).Add(linkedChild); } } diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index e8e63d286..bbb3938dc 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -189,7 +189,7 @@ namespace Emby.Server.Implementations.Session _logger); } - private void OnSessionEnded(SessionInfo info) + private async ValueTask OnSessionEnded(SessionInfo info) { EventHelper.QueueEventIfNotNull( SessionEnded, @@ -202,7 +202,7 @@ namespace Emby.Server.Implementations.Session _eventManager.Publish(new SessionEndedEventArgs(info)); - info.Dispose(); + await info.DisposeAsync().ConfigureAwait(false); } /// <inheritdoc /> @@ -301,12 +301,12 @@ namespace Emby.Server.Implementations.Session await _mediaSourceManager.CloseLiveStream(session.PlayState.LiveStreamId).ConfigureAwait(false); } - OnSessionEnded(session); + await OnSessionEnded(session).ConfigureAwait(false); } } /// <inheritdoc /> - public void ReportSessionEnded(string sessionId) + public async ValueTask ReportSessionEnded(string sessionId) { CheckDisposed(); var session = GetSession(sessionId, false); @@ -317,7 +317,7 @@ namespace Emby.Server.Implementations.Session _activeConnections.TryRemove(key, out _); - OnSessionEnded(session); + await OnSessionEnded(session).ConfigureAwait(false); } } @@ -337,7 +337,7 @@ namespace Emby.Server.Implementations.Session info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture); } - if (!info.ItemId.Equals(default) && info.Item is null && libraryItem is not null) + if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null) { var current = session.NowPlayingItem; @@ -529,7 +529,7 @@ namespace Emby.Server.Implementations.Session { var users = new List<User>(); - if (session.UserId.Equals(default)) + if (session.UserId.IsEmpty()) { return users; } @@ -690,7 +690,7 @@ namespace Emby.Server.Implementations.Session var session = GetSession(info.SessionId); - var libraryItem = info.ItemId.Equals(default) + var libraryItem = info.ItemId.IsEmpty() ? null : GetNowPlayingItem(session, info.ItemId); @@ -784,7 +784,7 @@ namespace Emby.Server.Implementations.Session var session = GetSession(info.SessionId); - var libraryItem = info.ItemId.Equals(default) + var libraryItem = info.ItemId.IsEmpty() ? null : GetNowPlayingItem(session, info.ItemId); @@ -923,7 +923,7 @@ namespace Emby.Server.Implementations.Session session.StopAutomaticProgress(); - var libraryItem = info.ItemId.Equals(default) + var libraryItem = info.ItemId.IsEmpty() ? null : GetNowPlayingItem(session, info.ItemId); @@ -933,7 +933,7 @@ namespace Emby.Server.Implementations.Session info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture); } - if (!info.ItemId.Equals(default) && info.Item is null && libraryItem is not null) + if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null) { var current = session.NowPlayingItem; @@ -1154,7 +1154,7 @@ namespace Emby.Server.Implementations.Session var session = GetSessionToRemoteControl(sessionId); - var user = session.UserId.Equals(default) ? null : _userManager.GetUserById(session.UserId); + var user = session.UserId.IsEmpty() ? null : _userManager.GetUserById(session.UserId); List<BaseItem> items; @@ -1223,7 +1223,7 @@ namespace Emby.Server.Implementations.Session { var controllingSession = GetSession(controllingSessionId); AssertCanControl(session, controllingSession); - if (!controllingSession.UserId.Equals(default)) + if (!controllingSession.UserId.IsEmpty()) { command.ControllingUserId = controllingSession.UserId; } @@ -1342,7 +1342,7 @@ namespace Emby.Server.Implementations.Session { var controllingSession = GetSession(controllingSessionId); AssertCanControl(session, controllingSession); - if (!controllingSession.UserId.Equals(default)) + if (!controllingSession.UserId.IsEmpty()) { command.ControllingUserId = controllingSession.UserId.ToString("N", CultureInfo.InvariantCulture); } @@ -1463,7 +1463,7 @@ namespace Emby.Server.Implementations.Session ArgumentException.ThrowIfNullOrEmpty(request.AppVersion); User user = null; - if (!request.UserId.Equals(default)) + if (!request.UserId.IsEmpty()) { user = _userManager.GetUserById(request.UserId); } @@ -1590,7 +1590,7 @@ namespace Emby.Server.Implementations.Session { try { - ReportSessionEnded(session.Id); + await ReportSessionEnded(session.Id).ConfigureAwait(false); } catch (Exception ex) { @@ -1670,7 +1670,6 @@ namespace Emby.Server.Implementations.Session var fields = dtoOptions.Fields.ToList(); - fields.Remove(ItemFields.BasicSyncInfo); fields.Remove(ItemFields.CanDelete); fields.Remove(ItemFields.CanDownload); fields.Remove(ItemFields.ChildCount); @@ -1767,7 +1766,7 @@ namespace Emby.Server.Implementations.Session { ArgumentNullException.ThrowIfNull(info); - var user = info.UserId.Equals(default) + var user = info.UserId.IsEmpty() ? null : _userManager.GetUserById(info.UserId); diff --git a/Emby.Server.Implementations/SyncPlay/Group.cs b/Emby.Server.Implementations/SyncPlay/Group.cs index da8f94932..a7821c0e0 100644 --- a/Emby.Server.Implementations/SyncPlay/Group.cs +++ b/Emby.Server.Implementations/SyncPlay/Group.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.SyncPlay; @@ -553,7 +554,7 @@ namespace Emby.Server.Implementations.SyncPlay if (playingItemRemoved) { var itemId = PlayQueue.GetPlayingItemId(); - if (!itemId.Equals(default)) + if (!itemId.IsEmpty()) { var item = _libraryManager.GetItemById(itemId); RunTimeTicks = item.RunTimeTicks ?? 0; diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index ef890aeb4..34c9e86f2 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -41,7 +42,7 @@ namespace Emby.Server.Implementations.TV } string? presentationUniqueKey = null; - if (query.SeriesId.HasValue && !query.SeriesId.Value.Equals(default)) + if (!query.SeriesId.IsNullOrEmpty()) { if (_libraryManager.GetItemById(query.SeriesId.Value) is Series series) { @@ -91,7 +92,7 @@ namespace Emby.Server.Implementations.TV string? presentationUniqueKey = null; int? limit = null; - if (request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default)) + if (!request.SeriesId.IsNullOrEmpty()) { if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series) { @@ -146,7 +147,7 @@ namespace Emby.Server.Implementations.TV // If viewing all next up for all series, remove first episodes // But if that returns empty, keep those first episodes (avoid completely empty view) - var alwaysEnableFirstEpisode = request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default); + var alwaysEnableFirstEpisode = !request.SeriesId.IsNullOrEmpty(); var anyFound = false; return allNextUp diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 15c4cfdf0..ce3d6cab8 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -11,6 +11,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Events; +using Jellyfin.Extensions; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; @@ -227,7 +228,7 @@ namespace Emby.Server.Implementations.Updates availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); } - if (!id.Equals(default)) + if (!id.IsEmpty()) { availablePackages = availablePackages.Where(x => x.Id.Equals(id)); } |
