// Copyright (c) Arlo Godfrey. All Rights Reserved. // Licensed under the GNU Lesser General Public License, Version 3.0 with additional terms. // See the LICENSE, LICENSE.LESSER and LICENSE.ADDITIONAL files in the project root for more information. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Diagnostics; using OwlCore; using OwlCore.AbstractStorage; using OwlCore.AbstractStorage.Scanners; using StrixMusic.Sdk.FileMetadata.Models; namespace StrixMusic.Sdk.FileMetadata.Scanners { /// /// Handles extracting playlist metadata from files. /// public sealed partial class PlaylistMetadataScanner : IDisposable { private static readonly string[] _supportedPlaylistFileFormats = { ".zpl", ".wpl", ".smil", ".m3u", ".m3u8", ".vlc", ".xspf", ".asx", ".mpcpl", ".fpl", ".pls", ".aimppl4" }; private readonly AudioMetadataScanner _audioFileMetadataScanner; private readonly IFileScanner _fileScanner; private readonly SemaphoreSlim _batchLock; private readonly IFolderData _rootFolder; private readonly FileMetadataManager _metadataManager; private readonly string _emitDebouncerId = Guid.NewGuid().ToString(); private readonly List _batchMetadataToEmit = new List(); private CancellationTokenSource? _scanningCancellationTokenSource; private int _filesProcessed; private int _totalFiles; /// /// Creates a new instance . /// /// /// /// public PlaylistMetadataScanner(FileMetadataManager metadataManager, AudioMetadataScanner fileMetadataScanner, IFileScanner fileScanner) { _audioFileMetadataScanner = fileMetadataScanner; _fileScanner = fileScanner; _metadataManager = metadataManager; _rootFolder = fileScanner.RootFolder; _batchLock = new SemaphoreSlim(1, 1); AttachEvents(); } private void AttachEvents() { // TODO attach file system events } private void DetachEvents() { // TODO attach file system events } /// /// Flag that tells if the scanner is initialized or not. /// public bool IsInitialized { get; private set; } /// /// Playlist metadata scanning completed. /// public event EventHandler? PlaylistMetadataScanCompleted; /// /// Raised when a new playlist with metadata is discovered. /// public event EventHandler>? PlaylistMetadataAdded; /// /// Raised when a previously scanned file has been removed from the file system. /// public event EventHandler>? PlaylistMetadataRemoved; /// /// Scans the given for playlist metadata, linking it to the given . /// /// The files to scan for playlists. /// The file metadata to use when linking playlist data. /// An with playlist data linked to the given . public Task> ScanPlaylists(IEnumerable files, IEnumerable fileMetadata) { return ScanPlaylists(files, fileMetadata, new CancellationToken()); } /// /// Scans the given for playlist metadata, linking it to the given . /// /// The files to scan for playlists. /// The file metadata to use when linking playlist data. /// A that cancels the ongoing task. /// An with playlist data linked to the given . public async Task> ScanPlaylists(IEnumerable files, IEnumerable fileMetadata, CancellationToken cancellationToken) { _filesProcessed = 0; if (cancellationToken.IsCancellationRequested) cancellationToken.ThrowIfCancellationRequested(); _scanningCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var playlists = files.Where(c => _supportedPlaylistFileFormats.Contains(c.FileExtension)).ToList(); _totalFiles = playlists.Count; _metadataManager.FilesFound = playlists.Count; var scannedMetadata = new List(); if (_totalFiles == 0) { PlaylistMetadataScanCompleted?.Invoke(this, EventArgs.Empty); return scannedMetadata; } foreach (var item in playlists) { if (cancellationToken.IsCancellationRequested) cancellationToken.ThrowIfCancellationRequested(); var playlistMetadata = await ProcessFile(item, fileMetadata); if (playlistMetadata != null) scannedMetadata.Add(playlistMetadata); } return scannedMetadata; } private async Task ProcessFile(IFileData file, IEnumerable files) { var playlistMetadata = await ScanPlaylistMetadata(_rootFolder, file, files); await _batchLock.WaitAsync(); if (playlistMetadata != null) { _batchMetadataToEmit.Add(playlistMetadata); _metadataManager.FilesProcessed = ++_filesProcessed; } _batchLock.Release(); _ = HandleChanged(); return playlistMetadata; } private async Task HandleChanged() { await _batchLock.WaitAsync(); if (_totalFiles != _filesProcessed && _batchMetadataToEmit.Count < 100 && !await Flow.Debounce(_emitDebouncerId, TimeSpan.FromSeconds(5))) return; Guard.IsNotNull(_scanningCancellationTokenSource, nameof(_scanningCancellationTokenSource)); if (_scanningCancellationTokenSource.Token.IsCancellationRequested) _scanningCancellationTokenSource.Token.ThrowIfCancellationRequested(); PlaylistMetadataAdded?.Invoke(this, _batchMetadataToEmit.ToArray()); _batchMetadataToEmit.Clear(); _batchLock.Release(); if (_totalFiles == _filesProcessed) PlaylistMetadataScanCompleted?.Invoke(this, EventArgs.Empty); } private enum AimpplPlaylistMode { Summary, Settings, Content, } /// public void Dispose() { if (!IsInitialized) return; DetachEvents(); _scanningCancellationTokenSource?.Cancel(); IsInitialized = false; } } }