#nullable disable #pragma warning disable CS1591 using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; using PlaylistsNET.Content; namespace MediaBrowser.Providers.Playlists { public class PlaylistItemsProvider : ICustomMetadataProvider, IHasOrder, IForcedProvider, IPreRefreshProvider, IHasItemChangeMonitor { private readonly IFileSystem _fileSystem; private readonly ILibraryManager _libraryManager; private readonly ILogger _logger; private readonly CollectionType[] _ignoredCollections = [CollectionType.livetv, CollectionType.boxsets, CollectionType.playlists]; public PlaylistItemsProvider(ILogger logger, ILibraryManager libraryManager, IFileSystem fileSystem) { _logger = logger; _libraryManager = libraryManager; _fileSystem = fileSystem; } public string Name => "Playlist Reader"; // Run last public int Order => 100; public Task FetchAsync(Playlist item, MetadataRefreshOptions options, CancellationToken cancellationToken) { var path = item.Path; if (!Playlist.IsPlaylistFile(path)) { return Task.FromResult(ItemUpdateType.None); } var extension = Path.GetExtension(path); if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { return Task.FromResult(ItemUpdateType.None); } var items = GetItems(path, extension).ToArray(); item.LinkedChildren = items; return Task.FromResult(ItemUpdateType.MetadataImport); } private IEnumerable GetItems(string path, string extension) { var libraryRoots = _libraryManager.GetUserRootFolder().Children .OfType() .Where(f => f.CollectionType.HasValue && !_ignoredCollections.Contains(f.CollectionType.Value)) .SelectMany(f => f.PhysicalLocations) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); using (var stream = File.OpenRead(path)) { if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase)) { return GetWplItems(stream, path, libraryRoots); } if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase)) { return GetZplItems(stream, path, libraryRoots); } if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase)) { return GetM3uItems(stream, path, libraryRoots); } if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase)) { return GetM3uItems(stream, path, libraryRoots); } if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase)) { return GetPlsItems(stream, path, libraryRoots); } } return Enumerable.Empty(); } private IEnumerable GetPlsItems(Stream stream, string playlistPath, List libraryRoots) { var content = new PlsContent(); var playlist = content.GetFromStream(stream); return playlist.PlaylistEntries .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots)) .Where(i => i is not null); } private IEnumerable GetM3uItems(Stream stream, string playlistPath, List libraryRoots) { var content = new M3uContent(); var playlist = content.GetFromStream(stream); return playlist.PlaylistEntries .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots)) .Where(i => i is not null); } private IEnumerable GetZplItems(Stream stream, string playlistPath, List libraryRoots) { var content = new ZplContent(); var playlist = content.GetFromStream(stream); return playlist.PlaylistEntries .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots)) .Where(i => i is not null); } private IEnumerable GetWplItems(Stream stream, string playlistPath, List libraryRoots) { var content = new WplContent(); var playlist = content.GetFromStream(stream); return playlist.PlaylistEntries .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots)) .Where(i => i is not null); } private LinkedChild GetLinkedChild(string itemPath, string playlistPath, List libraryRoots) { if (TryGetPlaylistItemPath(itemPath, playlistPath, libraryRoots, out var parsedPath)) { return new LinkedChild { Path = parsedPath, Type = LinkedChildType.Manual }; } return null; } private bool TryGetPlaylistItemPath(string itemPath, string playlistPath, List libraryPaths, out string path) { path = null; string pathToCheck = _fileSystem.MakeAbsolutePath(Path.GetDirectoryName(playlistPath), itemPath); if (!File.Exists(pathToCheck)) { return false; } foreach (var libraryPath in libraryPaths) { if (pathToCheck.StartsWith(libraryPath, StringComparison.OrdinalIgnoreCase)) { path = pathToCheck; return true; } } return false; } public bool HasChanged(BaseItem item, IDirectoryService directoryService) { var path = item.Path; if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol) { var file = directoryService.GetFile(path); if (file is not null && file.LastWriteTimeUtc != item.DateModified) { _logger.LogDebug("Refreshing {Path} due to date modified timestamp change.", path); return true; } } return false; } } }