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:
parent
c9a6c8fd35
commit
df90094884
@ -87,6 +87,30 @@ namespace AaxDecrypter
|
|||||||
return success;
|
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()
|
private bool Step2_DownloadAudiobookAsMultipleFilesPerChapter()
|
||||||
{
|
{
|
||||||
var zeroProgress = Step2_Start();
|
var zeroProgress = Step2_Start();
|
||||||
|
|||||||
@ -104,7 +104,9 @@ namespace AaxDecrypter
|
|||||||
// not a critical step. its failure should not prevent future steps from running
|
// not a critical step. its failure should not prevent future steps from running
|
||||||
try
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<Version>6.2.4.0</Version>
|
<Version>6.2.5.0</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -211,11 +211,11 @@ namespace AppScaffolding
|
|||||||
|
|
||||||
config.InProgress,
|
config.InProgress,
|
||||||
|
|
||||||
DownloadsInProgressDir = AudibleFileStorage.DownloadsInProgress,
|
DownloadsInProgressDirectory = AudibleFileStorage.DownloadsInProgressDirectory,
|
||||||
DownloadsInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsInProgress).Count(),
|
DownloadsInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DownloadsInProgressDirectory).Count(),
|
||||||
|
|
||||||
DecryptInProgressDir = AudibleFileStorage.DecryptInProgress,
|
DecryptInProgressDirectory = AudibleFileStorage.DecryptInProgressDirectory,
|
||||||
DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgress).Count(),
|
DecryptInProgressFiles = Directory.EnumerateFiles(AudibleFileStorage.DecryptInProgressDirectory).Count(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -55,7 +55,7 @@ namespace FileLiberator
|
|||||||
var mp3Path = Mp3FileName(m4bPath);
|
var mp3Path = Mp3FileName(m4bPath);
|
||||||
|
|
||||||
FileExt.SafeMove(mp3File.Name, mp3Path);
|
FileExt.SafeMove(mp3File.Name, mp3Path);
|
||||||
OnFileCreated(libraryBook.Book.AudibleProductId, FileManager.FileType.Audio, mp3Path);
|
OnFileCreated(libraryBook.Book.AudibleProductId, mp3Path);
|
||||||
|
|
||||||
var statusHandler = new StatusHandler();
|
var statusHandler = new StatusHandler();
|
||||||
|
|
||||||
|
|||||||
@ -16,8 +16,27 @@ namespace FileLiberator
|
|||||||
{
|
{
|
||||||
private AudiobookDownloadBase abDownloader;
|
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)
|
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);
|
OnBegin(libraryBook);
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -25,15 +44,19 @@ namespace FileLiberator
|
|||||||
if (libraryBook.Book.Audio_Exists)
|
if (libraryBook.Book.Audio_Exists)
|
||||||
return new StatusHandler { "Cannot find decrypt. Final audio file already 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
|
// decrypt failed
|
||||||
if (outputAudioFilename is null)
|
if (!success)
|
||||||
return new StatusHandler { "Decrypt failed" };
|
return new StatusHandler { "Decrypt failed" };
|
||||||
|
|
||||||
// moves new files from temp dir to final dest
|
// 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)
|
if (!movedAudioFile)
|
||||||
return new StatusHandler { "Cannot find final audio file after decryption" };
|
return new StatusHandler { "Cannot find final audio file after decryption" };
|
||||||
|
|
||||||
@ -43,17 +66,20 @@ namespace FileLiberator
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
FilePathCache.Inserted -= FilePathCache_Inserted;
|
||||||
|
FilePathCache.Removed -= FilePathCache_Removed;
|
||||||
|
|
||||||
OnCompleted(libraryBook);
|
OnCompleted(libraryBook);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> downloadAudiobookAsync(LibraryBook libraryBook)
|
private async Task<bool> downloadAudiobookAsync(LibraryBook libraryBook)
|
||||||
{
|
{
|
||||||
OnStreamingBegin($"Begin decrypting {libraryBook}");
|
OnStreamingBegin($"Begin decrypting {libraryBook}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
validate(libraryBook);
|
downloadValidation(libraryBook);
|
||||||
|
|
||||||
var api = await libraryBook.GetApiAsync();
|
var api = await libraryBook.GetApiAsync();
|
||||||
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
|
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
|
||||||
@ -82,9 +108,9 @@ namespace FileLiberator
|
|||||||
audiobookDlLic.ChapterInfo.AddChapter(chap.Title, TimeSpan.FromMilliseconds(chap.LengthMs));
|
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
|
abDownloader = contentLic.DrmType == AudibleApi.Common.DrmType.Adrm
|
||||||
? new AaxcDownloadConverter(outFileName, cacheDir, audiobookDlLic, outputFormat, Configuration.Instance.SplitFilesByChapter)
|
? new AaxcDownloadConverter(outFileName, cacheDir, audiobookDlLic, outputFormat, Configuration.Instance.SplitFilesByChapter)
|
||||||
@ -95,16 +121,12 @@ namespace FileLiberator
|
|||||||
abDownloader.RetrievedAuthors += (_, authors) => OnAuthorsDiscovered(authors);
|
abDownloader.RetrievedAuthors += (_, authors) => OnAuthorsDiscovered(authors);
|
||||||
abDownloader.RetrievedNarrators += (_, narrators) => OnNarratorsDiscovered(narrators);
|
abDownloader.RetrievedNarrators += (_, narrators) => OnNarratorsDiscovered(narrators);
|
||||||
abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
|
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
|
// REAL WORK DONE HERE
|
||||||
var success = await Task.Run(abDownloader.Run);
|
var success = await Task.Run(abDownloader.Run);
|
||||||
|
|
||||||
// decrypt failed
|
return success;
|
||||||
if (!success)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return outFileName;
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -112,81 +134,7 @@ namespace FileLiberator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
|
private static void downloadValidation(LibraryBook libraryBook)
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
string errorString(string field)
|
string errorString(string field)
|
||||||
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
|
=> $"{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"));
|
throw new Exception(errorString("Locale"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists;
|
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
|
||||||
|
|
||||||
public override void Cancel()
|
|
||||||
{
|
{
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ namespace FileLiberator
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
|
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
|
||||||
OnFileCreated("Upgrade", FileManager.FileType.Zip, actualDownloadedFilePath);
|
OnFileCreated("Upgrade", actualDownloadedFilePath);
|
||||||
return actualDownloadedFilePath;
|
return actualDownloadedFilePath;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|||||||
@ -47,7 +47,7 @@ namespace FileLiberator
|
|||||||
return Path.Combine(existingPath, Path.GetFileName(file));
|
return Path.Combine(existingPath, Path.GetFileName(file));
|
||||||
|
|
||||||
var full = FileUtility.GetValidFilename(
|
var full = FileUtility.GetValidFilename(
|
||||||
AudibleFileStorage.PdfStorageDirectory,
|
AudibleFileStorage.PdfDirectory,
|
||||||
libraryBook.Book.Title,
|
libraryBook.Book.Title,
|
||||||
Path.GetExtension(file),
|
Path.GetExtension(file),
|
||||||
libraryBook.Book.AudibleProductId);
|
libraryBook.Book.AudibleProductId);
|
||||||
@ -72,7 +72,7 @@ namespace FileLiberator
|
|||||||
var client = new HttpClient();
|
var client = new HttpClient();
|
||||||
|
|
||||||
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
|
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
|
||||||
OnFileCreated(libraryBook.Book.AudibleProductId, FileType.PDF, actualDownloadedFilePath);
|
OnFileCreated(libraryBook.Book.AudibleProductId, actualDownloadedFilePath);
|
||||||
|
|
||||||
OnStatusUpdate(actualDownloadedFilePath);
|
OnStatusUpdate(actualDownloadedFilePath);
|
||||||
return actualDownloadedFilePath;
|
return actualDownloadedFilePath;
|
||||||
|
|||||||
@ -10,7 +10,7 @@ namespace FileLiberator
|
|||||||
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
||||||
public event EventHandler<string> StreamingCompleted;
|
public event EventHandler<string> StreamingCompleted;
|
||||||
/// <summary>Fired when a file is successfully saved to disk</summary>
|
/// <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)
|
protected void OnStreamingBegin(string filePath)
|
||||||
{
|
{
|
||||||
@ -34,11 +34,11 @@ namespace FileLiberator
|
|||||||
StreamingCompleted?.Invoke(this, filePath);
|
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 });
|
Serilog.Log.Logger.Information("File created {@DebugInfo}", new { Name = nameof(FileCreated), id, path });
|
||||||
FileManager.FilePathCache.Upsert(productId, type, path);
|
FileManager.FilePathCache.Insert(id, path);
|
||||||
FileCreated?.Invoke(this, (productId, type, path));
|
FileCreated?.Invoke(this, (id, path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,28 +3,22 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Dinah.Core;
|
|
||||||
using Dinah.Core.Collections.Generic;
|
|
||||||
|
|
||||||
namespace FileManager
|
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>
|
#region static
|
||||||
{
|
public static string DownloadsInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
|
||||||
protected abstract string[] Extensions { get; }
|
public static string DecryptInProgressDirectory => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DecryptInProgress")).FullName;
|
||||||
public abstract string StorageDirectory { get; }
|
public static string PdfDirectory => BooksDirectory;
|
||||||
|
|
||||||
public static string DownloadsInProgress => Directory.CreateDirectory(Path.Combine(Configuration.Instance.InProgress, "DownloadsInProgress")).FullName;
|
private static AaxcFileStorage AAXC { get; } = new AaxcFileStorage();
|
||||||
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();
|
|
||||||
public static bool AaxcExists(string productId) => AAXC.Exists(productId);
|
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
|
public static string BooksDirectory
|
||||||
{
|
{
|
||||||
@ -35,69 +29,60 @@ namespace FileManager
|
|||||||
return Directory.CreateDirectory(Configuration.Instance.Books).FullName;
|
return Directory.CreateDirectory(Configuration.Instance.Books).FullName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object bookDirectoryFilesLocker { get; } = new();
|
|
||||||
internal static BackgroundFileSystem BookDirectoryFiles { get; set; }
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region instance
|
#region instance
|
||||||
public FileType FileType => (FileType)Value;
|
private FileType FileType { get; }
|
||||||
|
private string regexTemplate { get; }
|
||||||
|
|
||||||
protected IEnumerable<string> extensions_noDots { get; }
|
protected AudibleFileStorage(FileType fileType)
|
||||||
private string extAggr { get; }
|
{
|
||||||
|
FileType = fileType;
|
||||||
|
|
||||||
protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString())
|
var extAggr = FileTypes.GetExtensions(FileType).Aggregate((a, b) => $"{a}|{b}");
|
||||||
{
|
regexTemplate = $@"{{0}}.*?\.({extAggr})$";
|
||||||
extensions_noDots = Extensions.Select(ext => ext.ToLower().Trim('.')).ToList();
|
|
||||||
extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}");
|
|
||||||
BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected string GetFilePath(string productId)
|
protected string GetFilePath(string productId)
|
||||||
{
|
{
|
||||||
var cachedFile = FilePathCache.GetPath(productId, FileType);
|
// primary lookup
|
||||||
|
var cachedFile = FilePathCache.GetFirstPath(productId, FileType);
|
||||||
if (cachedFile != null)
|
if (cachedFile != null)
|
||||||
return cachedFile;
|
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)
|
protected Regex GetBookSearchRegex(string productId)
|
||||||
{
|
{
|
||||||
//If user changed the BooksDirectory, reinitialize.
|
var pattern = string.Format(regexTemplate, productId);
|
||||||
lock (bookDirectoryFilesLocker)
|
return new Regex(pattern, RegexOptions.IgnoreCase);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AudioFileStorage : AudibleFileStorage
|
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
|
var regex = GetBookSearchRegex(productId);
|
||||||
// - DO use 'get' arrow "=>"
|
return BookDirectoryFiles.FindFile(regex);
|
||||||
// - do NOT use assign "="
|
}
|
||||||
public override string StorageDirectory => BooksDirectory;
|
|
||||||
|
|
||||||
public AudioFileStorage() : base(FileType.Audio) { }
|
internal AudioFileStorage() : base(FileType.Audio)
|
||||||
|
=> BookDirectoryFiles ??= new BackgroundFileSystem(BooksDirectory, "*.*", SearchOption.AllDirectories);
|
||||||
|
|
||||||
public void Refresh() => BookDirectoryFiles.RefreshFiles();
|
public void Refresh() => BookDirectoryFiles.RefreshFiles();
|
||||||
|
|
||||||
@ -109,33 +94,25 @@ namespace FileManager
|
|||||||
= underscoreIndex < 4
|
= underscoreIndex < 4
|
||||||
? title
|
? title
|
||||||
: title.Substring(0, underscoreIndex);
|
: title.Substring(0, underscoreIndex);
|
||||||
var finalDir = FileUtility.GetValidFilename(StorageDirectory, titleDir, null, asin);
|
var finalDir = FileUtility.GetValidFilename(BooksDirectory, titleDir, null, asin);
|
||||||
return finalDir;
|
return finalDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsFileTypeMatch(FileInfo fileInfo)
|
|
||||||
=> extensions_noDots.ContainsInsensative(fileInfo.Extension.Trim('.'));
|
|
||||||
|
|
||||||
public string GetPath(string productId) => GetFilePath(productId);
|
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
|
internal AaxcFileStorage() : base(FileType.AAXC) { }
|
||||||
// - DO use 'get' arrow "=>"
|
|
||||||
// - do NOT use assign "="
|
|
||||||
public override string StorageDirectory => DownloadsInProgress;
|
|
||||||
|
|
||||||
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;
|
public bool Exists(string productId) => GetFilePath(productId) != null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ namespace FileManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tracks actual locations of files. This is especially useful for clicking button to navigate to the book's files.
|
/// 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 paradigm is what allows users to manually choose to not download books. Also allows them to manually toggle
|
||||||
/// this state and download again.
|
/// this state and download again.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -8,14 +8,13 @@ using Newtonsoft.Json;
|
|||||||
namespace FileManager
|
namespace FileManager
|
||||||
{
|
{
|
||||||
public static class FilePathCache
|
public static class FilePathCache
|
||||||
{
|
{
|
||||||
|
public record CacheEntry(string Id, FileType FileType, string Path);
|
||||||
|
|
||||||
private const string FILENAME = "FileLocations.json";
|
private const string FILENAME = "FileLocations.json";
|
||||||
internal class CacheEntry
|
|
||||||
{
|
public static event EventHandler<CacheEntry> Inserted;
|
||||||
public string Id { get; set; }
|
public static event EventHandler<CacheEntry> Removed;
|
||||||
public FileType FileType { get; set; }
|
|
||||||
public string Path { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>();
|
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)
|
public static List<(FileType fileType, string path)> GetFiles(string id)
|
||||||
{
|
=> getEntries(entry => entry.Id == id)
|
||||||
var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type);
|
.Select(entry => (entry.FileType, entry.Path))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
if (entry == null)
|
public static string GetFirstPath(string id, FileType type)
|
||||||
return null;
|
=> getEntries(entry => entry.Id == id && entry.FileType == type)
|
||||||
|
.FirstOrDefault()
|
||||||
|
?.Path;
|
||||||
|
|
||||||
if (!File.Exists(entry.Path))
|
private static List<CacheEntry> getEntries(Func<CacheEntry, bool> predicate)
|
||||||
{
|
|
||||||
remove(entry);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry.Path;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void remove(CacheEntry entry)
|
|
||||||
{
|
{
|
||||||
cache.Remove(entry);
|
var entries = cache.Where(predicate).ToList();
|
||||||
save();
|
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
|
foreach (var entry in entries)
|
||||||
System.Threading.Thread.Sleep(100);
|
{
|
||||||
|
cache.Remove(entry);
|
||||||
if (!File.Exists(path))
|
Removed?.Invoke(null, entry);
|
||||||
throw new FileNotFoundException($"Cannot add path to cache. File not found. Id={id} FileType={type}", path);
|
}
|
||||||
|
save();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type);
|
public static void Insert(string id, string path)
|
||||||
|
{
|
||||||
if (entry is null)
|
var type = FileTypes.GetFileTypeFromPath(path);
|
||||||
cache.Add(new CacheEntry { Id = id, FileType = type, Path = path });
|
var entry = new CacheEntry(id, type, path);
|
||||||
else
|
cache.Add(entry);
|
||||||
entry.Path = path;
|
Inserted?.Invoke(null, entry);
|
||||||
|
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
39
FileManager/FileTypes.cs
Normal file
39
FileManager/FileTypes.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user