mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-19 11:18:46 -05:00
Compare commits
5 Commits
v3.1-beta.
...
v3.1-beta.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1aacd92ad | ||
|
|
b1b426427c | ||
|
|
0683e5f55b | ||
|
|
5c81441f83 | ||
|
|
57bc74cd23 |
@@ -72,8 +72,9 @@ namespace AaxDecrypter
|
||||
}
|
||||
private AaxToM4bConverter(string inputFile, string decryptKey)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(inputFile)) throw new ArgumentNullException(nameof(inputFile), "Input file may not be null or whitespace");
|
||||
if (!File.Exists(inputFile)) throw new ArgumentNullException(nameof(inputFile), "File does not exist");
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(inputFile, nameof(inputFile));
|
||||
if (!File.Exists(inputFile))
|
||||
throw new ArgumentNullException(nameof(inputFile), "File does not exist");
|
||||
|
||||
steps = new StepSequence
|
||||
{
|
||||
@@ -89,22 +90,24 @@ namespace AaxDecrypter
|
||||
["End: Create Nfo"] = End_CreateNfo
|
||||
};
|
||||
|
||||
this.inputFileName = inputFile;
|
||||
inputFileName = inputFile;
|
||||
this.decryptKey = decryptKey;
|
||||
}
|
||||
|
||||
private async Task prelimProcessing()
|
||||
{
|
||||
this.tags = new Tags(this.inputFileName);
|
||||
this.encodingInfo = new EncodingInfo(this.inputFileName);
|
||||
this.chapters = new Chapters(this.inputFileName, this.tags.duration.TotalSeconds);
|
||||
tags = new Tags(inputFileName);
|
||||
encodingInfo = new EncodingInfo(inputFileName);
|
||||
chapters = new Chapters(inputFileName, tags.duration.TotalSeconds);
|
||||
|
||||
var defaultFilename = Path.Combine(
|
||||
Path.GetDirectoryName(this.inputFileName),
|
||||
getASCIITag(this.tags.author),
|
||||
getASCIITag(this.tags.title) + ".m4b"
|
||||
Path.GetDirectoryName(inputFileName),
|
||||
getASCIITag(tags.author),
|
||||
getASCIITag(tags.title) + ".m4b"
|
||||
);
|
||||
SetOutputFilename(defaultFilename);
|
||||
|
||||
// set default name
|
||||
SetOutputFilename(defaultFilename);
|
||||
|
||||
await Task.Run(() => saveCover(inputFileName));
|
||||
}
|
||||
@@ -118,7 +121,7 @@ namespace AaxDecrypter
|
||||
private void saveCover(string aaxFile)
|
||||
{
|
||||
using var file = TagLib.File.Create(aaxFile, "audio/mp4", TagLib.ReadStyle.Average);
|
||||
this.coverBytes = file.Tag.Pictures[0].Data.Data;
|
||||
coverBytes = file.Tag.Pictures[0].Data.Data;
|
||||
}
|
||||
|
||||
private void printPrelim()
|
||||
@@ -156,21 +159,24 @@ namespace AaxDecrypter
|
||||
|
||||
public void SetOutputFilename(string outFileName)
|
||||
{
|
||||
this.outputFileName = outFileName;
|
||||
outputFileName = outFileName;
|
||||
|
||||
if (Path.GetExtension(this.outputFileName) != ".m4b")
|
||||
this.outputFileName = outputFileWithNewExt(".m4b");
|
||||
if (Path.GetExtension(outputFileName) != ".m4b")
|
||||
outputFileName = outputFileWithNewExt(".m4b");
|
||||
|
||||
this.outDir = Path.GetDirectoryName(this.outputFileName);
|
||||
if (File.Exists(outputFileName))
|
||||
File.Delete(outputFileName);
|
||||
|
||||
outDir = Path.GetDirectoryName(outputFileName);
|
||||
}
|
||||
|
||||
private string outputFileWithNewExt(string extension)
|
||||
=> Path.Combine(this.outDir, Path.GetFileNameWithoutExtension(this.outputFileName) + '.' + extension.Trim('.'));
|
||||
=> Path.Combine(outDir, Path.GetFileNameWithoutExtension(outputFileName) + '.' + extension.Trim('.'));
|
||||
|
||||
public bool Step1_CreateDir()
|
||||
{
|
||||
ProcessRunner.WorkingDir = this.outDir;
|
||||
Directory.CreateDirectory(this.outDir);
|
||||
ProcessRunner.WorkingDir = outDir;
|
||||
Directory.CreateDirectory(outDir);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -178,7 +184,7 @@ namespace AaxDecrypter
|
||||
{
|
||||
DecryptProgressUpdate?.Invoke(this, 0);
|
||||
|
||||
var tempRipFile = Path.Combine(this.outDir, "funny.aac");
|
||||
var tempRipFile = Path.Combine(outDir, "funny.aac");
|
||||
|
||||
var fail = "WARNING-Decrypt failure. ";
|
||||
|
||||
@@ -193,7 +199,7 @@ namespace AaxDecrypter
|
||||
if (returnCode == -99)
|
||||
{
|
||||
Console.WriteLine($"{fail}Incorrect decrypt key: {decryptKey}");
|
||||
this.decryptKey = null;
|
||||
decryptKey = null;
|
||||
returnCode = getKey_decrypt(tempRipFile);
|
||||
}
|
||||
}
|
||||
@@ -232,7 +238,7 @@ namespace AaxDecrypter
|
||||
|
||||
Console.WriteLine("Cracking activation bytes");
|
||||
var activation_bytes = BytesCracker.GetActivationBytes(checksum);
|
||||
this.decryptKey = activation_bytes;
|
||||
decryptKey = activation_bytes;
|
||||
Console.WriteLine("Activation bytes cracked. Decrypt key: " + activation_bytes);
|
||||
}
|
||||
|
||||
@@ -243,10 +249,10 @@ namespace AaxDecrypter
|
||||
Console.WriteLine("Decrypting with key " + decryptKey);
|
||||
|
||||
var returnCode = 100;
|
||||
var thread = new Thread(() => returnCode = this.ngDecrypt());
|
||||
var thread = new Thread(() => returnCode = ngDecrypt());
|
||||
thread.Start();
|
||||
|
||||
double fileLen = new FileInfo(this.inputFileName).Length;
|
||||
double fileLen = new FileInfo(inputFileName).Length;
|
||||
while (thread.IsAlive && returnCode == 100)
|
||||
{
|
||||
Thread.Sleep(500);
|
||||
@@ -266,7 +272,7 @@ namespace AaxDecrypter
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
FileName = DecryptSupportLibraries.mp4trackdumpPath,
|
||||
Arguments = "-c " + this.encodingInfo.channels + " -r " + this.encodingInfo.sampleRate + " \"" + this.inputFileName + "\""
|
||||
Arguments = "-c " + encodingInfo.channels + " -r " + encodingInfo.sampleRate + " \"" + inputFileName + "\""
|
||||
};
|
||||
info.EnvironmentVariables["VARIABLE"] = decryptKey;
|
||||
|
||||
@@ -279,21 +285,22 @@ namespace AaxDecrypter
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
// temp file names for steps 3, 4, 5
|
||||
string tempChapsPath => Path.Combine(this.outDir, "tempChaps.mp4");
|
||||
// temp file names for steps 3, 4, 5
|
||||
string tempChapsGuid { get; } = Guid.NewGuid().ToString().ToUpper().Replace("-", "");
|
||||
string tempChapsPath => Path.Combine(outDir, $"tempChaps_{tempChapsGuid}.mp4");
|
||||
string mp4_file => outputFileWithNewExt(".mp4");
|
||||
string ff_txt_file => mp4_file + ".ff.txt";
|
||||
|
||||
public bool Step3_Chapterize()
|
||||
{
|
||||
string str1 = "";
|
||||
if (this.chapters.FirstChapterStart != 0.0)
|
||||
if (chapters.FirstChapterStart != 0.0)
|
||||
{
|
||||
str1 = " -ss " + this.chapters.FirstChapterStart.ToString("0.000", CultureInfo.InvariantCulture) + " -t " + (this.chapters.LastChapterStart - 1.0).ToString("0.000", CultureInfo.InvariantCulture) + " ";
|
||||
str1 = " -ss " + chapters.FirstChapterStart.ToString("0.000", CultureInfo.InvariantCulture) + " -t " + (chapters.LastChapterStart - 1.0).ToString("0.000", CultureInfo.InvariantCulture) + " ";
|
||||
}
|
||||
|
||||
string ffmpegTags = this.tags.GenerateFfmpegTags();
|
||||
string ffmpegChapters = this.chapters.GenerateFfmpegChapters();
|
||||
string ffmpegTags = tags.GenerateFfmpegTags();
|
||||
string ffmpegChapters = chapters.GenerateFfmpegChapters();
|
||||
File.WriteAllText(ff_txt_file, ffmpegTags + ffmpegChapters);
|
||||
|
||||
var tagAndChapterInfo = new ProcessStartInfo
|
||||
@@ -309,8 +316,8 @@ namespace AaxDecrypter
|
||||
public bool Step4_InsertCoverArt()
|
||||
{
|
||||
// save cover image as temp file
|
||||
var coverPath = Path.Combine(this.outDir, "cover-" + Guid.NewGuid() + ".jpg");
|
||||
FileExt.CreateFile(coverPath, this.coverBytes);
|
||||
var coverPath = Path.Combine(outDir, "cover-" + Guid.NewGuid() + ".jpg");
|
||||
FileExt.CreateFile(coverPath, coverBytes);
|
||||
|
||||
var insertCoverArtInfo = new ProcessStartInfo
|
||||
{
|
||||
@@ -329,26 +336,26 @@ namespace AaxDecrypter
|
||||
{
|
||||
FileExt.SafeDelete(mp4_file);
|
||||
FileExt.SafeDelete(ff_txt_file);
|
||||
FileExt.SafeMove(tempChapsPath, this.outputFileName);
|
||||
FileExt.SafeMove(tempChapsPath, outputFileName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Step6_AddTags()
|
||||
{
|
||||
this.tags.AddAppleTags(this.outputFileName);
|
||||
tags.AddAppleTags(outputFileName);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool End_CreateCue()
|
||||
{
|
||||
File.WriteAllText(outputFileWithNewExt(".cue"), this.chapters.GetCuefromChapters(Path.GetFileName(this.outputFileName)));
|
||||
File.WriteAllText(outputFileWithNewExt(".cue"), chapters.GetCuefromChapters(Path.GetFileName(outputFileName)));
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool End_CreateNfo()
|
||||
{
|
||||
File.WriteAllText(outputFileWithNewExt(".nfo"), NFO.CreateNfoContents(AppName, this.tags, this.encodingInfo, this.chapters));
|
||||
File.WriteAllText(outputFileWithNewExt(".nfo"), NFO.CreateNfoContents(AppName, tags, encodingInfo, chapters));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace AaxDecrypter
|
||||
using TagLib.File tagLibFile = TagLib.File.Create(file, "audio/mp4", ReadStyle.Average);
|
||||
this.title = tagLibFile.Tag.Title.Replace(" (Unabridged)", "");
|
||||
this.album = tagLibFile.Tag.Album.Replace(" (Unabridged)", "");
|
||||
this.author = tagLibFile.Tag.FirstPerformer;
|
||||
this.author = tagLibFile.Tag.FirstPerformer ?? "[unknown]";
|
||||
this.year = tagLibFile.Tag.Year.ToString();
|
||||
this.comments = tagLibFile.Tag.Comment;
|
||||
this.duration = tagLibFile.Properties.Duration;
|
||||
|
||||
@@ -123,16 +123,10 @@ namespace DataLayer
|
||||
ArgumentValidator.EnsureEnumerableNotNullOrEmpty(newContributors, nameof(newContributors));
|
||||
|
||||
// the edge cases of doing local-loaded vs remote-only got weird. just load it
|
||||
if (_contributorsLink == null)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(context, nameof(context));
|
||||
if (!context.Entry(this).IsKeySet)
|
||||
throw new InvalidOperationException("Could not add contributors");
|
||||
if (_contributorsLink is null)
|
||||
getEntry(context).Collection(s => s.ContributorsLink).Load();
|
||||
|
||||
context.Entry(this).Collection(s => s.ContributorsLink).Load();
|
||||
}
|
||||
|
||||
var roleContributions = getContributions(role);
|
||||
var roleContributions = getContributions(role);
|
||||
var isIdentical = roleContributions.Select(c => c.Contributor).SequenceEqual(newContributors);
|
||||
if (isIdentical)
|
||||
return;
|
||||
@@ -140,7 +134,8 @@ namespace DataLayer
|
||||
_contributorsLink.RemoveWhere(bc => bc.Role == role);
|
||||
addNewContributors(newContributors, role);
|
||||
}
|
||||
private void addNewContributors(IEnumerable<Contributor> newContributors, Role role)
|
||||
|
||||
private void addNewContributors(IEnumerable<Contributor> newContributors, Role role)
|
||||
{
|
||||
byte order = 0;
|
||||
var newContributionsEnum = newContributors.Select(c => new BookContributor(this, c, role, order++));
|
||||
@@ -155,6 +150,18 @@ namespace DataLayer
|
||||
.ToList();
|
||||
#endregion
|
||||
|
||||
private Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry<Book> getEntry(DbContext context)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(context, nameof(context));
|
||||
|
||||
var entry = context.Entry(this);
|
||||
|
||||
if (!entry.IsKeySet)
|
||||
throw new InvalidOperationException("Could not load a valid Book from database");
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
#region series
|
||||
private HashSet<SeriesBook> _seriesLink;
|
||||
public IEnumerable<SeriesBook> SeriesLink => _seriesLink?.ToList();
|
||||
@@ -186,16 +193,10 @@ namespace DataLayer
|
||||
|
||||
// our add() is conditional upon what's already included in the collection.
|
||||
// therefore if not loaded, a trip is required. might as well just load it
|
||||
if (_seriesLink == null)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(context, nameof(context));
|
||||
if (!context.Entry(this).IsKeySet)
|
||||
throw new InvalidOperationException("Could not add series");
|
||||
if (_seriesLink is null)
|
||||
getEntry(context).Collection(s => s.SeriesLink).Load();
|
||||
|
||||
context.Entry(this).Collection(s => s.SeriesLink).Load();
|
||||
}
|
||||
|
||||
var singleSeriesBook = _seriesLink.SingleOrDefault(sb => sb.Series == series);
|
||||
var singleSeriesBook = _seriesLink.SingleOrDefault(sb => sb.Series == series);
|
||||
if (singleSeriesBook == null)
|
||||
_seriesLink.Add(new SeriesBook(series, this, index));
|
||||
else
|
||||
@@ -211,8 +212,7 @@ namespace DataLayer
|
||||
public void AddSupplementDownloadUrl(string url)
|
||||
{
|
||||
// supplements are owned by Book, so no need to Load():
|
||||
// OwnsMany: "Can only ever appear on navigation properties of other entity types.
|
||||
// Are automatically loaded, and can only be tracked by a DbContext alongside their owner."
|
||||
// Are automatically loaded, and can only be tracked by a DbContext alongside their owner.
|
||||
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(url, nameof(url));
|
||||
|
||||
@@ -232,19 +232,12 @@ namespace DataLayer
|
||||
}
|
||||
|
||||
public void UpdateCategory(Category category, DbContext context = null)
|
||||
{
|
||||
// since category is never null, nullity means it hasn't been loaded. non null means we're correctly loaded. just overwrite
|
||||
if (Category != null)
|
||||
{
|
||||
Category = category;
|
||||
return;
|
||||
}
|
||||
{
|
||||
// since category is never null, nullity means it hasn't been loaded
|
||||
if (Category is null)
|
||||
getEntry(context).Reference(s => s.Category).Load();
|
||||
|
||||
if (context == null)
|
||||
throw new Exception("need context");
|
||||
|
||||
context.Entry(this).Reference(s => s.Category).Load();
|
||||
Category = category;
|
||||
Category = category;
|
||||
}
|
||||
|
||||
public override string ToString() => $"[{AudibleProductId}] {Title}";
|
||||
|
||||
@@ -64,7 +64,8 @@ namespace DataLayer
|
||||
.Entity<Contributor>()
|
||||
.HasData(Contributor.GetEmpty());
|
||||
|
||||
// views are now supported via "query types" (instead of "entity types"): https://docs.microsoft.com/en-us/ef/core/modeling/query-types
|
||||
}
|
||||
}
|
||||
// views are now supported via "keyless entity types" (instead of "entity types" or the prev "query types"):
|
||||
// https://docs.microsoft.com/en-us/ef/core/modeling/keyless-entity-types
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.Linq;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using Dinah.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
|
||||
namespace DataLayer
|
||||
{
|
||||
@@ -14,25 +13,18 @@ namespace DataLayer
|
||||
|
||||
public void Executing(DbContext context)
|
||||
{
|
||||
// persist tags:
|
||||
var modifiedEntities = context
|
||||
var tagsCollection
|
||||
= context
|
||||
.ChangeTracker
|
||||
.Entries()
|
||||
.Where(p => p.State.In(EntityState.Modified, EntityState.Added))
|
||||
.ToList();
|
||||
|
||||
persistTags(modifiedEntities);
|
||||
}
|
||||
|
||||
private static void persistTags(List<EntityEntry> modifiedEntities)
|
||||
{
|
||||
var tagSets = modifiedEntities
|
||||
.Where(e => e.State.In(EntityState.Modified, EntityState.Added))
|
||||
.Select(e => e.Entity as UserDefinedItem)
|
||||
// filter by null but NOT by blank. blank is the valid way to show the absence of tags
|
||||
.Where(a => a != null)
|
||||
.Where(udi => udi != null)
|
||||
// do NOT filter out entires with blank tags. blank is the valid way to show the absence of tags
|
||||
.Select(t => (t.Book.AudibleProductId, t.Tags))
|
||||
.ToList();
|
||||
foreach (var t in tagSets)
|
||||
FileManager.TagsPersistence.Save(t.Book.AudibleProductId, t.Tags);
|
||||
|
||||
FileManager.TagsPersistence.Save(tagsCollection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,22 @@ nuget
|
||||
Microsoft.EntityFrameworkCore.Tools (needed for using Package Manager Console)
|
||||
Microsoft.EntityFrameworkCore.Sqlite
|
||||
|
||||
MIGRATIONS require standard, not core
|
||||
using standard instead of core. edit 3 things in csproj
|
||||
1of3: pluralize xml TargetFramework tag to TargetFrameworks
|
||||
2of2: TargetFrameworks from: netstandard2.1
|
||||
to: netcoreapp3.0;netstandard2.1
|
||||
3of3: add
|
||||
<PropertyGroup>
|
||||
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||
</PropertyGroup>
|
||||
MIGRATIONS
|
||||
require core, not standard
|
||||
this can be a problem b/c standard and framework can only reference standard, not core
|
||||
TO USE MIGRATIONS (core and/or standard)
|
||||
add to csproj
|
||||
<PropertyGroup>
|
||||
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||
</PropertyGroup>
|
||||
TO USE MIGRATIONS AS *BOTH* CORE AND STANDARD
|
||||
edit csproj
|
||||
pluralize this xml tag
|
||||
from: TargetFramework
|
||||
to: TargetFrameworks
|
||||
inside of TargetFrameworks
|
||||
from: netstandard2.1
|
||||
to: netcoreapp3.0;netstandard2.1
|
||||
|
||||
run. error
|
||||
SQLite Error 1: 'no such table: Blogs'.
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
"LibationContext_sqlserver": "Server=(LocalDb)\\MSSQLLocalDB;Database=DataLayer.LibationContext;Integrated Security=true;",
|
||||
"LibationContext": "Data Source=LibationContext.db;Foreign Keys=False;",
|
||||
|
||||
"// on windows sqlite paths accept windows and/or unix slashes": "",
|
||||
"// sqlite notes": "",
|
||||
"// absolute path example": "Data Source=C:/foo/bar/sample.db",
|
||||
"// relative path example": "Data Source=sample.db",
|
||||
"// on windows: sqlite paths accept windows and/or unix slashes": "",
|
||||
"MyTestContext": "Data Source=%DESKTOP%/sample.db"
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,7 @@ namespace FileLiberator
|
||||
NarratorsDiscovered?.Invoke(this, converter.tags.narrator);
|
||||
CoverImageFilepathDiscovered?.Invoke(this, converter.coverBytes);
|
||||
|
||||
// override default which was set in CreateAsync
|
||||
converter.SetOutputFilename(proposedOutputFile);
|
||||
converter.DecryptProgressUpdate += (s, progress) => UpdateProgress?.Invoke(this, progress);
|
||||
|
||||
@@ -117,46 +118,58 @@ namespace FileLiberator
|
||||
}
|
||||
|
||||
private static void moveFilesToBooksDir(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();
|
||||
{
|
||||
// create final directory. move each file into it. MOVE AUDIO FILE LAST
|
||||
// new dir: safetitle_limit50char + " [" + productId + "]"
|
||||
|
||||
// create final directory. move each file into it. MOVE AUDIO FILE LAST
|
||||
// new dir: safetitle_limit50char + " [" + productId + "]"
|
||||
var destinationDir = getDestDir(product);
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
|
||||
// to prevent the paths from getting too long, we don't need after the 1st ":" for the folder
|
||||
var underscoreIndex = product.Title.IndexOf(':');
|
||||
var titleDir = (underscoreIndex < 4) ? product.Title : product.Title.Substring(0, underscoreIndex);
|
||||
var finalDir = FileUtility.GetValidFilename(AudibleFileStorage.BooksDirectory, titleDir, null, product.AudibleProductId);
|
||||
Directory.CreateDirectory(finalDir);
|
||||
var sortedFiles = getProductFilesSorted(product, outputAudioFilename);
|
||||
|
||||
// move audio files to the end of the collection so these files are moved last
|
||||
var musicFiles = files.Where(f => AudibleFileStorage.Audio.IsFileTypeMatch(f));
|
||||
files = files
|
||||
.Except(musicFiles)
|
||||
.Concat(musicFiles)
|
||||
.ToList();
|
||||
var musicFileExt = Path.GetExtension(outputAudioFilename).Trim('.');
|
||||
|
||||
var musicFileExt = musicFiles
|
||||
.Select(f => f.Extension)
|
||||
.Distinct()
|
||||
.Single()
|
||||
.Trim('.');
|
||||
foreach (var f in sortedFiles)
|
||||
{
|
||||
var dest = AudibleFileStorage.Audio.IsFileTypeMatch(f)
|
||||
// audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
|
||||
? FileUtility.GetValidFilename(destinationDir, product.Title, musicFileExt, product.AudibleProductId)
|
||||
// non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext +"]." + non_audio_ext
|
||||
: FileUtility.GetValidFilename(destinationDir, product.Title, f.Extension, product.AudibleProductId, musicFileExt);
|
||||
|
||||
foreach (var f in files)
|
||||
{
|
||||
var dest = AudibleFileStorage.Audio.IsFileTypeMatch(f)
|
||||
// audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
|
||||
? FileUtility.GetValidFilename(finalDir, product.Title, musicFileExt, product.AudibleProductId)
|
||||
// non-audio filename: safetitle_limit50char + " [" + productId + "][" + audio_ext +"]." + non_audio_ext
|
||||
: FileUtility.GetValidFilename(finalDir, product.Title, f.Extension, product.AudibleProductId, musicFileExt);
|
||||
File.Move(f.FullName, dest);
|
||||
}
|
||||
}
|
||||
|
||||
File.Move(f.FullName, dest);
|
||||
}
|
||||
}
|
||||
}
|
||||
private static string getDestDir(Book product)
|
||||
{
|
||||
// to prevent the paths from getting too long, we don't need after the 1st ":" for the folder
|
||||
var underscoreIndex = product.Title.IndexOf(':');
|
||||
var titleDir
|
||||
= underscoreIndex < 4
|
||||
? product.Title
|
||||
: product.Title.Substring(0, underscoreIndex);
|
||||
var finalDir = FileUtility.GetValidFilename(AudibleFileStorage.BooksDirectory, titleDir, null, product.AudibleProductId);
|
||||
return finalDir;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,14 @@ namespace FileLiberator
|
||||
tempAaxFilename,
|
||||
(p) => api.DownloadAaxWorkaroundAsync(libraryBook.Book.AudibleProductId, tempAaxFilename, p));
|
||||
|
||||
// if bad file download, a 0-33 byte file will be created
|
||||
System.Threading.Thread.Sleep(100);
|
||||
if (new FileInfo(actualFilePath).Length < 100)
|
||||
{
|
||||
File.Delete(actualFilePath);
|
||||
throw new Exception("Error downloading file");
|
||||
}
|
||||
|
||||
return actualFilePath;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dinah.Core.Collections.Immutable;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
public static class FilePathCache
|
||||
public static class FilePathCache
|
||||
{
|
||||
internal class CacheEntry
|
||||
{
|
||||
@@ -15,22 +16,25 @@ namespace FileManager
|
||||
public string Path { get; set; }
|
||||
}
|
||||
|
||||
static List<CacheEntry> inMemoryCache { get; } = new List<CacheEntry>();
|
||||
static Cache<CacheEntry> cache { get; } = new Cache<CacheEntry>();
|
||||
|
||||
public static string JsonFile => Path.Combine(Configuration.Instance.LibationFiles, "FilePaths.json");
|
||||
|
||||
static FilePathCache()
|
||||
{
|
||||
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
|
||||
if (FileUtility.FileExists(JsonFile))
|
||||
inMemoryCache = JsonConvert.DeserializeObject<List<CacheEntry>>(File.ReadAllText(JsonFile));
|
||||
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
|
||||
if (FileUtility.FileExists(JsonFile))
|
||||
{
|
||||
var list = JsonConvert.DeserializeObject<List<CacheEntry>>(File.ReadAllText(JsonFile));
|
||||
cache = new Cache<CacheEntry>(list);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool Exists(string id, FileType type) => GetPath(id, type) != null;
|
||||
|
||||
public static string GetPath(string id, FileType type)
|
||||
{
|
||||
var entry = inMemoryCache.SingleOrDefault(i => i.Id == id && i.FileType == type);
|
||||
var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type);
|
||||
|
||||
if (entry == null)
|
||||
return null;
|
||||
@@ -44,51 +48,47 @@ namespace FileManager
|
||||
return entry.Path;
|
||||
}
|
||||
|
||||
private static object locker { get; } = new object();
|
||||
|
||||
private static void remove(CacheEntry entry)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
inMemoryCache.Remove(entry);
|
||||
save();
|
||||
}
|
||||
}
|
||||
{
|
||||
cache.Remove(entry);
|
||||
save();
|
||||
}
|
||||
|
||||
public static void Upsert(string id, FileType type, string path)
|
||||
{
|
||||
if (!FileUtility.FileExists(path))
|
||||
throw new FileNotFoundException("Cannot add path to cache. File not found");
|
||||
|
||||
lock (locker)
|
||||
{
|
||||
var entry = inMemoryCache.SingleOrDefault(i => i.Id == id && i.FileType == type);
|
||||
if (entry != null)
|
||||
entry.Path = path;
|
||||
else
|
||||
{
|
||||
entry = new CacheEntry { Id = id, FileType = type, Path = path };
|
||||
inMemoryCache.Add(entry);
|
||||
}
|
||||
save();
|
||||
}
|
||||
}
|
||||
var entry = cache.SingleOrDefault(i => i.Id == id && i.FileType == type);
|
||||
|
||||
// ONLY call this within lock()
|
||||
private static void save()
|
||||
{
|
||||
// create json if not exists
|
||||
void resave() => File.WriteAllText(JsonFile, JsonConvert.SerializeObject(inMemoryCache, Formatting.Indented));
|
||||
try { resave(); }
|
||||
catch (IOException)
|
||||
{
|
||||
try { resave(); }
|
||||
catch (IOException ex)
|
||||
if (entry is null)
|
||||
cache.Add(new CacheEntry { Id = id, FileType = type, Path = path });
|
||||
else
|
||||
entry.Path = path;
|
||||
|
||||
save();
|
||||
}
|
||||
|
||||
// cache is thread-safe and lock free. but file saving is not
|
||||
private static object locker { get; } = new object();
|
||||
private static void save()
|
||||
{
|
||||
// create json if not exists
|
||||
static void resave() => File.WriteAllText(JsonFile, JsonConvert.SerializeObject(cache.ToList(), Formatting.Indented));
|
||||
|
||||
lock (locker)
|
||||
{
|
||||
try { resave(); }
|
||||
catch (IOException)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error saving FilePaths.json");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
try { resave(); }
|
||||
catch (IOException ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error saving FilePaths.json");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,13 @@ namespace FileManager
|
||||
= Policy.Handle<Exception>()
|
||||
.WaitAndRetry(new[] { TimeSpan.FromMilliseconds(100) });
|
||||
|
||||
public static void Save(string productId, string tags)
|
||||
public static void Save(IEnumerable<(string productId, string tags)> tagsCollection)
|
||||
{
|
||||
ensureCache();
|
||||
|
||||
cache[productId] = tags;
|
||||
// on initial reload, there's a huge benefit to adding to cache individually then updating the file only once
|
||||
foreach ((string productId, string tags) in tagsCollection)
|
||||
cache[productId] = tags;
|
||||
|
||||
lock (locker)
|
||||
policy.Execute(() => File.WriteAllText(TagsFile, JsonConvert.SerializeObject(cache, Formatting.Indented)));
|
||||
|
||||
@@ -8,7 +8,13 @@ namespace LibationWinForm.BookLiberation
|
||||
{
|
||||
public partial class DecryptForm : Form
|
||||
{
|
||||
public DecryptForm()
|
||||
class SerilogTextWriter : System.IO.TextWriter
|
||||
{
|
||||
public override System.Text.Encoding Encoding => System.Text.Encoding.ASCII;
|
||||
public override void WriteLine(string value) => Serilog.Log.Logger.Debug(value);
|
||||
}
|
||||
|
||||
public DecryptForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
@@ -18,7 +24,7 @@ namespace LibationWinForm.BookLiberation
|
||||
{
|
||||
// redirect Console.WriteLine to console, textbox
|
||||
var controlWriter = new RichTextBoxTextWriter(this.rtbLog);
|
||||
var multiLogger = new MultiTextWriter(origOut, controlWriter);
|
||||
var multiLogger = new MultiTextWriter(origOut, controlWriter, new SerilogTextWriter());
|
||||
Console.SetOut(multiLogger);
|
||||
}
|
||||
|
||||
@@ -57,19 +63,6 @@ namespace LibationWinForm.BookLiberation
|
||||
public void SetCoverImage(byte[] coverBytes)
|
||||
=> pictureBox1.UIThread(() => pictureBox1.Image = ImageReader.ToImage(coverBytes));
|
||||
|
||||
public void AppendError(Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Decrypt form: error");
|
||||
appendText("ERROR: " + ex.Message);
|
||||
}
|
||||
public void AppendText(string text)
|
||||
{
|
||||
Serilog.Log.Logger.Debug($"Decrypt form: {text}");
|
||||
appendText(text);
|
||||
}
|
||||
private void appendText(string text) => Console.WriteLine($"{DateTime.Now} {text}");
|
||||
|
||||
|
||||
public void UpdateProgress(int percentage) => progressBar1.UIThread(() => progressBar1.Value = percentage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace LibationWinForm
|
||||
|
||||
setBookBackupCounts(books);
|
||||
setPdfBackupCounts(books);
|
||||
}
|
||||
}
|
||||
enum AudioFileState { full, aax, none }
|
||||
private void setBookBackupCounts(IEnumerable<Book> books)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
-- begin VERSIONING ---------------------------------------------------------------------------------------------------------------------
|
||||
https://github.com/rmcrackan/Libation/releases
|
||||
|
||||
v3.1-beta.8 : Bugfix: decrypt file conflict
|
||||
v3.1-beta.7 : Bugfix: decrypt book with no author
|
||||
v3.1-beta.6 : Improved logging
|
||||
v3.1-beta.5 : Improved importing
|
||||
v3.1-beta.4 : Added beta-specific logging
|
||||
|
||||
Reference in New Issue
Block a user