Replaced another id dependency with cache. Now safe for multi-file audiobooks. Also safe for current session not trying to move files created in a previous session or a parallel session of a different title

This commit is contained in:
Robert McRackan 2021-10-08 21:34:42 -04:00
parent c9a6c8fd35
commit df90094884
13 changed files with 258 additions and 221 deletions

View File

@ -87,6 +87,30 @@ namespace AaxDecrypter
return success;
}
/*
https://github.com/rmcrackan/Libation/pull/127#issuecomment-939088489
If the chapter truly is empty, that is, 0 audio frames in length, then yes it is ignored.
If the chapter is shorter than 3 seconds long but still has some audio frames, those frames are combined with the following chapter and not split into a new file.
I also implemented file naming by chapter title. When 2 or more consecutive chapters are combined, the first of the combined chapter's title is used in the file name. For example, given an audiobook with the following chapters:
00:00:00 - 00:00:02 | Part 1
00:00:02 - 00:35:00 | Chapter 1
00:35:02 - 01:02:00 | Chapter 2
01:02:00 - 01:02:02 | Part 2
01:02:02 - 01:41:00 | Chapter 3
01:41:00 - 02:05:00 | Chapter 4
The book will be split into the following files:
00:00:00 - 00:35:00 | Book - 01 - Part 1.m4b
00:35:00 - 01:02:00 | Book - 02 - Chapter 2.m4b
01:02:00 - 01:41:00 | Book - 03 - Part 2.m4b
01:41:00 - 02:05:00 | Book - 04 - Chapter 4.m4b
That naming may not be desirable for everyone, but it's an easy change to instead use the last of the combined chapter's title in the file name.
*/
private bool Step2_DownloadAudiobookAsMultipleFilesPerChapter()
{
var zeroProgress = Step2_Start();

View File

@ -104,7 +104,9 @@ namespace AaxDecrypter
// not a critical step. its failure should not prevent future steps from running
try
{
File.WriteAllText(PathLib.ReplaceExtension(OutputFileName, ".cue"), Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadLicense.ChapterInfo));
var path = PathLib.ReplaceExtension(OutputFileName, ".cue");
File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadLicense.ChapterInfo));
OnFileCreated(path);
}
catch (Exception ex)
{

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Version>6.2.4.0</Version>
<Version>6.2.5.0</Version>
</PropertyGroup>
<ItemGroup>

View File

@ -211,11 +211,11 @@ namespace AppScaffolding
config.InProgress,
DownloadsInProgressDir = AudibleFileStorage.DownloadsInProgress,
DownloadsInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsInProgress).Count(),
DownloadsInProgressDirectory = AudibleFileStorage.DownloadsInProgressDirectory,
DownloadsInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsInProgressDirectory).Count(),
DecryptInProgressDir = AudibleFileStorage.DecryptInProgress,
DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgress).Count(),
DecryptInProgressDirectory = AudibleFileStorage.DecryptInProgressDirectory,
DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgressDirectory).Count(),
});
}

View File

@ -55,7 +55,7 @@ namespace FileLiberator
var mp3Path = Mp3FileName(m4bPath);
FileExt.SafeMove(mp3File.Name, mp3Path);
OnFileCreated(libraryBook.Book.AudibleProductId, FileManager.FileType.Audio, mp3Path);
OnFileCreated(libraryBook.Book.AudibleProductId, mp3Path);
var statusHandler = new StatusHandler();

View File

@ -16,8 +16,27 @@ namespace FileLiberator
{
private AudiobookDownloadBase abDownloader;
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists;
public override void Cancel() => abDownloader?.Cancel();
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{
var entries = new List<FilePathCache.CacheEntry>();
// these only work so minimally b/c CacheEntry is a record.
// in case of parallel decrypts, only capture the ones for this book id.
// if user somehow starts multiple decrypts of the same book in parallel: on their own head be it
void FilePathCache_Inserted(object sender, FilePathCache.CacheEntry e)
{
if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId))
entries.Add(e);
}
void FilePathCache_Removed(object sender, FilePathCache.CacheEntry e)
{
if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId))
entries.Remove(e);
}
OnBegin(libraryBook);
try
@ -25,15 +44,19 @@ namespace FileLiberator
if (libraryBook.Book.Audio_Exists)
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
var outputAudioFilename = await downloadAudiobookAsync(libraryBook);
FilePathCache.Inserted += FilePathCache_Inserted;
FilePathCache.Removed += FilePathCache_Removed;
var success = await downloadAudiobookAsync(libraryBook);
// decrypt failed
if (outputAudioFilename is null)
if (!success)
return new StatusHandler { "Decrypt failed" };
// moves new files from temp dir to final dest
var movedAudioFile = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
var movedAudioFile = moveFilesToBooksDir(libraryBook.Book, entries);
// decrypt failed
if (!movedAudioFile)
return new StatusHandler { "Cannot find final audio file after decryption" };
@ -43,17 +66,20 @@ namespace FileLiberator
}
finally
{
FilePathCache.Inserted -= FilePathCache_Inserted;
FilePathCache.Removed -= FilePathCache_Removed;
OnCompleted(libraryBook);
}
}
private async Task<string> downloadAudiobookAsync(LibraryBook libraryBook)
private async Task<bool> downloadAudiobookAsync(LibraryBook libraryBook)
{
OnStreamingBegin($"Begin decrypting {libraryBook}");
try
{
validate(libraryBook);
downloadValidation(libraryBook);
var api = await libraryBook.GetApiAsync();
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
@ -82,9 +108,9 @@ namespace FileLiberator
audiobookDlLic.ChapterInfo.AddChapter(chap.Title, TimeSpan.FromMilliseconds(chap.LengthMs));
}
var outFileName = Path.Combine(AudibleFileStorage.DecryptInProgress, $"{PathLib.ToPathSafeString(libraryBook.Book.Title)} [{libraryBook.Book.AudibleProductId}].{outputFormat.ToString().ToLower()}");
var outFileName = Path.Combine(AudibleFileStorage.DecryptInProgressDirectory, $"{PathLib.ToPathSafeString(libraryBook.Book.Title)} [{libraryBook.Book.AudibleProductId}].{outputFormat.ToString().ToLower()}");
var cacheDir = AudibleFileStorage.DownloadsInProgress;
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
abDownloader = contentLic.DrmType == AudibleApi.Common.DrmType.Adrm
? new AaxcDownloadConverter(outFileName, cacheDir, audiobookDlLic, outputFormat, Configuration.Instance.SplitFilesByChapter)
@ -95,16 +121,12 @@ namespace FileLiberator
abDownloader.RetrievedAuthors += (_, authors) => OnAuthorsDiscovered(authors);
abDownloader.RetrievedNarrators += (_, narrators) => OnNarratorsDiscovered(narrators);
abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook.Book.AudibleProductId, FileType.Audio, path);
abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook.Book.AudibleProductId, path);
// REAL WORK DONE HERE
var success = await Task.Run(abDownloader.Run);
// decrypt failed
if (!success)
return null;
return outFileName;
return success;
}
finally
{
@ -112,81 +134,7 @@ namespace FileLiberator
}
}
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
{
if (e is null && Configuration.Instance.AllowLibationFixup)
{
OnRequestCoverArt(abDownloader.SetCoverArt);
}
if (e is not null)
{
OnCoverImageDiscovered(e);
}
}
/// <summary>Move new files to 'Books' directory</summary>
/// <returns>True if audiobook file(s) were successfully created and can be located on disk. Else false.</returns>
private static bool moveFilesToBooksDir(Book book, string outputAudioFilename)
{
// create final directory. move each file into it. MOVE AUDIO FILE LAST
// new dir: safetitle_limit50char + " [" + productId + "]"
// TODO make this method handle multiple audio files or a single audio file.
var destinationDir = AudibleFileStorage.Audio.GetDestDir(book.Title, book.AudibleProductId);
Directory.CreateDirectory(destinationDir);
var sortedFiles = getProductFilesSorted(book, outputAudioFilename);
var musicFileExt = Path.GetExtension(outputAudioFilename).Trim('.');
// audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
var audioFileName = FileUtility.GetValidFilename(destinationDir, book.Title, musicFileExt, book.AudibleProductId);
bool movedAudioFile = false;
foreach (var f in sortedFiles)
{
var isAudio = AudibleFileStorage.Audio.IsFileTypeMatch(f);
var dest
= isAudio
? Path.Join(destinationDir, f.Name)
// non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext + "]." + non_audio_ext
: FileUtility.GetValidFilename(destinationDir, book.Title, f.Extension, book.AudibleProductId, musicFileExt);
if (Path.GetExtension(dest).Trim('.').ToLower() == "cue")
Cue.UpdateFileName(f, audioFileName);
File.Move(f.FullName, dest);
if (isAudio)
FilePathCache.Upsert(book.AudibleProductId, FileType.Audio, dest);
movedAudioFile |= AudibleFileStorage.Audio.IsFileTypeMatch(f);
}
AudibleFileStorage.Audio.Refresh();
return movedAudioFile;
}
private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename)
{
// files are: temp path\author\[asin].ext
var m4bDir = new FileInfo(outputAudioFilename).Directory;
var files = m4bDir
.EnumerateFiles()
.Where(f => f.Name.ContainsInsensitive(product.AudibleProductId))
.ToList();
// move audio files to the end of the collection so these files are moved last
var musicFiles = files.Where(f => AudibleFileStorage.Audio.IsFileTypeMatch(f));
var sortedFiles = files
.Except(musicFiles)
.Concat(musicFiles)
.ToList();
return sortedFiles;
}
private static void validate(LibraryBook libraryBook)
private static void downloadValidation(LibraryBook libraryBook)
{
string errorString(string field)
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
@ -208,11 +156,56 @@ namespace FileLiberator
throw new Exception(errorString("Locale"));
}
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists;
public override void Cancel()
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
{
abDownloader?.Cancel();
if (e is null && Configuration.Instance.AllowLibationFixup)
OnRequestCoverArt(abDownloader.SetCoverArt);
if (e is not null)
OnCoverImageDiscovered(e);
}
/// <summary>Move new files to 'Books' directory</summary>
/// <returns>True if audiobook file(s) were successfully created and can be located on disk. Else false.</returns>
private static bool moveFilesToBooksDir(Book book, List<FilePathCache.CacheEntry> entries)
{
// create final directory. move each file into it. MOVE AUDIO FILE LAST
// new dir: safetitle_limit50char + " [" + productId + "]"
// TODO make this method handle multiple audio files or a single audio file.
var destinationDir = AudibleFileStorage.Audio.GetDestDir(book.Title, book.AudibleProductId);
Directory.CreateDirectory(destinationDir);
var music = entries.FirstOrDefault(f => f.FileType == FileType.Audio);
if (music == default)
return false;
var musicFileExt = Path.GetExtension(music.Path).Trim('.');
// audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
var audioFileName = FileUtility.GetValidFilename(destinationDir, book.Title, musicFileExt, book.AudibleProductId);
foreach (var entry in entries)
{
var fileInfo = new FileInfo(entry.Path);
var isAudio = entry.FileType == FileType.Audio;
var dest
= isAudio
? Path.Join(destinationDir, fileInfo.Name)
// non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext + "]." + non_audio_ext
: FileUtility.GetValidFilename(destinationDir, book.Title, fileInfo.Extension, book.AudibleProductId, musicFileExt);
if (Path.GetExtension(dest).Trim('.').ToLower() == "cue")
Cue.UpdateFileName(fileInfo, audioFileName);
File.Move(fileInfo.FullName, dest);
FilePathCache.Insert(book.AudibleProductId, dest);
}
AudibleFileStorage.Audio.Refresh();
return true;
}
}
}

View File

@ -20,7 +20,7 @@ namespace FileLiberator
try
{
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
OnFileCreated("Upgrade", FileManager.FileType.Zip, actualDownloadedFilePath);
OnFileCreated("Upgrade", actualDownloadedFilePath);
return actualDownloadedFilePath;
}
finally

View File

@ -47,7 +47,7 @@ namespace FileLiberator
return Path.Combine(existingPath, Path.GetFileName(file));
var full = FileUtility.GetValidFilename(
AudibleFileStorage.PdfStorageDirectory,
AudibleFileStorage.PdfDirectory,
libraryBook.Book.Title,
Path.GetExtension(file),
libraryBook.Book.AudibleProductId);
@ -72,7 +72,7 @@ namespace FileLiberator
var client = new HttpClient();
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
OnFileCreated(libraryBook.Book.AudibleProductId, FileType.PDF, actualDownloadedFilePath);
OnFileCreated(libraryBook.Book.AudibleProductId, actualDownloadedFilePath);
OnStatusUpdate(actualDownloadedFilePath);
return actualDownloadedFilePath;

View File

@ -10,7 +10,7 @@ namespace FileLiberator
public event EventHandler<TimeSpan> StreamingTimeRemaining;
public event EventHandler<string> StreamingCompleted;
/// <summary>Fired when a file is successfully saved to disk</summary>
public event EventHandler<(string id, FileManager.FileType type, string path)> FileCreated;
public event EventHandler<(string id, string path)> FileCreated;
protected void OnStreamingBegin(string filePath)
{
@ -34,11 +34,11 @@ namespace FileLiberator
StreamingCompleted?.Invoke(this, filePath);
}
protected void OnFileCreated(string productId, FileManager.FileType type, string path)
protected void OnFileCreated(string id, string path)
{
Serilog.Log.Logger.Information("File created {@DebugInfo}", new { Name = nameof(FileCreated), productId, TypeId = (int)type, TypeName = type.ToString(), path });
FileManager.FilePathCache.Upsert(productId, type, path);
FileCreated?.Invoke(this, (productId, type, path));
Serilog.Log.Logger.Information("File created {@DebugInfo}", new { Name = nameof(FileCreated), id, path });
FileManager.FilePathCache.Insert(id, path);
FileCreated?.Invoke(this, (id, path));
}
}
}

View File

@ -3,28 +3,22 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Dinah.Core;
using Dinah.Core.Collections.Generic;
namespace FileManager
{
public enum FileType { Unknown, Audio, AAXC, PDF, Zip }
public abstract class AudibleFileStorage
{
protected abstract string GetFilePathCustom(string productId);
public abstract class AudibleFileStorage : Enumeration<AudibleFileStorage>
{
protected abstract string[] Extensions { get; }
public abstract string StorageDirectory { get; }
#region static
public static string DownloadsInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
public static string DecryptInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName;
public static string PdfDirectory => BooksDirectory;
public static string DownloadsInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
public static string DecryptInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName;
public static string PdfStorageDirectory => BooksDirectory;
private static AaxcFileStorage AAXC { get; } = new AaxcFileStorage();
private static AaxcFileStorage AAXC { get; } = new AaxcFileStorage();
public static bool AaxcExists(string productId) => AAXC.Exists(productId);
#region static
public static AudioFileStorage Audio { get; } = new AudioFileStorage();
public static AudioFileStorage Audio { get; } = new AudioFileStorage();
public static string BooksDirectory
{
@ -35,69 +29,60 @@ namespace FileManager
return Directory.CreateDirectory(Configuration.Instance.Books).FullName;
}
}
private static object bookDirectoryFilesLocker { get; } = new();
internal static BackgroundFileSystem BookDirectoryFiles { get; set; }
#endregion
#region instance
public FileType FileType => (FileType)Value;
private FileType FileType { get; }
private string regexTemplate { get; }
protected IEnumerable<string> extensions_noDots { get; }
private string extAggr { get; }
protected AudibleFileStorage(FileType fileType)
{
FileType = fileType;
protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString())
{
extensions_noDots = Extensions.Select(ext => ext.ToLower().Trim('.')).ToList();
extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}");
BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
var extAggr = FileTypes.GetExtensions(FileType).Aggregate((a, b) => $"{a}|{b}");
regexTemplate = $@"{{0}}.*?\.({extAggr})$";
}
protected string GetFilePath(string productId)
{
var cachedFile = FilePathCache.GetPath(productId, FileType);
// primary lookup
var cachedFile = FilePathCache.GetFirstPath(productId, FileType);
if (cachedFile != null)
return cachedFile;
var regex = new Regex($@"{productId}.*?\.({extAggr})$", RegexOptions.IgnoreCase);
// secondary lookup attempt
var firstOrNull = GetFilePathCustom(productId);
if (firstOrNull is not null)
FilePathCache.Insert(productId, firstOrNull);
string firstOrNull;
return firstOrNull;
}
if (StorageDirectory == BooksDirectory)
{
//If user changed the BooksDirectory, reinitialize.
lock (bookDirectoryFilesLocker)
if (StorageDirectory != BookDirectoryFiles.RootDirectory)
BookDirectoryFiles = new BackgroundFileSystem(StorageDirectory, "*.*", SearchOption.AllDirectories);
firstOrNull = BookDirectoryFiles.FindFile(regex);
}
else
{
firstOrNull =
Directory
.EnumerateFiles(StorageDirectory, "*.*", SearchOption.AllDirectories)
.FirstOrDefault(s => regex.IsMatch(s));
}
if (firstOrNull is not null)
FilePathCache.Upsert(productId, FileType, firstOrNull);
return firstOrNull;
protected Regex GetBookSearchRegex(string productId)
{
var pattern = string.Format(regexTemplate, productId);
return new Regex(pattern, RegexOptions.IgnoreCase);
}
#endregion
}
public class AudioFileStorage : AudibleFileStorage
{
protected override string[] Extensions { get; } = new[] { "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac" };
private static BackgroundFileSystem BookDirectoryFiles { get; set; }
private static object bookDirectoryFilesLocker { get; } = new();
protected override string GetFilePathCustom(string productId)
{
// If user changed the BooksDirectory: reinitialize
lock (bookDirectoryFilesLocker)
if (BooksDirectory != BookDirectoryFiles.RootDirectory)
BookDirectoryFiles = new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
// we always want to use the latest config value, therefore
// - DO use 'get' arrow "=>"
// - do NOT use assign "="
public override string StorageDirectory => BooksDirectory;
var regex = GetBookSearchRegex(productId);
return BookDirectoryFiles.FindFile(regex);
}
public AudioFileStorage() : base(FileType.Audio) { }
internal AudioFileStorage() : base(FileType.Audio)
=> BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
public void Refresh() => BookDirectoryFiles.RefreshFiles();
@ -109,33 +94,25 @@ namespace FileManager
= underscoreIndex < 4
? title
: title.Substring(0, underscoreIndex);
var finalDir = FileUtility.GetValidFilename(StorageDirectory, titleDir, null, asin);
var finalDir = FileUtility.GetValidFilename(BooksDirectory, titleDir, null, asin);
return finalDir;
}
public bool IsFileTypeMatch(FileInfo fileInfo)
=> extensions_noDots.ContainsInsensative(fileInfo.Extension.Trim('.'));
public string GetPath(string productId) => GetFilePath(productId);
}
public class AaxcFileStorage : AudibleFileStorage
internal class AaxcFileStorage : AudibleFileStorage
{
protected override string[] Extensions { get; } = new[] { "aaxc" };
protected override string GetFilePathCustom(string productId)
{
var regex = GetBookSearchRegex(productId);
return Directory
.EnumerateFiles(DownloadsInProgressDirectory, "*.*", SearchOption.AllDirectories)
.FirstOrDefault(s => regex.IsMatch(s));
}
// we always want to use the latest config value, therefore
// - DO use 'get' arrow "=>"
// - do NOT use assign "="
public override string StorageDirectory => DownloadsInProgress;
internal AaxcFileStorage() : base(FileType.AAXC) { }
public AaxcFileStorage() : base(FileType.AAXC) { }
/// <summary>
/// Example for full books:
/// Search recursively in _books directory. Full book exists if either are true
/// - a directory name has the product id and an audio file is immediately inside
/// - any audio filename contains the product id
/// </summary>
public bool Exists(string productId) => GetFilePath(productId) != null;
}
}

View File

@ -10,7 +10,7 @@ namespace FileManager
/// <summary>
/// Tracks actual locations of files. This is especially useful for clicking button to navigate to the book's files.
///
/// Note: this is no longer how Libation manages "Liberated" state. That is not statefully managed in the database.
/// Note: this is no longer how Libation manages "Liberated" state. That is now statefully managed in the database.
/// This paradigm is what allows users to manually choose to not download books. Also allows them to manually toggle
/// this state and download again.
/// </summary>

View File

@ -8,14 +8,13 @@ using Newtonsoft.Json;
namespace FileManager
{
public static class FilePathCache
{
{
public record CacheEntry(string Id, FileType FileType, string Path);
private const string FILENAME = "FileLocations.json";
internal class CacheEntry
{
public string Id { get; set; }
public FileType FileType { get; set; }
public string Path { get; set; }
}
public static event EventHandler<CacheEntry> Inserted;
public static event EventHandler<CacheEntry> Removed;
private static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>();
@ -31,48 +30,51 @@ namespace FileManager
}
}
public static bool Exists(string id, FileType type) => GetPath(id, type) != null;
public static bool Exists(string id, FileType type) => GetFirstPath(id, type) != null;
public static string GetPath(string id, FileType type)
{
var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type);
public static List<(FileType fileType, string path)> GetFiles(string id)
=> getEntries(entry => entry.Id == id)
.Select(entry => (entry.FileType, entry.Path))
.ToList();
if (entry == null)
return null;
public static string GetFirstPath(string id, FileType type)
=> getEntries(entry => entry.Id == id && entry.FileType == type)
.FirstOrDefault()
?.Path;
if (!File.Exists(entry.Path))
{
remove(entry);
return null;
}
return entry.Path;
}
private static void remove(CacheEntry entry)
private static List<CacheEntry> getEntries(Func<CacheEntry, bool> predicate)
{
cache.Remove(entry);
save();
var entries = cache.Where(predicate).ToList();
if (entries is null || !entries.Any())
return null;
remove(entries.Where(e => !File.Exists(e.Path)).ToList());
return entries;
}
public static void Upsert(string id, FileType type, string path)
private static void remove(List<CacheEntry> entries)
{
if (!File.Exists(path))
if (entries is null)
return;
lock (locker)
{
// file not found can happen after rapid move
System.Threading.Thread.Sleep(100);
if (!File.Exists(path))
throw new FileNotFoundException($"Cannot add path to cache. File not found. Id={id} FileType={type}", path);
foreach (var entry in entries)
{
cache.Remove(entry);
Removed?.Invoke(null, entry);
}
save();
}
}
var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type);
if (entry is null)
cache.Add(new CacheEntry { Id = id, FileType = type, Path = path });
else
entry.Path = path;
public static void Insert(string id, string path)
{
var type = FileTypes.GetFileTypeFromPath(path);
var entry = new CacheEntry(id, type, path);
cache.Add(entry);
Inserted?.Invoke(null, entry);
save();
}

39
FileManager/FileTypes.cs Normal file
View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace FileManager
{
public enum FileType { Unknown, Audio, AAXC, PDF, Zip, Cue }
public static class FileTypes
{
private static Dictionary<string, FileType> dic => new()
{
["aaxc"] = FileType.AAXC,
["cue"] = FileType.Cue,
["pdf"] = FileType.PDF,
["zip"] = FileType.Zip,
["aac"] = FileType.Audio,
["flac"] = FileType.Audio,
["m4a"] = FileType.Audio,
["m4b"] = FileType.Audio,
["mp3"] = FileType.Audio,
["mp4"] = FileType.Audio,
["ogg"] = FileType.Audio,
};
public static FileType GetFileTypeFromPath(string path)
=> dic.TryGetValue(Path.GetExtension(path).ToLower().Trim('.'), out var fileType)
? fileType
: FileType.Unknown;
public static List<string> GetExtensions(FileType fileType)
=> dic
.Where(kvp => kvp.Value == fileType)
.Select(kvp => kvp.Key)
.ToList();
}
}