mirror of
https://github.com/rmcrackan/Libation.git
synced 2025-12-31 09:58:43 -05:00
Compare commits
4 Commits
v3.0.2
...
v3.1-beta.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3128b562d | ||
|
|
6734dec55c | ||
|
|
b9314ac678 | ||
|
|
e319326c30 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -328,3 +328,7 @@ ASALocalRun/
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
|
||||
# manually ignored files
|
||||
/__TODO.txt
|
||||
|
||||
@@ -23,14 +23,12 @@ namespace ApplicationServices
|
||||
return (totalCount, newCount);
|
||||
}
|
||||
|
||||
public static int IndexChangedTags(Book book)
|
||||
public static int UpdateTags(this LibationContext context, Book book, string newTags)
|
||||
{
|
||||
// update disconnected entity
|
||||
using var context = LibationContext.Create();
|
||||
context.Update(book);
|
||||
book.UserDefinedItem.Tags = newTags;
|
||||
|
||||
var qtyChanges = context.SaveChanges();
|
||||
|
||||
// this part is tags-specific
|
||||
if (qtyChanges > 0)
|
||||
SearchEngineCommands.UpdateBookTags(book);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using DataLayer;
|
||||
using LibationSearchEngine;
|
||||
|
||||
namespace ApplicationServices
|
||||
|
||||
@@ -61,12 +61,17 @@ namespace DataLayer
|
||||
string title,
|
||||
string description,
|
||||
int lengthInMinutes,
|
||||
IEnumerable<Contributor> authors)
|
||||
IEnumerable<Contributor> authors,
|
||||
IEnumerable<Contributor> narrators)
|
||||
{
|
||||
// validate
|
||||
ArgumentValidator.EnsureNotNull(audibleProductId, nameof(audibleProductId));
|
||||
var productId = audibleProductId.Id;
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(productId, nameof(productId));
|
||||
|
||||
// assign as soon as possible. stuff below relies on this
|
||||
AudibleProductId = productId;
|
||||
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(title, nameof(title));
|
||||
|
||||
// non-ef-ctor init.s
|
||||
@@ -79,19 +84,13 @@ namespace DataLayer
|
||||
CategoryId = Category.GetEmpty().CategoryId;
|
||||
|
||||
// simple assigns
|
||||
AudibleProductId = productId;
|
||||
Title = title;
|
||||
Description = description;
|
||||
LengthInMinutes = lengthInMinutes;
|
||||
|
||||
// assigns with biz logic
|
||||
ReplaceAuthors(authors);
|
||||
//ReplaceNarrators(narrators);
|
||||
|
||||
// import previously saved tags
|
||||
// do this immediately. any save occurs before reloading tags will overwrite persistent tags with new blank entries; all old persisted tags will be lost
|
||||
// if refactoring, DO NOT use "ProductId" before it's assigned to. to be safe, just use "productId"
|
||||
UserDefinedItem = new UserDefinedItem(this) { Tags = FileManager.TagsPersistence.GetTags(productId) };
|
||||
ReplaceNarrators(narrators);
|
||||
}
|
||||
|
||||
#region contributors, authors, narrators
|
||||
|
||||
@@ -13,29 +13,36 @@ namespace DataLayer
|
||||
|
||||
private UserDefinedItem() { }
|
||||
internal UserDefinedItem(Book book)
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
Book = book;
|
||||
}
|
||||
|
||||
// import previously saved tags
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(book.AudibleProductId, nameof(book.AudibleProductId));
|
||||
Tags = FileManager.TagsPersistence.GetTags(book.AudibleProductId);
|
||||
}
|
||||
|
||||
private string _tags = "";
|
||||
public string Tags
|
||||
{
|
||||
get => _tags;
|
||||
set => _tags = sanitize(value);
|
||||
}
|
||||
#region sanitize tags: space delimited. Inline/denormalized. Lower case. Alpha numeric and hyphen
|
||||
// only legal chars are letters numbers underscores and separating whitespace
|
||||
//
|
||||
// technically, the only char.s which aren't easily supported are \ [ ]
|
||||
// however, whitelisting is far safer than blacklisting (eg: new lines, non-printable character)
|
||||
// it's easy to expand whitelist as needed
|
||||
// for lucene, ToLower() isn't needed because search is case-inspecific. for here, it prevents duplicates
|
||||
//
|
||||
// there are also other allowed but misleading characters. eg: the ^ operator defines a 'boost' score
|
||||
// full list of characters which must be escaped:
|
||||
// + - && || ! ( ) { } [ ] ^ " ~ * ? : \
|
||||
static Regex regex = new Regex(@"[^\w\d\s_]", RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
public IEnumerable<string> TagsEnumerated => Tags == "" ? new string[0] : Tags.Split(null as char[], StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
#region sanitize tags: space delimited. Inline/denormalized. Lower case. Alpha numeric and hyphen
|
||||
// only legal chars are letters numbers underscores and separating whitespace
|
||||
//
|
||||
// technically, the only char.s which aren't easily supported are \ [ ]
|
||||
// however, whitelisting is far safer than blacklisting (eg: new lines, non-printable character)
|
||||
// it's easy to expand whitelist as needed
|
||||
// for lucene, ToLower() isn't needed because search is case-inspecific. for here, it prevents duplicates
|
||||
//
|
||||
// there are also other allowed but misleading characters. eg: the ^ operator defines a 'boost' score
|
||||
// full list of characters which must be escaped:
|
||||
// + - && || ! ( ) { } [ ] ^ " ~ * ? : \
|
||||
static Regex regex { get; } = new Regex(@"[^\w\d\s_]", RegexOptions.Compiled);
|
||||
private static string sanitize(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
@@ -63,8 +70,6 @@ namespace DataLayer
|
||||
|
||||
return string.Join(" ", unique);
|
||||
}
|
||||
|
||||
public IEnumerable<string> TagsEnumerated => Tags == "" ? new string[0] : Tags.Split(null as char[], StringSplitOptions.RemoveEmptyEntries);
|
||||
#endregion
|
||||
|
||||
// owned: not an optional one-to-one
|
||||
|
||||
@@ -6,6 +6,9 @@ namespace DataLayer
|
||||
public class LibationContextFactory : DesignTimeDbContextFactoryBase<LibationContext>
|
||||
{
|
||||
protected override LibationContext CreateNewInstance(DbContextOptions<LibationContext> options) => new LibationContext(options);
|
||||
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString) => optionsBuilder.UseSqlServer(connectionString);
|
||||
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString) => optionsBuilder
|
||||
//.UseSqlServer
|
||||
.UseSqlite
|
||||
(connectionString);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,19 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace DataLayer
|
||||
{
|
||||
public static class LibraryQueries
|
||||
{
|
||||
public static List<LibraryBook> GetLibrary_Flat_NoTracking()
|
||||
{
|
||||
public static List<LibraryBook> GetLibrary_Flat_WithTracking(this LibationContext context)
|
||||
=> context
|
||||
.Library
|
||||
.GetLibrary()
|
||||
.ToList();
|
||||
|
||||
public static List<LibraryBook> GetLibrary_Flat_NoTracking()
|
||||
{
|
||||
using var context = LibationContext.Create();
|
||||
return context
|
||||
.Library
|
||||
//.AsNoTracking()
|
||||
.AsNoTracking()
|
||||
.GetLibrary()
|
||||
.ToList();
|
||||
}
|
||||
@@ -21,7 +27,7 @@ namespace DataLayer
|
||||
using var context = LibationContext.Create();
|
||||
return context
|
||||
.Library
|
||||
//.AsNoTracking()
|
||||
.AsNoTracking()
|
||||
.GetLibraryBook(productId);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,57 +4,35 @@ using System.Linq;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using Dinah.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||||
|
||||
namespace DataLayer
|
||||
{
|
||||
internal class TagPersistenceInterceptor : IDbInterceptor
|
||||
{
|
||||
public void Executing(DbContext context)
|
||||
{
|
||||
doWork__EFCore(context);
|
||||
}
|
||||
|
||||
public void Executed(DbContext context) { }
|
||||
|
||||
static void doWork__EFCore(DbContext context)
|
||||
{
|
||||
// persist tags:
|
||||
var modifiedEntities = context.ChangeTracker.Entries().Where(p => p.State.In(EntityState.Modified, EntityState.Added)).ToList();
|
||||
var tagSets = modifiedEntities.Select(e => e.Entity as UserDefinedItem).Where(a => a != null).ToList();
|
||||
foreach (var t in tagSets)
|
||||
FileManager.TagsPersistence.Save(t.Book.AudibleProductId, t.Tags);
|
||||
}
|
||||
public void Executing(DbContext context)
|
||||
{
|
||||
// persist tags:
|
||||
var modifiedEntities = context
|
||||
.ChangeTracker
|
||||
.Entries()
|
||||
.Where(p => p.State.In(EntityState.Modified, EntityState.Added))
|
||||
.ToList();
|
||||
|
||||
#region // notes: working with proxies, esp EF 6
|
||||
// EF 6: entities are proxied with lazy loading when collections are virtual
|
||||
// EF Core: lazy loading is supported in 2.1 (there is a version of lazy loading with proxy-wrapping and a proxy-less version with DI) but not on by default and are not supported here
|
||||
persistTags(modifiedEntities);
|
||||
}
|
||||
|
||||
//static void doWork_EF6(DbContext context)
|
||||
//{
|
||||
// var modifiedEntities = context.ChangeTracker.Entries().Where(p => p.State == EntityState.Modified).ToList();
|
||||
// var unproxiedEntities = modifiedEntities.Select(me => UnProxy(context, me.Entity)).ToList();
|
||||
|
||||
// // persist tags
|
||||
// var tagSets = unproxiedEntities.Select(ue => ue as UserDefinedItem).Where(a => a != null).ToList();
|
||||
// foreach (var t in tagSets)
|
||||
// FileManager.TagsPersistence.Save(t.ProductId, t.TagsRaw);
|
||||
//}
|
||||
|
||||
//// https://stackoverflow.com/a/25774651
|
||||
//private static T UnProxy<T>(DbContext context, T proxyObject) where T : class
|
||||
//{
|
||||
// // alternative: https://docs.microsoft.com/en-us/ef/ef6/fundamentals/proxies
|
||||
// var proxyCreationEnabled = context.Configuration.ProxyCreationEnabled;
|
||||
// try
|
||||
// {
|
||||
// context.Configuration.ProxyCreationEnabled = false;
|
||||
// return context.Entry(proxyObject).CurrentValues.ToObject() as T;
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// context.Configuration.ProxyCreationEnabled = proxyCreationEnabled;
|
||||
// }
|
||||
//}
|
||||
#endregion
|
||||
}
|
||||
private static void persistTags(List<EntityEntry> modifiedEntities)
|
||||
{
|
||||
var tagSets = modifiedEntities
|
||||
.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)
|
||||
.ToList();
|
||||
foreach (var t in tagSets)
|
||||
FileManager.TagsPersistence.Save(t.Book.AudibleProductId, t.Tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
38
DataLayer/UNTESTED/Utilities/LocalDatabaseInfo.cs
Normal file
38
DataLayer/UNTESTED/Utilities/LocalDatabaseInfo.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace DataLayer.Utilities
|
||||
{
|
||||
public static class LocalDatabaseInfo
|
||||
{
|
||||
public static List<string> GetLocalDBInstances()
|
||||
{
|
||||
// Start the child process.
|
||||
using var p = new System.Diagnostics.Process
|
||||
{
|
||||
StartInfo = new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
FileName = "cmd.exe",
|
||||
Arguments = "/C sqllocaldb info",
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
|
||||
}
|
||||
};
|
||||
p.Start();
|
||||
var output = p.StandardOutput.ReadToEnd();
|
||||
p.WaitForExit();
|
||||
|
||||
// if LocalDb is not installed then it will return that 'sqllocaldb' is not recognized as an internal or external command operable program or batch file
|
||||
return string.IsNullOrWhiteSpace(output) || output.Contains("not recognized")
|
||||
? new List<string>()
|
||||
: output
|
||||
.Split(new string[] { Environment.NewLine }, StringSplitOptions.None)
|
||||
.Select(i => i.Trim())
|
||||
.Where(i => !string.IsNullOrEmpty(i))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"LibationContext": "Server=(LocalDb)\\MSSQLLocalDB;Database=DataLayer.LibationContext;Integrated Security=true;",
|
||||
"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": "",
|
||||
"MyTestContext": "Data Source=%DESKTOP%/sample.db"
|
||||
|
||||
@@ -57,38 +57,40 @@ namespace DtoImporterService
|
||||
.Select(a => context.Contributors.Local.Single(c => a.Name == c.Name))
|
||||
.ToList();
|
||||
|
||||
// if no narrators listed, author is the narrator
|
||||
if (item.Narrators is null || !item.Narrators.Any())
|
||||
item.Narrators = item.Authors;
|
||||
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
|
||||
var narrators = item
|
||||
.Narrators
|
||||
.Select(n => context.Contributors.Local.Single(c => n.Name == c.Name))
|
||||
.ToList();
|
||||
|
||||
book = context.Books.Add(new Book(
|
||||
new AudibleProductId(item.ProductId), item.Title, item.Description, item.LengthInMinutes, authors))
|
||||
.Entity;
|
||||
new AudibleProductId(item.ProductId),
|
||||
item.Title,
|
||||
item.Description,
|
||||
item.LengthInMinutes,
|
||||
authors,
|
||||
narrators)
|
||||
).Entity;
|
||||
|
||||
var publisherName = item.Publisher;
|
||||
if (!string.IsNullOrWhiteSpace(publisherName))
|
||||
{
|
||||
var publisher = context.Contributors.Local.Single(c => publisherName == c.Name);
|
||||
book.ReplacePublisher(publisher);
|
||||
}
|
||||
|
||||
qtyNew++;
|
||||
}
|
||||
|
||||
// if no narrators listed, author is the narrator
|
||||
if (item.Narrators is null || !item.Narrators.Any())
|
||||
item.Narrators = item.Authors;
|
||||
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
|
||||
var narrators = item
|
||||
.Narrators
|
||||
.Select(n => context.Contributors.Local.Single(c => n.Name == c.Name))
|
||||
.ToList();
|
||||
// not all books have narrators. these will already be using author as narrator. don't undo this
|
||||
if (narrators.Any())
|
||||
book.ReplaceNarrators(narrators);
|
||||
|
||||
// set/update book-specific info which may have changed
|
||||
book.PictureId = item.PictureId;
|
||||
book.UpdateProductRating(item.Product_OverallStars, item.Product_PerformanceStars, item.Product_StoryStars);
|
||||
if (!string.IsNullOrWhiteSpace(item.SupplementUrl))
|
||||
book.AddSupplementDownloadUrl(item.SupplementUrl);
|
||||
|
||||
var publisherName = item.Publisher;
|
||||
if (!string.IsNullOrWhiteSpace(publisherName))
|
||||
{
|
||||
var publisher = context.Contributors.Local.Single(c => publisherName == c.Name);
|
||||
book.ReplacePublisher(publisher);
|
||||
}
|
||||
|
||||
// important to update user-specific info. this will have changed if user has rated/reviewed the book since last library import
|
||||
book.UserDefinedItem.UpdateRating(item.MyUserRating_Overall, item.MyUserRating_Performance, item.MyUserRating_Story);
|
||||
|
||||
|
||||
@@ -43,19 +43,19 @@ namespace FileLiberator
|
||||
try
|
||||
{
|
||||
{
|
||||
var statusHandler = await processAsync(libraryBook, AudibleFileStorage.AAX, DownloadBook);
|
||||
var statusHandler = await DownloadBook.TryProcessAsync(libraryBook);
|
||||
if (statusHandler.HasErrors)
|
||||
return statusHandler;
|
||||
}
|
||||
|
||||
{
|
||||
var statusHandler = await processAsync(libraryBook, AudibleFileStorage.Audio, DecryptBook);
|
||||
var statusHandler = await DecryptBook.TryProcessAsync(libraryBook);
|
||||
if (statusHandler.HasErrors)
|
||||
return statusHandler;
|
||||
}
|
||||
|
||||
{
|
||||
var statusHandler = await processAsync(libraryBook, AudibleFileStorage.PDF, DownloadPdf);
|
||||
var statusHandler = await DownloadPdf.TryProcessAsync(libraryBook);
|
||||
if (statusHandler.HasErrors)
|
||||
return statusHandler;
|
||||
}
|
||||
@@ -67,10 +67,5 @@ namespace FileLiberator
|
||||
Completed?.Invoke(this, displayMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<StatusHandler> processAsync(LibraryBook libraryBook, AudibleFileStorage afs, IProcessable processable)
|
||||
=> !await afs.ExistsAsync(libraryBook.Book.AudibleProductId)
|
||||
? await processable.ProcessAsync(libraryBook)
|
||||
: new StatusHandler();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,8 @@ namespace FileLiberator
|
||||
public class DownloadPdf : DownloadableBase
|
||||
{
|
||||
public override async Task<bool> ValidateAsync(LibraryBook libraryBook)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook)))
|
||||
return false;
|
||||
|
||||
return !await AudibleFileStorage.PDF.ExistsAsync(libraryBook.Book.AudibleProductId);
|
||||
}
|
||||
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
|
||||
&& !await AudibleFileStorage.PDF.ExistsAsync(libraryBook.Book.AudibleProductId);
|
||||
|
||||
private static string getdownloadUrl(LibraryBook libraryBook)
|
||||
=> libraryBook?.Book?.Supplements?.FirstOrDefault()?.Url;
|
||||
|
||||
@@ -40,5 +40,10 @@ namespace FileLiberator
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<StatusHandler> TryProcessAsync(this IProcessable processable, LibraryBook libraryBook)
|
||||
=> await processable.ValidateAsync(libraryBook)
|
||||
? await processable.ProcessAsync(libraryBook)
|
||||
: new StatusHandler();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Polly" Version="7.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -3,64 +3,57 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
|
||||
namespace FileManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Tags must also be stored in db for search performance. Stored in json file to survive a db reset.
|
||||
/// json is only read when a product is first loaded
|
||||
/// json is only read when a product is first loaded into the db
|
||||
/// json is only written to when tags are edited
|
||||
/// json access is infrequent and one-off
|
||||
/// all other reads happen against db. No volitile storage
|
||||
/// </summary>
|
||||
public static class TagsPersistence
|
||||
{
|
||||
public static string TagsFile => Path.Combine(Configuration.Instance.LibationFiles, "BookTags.json");
|
||||
private static string TagsFile => Path.Combine(Configuration.Instance.LibationFiles, "BookTags.json");
|
||||
|
||||
private static object locker { get; } = new object();
|
||||
|
||||
// if failed, retry only 1 time after a wait of 100 ms
|
||||
// 1st save attempt sometimes fails with
|
||||
// The requested operation cannot be performed on a file with a user-mapped section open.
|
||||
private static RetryPolicy policy { get; }
|
||||
= Policy.Handle<Exception>()
|
||||
.WaitAndRetry(new[] { TimeSpan.FromMilliseconds(100) });
|
||||
|
||||
public static void Save(string productId, string tags)
|
||||
=> System.Threading.Tasks.Task.Run(() => save_fireAndForget(productId, tags));
|
||||
|
||||
private static void save_fireAndForget(string productId, string tags)
|
||||
{
|
||||
ensureCache();
|
||||
|
||||
cache[productId] = tags;
|
||||
|
||||
lock (locker)
|
||||
{
|
||||
// get all
|
||||
var allDictionary = retrieve();
|
||||
|
||||
// update/upsert tag list
|
||||
allDictionary[productId] = tags;
|
||||
|
||||
// re-save:
|
||||
// this often fails the first time with
|
||||
// The requested operation cannot be performed on a file with a user-mapped section open.
|
||||
// 2nd immediate attempt failing was rare. So I added sleep. We'll see...
|
||||
void resave() => File.WriteAllText(TagsFile, JsonConvert.SerializeObject(allDictionary, Formatting.Indented));
|
||||
try { resave(); }
|
||||
catch (IOException debugEx)
|
||||
{
|
||||
// 1000 was always reliable but very slow. trying other values
|
||||
var waitMs = 100;
|
||||
|
||||
System.Threading.Thread.Sleep(waitMs);
|
||||
resave();
|
||||
}
|
||||
}
|
||||
policy.Execute(() => File.WriteAllText(TagsFile, JsonConvert.SerializeObject(cache, Formatting.Indented)));
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> cache;
|
||||
|
||||
public static string GetTags(string productId)
|
||||
{
|
||||
var dic = retrieve();
|
||||
return dic.ContainsKey(productId) ? dic[productId] : null;
|
||||
ensureCache();
|
||||
|
||||
cache.TryGetValue(productId, out string value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> retrieve()
|
||||
{
|
||||
if (!FileUtility.FileExists(TagsFile))
|
||||
return new Dictionary<string, string>();
|
||||
lock (locker)
|
||||
return JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(TagsFile));
|
||||
}
|
||||
}
|
||||
private static void ensureCache()
|
||||
{
|
||||
if (cache is null)
|
||||
lock (locker)
|
||||
cache = !FileUtility.FileExists(TagsFile)
|
||||
? new Dictionary<string, string>()
|
||||
: JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(TagsFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Polly" Version="7.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj" />
|
||||
<ProjectReference Include="..\FileManager\FileManager.csproj" />
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<!-- <PublishSingleFile>true</PublishSingleFile> -->
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -16,7 +16,14 @@ namespace LibationWinForm
|
||||
|
||||
private async void IndexLibraryDialog_Shown(object sender, System.EventArgs e)
|
||||
{
|
||||
(TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.IndexLibraryAsync(new Login.WinformResponder());
|
||||
try
|
||||
{
|
||||
(TotalBooksProcessed, NewBooksAdded) = await LibraryCommands.IndexLibraryAsync(new Login.WinformResponder());
|
||||
}
|
||||
catch
|
||||
{
|
||||
MessageBox.Show("Error importing library. Please try again. If this happens after 2 or 3 tries, contact administrator", "Error importing library", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
|
||||
this.Close();
|
||||
}
|
||||
|
||||
@@ -27,10 +27,15 @@ namespace LibationWinForm
|
||||
public event EventHandler<int> VisibleCountChanged;
|
||||
|
||||
private DataGridView dataGridView;
|
||||
private LibationContext context;
|
||||
|
||||
public ProductsGrid() => InitializeComponent();
|
||||
public ProductsGrid()
|
||||
{
|
||||
InitializeComponent();
|
||||
Disposed += (_, __) => { if (context != null) context.Dispose(); };
|
||||
}
|
||||
|
||||
private bool hasBeenDisplayed = false;
|
||||
private bool hasBeenDisplayed = false;
|
||||
public void Display()
|
||||
{
|
||||
if (hasBeenDisplayed)
|
||||
@@ -87,10 +92,11 @@ namespace LibationWinForm
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// transform into sorted GridEntry.s BEFORE binding
|
||||
//
|
||||
var lib = LibraryQueries.GetLibrary_Flat_NoTracking();
|
||||
//
|
||||
// transform into sorted GridEntry.s BEFORE binding
|
||||
//
|
||||
context = LibationContext.Create();
|
||||
var lib = context.GetLibrary_Flat_WithTracking();
|
||||
|
||||
// if no data. hide all columns. return
|
||||
if (!lib.Any())
|
||||
@@ -174,8 +180,8 @@ namespace LibationWinForm
|
||||
if (editTagsForm.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var qtyChanges = saveChangedTags(liveGridEntry.GetBook(), editTagsForm.NewTags);
|
||||
if (qtyChanges == 0)
|
||||
var qtyChanges = context.UpdateTags(liveGridEntry.GetBook(), editTagsForm.NewTags);
|
||||
if (qtyChanges == 0)
|
||||
return;
|
||||
|
||||
// force a re-draw, and re-apply filters
|
||||
@@ -186,14 +192,6 @@ namespace LibationWinForm
|
||||
filter();
|
||||
}
|
||||
|
||||
private static int saveChangedTags(Book book, string newTags)
|
||||
{
|
||||
book.UserDefinedItem.Tags = newTags;
|
||||
|
||||
var qtyChanges = LibraryCommands.IndexChangedTags(book);
|
||||
return qtyChanges;
|
||||
}
|
||||
|
||||
#region Cell Formatting
|
||||
private void replaceFormatted(object sender, DataGridViewCellFormattingEventArgs e)
|
||||
{
|
||||
@@ -213,22 +211,6 @@ namespace LibationWinForm
|
||||
}
|
||||
#endregion
|
||||
|
||||
public void UpdateRow(string productId)
|
||||
{
|
||||
for (var r = dataGridView.RowCount - 1; r >= 0; r--)
|
||||
{
|
||||
var gridEntry = getGridEntry(r);
|
||||
if (gridEntry.GetBook().AudibleProductId == productId)
|
||||
{
|
||||
var libBook = LibraryQueries.GetLibraryBook_Flat_NoTracking(productId);
|
||||
gridEntry.REPLACE_Library_Book(libBook);
|
||||
dataGridView.InvalidateRow(r);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region filter
|
||||
string _filterSearchString;
|
||||
private void filter() => Filter(_filterSearchString);
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
-- begin VERSIONING ---------------------------------------------------------------------------------------------------------------------
|
||||
https://github.com/rmcrackan/Libation/releases
|
||||
|
||||
v3.1-beta.1 : RELEASE TO BETA
|
||||
v3.0.3 : Switch to SQLite. No longer relies on LocalDB, which must be installed separately
|
||||
v3.0.2 : Final using LocalDB
|
||||
v3.0.1 : Legacy inAudible wire-up code is still present but is commented out. All future check-ins are not guaranteed to have inAudible wire-up code
|
||||
v3.0 : This version is fully powered by the Audible API. Legacy scraping code is still present but is commented out. All future check-ins are not guaranteed to have any scraping code
|
||||
v2 : new library page scraping. still chrome cookies. all decryption is handled natively. no inAudible dependency
|
||||
v1 : old library ajax scraping. wish list scraping. chrome cookies. directly call local inAudible. .net framework
|
||||
-- end VERSIONING ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin HOW TO PUBLISH ---------------------------------------------------------------------------------------------------------------------
|
||||
OPTION 1: UI
|
||||
rt-clk project > Publish...
|
||||
rt-clk project project > Publish...
|
||||
click Publish
|
||||
|
||||
OPTION 2: cmd line
|
||||
|
||||
207
__TODO.txt
207
__TODO.txt
@@ -1,207 +0,0 @@
|
||||
-- begin BETA ---------------------------------------------------------------------------------------------------------------------
|
||||
TESTING BUG
|
||||
dbl clk. long pause. exception:
|
||||
System.ComponentModel.Win32Exception (2): The system cannot find the file specified.
|
||||
"continue" button allows me to keep using
|
||||
bottom #s do not update
|
||||
login succeeded. IdentityTokens.json successfully created
|
||||
received above error again during scan. continue
|
||||
stuck on scan. force quit from task manager
|
||||
only files:
|
||||
Images -- empty dir
|
||||
IdentityTokens.json -- populated
|
||||
no mdf, ldf
|
||||
|
||||
REPLACE DB
|
||||
need completely need db? replace LocalDb with sqlite? embedded document nosql?
|
||||
|
||||
CREATE INSTALLER
|
||||
see REFERENCE.txt > HOW TO PUBLISH
|
||||
|
||||
RELEASE TO BETA
|
||||
Warn of known performance issues
|
||||
- Library import
|
||||
- Tag add/edit
|
||||
- Grid is slow to respond loading when books aren't liberated
|
||||
- get decrypt key -- unavoidable
|
||||
-- end BETA ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT, IMPORT UI ---------------------------------------------------------------------------------------------------------------------
|
||||
scan library in background?
|
||||
can include a notice somewhere that a scan is in-process
|
||||
why block the UI at all?
|
||||
what to do if new books? don't want to refresh grid when user isn't expecting it
|
||||
-- end ENHANCEMENT, IMPORT UI ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin BUG, FILE DOWNLOAD ---------------------------------------------------------------------------------------------------------------------
|
||||
reproduce: try to do the api download with a bad codec
|
||||
result: DownloadsFinal dir .aax file 1 kb
|
||||
this resulted from an exception. we should not be keeping a file after exception
|
||||
if error: show error. DownloadBook delete bad file
|
||||
-- end BUG, FILE DOWNLOAD ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT, PERFORMANCE: IMPORT ---------------------------------------------------------------------------------------------------------------------
|
||||
imports are PAINFULLY slow for just a few hundred items. wtf is taking so long?
|
||||
-- end ENHANCEMENT, PERFORMANCE: IMPORT ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT, PERFORMANCE: GRID ---------------------------------------------------------------------------------------------------------------------
|
||||
when a book/pdf is NOT liberated, calculating the grid's [Liberated][NOT d/l'ed] label is very slow. use something similar to PictureStorage's timer to run on a separate thread
|
||||
https://stackoverflow.com/a/12046333
|
||||
https://codereview.stackexchange.com/a/135074
|
||||
// do NOT use lock() or Monitor with async/await
|
||||
private static int _lockFlag = 0; // 0 - free
|
||||
if (Interlocked.CompareExchange(ref _lockFlag, 1, 0) != 0) return;
|
||||
// only 1 thread will enter here without locking the object/put the other threads to sleep
|
||||
try { await DoWorkAsync(); }
|
||||
// free the lock
|
||||
finally { Interlocked.Decrement(ref _lockFlag); }
|
||||
|
||||
use stop light icons for liberated state: red=none, yellow=downloaded encrypted, green=liberated
|
||||
|
||||
need a way to liberate ad hoc books and pdf.s
|
||||
|
||||
use pdf icon with and without and X over it to indicate status
|
||||
-- end ENHANCEMENT, PERFORMANCE: GRID ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT, GET LIBRARY ---------------------------------------------------------------------------------------------------------------------
|
||||
Audible API. GET /1.0/library , GET /1.0/library/{asin}
|
||||
TONS of expensive conversion: GetLibraryAsync > string > JObject > string > LibraryDtoV10
|
||||
same for GetLibraryBookAsync > ... > BookDtoV10
|
||||
-- end ENHANCEMENT, GET LIBRARY ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT, DEBUGGING ---------------------------------------------------------------------------------------------------------------------
|
||||
datalayer stuff (eg: Book) need better ToString
|
||||
-- end ENHANCEMENT, DEBUGGING ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin BUG, MOVING FILES ---------------------------------------------------------------------------------------------------------------------
|
||||
with libation closed, move files
|
||||
start libation
|
||||
can get error below
|
||||
fixed on restart
|
||||
|
||||
Form1_Load ... await setBackupCountsAsync();
|
||||
Collection was modified; enumeration operation may not execute.
|
||||
stack trace
|
||||
at System.ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion()
|
||||
at System.Collections.Generic.List`1.Enumerator.MoveNextRare()
|
||||
at System.Linq.Enumerable.SingleOrDefault[TSource](IEnumerable`1 source, Func`2 predicate)
|
||||
at FileManager.FilePathCache.GetPath(String id, FileType type) in \Libation\FileManager\UNTESTED\FilePathCache.cs:line 33
|
||||
at FileManager.AudibleFileStorage.<getAsync>d__32.MoveNext() in \Libation\FileManager\UNTESTED\AudibleFileStorage.cs:line 112
|
||||
at FileManager.AudibleFileStorage.<GetAsync>d__31.MoveNext() in \Libation\FileManager\UNTESTED\AudibleFileStorage.cs:line 107
|
||||
at FileManager.AudibleFileStorage.<ExistsAsync>d__30.MoveNext() in \Libation\FileManager\UNTESTED\AudibleFileStorage.cs:line 104
|
||||
at LibationWinForm.Form1.<<setBookBackupCountsAsync>g__getAudioFileStateAsync|15_1>d.MoveNext() in \Libation\LibationWinForm\UNTESTED\Form1.cs:line 110
|
||||
at LibationWinForm.Form1.<setBookBackupCountsAsync>d__15.MoveNext() in \Libation\LibationWinForm\UNTESTED\Form1.cs:line 117
|
||||
at LibationWinForm.Form1.<setBackupCountsAsync>d__13.MoveNext() in \Libation\LibationWinForm\UNTESTED\Form1.cs:line 81
|
||||
at LibationWinForm.Form1.<Form1_Load>d__11.MoveNext() in \Libation\LibationWinForm\UNTESTED\Form1.cs:line 60
|
||||
-- end BUG, MOVING FILES ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin CONFIG FILES ---------------------------------------------------------------------------------------------------------------------
|
||||
.\appsettings.json should only be a pointer to the real settings file location: LibationSettings.json
|
||||
replace complex config saving throughout with new way in my ConsoleDependencyInjection solution
|
||||
all settings should be strongly typed
|
||||
re-create my shortcuts and bak
|
||||
|
||||
for appsettings.json to get copied in the single-file release, project must incl <ExcludeFromSingleFile>true
|
||||
|
||||
multiple files named "appsettings.json" will overwrite each other
|
||||
libraries should avoid this generic name. in general: ok for applications to use them
|
||||
there are exceptions: datalayer has appsettings which is copied to winform. if winform uses appsettings also, it will override datalayer's
|
||||
|
||||
|
||||
Audible API
|
||||
\AudibleApi\_Tests\AudibleApi.Tests\bin\Debug\netcoreapp3.0\L1
|
||||
\AudibleApi\_Tests\AudibleApi.Tests\bin\Debug\netcoreapp3.0\ComputedTestValues
|
||||
14+ json files
|
||||
these can go in a shared solution folder
|
||||
BasePath => recursively search directories upward-only until fild dir with .sln
|
||||
from here can set up a shared dir anywhere. use recursive upward search to find it. store shared files here. eg: identityTokens.json, ComputedTestValues
|
||||
-- end CONFIG FILES ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin TAGS ---------------------------------------------------------------------------------------------------------------------
|
||||
pulling previous tags into new Books. think: reloading db
|
||||
move out of Book and into DtoMapper?
|
||||
|
||||
Extract file and tag stuff from domain objects. This should exist only in data layer. If domain objects are able to call EF context, it should go through data layer
|
||||
Why are tags in file AND database?
|
||||
|
||||
extract FileManager dependency from data layer
|
||||
-- end TAGS ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT, PERFORMANCE: TAGS ---------------------------------------------------------------------------------------------------------------------
|
||||
tag edits still take forever and block UI
|
||||
unlikely to be an issue with file write. in fact, should probably roll back this change
|
||||
also touches parts of code which: db write via a hook, search engine re-index
|
||||
-- end ENHANCEMENT, PERFORMANCE: TAGS ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT, CATEGORIES ---------------------------------------------------------------------------------------------------------------------
|
||||
add support for multiple categories
|
||||
when i do this, learn about the different CategoryLadder.Root enums. probably only need Root.Genres
|
||||
-- end ENHANCEMENT, CATEGORIES ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin CLEAN UP ARCHITECTURE ---------------------------------------------------------------------------------------------------------------------
|
||||
my ui sucks. it's also tightly coupled with biz logic. can't replace ui until biz logic is extracted and loosely. remove all biz logic from presentation/winforms layer
|
||||
-- end CLEAN UP ARCHITECTURE ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin UNIT TESTS ---------------------------------------------------------------------------------------------------------------------
|
||||
all "UNTESTED" code needs unit tests
|
||||
Turn into unit tests or demos
|
||||
TextBoxBaseTextWriter.cs
|
||||
EnumerationExamples.cs
|
||||
EnumExt.cs
|
||||
IEnumerable[T]Ext.cs
|
||||
MultiTextWriter.cs
|
||||
Selenium.Examples.cs
|
||||
ScratchPad.cs
|
||||
ProcessorAutomationController.Examples.cs
|
||||
ScraperRules.Examples.cs
|
||||
ByFactory.Example.cs
|
||||
search 'example code' on: LibationWinForm\...\Form1.cs
|
||||
EnumerationFlagsExtensions.EXAMPLES()
|
||||
// examples
|
||||
scratchpad
|
||||
scratch pad
|
||||
scratch_pad
|
||||
-- end UNIT TESTS ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin DECRYPTING ---------------------------------------------------------------------------------------------------------------------
|
||||
replace inaudible/inaudible lite with pure ffmpeg
|
||||
benefits of inaudible:
|
||||
highly configurable
|
||||
embedded cover image
|
||||
chapter-ized
|
||||
cue and nfo files
|
||||
can hopefully get most of this with simple decrypt. possibly including the new chapter titles
|
||||
|
||||
better chapters in many m4b files. to see, try re-downloading. examples: bobiverse, sharp objects
|
||||
|
||||
raw ffmpeg decrypting is significantly faster than inAudible/AaxDecrypter method:
|
||||
inAudible/AaxDecrypter
|
||||
tiny file
|
||||
40 sec decrypt
|
||||
40 sec chapterize + tag
|
||||
huge file
|
||||
60 sec decrypt
|
||||
120 sec chapterize + tag
|
||||
directly call ffmpeg (decrypt only)
|
||||
tiny file
|
||||
17 sec decrypt
|
||||
huge file
|
||||
39 sec decrypt
|
||||
-- end DECRYPTING ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT: REMOVE BOOK ---------------------------------------------------------------------------------------------------------------------
|
||||
how to remove a book?
|
||||
previously difficult due to implementation details regarding scraping and importing. should now be trivial
|
||||
-- end ENHANCEMENT: REMOVE BOOK ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT: NEW VIEWS ---------------------------------------------------------------------------------------------------------------------
|
||||
menu views. filter could work for grid display; just use the lucene query language
|
||||
1) menu to show all tags and count of each. click on tag so see only those books
|
||||
2) tree to show all categories and subcategories. click on category so see only those books
|
||||
-- end ENHANCEMENT: NEW VIEWS ---------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- begin ENHANCEMENT: LOGGING, ERROR HANDLING ---------------------------------------------------------------------------------------------------------------------
|
||||
LibationWinForm and Audible API need better logging and error handling
|
||||
incl log levels, db query logging
|
||||
see AaxDecryptorWinForms.initLogging()
|
||||
-- end ENHANCEMENT: LOGGING ---------------------------------------------------------------------------------------------------------------------
|
||||
Reference in New Issue
Block a user