// 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;
}
}
}