repo creation with initial code after cloning public repo
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// The audio normalization task.
|
||||
/// </summary>
|
||||
public partial class AudioNormalizationTask : IScheduledTask
|
||||
{
|
||||
private readonly IItemRepository _itemRepository;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IApplicationPaths _applicationPaths;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly ILogger<AudioNormalizationTask> _logger;
|
||||
|
||||
private static readonly TimeSpan _dbSaveInterval = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AudioNormalizationTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||
/// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
||||
/// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{AudioNormalizationTask}"/> interface.</param>
|
||||
public AudioNormalizationTask(
|
||||
IItemRepository itemRepository,
|
||||
ILibraryManager libraryManager,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IApplicationPaths applicationPaths,
|
||||
ILocalizationManager localizationManager,
|
||||
ILogger<AudioNormalizationTask> logger)
|
||||
{
|
||||
_itemRepository = itemRepository;
|
||||
_libraryManager = libraryManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_applicationPaths = applicationPaths;
|
||||
_localization = localizationManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => _localization.GetLocalizedString("TaskAudioNormalization");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => _localization.GetLocalizedString("TaskAudioNormalizationDescription");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "AudioNormalization";
|
||||
|
||||
[GeneratedRegex(@"^\s+I:\s+(.*?)\s+LUFS")]
|
||||
private static partial Regex LUFSRegex();
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var numComplete = 0;
|
||||
var libraries = _libraryManager.RootFolder.Children.Where(library => _libraryManager.GetLibraryOptions(library).EnableLUFSScan).ToArray();
|
||||
double percent = 0;
|
||||
|
||||
foreach (var library in libraries)
|
||||
{
|
||||
var startDbSaveInterval = Stopwatch.GetTimestamp();
|
||||
var albums = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = [BaseItemKind.MusicAlbum], Parent = library, Recursive = true });
|
||||
var toSaveDbItems = new List<BaseItem>();
|
||||
|
||||
double nextPercent = numComplete + 1;
|
||||
nextPercent /= libraries.Length;
|
||||
nextPercent -= percent;
|
||||
// Split the progress for this single library into two halves: album gain and track gain.
|
||||
// The first half will be for album gain, the second half for track gain.
|
||||
nextPercent /= 2;
|
||||
var albumComplete = 0;
|
||||
|
||||
foreach (var a in albums)
|
||||
{
|
||||
if (!a.NormalizationGain.HasValue && !a.LUFS.HasValue)
|
||||
{
|
||||
// Album gain
|
||||
var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList();
|
||||
|
||||
// Skip albums that don't have multiple tracks, album gain is useless here
|
||||
if (albumTracks.Count > 1)
|
||||
{
|
||||
_logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id);
|
||||
var tempDir = _applicationPaths.TempDirectory;
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var tempFile = Path.Join(tempDir, a.Id + ".concat");
|
||||
var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal)));
|
||||
await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
a.LUFS = await CalculateLUFSAsync(
|
||||
string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
|
||||
OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
toSaveDbItems.Add(a);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to delete concat file: {FileName}.", tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Stopwatch.GetElapsedTime(startDbSaveInterval) > _dbSaveInterval)
|
||||
{
|
||||
if (toSaveDbItems.Count > 1)
|
||||
{
|
||||
_itemRepository.SaveItems(toSaveDbItems, cancellationToken);
|
||||
toSaveDbItems.Clear();
|
||||
}
|
||||
|
||||
startDbSaveInterval = Stopwatch.GetTimestamp();
|
||||
}
|
||||
|
||||
// Update sub-progress for album gain
|
||||
albumComplete++;
|
||||
double albumPercent = albumComplete;
|
||||
albumPercent /= albums.Count;
|
||||
|
||||
progress.Report(100 * (percent + (albumPercent * nextPercent)));
|
||||
}
|
||||
|
||||
// Update progress to start at the track gain percent calculation
|
||||
percent += nextPercent;
|
||||
|
||||
if (toSaveDbItems.Count > 1)
|
||||
{
|
||||
_itemRepository.SaveItems(toSaveDbItems, cancellationToken);
|
||||
toSaveDbItems.Clear();
|
||||
}
|
||||
|
||||
startDbSaveInterval = Stopwatch.GetTimestamp();
|
||||
|
||||
// Track gain
|
||||
var tracks = _libraryManager.GetItemList(new InternalItemsQuery { MediaTypes = [MediaType.Audio], IncludeItemTypes = [BaseItemKind.Audio], Parent = library, Recursive = true });
|
||||
|
||||
var tracksComplete = 0;
|
||||
foreach (var t in tracks)
|
||||
{
|
||||
if (!t.NormalizationGain.HasValue && !t.LUFS.HasValue && t.IsFileProtocol)
|
||||
{
|
||||
t.LUFS = await CalculateLUFSAsync(
|
||||
string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)),
|
||||
false,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
toSaveDbItems.Add(t);
|
||||
}
|
||||
|
||||
if (Stopwatch.GetElapsedTime(startDbSaveInterval) > _dbSaveInterval)
|
||||
{
|
||||
if (toSaveDbItems.Count > 1)
|
||||
{
|
||||
_itemRepository.SaveItems(toSaveDbItems, cancellationToken);
|
||||
toSaveDbItems.Clear();
|
||||
}
|
||||
|
||||
startDbSaveInterval = Stopwatch.GetTimestamp();
|
||||
}
|
||||
|
||||
// Update sub-progress for track gain
|
||||
tracksComplete++;
|
||||
double trackPercent = tracksComplete;
|
||||
trackPercent /= tracks.Count;
|
||||
|
||||
progress.Report(100 * (percent + (trackPercent * nextPercent)));
|
||||
}
|
||||
|
||||
if (toSaveDbItems.Count > 1)
|
||||
{
|
||||
_itemRepository.SaveItems(toSaveDbItems, cancellationToken);
|
||||
}
|
||||
|
||||
// Update progress
|
||||
numComplete++;
|
||||
percent = numComplete;
|
||||
percent /= libraries.Length;
|
||||
|
||||
progress.Report(100 * percent);
|
||||
}
|
||||
|
||||
progress.Report(100.0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
yield return new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.IntervalTrigger,
|
||||
IntervalTicks = TimeSpan.FromHours(24).Ticks
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<float?> CalculateLUFSAsync(string inputArgs, bool waitForExit, CancellationToken cancellationToken)
|
||||
{
|
||||
var args = $"-hide_banner {inputArgs} -af ebur128=framelog=verbose -f null -";
|
||||
|
||||
using (var process = new Process()
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
Arguments = args,
|
||||
RedirectStandardOutput = false,
|
||||
RedirectStandardError = true
|
||||
},
|
||||
})
|
||||
{
|
||||
_logger.LogDebug("Starting ffmpeg with arguments: {Arguments}", args);
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error starting ffmpeg with arguments: {Arguments}", args);
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
process.PriorityClass = ProcessPriorityClass.BelowNormal;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error setting ffmpeg process priority");
|
||||
}
|
||||
|
||||
using var reader = process.StandardError;
|
||||
float? lufs = null;
|
||||
var foundLufs = false;
|
||||
await foreach (var line in reader.ReadAllLinesAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (foundLufs)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Match match = LUFSRegex().Match(line);
|
||||
if (!match.Success)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
lufs = float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
|
||||
foundLufs = true;
|
||||
}
|
||||
|
||||
if (lufs is null)
|
||||
{
|
||||
_logger.LogError("Failed to find LUFS value in output");
|
||||
}
|
||||
|
||||
if (waitForExit)
|
||||
{
|
||||
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return lufs;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
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.Common.Configuration;
|
||||
using MediaBrowser.Controller.Chapters;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Class ChapterImagesTask.
|
||||
/// </summary>
|
||||
public class ChapterImagesTask : IScheduledTask
|
||||
{
|
||||
private readonly ILogger<ChapterImagesTask> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly IChapterManager _chapterManager;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
||||
/// <param name="chapterManager">Instance of the <see cref="IChapterManager"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
public ChapterImagesTask(
|
||||
ILogger<ChapterImagesTask> logger,
|
||||
ILibraryManager libraryManager,
|
||||
IApplicationPaths appPaths,
|
||||
IChapterManager chapterManager,
|
||||
IFileSystem fileSystem,
|
||||
ILocalizationManager localization)
|
||||
{
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
_appPaths = appPaths;
|
||||
_chapterManager = chapterManager;
|
||||
_fileSystem = fileSystem;
|
||||
_localization = localization;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => _localization.GetLocalizedString("TaskRefreshChapterImages");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => _localization.GetLocalizedString("TaskRefreshChapterImagesDescription");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "RefreshChapterImages";
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
yield return new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.DailyTrigger,
|
||||
TimeOfDayTicks = TimeSpan.FromHours(2).Ticks,
|
||||
MaxRuntimeTicks = TimeSpan.FromHours(4).Ticks
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var videos = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
MediaTypes = [MediaType.Video],
|
||||
IsFolder = false,
|
||||
Recursive = true,
|
||||
DtoOptions = new DtoOptions(false)
|
||||
{
|
||||
EnableImages = false
|
||||
},
|
||||
SourceTypes = [SourceType.Library],
|
||||
IsVirtualItem = false
|
||||
})
|
||||
.OfType<Video>()
|
||||
.ToList();
|
||||
|
||||
var numComplete = 0;
|
||||
|
||||
var failHistoryPath = Path.Combine(_appPaths.CachePath, "chapter-failures.txt");
|
||||
|
||||
List<string> previouslyFailedImages;
|
||||
|
||||
if (File.Exists(failHistoryPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
previouslyFailedImages = (await File.ReadAllTextAsync(failHistoryPath, cancellationToken).ConfigureAwait(false))
|
||||
.Split('|', StringSplitOptions.RemoveEmptyEntries)
|
||||
.ToList();
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
previouslyFailedImages = [];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
previouslyFailedImages = [];
|
||||
}
|
||||
|
||||
var directoryService = new DirectoryService(_fileSystem);
|
||||
|
||||
foreach (var video in videos)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var key = video.Path + video.DateModified.Ticks;
|
||||
|
||||
var extract = !previouslyFailedImages.Contains(key, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
try
|
||||
{
|
||||
var chapters = _chapterManager.GetChapters(video.Id);
|
||||
|
||||
var success = await _chapterManager.RefreshChapterImages(video, directoryService, chapters, extract, true, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
previouslyFailedImages.Add(key);
|
||||
|
||||
var parentPath = Path.GetDirectoryName(failHistoryPath);
|
||||
if (parentPath is not null)
|
||||
{
|
||||
Directory.CreateDirectory(parentPath);
|
||||
}
|
||||
|
||||
string text = string.Join('|', previouslyFailedImages);
|
||||
await File.WriteAllTextAsync(failHistoryPath, text, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= videos.Count;
|
||||
|
||||
progress.Report(100 * percent);
|
||||
}
|
||||
catch (ObjectDisposedException ex)
|
||||
{
|
||||
// TODO Investigate and properly fix.
|
||||
_logger.LogError(ex, "Object Disposed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.Activity;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
|
||||
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Deletes old activity log entries.
|
||||
/// </summary>
|
||||
public class CleanActivityLogTask : IScheduledTask, IConfigurableScheduledTask
|
||||
{
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IActivityManager _activityManager;
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CleanActivityLogTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
/// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
public CleanActivityLogTask(
|
||||
ILocalizationManager localization,
|
||||
IActivityManager activityManager,
|
||||
IServerConfigurationManager serverConfigurationManager)
|
||||
{
|
||||
_localization = localization;
|
||||
_activityManager = activityManager;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => _localization.GetLocalizedString("TaskCleanActivityLog");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "CleanActivityLog";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => _localization.GetLocalizedString("TaskCleanActivityLogDescription");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsHidden => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsLogged => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var retentionDays = _serverConfigurationManager.Configuration.ActivityLogRetentionDays;
|
||||
if (!retentionDays.HasValue || retentionDays < 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Activity Log Retention days must be at least 0. Currently: {retentionDays}");
|
||||
}
|
||||
|
||||
var startDate = DateTime.UtcNow.AddDays(-retentionDays.Value);
|
||||
return _activityManager.CleanAsync(startDate);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Collections;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Deletes path references from collections and playlists that no longer exists.
|
||||
/// </summary>
|
||||
public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
|
||||
{
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly ICollectionManager _collectionManager;
|
||||
private readonly IPlaylistManager _playlistManager;
|
||||
private readonly ILogger<CleanupCollectionAndPlaylistPathsTask> _logger;
|
||||
private readonly IProviderManager _providerManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CleanupCollectionAndPlaylistPathsTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
/// <param name="collectionManager">Instance of the <see cref="ICollectionManager"/> interface.</param>
|
||||
/// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||
public CleanupCollectionAndPlaylistPathsTask(
|
||||
ILocalizationManager localization,
|
||||
ICollectionManager collectionManager,
|
||||
IPlaylistManager playlistManager,
|
||||
ILogger<CleanupCollectionAndPlaylistPathsTask> logger,
|
||||
IProviderManager providerManager)
|
||||
{
|
||||
_localization = localization;
|
||||
_collectionManager = collectionManager;
|
||||
_playlistManager = playlistManager;
|
||||
_logger = logger;
|
||||
_providerManager = providerManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => _localization.GetLocalizedString("TaskCleanCollectionsAndPlaylists");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "CleanCollectionsAndPlaylists";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => _localization.GetLocalizedString("TaskCleanCollectionsAndPlaylistsDescription");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false);
|
||||
if (collectionsFolder is null)
|
||||
{
|
||||
_logger.LogDebug("There is no collections folder to be found");
|
||||
}
|
||||
else
|
||||
{
|
||||
var collections = collectionsFolder.Children.OfType<BoxSet>().ToArray();
|
||||
_logger.LogDebug("Found {CollectionLength} boxsets", collections.Length);
|
||||
|
||||
for (var index = 0; index < collections.Length; index++)
|
||||
{
|
||||
var collection = collections[index];
|
||||
_logger.LogDebug("Checking boxset {CollectionName}", collection.Name);
|
||||
|
||||
await CleanupLinkedChildrenAsync(collection, cancellationToken).ConfigureAwait(false);
|
||||
progress.Report(50D / collections.Length * (index + 1));
|
||||
}
|
||||
}
|
||||
|
||||
var playlistsFolder = _playlistManager.GetPlaylistsFolder();
|
||||
if (playlistsFolder is null)
|
||||
{
|
||||
_logger.LogDebug("There is no playlists folder to be found");
|
||||
return;
|
||||
}
|
||||
|
||||
var playlists = playlistsFolder.Children.OfType<Playlist>().ToArray();
|
||||
_logger.LogDebug("Found {PlaylistLength} playlists", playlists.Length);
|
||||
|
||||
for (var index = 0; index < playlists.Length; index++)
|
||||
{
|
||||
var playlist = playlists[index];
|
||||
_logger.LogDebug("Checking playlist {PlaylistName}", playlist.Name);
|
||||
|
||||
await CleanupLinkedChildrenAsync(playlist, cancellationToken).ConfigureAwait(false);
|
||||
progress.Report(50D / playlists.Length * (index + 1));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CleanupLinkedChildrenAsync<T>(T folder, CancellationToken cancellationToken)
|
||||
where T : Folder
|
||||
{
|
||||
List<LinkedChild>? itemsToRemove = null;
|
||||
foreach (var linkedChild in folder.LinkedChildren)
|
||||
{
|
||||
var path = linkedChild.Path;
|
||||
if (!File.Exists(path) && !Directory.Exists(path))
|
||||
{
|
||||
_logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, path);
|
||||
(itemsToRemove ??= new List<LinkedChild>()).Add(linkedChild);
|
||||
}
|
||||
}
|
||||
|
||||
if (itemsToRemove is not null)
|
||||
{
|
||||
_logger.LogDebug("Updating {FolderName}", folder.Name);
|
||||
folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray();
|
||||
await _providerManager.SaveMetadataAsync(folder, ItemUpdateType.MetadataEdit).ConfigureAwait(false);
|
||||
await folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
yield return new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.StartupTrigger,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma warning disable RS0030 // Do not use banned APIs
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Server.Implementations.Item;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Task to clean up any detached userdata from the database.
|
||||
/// </summary>
|
||||
public class CleanupUserDataTask : IScheduledTask
|
||||
{
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||
private readonly ILogger<CleanupUserDataTask> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CleanupUserDataTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="localization">The localisation Provider.</param>
|
||||
/// <param name="dbProvider">The DB context factory.</param>
|
||||
/// <param name="logger">A logger.</param>
|
||||
public CleanupUserDataTask(ILocalizationManager localization, IDbContextFactory<JellyfinDbContext> dbProvider, ILogger<CleanupUserDataTask> logger)
|
||||
{
|
||||
_localization = localization;
|
||||
_dbProvider = dbProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => _localization.GetLocalizedString("CleanupUserDataTask");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => _localization.GetLocalizedString("CleanupUserDataTaskDescription");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => nameof(CleanupUserDataTask);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
const int LimitDays = 90;
|
||||
var userDataDate = DateTime.UtcNow.AddDays(LimitDays * -1);
|
||||
var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (dbContext.ConfigureAwait(false))
|
||||
{
|
||||
var detachedUserData = dbContext.UserData.Where(e => e.ItemId == BaseItemRepository.PlaceholderId);
|
||||
_logger.LogInformation("There are {NoDetached} detached UserData entries.", detachedUserData.Count());
|
||||
|
||||
detachedUserData = detachedUserData.Where(e => e.RetentionDate < userDataDate);
|
||||
|
||||
_logger.LogInformation("{NoDetached} are older then {Limit} days.", detachedUserData.Count(), LimitDays);
|
||||
|
||||
await detachedUserData.ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Deletes old cache files.
|
||||
/// </summary>
|
||||
public class DeleteCacheFileTask : IScheduledTask, IConfigurableScheduledTask
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the application paths.
|
||||
/// </summary>
|
||||
/// <value>The application paths.</value>
|
||||
private readonly IApplicationPaths _applicationPaths;
|
||||
private readonly ILogger<DeleteCacheFileTask> _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeleteCacheFileTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
public DeleteCacheFileTask(
|
||||
IApplicationPaths appPaths,
|
||||
ILogger<DeleteCacheFileTask> logger,
|
||||
IFileSystem fileSystem,
|
||||
ILocalizationManager localization)
|
||||
{
|
||||
_applicationPaths = appPaths;
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_localization = localization;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => _localization.GetLocalizedString("TaskCleanCache");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "DeleteCacheFiles";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsHidden => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsLogged => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
yield return new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.IntervalTrigger,
|
||||
IntervalTicks = TimeSpan.FromHours(24).Ticks
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var minDateModified = DateTime.UtcNow.AddDays(-30);
|
||||
|
||||
try
|
||||
{
|
||||
DeleteCacheFilesFromDirectory(_applicationPaths.CachePath, minDateModified, progress, cancellationToken);
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
// No biggie here. Nothing to delete
|
||||
}
|
||||
|
||||
progress.Report(90);
|
||||
|
||||
minDateModified = DateTime.UtcNow.AddDays(-1);
|
||||
|
||||
try
|
||||
{
|
||||
DeleteCacheFilesFromDirectory(_applicationPaths.TempDirectory, minDateModified, progress, cancellationToken);
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
// No biggie here. Nothing to delete
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the cache files from directory with a last write time less than a given date.
|
||||
/// </summary>
|
||||
/// <param name="directory">The directory.</param>
|
||||
/// <param name="minDateModified">The min date modified.</param>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The task cancellation token.</param>
|
||||
private void DeleteCacheFilesFromDirectory(string directory, DateTime minDateModified, IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var filesToDelete = _fileSystem.GetFiles(directory, true)
|
||||
.Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
|
||||
.ToList();
|
||||
|
||||
var index = 0;
|
||||
|
||||
foreach (var file in filesToDelete)
|
||||
{
|
||||
double percent = index;
|
||||
percent /= filesToDelete.Count;
|
||||
|
||||
progress.Report(100 * percent);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
|
||||
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Deletes old log files.
|
||||
/// </summary>
|
||||
public class DeleteLogFileTask : IScheduledTask, IConfigurableScheduledTask
|
||||
{
|
||||
private readonly IConfigurationManager _configurationManager;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeleteLogFileTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
public DeleteLogFileTask(IConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization)
|
||||
{
|
||||
_configurationManager = configurationManager;
|
||||
_fileSystem = fileSystem;
|
||||
_localization = localization;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => _localization.GetLocalizedString("TaskCleanLogs");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("TaskCleanLogsDescription"),
|
||||
_configurationManager.CommonConfiguration.LogFileRetentionDays);
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "CleanLogFiles";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsHidden => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsLogged => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
yield return new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.IntervalTrigger,
|
||||
IntervalTicks = TimeSpan.FromHours(24).Ticks
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
// Delete log files more than n days old
|
||||
var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays);
|
||||
|
||||
// Only delete files that serilog doesn't manage (anything that doesn't start with 'log_'
|
||||
var filesToDelete = _fileSystem.GetFiles(_configurationManager.CommonApplicationPaths.LogDirectoryPath, true)
|
||||
.Where(f => !f.Name.StartsWith("log_", StringComparison.Ordinal)
|
||||
&& _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
|
||||
.ToList();
|
||||
|
||||
var index = 0;
|
||||
|
||||
foreach (var file in filesToDelete)
|
||||
{
|
||||
double percent = index / (double)filesToDelete.Count;
|
||||
|
||||
progress.Report(100 * percent);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
_fileSystem.DeleteFile(file.FullName);
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all transcoding temp files.
|
||||
/// </summary>
|
||||
public class DeleteTranscodeFileTask : IScheduledTask, IConfigurableScheduledTask
|
||||
{
|
||||
private readonly ILogger<DeleteTranscodeFileTask> _logger;
|
||||
private readonly IConfigurationManager _configurationManager;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeleteTranscodeFileTask"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{DeleteTranscodeFileTask}"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
public DeleteTranscodeFileTask(
|
||||
ILogger<DeleteTranscodeFileTask> logger,
|
||||
IFileSystem fileSystem,
|
||||
IConfigurationManager configurationManager,
|
||||
ILocalizationManager localization)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_configurationManager = configurationManager;
|
||||
_localization = localization;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => _localization.GetLocalizedString("TaskCleanTranscode");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => _localization.GetLocalizedString("TaskCleanTranscodeDescription");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "DeleteTranscodeFiles";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsHidden => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsLogged => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
yield return new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.StartupTrigger
|
||||
};
|
||||
|
||||
yield return new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.IntervalTrigger,
|
||||
IntervalTicks = TimeSpan.FromHours(24).Ticks
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var minDateModified = DateTime.UtcNow.AddDays(-1);
|
||||
progress.Report(50);
|
||||
|
||||
DeleteTempFilesFromDirectory(_configurationManager.GetTranscodePath(), minDateModified, progress, cancellationToken);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the transcoded temp files from directory with a last write time less than a given date.
|
||||
/// </summary>
|
||||
/// <param name="directory">The directory.</param>
|
||||
/// <param name="minDateModified">The min date modified.</param>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The task cancellation token.</param>
|
||||
private void DeleteTempFilesFromDirectory(string directory, DateTime minDateModified, IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var filesToDelete = _fileSystem.GetFiles(directory, true)
|
||||
.Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
|
||||
.ToList();
|
||||
|
||||
var index = 0;
|
||||
|
||||
foreach (var file in filesToDelete)
|
||||
{
|
||||
double percent = index;
|
||||
percent /= filesToDelete.Count;
|
||||
|
||||
progress.Report(100 * percent);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger);
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger);
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaSegments;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
|
||||
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Task to obtain media segments.
|
||||
/// </summary>
|
||||
public class MediaSegmentExtractionTask : IScheduledTask
|
||||
{
|
||||
/// <summary>
|
||||
/// The library manager.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IMediaSegmentManager _mediaSegmentManager;
|
||||
private static readonly BaseItemKind[] _itemTypes = [BaseItemKind.Episode, BaseItemKind.Movie, BaseItemKind.Audio, BaseItemKind.AudioBook];
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MediaSegmentExtractionTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="localization">The localization manager.</param>
|
||||
/// <param name="mediaSegmentManager">The segment manager.</param>
|
||||
public MediaSegmentExtractionTask(ILibraryManager libraryManager, ILocalizationManager localization, IMediaSegmentManager mediaSegmentManager)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_localization = localization;
|
||||
_mediaSegmentManager = mediaSegmentManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name => _localization.GetLocalizedString("TaskExtractMediaSegments");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Description => _localization.GetLocalizedString("TaskExtractMediaSegmentsDescription");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Key => "TaskExtractMediaSegments";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
progress.Report(0);
|
||||
|
||||
var pagesize = 100;
|
||||
|
||||
var query = new InternalItemsQuery
|
||||
{
|
||||
MediaTypes = [MediaType.Video, MediaType.Audio],
|
||||
IsVirtualItem = false,
|
||||
IncludeItemTypes = _itemTypes,
|
||||
DtoOptions = new DtoOptions(true),
|
||||
SourceTypes = [SourceType.Library],
|
||||
Recursive = true,
|
||||
Limit = pagesize
|
||||
};
|
||||
|
||||
var numberOfVideos = _libraryManager.GetCount(query);
|
||||
|
||||
var startIndex = 0;
|
||||
var numComplete = 0;
|
||||
|
||||
while (startIndex < numberOfVideos)
|
||||
{
|
||||
query.StartIndex = startIndex;
|
||||
|
||||
var baseItems = _libraryManager.GetItemList(query);
|
||||
var currentPageCount = baseItems.Count;
|
||||
// TODO parallelize with Parallel.ForEach?
|
||||
for (var i = 0; i < currentPageCount; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var item = baseItems[i];
|
||||
// Only local files supported
|
||||
if (item.IsFileProtocol && File.Exists(item.Path))
|
||||
{
|
||||
var libraryOptions = _libraryManager.GetLibraryOptions(item);
|
||||
await _mediaSegmentManager.RunSegmentPluginProviders(item, libraryOptions, false, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Update progress
|
||||
numComplete++;
|
||||
double percent = (double)numComplete / numberOfVideos;
|
||||
progress.Report(100 * percent);
|
||||
}
|
||||
|
||||
startIndex += pagesize;
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
yield return new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.IntervalTrigger,
|
||||
IntervalTicks = TimeSpan.FromHours(12).Ticks
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Optimizes Jellyfin's database by issuing a VACUUM command.
|
||||
/// </summary>
|
||||
public class OptimizeDatabaseTask : IScheduledTask, IConfigurableScheduledTask
|
||||
{
|
||||
private readonly ILogger<OptimizeDatabaseTask> _logger;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IJellyfinDatabaseProvider _jellyfinDatabaseProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
||||
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
/// <param name="jellyfinDatabaseProvider">Instance of the JellyfinDatabaseProvider that can be used for provider specific operations.</param>
|
||||
public OptimizeDatabaseTask(
|
||||
ILogger<OptimizeDatabaseTask> logger,
|
||||
ILocalizationManager localization,
|
||||
IJellyfinDatabaseProvider jellyfinDatabaseProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_localization = localization;
|
||||
_jellyfinDatabaseProvider = jellyfinDatabaseProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => _localization.GetLocalizedString("TaskOptimizeDatabase");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => _localization.GetLocalizedString("TaskOptimizeDatabaseDescription");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "OptimizeDatabaseTask";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsHidden => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsLogged => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
yield return new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.IntervalTrigger,
|
||||
IntervalTicks = TimeSpan.FromHours(6).Ticks
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Optimizing and vacuuming jellyfin.db...");
|
||||
|
||||
try
|
||||
{
|
||||
await _jellyfinDatabaseProvider.RunScheduledOptimisation(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error while optimizing jellyfin.db");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Class PeopleValidationTask.
|
||||
/// </summary>
|
||||
public class PeopleValidationTask : IScheduledTask, IConfigurableScheduledTask
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PeopleValidationTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
/// <param name="dbContextFactory">Instance of the <see cref="IDbContextFactory{TContext}"/> interface.</param>
|
||||
public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization, IDbContextFactory<JellyfinDbContext> dbContextFactory)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_localization = localization;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => _localization.GetLocalizedString("TaskRefreshPeople");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => _localization.GetLocalizedString("TaskRefreshPeopleDescription");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "RefreshPeople";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsHidden => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsLogged => true;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the triggers that define when the task will run.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IEnumerable{TaskTriggerInfo}"/> containing the default trigger infos for this task.</returns>
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
yield return new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.IntervalTrigger,
|
||||
IntervalTicks = TimeSpan.FromDays(7).Ticks
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
|
||||
await _libraryManager.ValidatePeopleAsync(subProgress, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
|
||||
var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (context.ConfigureAwait(false))
|
||||
{
|
||||
var dupQuery = context.Peoples
|
||||
.GroupBy(e => new { e.Name, e.PersonType })
|
||||
.Where(e => e.Count() > 1)
|
||||
.Select(e => e.Select(f => f.Id).ToArray());
|
||||
|
||||
var total = dupQuery.Count();
|
||||
|
||||
const int PartitionSize = 100;
|
||||
var iterator = 0;
|
||||
int itemCounter;
|
||||
var buffer = ArrayPool<Guid[]>.Shared.Rent(PartitionSize)!;
|
||||
try
|
||||
{
|
||||
do
|
||||
{
|
||||
itemCounter = 0;
|
||||
await foreach (var item in dupQuery
|
||||
.Take(PartitionSize)
|
||||
.AsAsyncEnumerable()
|
||||
.WithCancellation(cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
{
|
||||
buffer[itemCounter++] = item;
|
||||
}
|
||||
|
||||
for (int i = 0; i < itemCounter; i++)
|
||||
{
|
||||
var item = buffer[i];
|
||||
var reference = item[0];
|
||||
var dups = item[1..];
|
||||
await context.PeopleBaseItemMap.WhereOneOrMany(dups, e => e.PeopleId)
|
||||
.ExecuteUpdateAsync(e => e.SetProperty(f => f.PeopleId, reference), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
await context.Peoples.Where(e => dups.Contains(e.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
|
||||
subProgress.Report(100f / total * ((iterator * PartitionSize) + i));
|
||||
}
|
||||
|
||||
iterator++;
|
||||
} while (itemCounter == PartitionSize && !cancellationToken.IsCancellationRequested);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<Guid[]>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
subProgress.Report(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Updates;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin Update Task.
|
||||
/// </summary>
|
||||
public class PluginUpdateTask : IScheduledTask, IConfigurableScheduledTask
|
||||
{
|
||||
private readonly ILogger<PluginUpdateTask> _logger;
|
||||
|
||||
private readonly IInstallationManager _installationManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginUpdateTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
||||
/// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
|
||||
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
public PluginUpdateTask(ILogger<PluginUpdateTask> logger, IInstallationManager installationManager, ILocalizationManager localization)
|
||||
{
|
||||
_logger = logger;
|
||||
_installationManager = installationManager;
|
||||
_localization = localization;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => _localization.GetLocalizedString("TaskUpdatePlugins");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Category => _localization.GetLocalizedString("TasksApplicationCategory");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "PluginUpdates";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsHidden => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnabled => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsLogged => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
yield return new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.StartupTrigger
|
||||
};
|
||||
|
||||
yield return new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.IntervalTrigger,
|
||||
IntervalTicks = TimeSpan.FromHours(24).Ticks
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
progress.Report(0);
|
||||
|
||||
var packageFetchTask = _installationManager.GetAvailablePluginUpdates(cancellationToken);
|
||||
var packagesToInstall = (await packageFetchTask.ConfigureAwait(false)).ToList();
|
||||
|
||||
progress.Report(10);
|
||||
|
||||
var numComplete = 0;
|
||||
|
||||
foreach (var package in packagesToInstall)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
await _installationManager.InstallPackage(package, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// InstallPackage has its own inner cancellation token, so only throw this if it's ours
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error downloading {Name}", package.Name);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error updating {Name}", package.Name);
|
||||
}
|
||||
catch (InvalidDataException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error updating {Name}", package.Name);
|
||||
}
|
||||
|
||||
// Update progress
|
||||
lock (progress)
|
||||
{
|
||||
progress.Report((90.0 * ++numComplete / packagesToInstall.Count) + 10);
|
||||
}
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Library;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
|
||||
namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Class RefreshMediaLibraryTask.
|
||||
/// </summary>
|
||||
public class RefreshMediaLibraryTask : IScheduledTask
|
||||
{
|
||||
/// <summary>
|
||||
/// The _library manager.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RefreshMediaLibraryTask" /> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
public RefreshMediaLibraryTask(ILibraryManager libraryManager, ILocalizationManager localization)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_localization = localization;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => _localization.GetLocalizedString("TaskRefreshLibrary");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => _localization.GetLocalizedString("TaskRefreshLibraryDescription");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => "RefreshLibrary";
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
||||
{
|
||||
yield return new TaskTriggerInfo
|
||||
{
|
||||
Type = TaskTriggerInfoType.IntervalTrigger,
|
||||
IntervalTicks = TimeSpan.FromHours(12).Ticks
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
progress.Report(0);
|
||||
|
||||
await ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user