< Summary - Jellyfin

Information
Class: MediaBrowser.Providers.Playlists.PlaylistItemsProvider
Assembly: MediaBrowser.Providers
File(s): /srv/git/jellyfin/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
Line coverage
36%
Covered lines: 40
Uncovered lines: 71
Coverable lines: 111
Total lines: 279
Line coverage: 36%
Branch coverage
37%
Covered branches: 18
Total branches: 48
Branch coverage: 37.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/16/2026 - 12:15:55 AM Line coverage: 5.4% (5/92) Branch coverage: 0% (0/34) Total lines: 22710/29/2026 - 12:17:11 AM Line coverage: 36% (40/111) Branch coverage: 37.5% (18/48) Total lines: 279 5/16/2026 - 12:15:55 AM Line coverage: 5.4% (5/92) Branch coverage: 0% (0/34) Total lines: 22710/29/2026 - 12:17:11 AM Line coverage: 36% (40/111) Branch coverage: 37.5% (18/48) Total lines: 279

Coverage delta

Coverage delta 38 -38

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Name()100%210%
get_Order()100%210%
GetMetadata(...)100%210%
Fetch(...)0%7280%
GetItems(...)0%110100%
GetPlsItems(...)100%210%
GetM3uItems(...)100%11100%
DetectEncoding(...)50%7671.42%
GetZplItems(...)100%210%
GetWplItems(...)100%210%
GetLinkedChild(...)100%22100%
TryResolvePlaylistItem(...)87.5%8890%
TryNormalizePath(...)100%7675%
HasChanged(...)0%7280%

File(s)

/srv/git/jellyfin/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs

#LineLine coverage
 1#nullable disable
 2
 3using System;
 4using System.Collections.Generic;
 5using System.IO;
 6using System.Linq;
 7using System.Text;
 8using System.Threading;
 9using System.Threading.Tasks;
 10using Jellyfin.Data.Enums;
 11using Jellyfin.Extensions;
 12using MediaBrowser.Controller.Entities;
 13using MediaBrowser.Controller.Library;
 14using MediaBrowser.Controller.Playlists;
 15using MediaBrowser.Controller.Providers;
 16using MediaBrowser.Model.IO;
 17using Microsoft.Extensions.Logging;
 18using PlaylistsNET.Content;
 19using UtfUnknown;
 20
 21namespace MediaBrowser.Providers.Playlists;
 22
 23/// <summary>
 24/// Local playlist provider.
 25/// </summary>
 26public class PlaylistItemsProvider : ILocalMetadataProvider<Playlist>,
 27    IHasOrder,
 28    IForcedProvider,
 29    IHasItemChangeMonitor
 30{
 31    /// <summary>
 32    /// Minimum confidence required before a detected encoding is preferred over UTF-8.
 33    /// </summary>
 34    private const float MinimumEncodingConfidence = 0.5f;
 35
 36    private readonly IFileSystem _fileSystem;
 37    private readonly ILibraryManager _libraryManager;
 38    private readonly ILogger<PlaylistItemsProvider> _logger;
 3439    private readonly CollectionType[] _ignoredCollections = [CollectionType.livetv, CollectionType.boxsets, CollectionTy
 40
 41    /// <summary>
 42    /// Initializes a new instance of the <see cref="PlaylistItemsProvider"/> class.
 43    /// </summary>
 44    /// <param name="logger">Instance of the <see cref="ILogger{PlaylistItemsProvider}"/> interface.</param>
 45    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 46    /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
 47    public PlaylistItemsProvider(ILogger<PlaylistItemsProvider> logger, ILibraryManager libraryManager, IFileSystem file
 48    {
 3449        _logger = logger;
 3450        _libraryManager = libraryManager;
 3451        _fileSystem = fileSystem;
 3452    }
 53
 54    /// <inheritdoc />
 055    public string Name => "Playlist Item Provider";
 56
 57    /// <inheritdoc />
 058    public int Order => 100;
 59
 60    /// <inheritdoc />
 61    public Task<MetadataResult<Playlist>> GetMetadata(
 62        ItemInfo info,
 63        IDirectoryService directoryService,
 64        CancellationToken cancellationToken)
 65    {
 066        var result = new MetadataResult<Playlist>()
 067        {
 068            Item = new Playlist
 069            {
 070                Path = info.Path
 071            }
 072        };
 073        Fetch(result);
 74
 075        return Task.FromResult(result);
 76    }
 77
 78    private void Fetch(MetadataResult<Playlist> result)
 79    {
 080        var item = result.Item;
 081        var path = item.Path;
 082        if (!Playlist.IsPlaylistFile(path))
 83        {
 084            return;
 85        }
 86
 087        var extension = Path.GetExtension(path);
 088        if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
 89        {
 090            return;
 91        }
 92
 093        var items = GetItems(path, extension).ToArray();
 094        if (items.Length > 0)
 95        {
 096            result.HasMetadata = true;
 097            item.LinkedChildren = items;
 98        }
 99
 0100        return;
 101    }
 102
 103    private IEnumerable<LinkedChild> GetItems(string path, string extension)
 104    {
 0105        var libraryRoots = _libraryManager.GetUserRootFolder().Children
 0106                            .OfType<CollectionFolder>()
 0107                            .Where(f => f.CollectionType.HasValue && !_ignoredCollections.Contains(f.CollectionType.Valu
 0108                            .SelectMany(f => f.PhysicalLocations)
 0109                            .Distinct()
 0110                            .ToList();
 111
 0112        using (var stream = File.OpenRead(path))
 113        {
 0114            if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase))
 115            {
 0116                return GetWplItems(stream, path, libraryRoots);
 117            }
 118
 0119            if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
 120            {
 0121                return GetZplItems(stream, path, libraryRoots);
 122            }
 123
 0124            if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
 125            {
 0126                return GetM3uItems(stream, path, libraryRoots);
 127            }
 128
 0129            if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
 130            {
 0131                return GetM3uItems(stream, path, libraryRoots);
 132            }
 133
 0134            if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
 135            {
 0136                return GetPlsItems(stream, path, libraryRoots);
 137            }
 0138        }
 139
 0140        return Enumerable.Empty<LinkedChild>();
 0141    }
 142
 143    private IEnumerable<LinkedChild> GetPlsItems(Stream stream, string playlistPath, List<string> libraryRoots)
 144    {
 0145        var content = new PlsContent();
 0146        var playlist = content.GetFromStream(stream, DetectEncoding(stream, playlistPath));
 147
 0148        return playlist.PlaylistEntries
 0149                .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
 0150                .Where(i => i is not null);
 151    }
 152
 153    internal IEnumerable<LinkedChild> GetM3uItems(Stream stream, string playlistPath, List<string> libraryRoots)
 154    {
 7155        var content = new M3uContent();
 7156        var playlist = content.GetFromStream(stream, DetectEncoding(stream, playlistPath));
 157
 7158        return playlist.PlaylistEntries
 7159                .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
 7160                .Where(i => i is not null);
 161    }
 162
 163    private Encoding DetectEncoding(Stream stream, string playlistPath)
 164    {
 7165        var detected = CharsetDetector.DetectFromStream(stream).Detected;
 7166        stream.Seek(0, SeekOrigin.Begin);
 167
 7168        if (detected?.Encoding is null || detected.Confidence < MinimumEncodingConfidence)
 169        {
 0170            _logger.LogDebug("Could not detect the encoding of playlist {Path}, assuming UTF-8", playlistPath);
 0171            return Encoding.UTF8;
 172        }
 173
 7174        _logger.LogDebug("Detected encoding {Encoding} for playlist {Path}", detected.Encoding.WebName, playlistPath);
 7175        return detected.Encoding;
 176    }
 177
 178    private IEnumerable<LinkedChild> GetZplItems(Stream stream, string playlistPath, List<string> libraryRoots)
 179    {
 0180        var content = new ZplContent();
 0181        var playlist = content.GetFromStream(stream);
 182
 0183        return playlist.PlaylistEntries
 0184                .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
 0185                .Where(i => i is not null);
 186    }
 187
 188    private IEnumerable<LinkedChild> GetWplItems(Stream stream, string playlistPath, List<string> libraryRoots)
 189    {
 0190        var content = new WplContent();
 0191        var playlist = content.GetFromStream(stream);
 192
 0193        return playlist.PlaylistEntries
 0194                .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots))
 0195                .Where(i => i is not null);
 196    }
 197
 198    private LinkedChild GetLinkedChild(string itemPath, string playlistPath, List<string> libraryRoots)
 199    {
 8200        if (TryResolvePlaylistItem(itemPath, playlistPath, libraryRoots, out var item))
 201        {
 7202            return new LinkedChild
 7203            {
 7204                ItemId = item.Id,
 7205                Type = LinkedChildType.Manual
 7206            };
 207        }
 208
 1209        return null;
 210    }
 211
 212    private bool TryResolvePlaylistItem(string itemPath, string playlistPath, List<string> libraryPaths, out BaseItem it
 213    {
 8214        item = null;
 8215        string pathToCheck = _fileSystem.MakeAbsolutePath(Path.GetDirectoryName(playlistPath), itemPath);
 8216        if (!File.Exists(pathToCheck) && !TryNormalizePath(ref pathToCheck))
 217        {
 1218            return false;
 219        }
 220
 21221        foreach (var libraryPath in libraryPaths)
 222        {
 7223            if (pathToCheck.StartsWith(libraryPath, StringComparison.OrdinalIgnoreCase))
 224            {
 7225                item = _libraryManager.FindByPath(pathToCheck, null);
 7226                return item is not null;
 227            }
 228        }
 229
 0230        return false;
 7231    }
 232
 233    private static bool TryNormalizePath(ref string path)
 234    {
 9235        foreach (var form in new[] { NormalizationForm.FormC, NormalizationForm.FormD })
 236        {
 237            string normalized;
 238            try
 239            {
 3240                if (path.IsNormalized(form))
 241                {
 1242                    continue;
 243                }
 244
 2245                normalized = path.Normalize(form);
 2246            }
 0247            catch (ArgumentException)
 248            {
 249                // The path is not valid Unicode, there is nothing to normalize.
 0250                return false;
 251            }
 252
 2253            if (File.Exists(normalized))
 254            {
 1255                path = normalized;
 1256                return true;
 257            }
 258        }
 259
 1260        return false;
 0261    }
 262
 263    /// <inheritdoc />
 264    public bool HasChanged(BaseItem item, IDirectoryService directoryService)
 265    {
 0266        var path = item.Path;
 0267        if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
 268        {
 0269            var file = directoryService.GetFile(path);
 0270            if (file is not null && item.HasChanged(file.LastWriteTimeUtc))
 271            {
 0272                _logger.LogDebug("Refreshing {Path} due to date modified timestamp change.", path);
 0273                return true;
 274            }
 275        }
 276
 0277        return false;
 278    }
 279}