< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.Data.CleanDatabaseScheduledTask
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
Line coverage
58%
Covered lines: 41
Uncovered lines: 29
Coverable lines: 70
Total lines: 153
Line coverage: 58.5%
Branch coverage
10%
Covered branches: 2
Total branches: 20
Branch coverage: 10%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 AM Line coverage: 100% (5/5) Total lines: 1194/19/2026 - 12:14:27 AM Line coverage: 54.9% (28/51) Branch coverage: 7.1% (1/14) Total lines: 1195/4/2026 - 12:15:16 AM Line coverage: 58.5% (41/70) Branch coverage: 10% (2/20) Total lines: 153 4/19/2026 - 12:14:27 AM Line coverage: 54.9% (28/51) Branch coverage: 7.1% (1/14) Total lines: 1195/4/2026 - 12:15:16 AM Line coverage: 58.5% (41/70) Branch coverage: 10% (2/20) Total lines: 153

Coverage delta

Coverage delta 46 -46

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
Run()100%11100%
CleanDeadItems()7.14%421447.72%
CleanOrphanedFilePlaylistsAsync()16.66%8662.5%

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs

#LineLine coverage
 1#pragma warning disable CS1591
 2
 3using System;
 4using System.IO;
 5using System.Linq;
 6using System.Threading;
 7using System.Threading.Tasks;
 8using Jellyfin.Data.Enums;
 9using Jellyfin.Database.Implementations;
 10using MediaBrowser.Controller.Entities;
 11using MediaBrowser.Controller.IO;
 12using MediaBrowser.Controller.Library;
 13using MediaBrowser.Controller.Playlists;
 14using Microsoft.EntityFrameworkCore;
 15using Microsoft.Extensions.Logging;
 16
 17namespace Emby.Server.Implementations.Data;
 18
 19public class CleanDatabaseScheduledTask : ILibraryPostScanTask
 20{
 21    private readonly ILibraryManager _libraryManager;
 22    private readonly ILogger<CleanDatabaseScheduledTask> _logger;
 23    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 24    private readonly IPathManager _pathManager;
 25
 26    public CleanDatabaseScheduledTask(
 27        ILibraryManager libraryManager,
 28        ILogger<CleanDatabaseScheduledTask> logger,
 29        IDbContextFactory<JellyfinDbContext> dbProvider,
 30        IPathManager pathManager)
 31    {
 2132        _libraryManager = libraryManager;
 2133        _logger = logger;
 2134        _dbProvider = dbProvider;
 2135        _pathManager = pathManager;
 2136    }
 37
 38    public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
 39    {
 1640        var deadItemsProgress = new Progress<double>(val => progress.Report(val * 0.8));
 1641        await CleanDeadItems(cancellationToken, deadItemsProgress).ConfigureAwait(false);
 42
 1443        var playlistProgress = new Progress<double>(val => progress.Report(80 + (val * 0.2)));
 1444        await CleanOrphanedFilePlaylistsAsync(cancellationToken, playlistProgress).ConfigureAwait(false);
 1445    }
 46
 47    private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
 48    {
 1649        var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
 1650        {
 1651            HasDeadParentId = true
 1652        });
 53
 1654        var numComplete = 0;
 1655        var numItems = itemIds.Count + 1;
 56
 1657        _logger.LogDebug("Cleaning {Number} items with dead parents", numItems);
 58
 1659        IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
 60
 3261        foreach (var itemId in itemIds)
 62        {
 063            cancellationToken.ThrowIfCancellationRequested();
 64
 065            var item = _libraryManager.GetItemById(itemId);
 066            if (item is not null)
 67            {
 068                _logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name,
 69
 070                foreach (var mediaSource in item.GetMediaSources(false))
 71                {
 72                    // Delete extracted data
 073                    var mediaSourceItem = _libraryManager.GetItemById(mediaSource.Id);
 074                    if (mediaSourceItem is null)
 75                    {
 76                        continue;
 77                    }
 78
 079                    var extractedDataFolders = _pathManager.GetExtractedDataPaths(mediaSourceItem);
 080                    foreach (var folder in extractedDataFolders)
 81                    {
 082                        if (Directory.Exists(folder))
 83                        {
 84                            try
 85                            {
 086                                Directory.Delete(folder, true);
 087                            }
 088                            catch (Exception e)
 89                            {
 090                                _logger.LogWarning("Failed to remove {Folder}: {Exception}", folder, e.Message);
 091                            }
 92                        }
 93                    }
 94                }
 95
 96                // Delete item
 097                _libraryManager.DeleteItem(item, new DeleteOptions
 098                {
 099                    DeleteFileLocation = false
 0100                });
 101            }
 102
 0103            numComplete++;
 0104            double percent = numComplete;
 0105            percent /= numItems;
 0106            subProgress.Report(percent * 100);
 107        }
 108
 16109        subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
 16110        var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
 16111        await using (context.ConfigureAwait(false))
 112        {
 16113            var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
 15114            await using (transaction.ConfigureAwait(false))
 115            {
 15116                await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).Co
 14117                subProgress.Report(50);
 14118                await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
 14119                subProgress.Report(100);
 120            }
 14121        }
 122
 14123        progress.Report(100);
 14124    }
 125
 126    private async Task CleanOrphanedFilePlaylistsAsync(CancellationToken cancellationToken, IProgress<double> progress)
 127    {
 14128        var playlists = _libraryManager.GetItemList(new InternalItemsQuery
 14129        {
 14130            IncludeItemTypes = [BaseItemKind.Playlist],
 14131            Recursive = true
 14132        }).OfType<Playlist>().ToList();
 133
 14134        var numComplete = 0;
 14135        var numItems = Math.Max(playlists.Count, 1);
 136
 28137        foreach (var playlist in playlists)
 138        {
 0139            cancellationToken.ThrowIfCancellationRequested();
 140
 0141            if (playlist.IsFile && !File.Exists(playlist.Path))
 142            {
 0143                _logger.LogInformation("Removing file-based playlist {Name} because source file {Path} no longer exists"
 0144                _libraryManager.DeleteItem(playlist, new DeleteOptions { DeleteFileLocation = false });
 145            }
 146
 0147            numComplete++;
 0148            progress.Report((double)numComplete / numItems * 100);
 149        }
 150
 14151        progress.Report(100);
 14152    }
 153}