Compare commits

...

59 Commits

Author SHA1 Message Date
Robert McRackan
6ed1307443 v5.6.3.1 : support for episodes ( issue #96 ) 2021-09-02 16:05:23 -04:00
Robert McRackan
c2c732b2b1 central event for library altered: books added or removed 2021-09-02 15:55:12 -04:00
Robert McRackan
9e0caf34d6 Rename db table Library => LibraryBooks 2021-09-02 15:26:20 -04:00
Robert McRackan
802763a4fb minor 2021-09-02 15:19:55 -04:00
Robert McRackan
b4803c42a5 Streamline GetLibrary_Flat_NoTracking with better context dispose 2021-09-02 14:51:06 -04:00
Robert McRackan
62c98c66a3 Merge branch 'master' of https://github.com/rmcrackan/Libation 2021-09-02 14:15:10 -04:00
Robert McRackan
6b289445e2 update dependencies 2021-09-02 14:14:25 -04:00
rmcrackan
52bf91f8aa Merge pull request #99 from Mbucari/master
Fixed crash when change RemoveBooksDialog checkbox via spacebar.
2021-09-02 13:47:57 -04:00
Michael Bucari-Tovo
6d2dff1a98 Code Cleanup 2021-09-02 11:21:20 -06:00
Mbucari
7c9970c0cb Merge branch 'rmcrackan:master' into master 2021-09-02 11:18:32 -06:00
Michael Bucari-Tovo
d2892f9076 Fix crash when checkbox checked via spacebar. 2021-09-02 11:11:40 -06:00
Robert McRackan
89f60a7ca3 fix wording 2021-09-02 11:35:32 -04:00
rmcrackan
ea37c09081 Merge pull request #98 from Mbucari/master
Added AAXClean as nuget package.
2021-09-02 10:54:42 -04:00
Michael Bucari-Tovo
76cb280933 Added AAXClean as nuget package. 2021-09-02 08:13:42 -06:00
Robert McRackan
0a54a8104c update counts label to reflect recent workflow changes 2021-09-02 09:54:02 -04:00
Robert McRackan
7464336535 remove WinFormsDesigner 2021-09-02 09:52:41 -04:00
Robert McRackan
dc0dd3474b separate the concepts of UserDefinedItem being updated in memory vs successful persistence 2021-09-02 09:51:17 -04:00
Robert McRackan
7b9c5c0f4f Add episode/podcast search engine bool 2021-09-01 16:56:09 -04:00
Robert McRackan
ad87f1851e Add episodes content type to Books in db 2021-09-01 16:51:59 -04:00
Robert McRackan
e8423341ef bug fix: bottom count numbers and menu options weren't updating on liberate-all 2021-09-01 14:28:01 -04:00
Robert McRackan
a9d3494af1 Added support for episodic content incl podcasts. Not yet complete. Need to mark them as such in the db somehow and also add search engine bools 2021-09-01 12:47:59 -04:00
Robert McRackan
90731a8948 'Convert all M4b to Mp3': add confirmation dialog with better explanation 2021-08-31 09:32:50 -04:00
Robert McRackan
e723467ca6 book liberation status Error:
* show system error icon in grid instead of stoplight
* list error count in bottom right #s
* SearchEngine bool: LiberatedError
2021-08-27 17:01:00 -04:00
Robert McRackan
722c33bf61 Add readme to help/annoy collaborators 2021-08-27 15:14:42 -04:00
Robert McRackan
f080215cbb Book details dialog: tags should get initial focus 2021-08-27 14:07:06 -04:00
Robert McRackan
d5c74d629f update dependencies 2021-08-27 11:16:13 -04:00
Robert McRackan
d12c246f6d version increm 2021-08-26 16:09:37 -04:00
Robert McRackan
8969c216af comments 2021-08-26 16:08:26 -04:00
Robert McRackan
9a4903f0dd Bug fix: after successful pdf download, this state wasn't being saved 2021-08-26 15:53:33 -04:00
Robert McRackan
3eda498a5e new AudibleApi nuget no longer relies on external json and js files which caused issues 2021-08-26 12:51:55 -04:00
Robert McRackan
8af7f28f04 (hopefully) final nuget pkg: Dinah.Core.WindowsDesktop 2021-08-26 12:49:37 -04:00
Robert McRackan
d9d7dfe1f7 update depandecies 2021-08-26 12:48:05 -04:00
Robert McRackan
b9c4d11946 remove TestCommon 2021-08-25 17:07:26 -04:00
Robert McRackan
68a5d7a58d nuget. done until I can figure out how to build .net5-windows nuget from github actions 2021-08-25 16:24:02 -04:00
Robert McRackan
4d69b222c5 nuget: Dinah.EntityFrameworkCore 2021-08-25 15:55:31 -04:00
Robert McRackan
42f94e7f6c more nuget migration 2021-08-25 15:33:09 -04:00
Robert McRackan
381d52be72 Better audible api to reduce captcha occurances 2021-08-24 13:42:14 -04:00
Robert McRackan
f16ad30891 bug fix from my last bug fix :( 2021-08-24 09:28:13 -04:00
Robert McRackan
ef53a6a8cb Bug fix: issue #92 2021-08-23 16:29:23 -04:00
Robert McRackan
9a37d434f1 FileLiberator is not db ignorant. It doesn't make context calls but still heavily uses the classes defined in the domain. Also uses internal util.s 2021-08-23 16:27:22 -04:00
Robert McRackan
d7eb190f69 reduce use of Book.Supplements 2021-08-23 16:16:08 -04:00
Robert McRackan
f19c46ee45 Book: add pdf url as is, not Absolute 2021-08-23 16:07:19 -04:00
rmcrackan
343c3b62d6 Merge pull request #90 from Mbucari/master
Fully implemented the MVVM pattern
2021-08-22 21:06:00 -04:00
Michael Bucari-Tovo
b1de10a71a Fix filtering. 2021-08-22 13:29:01 -06:00
Michael Bucari-Tovo
6beb5cc74a Made changes discussed. 2021-08-22 13:27:39 -06:00
Michael Bucari-Tovo
3767c3574a Merge branch 'master' of https://github.com/Mbucari/Libation 2021-08-21 22:09:27 -06:00
Michael Bucari-Tovo
4ceb4f9c03 Change back. 2021-08-21 22:09:13 -06:00
Mbucari
0f5149f7b4 Merge pull request #2 from rmcrackan/master
bug fix. race condition. can't check for book-exists-state before set…
2021-08-21 22:08:51 -06:00
Michael Bucari-Tovo
673451dc11 Git resolve 2021-08-21 22:08:35 -06:00
Michael Bucari-Tovo
e4257afc14 Version Num 2021-08-21 22:06:54 -06:00
Michael Bucari-Tovo
2a7e185dc3 Finish MVVM conversion 2021-08-21 22:03:16 -06:00
Michael Bucari-Tovo
9e06c343c1 Don't check if values have changed when updating the database. 2021-08-21 21:15:25 -06:00
Michael Bucari-Tovo
40b3a9990d FileLiberator is now DB ignorant. IProcessables update UserDaefinedData which notifies the view model. 2021-08-21 20:49:54 -06:00
Robert McRackan
d66c112a1e bug fix. race condition. can't check for book-exists-state before setting this state 2021-08-21 22:32:45 -04:00
Michael Bucari-Tovo
d826885728 Fix display for new LiberatedStatus values. 2021-08-21 18:37:07 -06:00
Michael Bucari-Tovo
263222d8cc Changed method signature. 2021-08-21 18:21:22 -06:00
Michael Bucari-Tovo
f25734334d Add separate command for updating tags 2021-08-21 18:16:24 -06:00
Michael Bucari-Tovo
ede8397f13 Needed to add check for actual file since Audio_Exists is now an application state. 2021-08-21 18:15:39 -06:00
Michael Bucari-Tovo
1369ee575a Replaced LiberatedState with LiberatedStatus and PdfState with LiberatedStatus? 2021-08-21 16:29:16 -06:00
65 changed files with 1651 additions and 3412 deletions

View File

@@ -5,8 +5,8 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\AAXClean\AAXClean.csproj" />
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
<PackageReference Include="AAXClean" Version="0.1.1" />
<PackageReference Include="Dinah.Core" Version="1.0.5.2" />
</ItemGroup>
</Project>

View File

@@ -6,14 +6,11 @@
<ItemGroup>
<PackageReference Include="CsvHelper" Version="27.1.1" />
<PackageReference Include="NPOI" Version="2.5.3" />
<PackageReference Include="NPOI" Version="2.5.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApiDTOs\AudibleApiDTOs.csproj" />
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj" />
<ProjectReference Include="..\DtoImporterService\DtoImporterService.csproj" />
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
<ProjectReference Include="..\LibationSearchEngine\LibationSearchEngine.csproj" />
</ItemGroup>

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using DataLayer;
using FileManager;
@@ -6,11 +7,15 @@ namespace ApplicationServices
{
public static class DbContexts
{
//// idea for future command/query separation
// public static LibationContext GetCommandContext() { }
// public static LibationContext GetQueryContext() { }
/// <summary>Use for fully functional context, incl. SaveChanges(). For query-only, use the other method</summary>
public static LibationContext GetContext()
=> LibationContext.Create(SqliteStorage.ConnectionString);
/// <summary>Use for full library querying. No lazy loading</summary>
public static List<LibraryBook> GetLibrary_Flat_NoTracking()
{
using var context = GetContext();
return context.GetLibrary_Flat_NoTracking();
}
}
}

View File

@@ -11,13 +11,6 @@ using Serilog;
namespace ApplicationServices
{
// subtly different from DataLayer.LiberatedStatus
// - DataLayer.LiberatedStatus: has no concept of partially downloaded
// - ApplicationServices.LiberatedState: has no concept of Error/skipped
public enum LiberatedState { NotDownloaded, PartialDownload, Liberated }
public enum PdfState { NoPdf, Downloaded, NotDownloaded }
public static class LibraryCommands
{
private static LibraryOptions.ResponseGroupOptions LibraryResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS;
@@ -71,6 +64,7 @@ namespace ApplicationServices
LibraryResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS;
}
}
#region FULL LIBRARY scan and import
public static async Task<(int totalCount, int newCount)> ImportAccountAsync(Func<Account, ILoginCallback> loginCallbackFactoryFunc, params Account[] accounts)
{
@@ -87,9 +81,6 @@ namespace ApplicationServices
var newCount = await importIntoDbAsync(importItems);
Log.Logger.Information($"Import: New count {newCount}");
await Task.Run(() => SearchEngineCommands.FullReIndex());
Log.Logger.Information("FullReIndex: success");
return (totalCount, newCount);
}
catch (AudibleApi.Authentication.LoginFailedException lfEx)
@@ -155,144 +146,114 @@ namespace ApplicationServices
using var context = DbContexts.GetContext();
var libraryImporter = new LibraryImporter(context);
var newCount = await Task.Run(() => libraryImporter.Import(importItems));
context.SaveChanges();
var qtyChanges = context.SaveChanges();
if (qtyChanges > 0)
await Task.Run(() => finalizeLibrarySizeChange());
return newCount;
}
#endregion
#region Update book details
public static int UpdateUserDefinedItem(Book book, string newTags, LiberatedStatus bookStatus, LiberatedStatus? pdfStatus)
#region remove books
public static Task<List<LibraryBook>> RemoveBooksAsync(List<string> idsToRemove) => Task.Run(() => removeBooks(idsToRemove));
private static List<LibraryBook> removeBooks(List<string> idsToRemove)
{
try
{
using var context = DbContexts.GetContext();
using var context = DbContexts.GetContext();
var libBooks = context.GetLibrary_Flat_NoTracking();
var udi = book.UserDefinedItem;
var removeLibraryBooks = libBooks.Where(lb => idsToRemove.Contains(lb.Book.AudibleProductId)).ToList();
context.LibraryBooks.RemoveRange(removeLibraryBooks);
var tagsChanged = udi.Tags != newTags;
var bookStatusChanged = udi.BookStatus != bookStatus;
var pdfStatusChanged = udi.PdfStatus != pdfStatus;
var qtyChanges = context.SaveChanges();
if (qtyChanges > 0)
finalizeLibrarySizeChange();
if (!tagsChanged && !bookStatusChanged && !pdfStatusChanged)
return 0;
return removeLibraryBooks;
}
#endregion
udi.Tags = newTags;
udi.BookStatus = bookStatus;
udi.PdfStatus = pdfStatus;
// Attach() NoTracking entities before SaveChanges()
context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
var qtyChanges = context.SaveChanges();
if (qtyChanges == 0)
return 0;
if (tagsChanged)
SearchEngineCommands.UpdateBookTags(book);
if (bookStatusChanged || pdfStatusChanged)
SearchEngineCommands.UpdateLiberatedStatus(book);
return qtyChanges;
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error updating tags");
throw;
}
// call this whenever books are added or removed from library
private static void finalizeLibrarySizeChange()
{
SearchEngineCommands.FullReIndex();
LibrarySizeChanged?.Invoke(null, null);
}
public static int UpdateBook(LibraryBook libraryBook, LiberatedStatus liberatedStatus)
/// <summary>Occurs when books are added or removed from library</summary>
public static event EventHandler LibrarySizeChanged;
/// <summary>
/// Occurs when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/>
/// changed values are successfully persisted.
/// </summary>
public static event EventHandler<string> BookUserDefinedItemCommitted;
#region Update book details
public static int UpdateUserDefinedItem(Book book)
{
try
{
using var context = DbContexts.GetContext();
var udi = libraryBook.Book.UserDefinedItem;
if (udi.BookStatus == liberatedStatus)
return 0;
// Attach() NoTracking entities before SaveChanges()
udi.BookStatus = liberatedStatus;
context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
context.Attach(book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
var qtyChanges = context.SaveChanges();
if (qtyChanges > 0)
SearchEngineCommands.UpdateLiberatedStatus(libraryBook.Book);
{
SearchEngineCommands.UpdateLiberatedStatus(book);
BookUserDefinedItemCommitted?.Invoke(null, book.AudibleProductId);
}
return qtyChanges;
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error updating tags");
throw;
}
}
public static int UpdatePdf(LibraryBook libraryBook, LiberatedStatus liberatedStatus)
{
try
{
using var context = DbContexts.GetContext();
var udi = libraryBook.Book.UserDefinedItem;
if (udi.PdfStatus == liberatedStatus)
return 0;
// Attach() NoTracking entities before SaveChanges()
udi.PdfStatus = liberatedStatus;
context.Attach(udi).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
var qtyChanges = context.SaveChanges();
return qtyChanges;
}
catch (Exception ex)
{
Log.Logger.Error(ex, "Error updating tags");
Log.Logger.Error(ex, $"Error updating {nameof(book.UserDefinedItem)}");
throw;
}
}
#endregion
public static LiberatedStatus Liberated_Status(Book book)
=> book.Audio_Exists ? LiberatedStatus.Liberated
: FileManager.AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedStatus.PartialDownload
: LiberatedStatus.NotLiberated;
public static LiberatedStatus? Pdf_Status(Book book)
=> !book.HasPdf ? null
: book.PDF_Exists ? LiberatedStatus.Liberated
: LiberatedStatus.NotLiberated;
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those...
public static LiberatedState Liberated_Status(Book book)
=> book.Audio_Exists ? LiberatedState.Liberated
: FileManager.AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedState.PartialDownload
: LiberatedState.NotDownloaded;
public static PdfState Pdf_Status(Book book)
=> !book.Supplements.Any() ? PdfState.NoPdf
: book.PDF_Exists ? PdfState.Downloaded
: PdfState.NotDownloaded;
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int pdfsDownloaded, int pdfsNotDownloaded) { }
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int pdfsDownloaded, int pdfsNotDownloaded) { }
public static LibraryStats GetCounts()
{
var libraryBooks = DbContexts.GetContext().GetLibrary_Flat_NoTracking();
var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
var results = libraryBooks
.AsParallel()
.Select(lb => Liberated_Status(lb.Book))
.ToList();
var booksFullyBackedUp = results.Count(r => r == LiberatedState.Liberated);
var booksDownloadedOnly = results.Count(r => r == LiberatedState.PartialDownload);
var booksNoProgress = results.Count(r => r == LiberatedState.NotDownloaded);
var booksFullyBackedUp = results.Count(r => r == LiberatedStatus.Liberated);
var booksDownloadedOnly = results.Count(r => r == LiberatedStatus.PartialDownload);
var booksNoProgress = results.Count(r => r == LiberatedStatus.NotLiberated);
var booksError = results.Count(r => r == LiberatedStatus.Error);
Log.Logger.Information("Book counts. {@DebugInfo}", new { total = results.Count, booksFullyBackedUp, booksDownloadedOnly, booksNoProgress });
Log.Logger.Information("Book counts. {@DebugInfo}", new { total = results.Count, booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError });
var boolResults = libraryBooks
.AsParallel()
.Where(lb => lb.Book.Supplements.Any())
.Where(lb => lb.Book.HasPdf)
.Select(lb => Pdf_Status(lb.Book))
.ToList();
var pdfsDownloaded = boolResults.Count(r => r == PdfState.Downloaded);
var pdfsNotDownloaded = boolResults.Count(r => r == PdfState.NotDownloaded);
var pdfsDownloaded = boolResults.Count(r => r == LiberatedStatus.Liberated);
var pdfsNotDownloaded = boolResults.Count(r => r == LiberatedStatus.NotLiberated);
Log.Logger.Information("PDF counts. {@DebugInfo}", new { total = boolResults.Count, pdfsDownloaded, pdfsNotDownloaded });
return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, pdfsDownloaded, pdfsNotDownloaded);
return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError, pdfsDownloaded, pdfsNotDownloaded);
}
}
}

View File

@@ -47,8 +47,8 @@ namespace ApplicationServices
[Name("Publisher")]
public string Publisher { get; set; }
[Name("Pdf url")]
public string PdfUrl { get; set; }
[Name("Has PDF")]
public bool HasPdf { get; set; }
[Name("Series Names")]
public string SeriesNames { get; set; }
@@ -89,11 +89,14 @@ namespace ApplicationServices
[Name("My Libation Tags")]
public string MyLibationTags { get; set; }
[Name("Book Liberation Status")]
[Name("Book Liberated Status")]
public string BookStatus { get; set; }
[Name("PDF Liberation Status")]
[Name("PDF Liberated Status")]
public string PdfStatus { get; set; }
[Name("Content Type")]
public string ContentType { get; set; }
}
public static class LibToDtos
{
@@ -109,7 +112,7 @@ namespace ApplicationServices
NarratorNames = a.Book.NarratorNames,
LengthInMinutes = a.Book.LengthInMinutes,
Publisher = a.Book.Publisher,
PdfUrl = a.Book.Supplements?.FirstOrDefault()?.Url,
HasPdf = a.Book.HasPdf,
SeriesNames = a.Book.SeriesNames,
SeriesOrder = a.Book.SeriesLink.Any() ? a.Book.SeriesLink?.Select(sl => $"{sl.Index} : {sl.Series.Name}").Aggregate((a, b) => $"{a}, {b}") : "",
CommunityRatingOverall = a.Book.Rating?.OverallRating,
@@ -124,16 +127,15 @@ namespace ApplicationServices
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,
MyLibationTags = a.Book.UserDefinedItem.Tags,
BookStatus = a.Book.UserDefinedItem.BookStatus.ToString(),
PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString()
PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString(),
ContentType = a.Book.ContentType.ToString()
}).ToList();
}
public static class LibraryExporter
{
public static void ToCsv(string saveFilePath)
{
using var context = DbContexts.GetContext();
var dtos = context.GetLibrary_Flat_NoTracking().ToDtos();
var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos();
if (!dtos.Any())
return;
@@ -147,17 +149,14 @@ namespace ApplicationServices
public static void ToJson(string saveFilePath)
{
using var context = DbContexts.GetContext();
var dtos = context.GetLibrary_Flat_NoTracking().ToDtos();
var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos();
var json = Newtonsoft.Json.JsonConvert.SerializeObject(dtos, Newtonsoft.Json.Formatting.Indented);
System.IO.File.WriteAllText(saveFilePath, json);
}
public static void ToXlsx(string saveFilePath)
{
using var context = DbContexts.GetContext();
var dtos = context.GetLibrary_Flat_NoTracking().ToDtos();
var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos();
var workbook = new XSSFWorkbook();
var sheet = workbook.CreateSheet("Library");
@@ -182,7 +181,7 @@ namespace ApplicationServices
nameof (ExportDto.NarratorNames),
nameof (ExportDto.LengthInMinutes),
nameof (ExportDto.Publisher),
nameof (ExportDto.PdfUrl),
nameof (ExportDto.HasPdf),
nameof (ExportDto.SeriesNames),
nameof (ExportDto.SeriesOrder),
nameof (ExportDto.CommunityRatingOverall),
@@ -197,7 +196,8 @@ namespace ApplicationServices
nameof (ExportDto.MyRatingStory),
nameof (ExportDto.MyLibationTags),
nameof (ExportDto.BookStatus),
nameof (ExportDto.PdfStatus)
nameof (ExportDto.PdfStatus),
nameof (ExportDto.ContentType)
};
var col = 0;
foreach (var c in columns)
@@ -234,7 +234,7 @@ namespace ApplicationServices
row.CreateCell(col++).SetCellValue(dto.NarratorNames);
row.CreateCell(col++).SetCellValue(dto.LengthInMinutes);
row.CreateCell(col++).SetCellValue(dto.Publisher);
row.CreateCell(col++).SetCellValue(dto.PdfUrl);
row.CreateCell(col++).SetCellValue(dto.HasPdf);
row.CreateCell(col++).SetCellValue(dto.SeriesNames);
row.CreateCell(col++).SetCellValue(dto.SeriesOrder);
@@ -261,6 +261,7 @@ namespace ApplicationServices
row.CreateCell(col++).SetCellValue(dto.MyLibationTags);
row.CreateCell(col++).SetCellValue(dto.BookStatus);
row.CreateCell(col++).SetCellValue(dto.PdfStatus);
row.CreateCell(col++).SetCellValue(dto.ContentType);
rowIndex++;
}

View File

@@ -10,7 +10,8 @@ namespace ApplicationServices
public static void FullReIndex(SearchEngine engine = null)
{
engine ??= new SearchEngine();
engine.CreateNewIndex(DbContexts.GetContext());
var library = DbContexts.GetLibrary_Flat_NoTracking();
engine.CreateNewIndex(library);
}
public static SearchResultSet Search(string searchString) => performSearchEngineFunc_safe(e =>
@@ -39,17 +40,17 @@ namespace ApplicationServices
}
}
private static T performSearchEngineFunc_safe<T>(Func<SearchEngine, T> action)
private static T performSearchEngineFunc_safe<T>(Func<SearchEngine, T> func)
{
var engine = new SearchEngine();
try
{
return action(engine);
return func(engine);
}
catch (FileNotFoundException)
{
FullReIndex(engine);
return action(engine);
return func(engine);
}
}
}

View File

@@ -12,19 +12,19 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.5">
<PackageReference Include="Dinah.EntityFrameworkCore" Version="1.0.2.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.5">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Dinah.Core\Dinah.EntityFrameworkCore\Dinah.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\FileManager\FileManager.csproj" />
</ItemGroup>

View File

@@ -15,6 +15,10 @@ namespace DataLayer
Id = id;
}
}
// enum will be easier than bool to extend later
public enum ContentType { Unknown = 0, Product = 1, Episode = 2 }
public class Book
{
// implementation detail. set by db only. only used by data layer
@@ -25,6 +29,7 @@ namespace DataLayer
public string Title { get; private set; }
public string Description { get; private set; }
public int LengthInMinutes { get; private set; }
public ContentType ContentType { get; private set; }
// immutable-ish. should be immutable. mutability is necessary for v3 => v4 upgrades
public string Locale { get; private set; }
@@ -82,6 +87,7 @@ namespace DataLayer
string title,
string description,
int lengthInMinutes,
ContentType contentType,
IEnumerable<Contributor> authors,
IEnumerable<Contributor> narrators,
Category category, string localeName)
@@ -109,6 +115,7 @@ namespace DataLayer
Title = title.Trim();
Description = description.Trim();
LengthInMinutes = lengthInMinutes;
ContentType = contentType;
// assigns with biz logic
ReplaceAuthors(authors);

View File

@@ -14,7 +14,10 @@ namespace DataLayer
NotLiberated = 0,
Liberated = 1,
/// <summary>Error occurred during liberation. Don't retry</summary>
Error = 2
Error = 2,
/// <summary>Application-state only. Not a valid persistence state.</summary>
PartialDownload = 0x1000
}
public class UserDefinedItem
@@ -38,7 +41,15 @@ namespace DataLayer
public string Tags
{
get => _tags;
set => _tags = sanitize(value);
set
{
var newTags = sanitize(value);
if (_tags != newTags)
{
_tags = newTags;
ItemChanged?.Invoke(this, nameof(Tags));
}
}
}
public IEnumerable<string> TagsEnumerated => Tags == "" ? new string[0] : Tags.Split(null as char[], StringSplitOptions.RemoveEmptyEntries);
@@ -95,10 +106,39 @@ namespace DataLayer
#endregion
#region LiberatedStatuses
public LiberatedStatus BookStatus { get; set; }
public LiberatedStatus? PdfStatus { get; set; }
#endregion
private LiberatedStatus _bookStatus;
private LiberatedStatus? _pdfStatus;
public LiberatedStatus BookStatus
{
get => _bookStatus;
set
{
if (_bookStatus != value)
{
_bookStatus = value;
ItemChanged?.Invoke(this, nameof(BookStatus));
}
}
}
public LiberatedStatus? PdfStatus
{
get => _pdfStatus;
set
{
if (_pdfStatus != value)
{
_pdfStatus = value;
ItemChanged?.Invoke(this, nameof(PdfStatus));
}
}
}
#endregion
/// <summary>
/// Occurs when <see cref="Tags"/>, <see cref="BookStatus"/>, or <see cref="PdfStatus"/> values change.
/// This signals the change of the in-memory value; it does not ensure that the new value has been persisted.
/// </summary>
public static event EventHandler<string> ItemChanged;
public override string ToString() => $"{Book} {Rating} {Tags}";
}
}

View File

@@ -19,7 +19,7 @@ namespace DataLayer
// // overwrite collection
// Entry(product).Collection(x => x.Narrators).Load();
// product.Narrators = narrators;
public DbSet<LibraryBook> Library { get; private set; }
public DbSet<LibraryBook> LibraryBooks { get; private set; }
public DbSet<Book> Books { get; private set; }
public DbSet<Contributor> Contributors { get; private set; }
public DbSet<Series> Series { get; private set; }

View File

@@ -0,0 +1,390 @@
// <auto-generated />
using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
[Migration("20210901205042_BookIsEpisode")]
partial class BookIsEpisode
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.9");
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleProductId")
.HasColumnType("TEXT");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER");
b.Property<int>("ContentType")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DatePublished")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<bool>("IsAbridged")
.HasColumnType("INTEGER");
b.Property<int>("LengthInMinutes")
.HasColumnType("INTEGER");
b.Property<string>("Locale")
.HasColumnType("TEXT");
b.Property<string>("PictureId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.HasIndex("CategoryId");
b.ToTable("Books");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<int>("ContributorId")
.HasColumnType("INTEGER");
b.Property<int>("Role")
.HasColumnType("INTEGER");
b.Property<byte>("Order")
.HasColumnType("INTEGER");
b.HasKey("BookId", "ContributorId", "Role");
b.HasIndex("BookId");
b.HasIndex("ContributorId");
b.ToTable("BookContributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleCategoryId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int?>("ParentCategoryCategoryId")
.HasColumnType("INTEGER");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.HasIndex("ParentCategoryCategoryId");
b.ToTable("Categories");
b.HasData(
new
{
CategoryId = -1,
AudibleCategoryId = "",
Name = ""
});
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleContributorId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("ContributorId");
b.HasIndex("Name");
b.ToTable("Contributors");
b.HasData(
new
{
ContributorId = -1,
Name = ""
});
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<string>("Account")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
b.HasKey("BookId");
b.ToTable("Library");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleSeriesId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("SeriesId");
b.HasIndex("AudibleSeriesId");
b.ToTable("Series");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<float?>("Index")
.HasColumnType("REAL");
b.HasKey("SeriesId", "BookId");
b.HasIndex("BookId");
b.HasIndex("SeriesId");
b.ToTable("SeriesBook");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.HasOne("DataLayer.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<float>("OverallRating")
.HasColumnType("REAL");
b1.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b1.Property<float>("StoryRating")
.HasColumnType("REAL");
b1.HasKey("BookId");
b1.ToTable("Books");
b1.WithOwner()
.HasForeignKey("BookId");
});
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<string>("Url")
.HasColumnType("TEXT");
b1.HasKey("SupplementId");
b1.HasIndex("BookId");
b1.ToTable("Supplement");
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.Navigation("Book");
});
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<int>("BookStatus")
.HasColumnType("INTEGER");
b1.Property<int?>("PdfStatus")
.HasColumnType("INTEGER");
b1.Property<string>("Tags")
.HasColumnType("TEXT");
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem");
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId")
.HasColumnType("INTEGER");
b2.Property<float>("OverallRating")
.HasColumnType("REAL");
b2.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b2.Property<float>("StoryRating")
.HasColumnType("REAL");
b2.HasKey("UserDefinedItemBookId");
b2.ToTable("UserDefinedItem");
b2.WithOwner()
.HasForeignKey("UserDefinedItemBookId");
});
b1.Navigation("Book");
b1.Navigation("Rating");
});
b.Navigation("Category");
b.Navigation("Rating");
b.Navigation("Supplements");
b.Navigation("UserDefinedItem");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("ContributorsLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Contributor", "Contributor")
.WithMany("BooksLink")
.HasForeignKey("ContributorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("Contributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.HasOne("DataLayer.Category", "ParentCategory")
.WithMany()
.HasForeignKey("ParentCategoryCategoryId");
b.Navigation("ParentCategory");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithOne()
.HasForeignKey("DataLayer.LibraryBook", "BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("SeriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Series", "Series")
.WithMany("BooksLink")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("Series");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Navigation("ContributorsLink");
b.Navigation("SeriesLink");
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Navigation("BooksLink");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Navigation("BooksLink");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace DataLayer.Migrations
{
public partial class BookIsEpisode : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "ContentType",
table: "Books",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ContentType",
table: "Books");
}
}
}

View File

@@ -0,0 +1,390 @@
// <auto-generated />
using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
[Migration("20210902192153_RenameLibraryBooks")]
partial class RenameLibraryBooks
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.9");
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleProductId")
.HasColumnType("TEXT");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER");
b.Property<int>("ContentType")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DatePublished")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<bool>("IsAbridged")
.HasColumnType("INTEGER");
b.Property<int>("LengthInMinutes")
.HasColumnType("INTEGER");
b.Property<string>("Locale")
.HasColumnType("TEXT");
b.Property<string>("PictureId")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.HasIndex("CategoryId");
b.ToTable("Books");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<int>("ContributorId")
.HasColumnType("INTEGER");
b.Property<int>("Role")
.HasColumnType("INTEGER");
b.Property<byte>("Order")
.HasColumnType("INTEGER");
b.HasKey("BookId", "ContributorId", "Role");
b.HasIndex("BookId");
b.HasIndex("ContributorId");
b.ToTable("BookContributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleCategoryId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int?>("ParentCategoryCategoryId")
.HasColumnType("INTEGER");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.HasIndex("ParentCategoryCategoryId");
b.ToTable("Categories");
b.HasData(
new
{
CategoryId = -1,
AudibleCategoryId = "",
Name = ""
});
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleContributorId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("ContributorId");
b.HasIndex("Name");
b.ToTable("Contributors");
b.HasData(
new
{
ContributorId = -1,
Name = ""
});
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<string>("Account")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
b.HasKey("BookId");
b.ToTable("LibraryBooks");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleSeriesId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("SeriesId");
b.HasIndex("AudibleSeriesId");
b.ToTable("Series");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<float?>("Index")
.HasColumnType("REAL");
b.HasKey("SeriesId", "BookId");
b.HasIndex("BookId");
b.HasIndex("SeriesId");
b.ToTable("SeriesBook");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.HasOne("DataLayer.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<float>("OverallRating")
.HasColumnType("REAL");
b1.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b1.Property<float>("StoryRating")
.HasColumnType("REAL");
b1.HasKey("BookId");
b1.ToTable("Books");
b1.WithOwner()
.HasForeignKey("BookId");
});
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<string>("Url")
.HasColumnType("TEXT");
b1.HasKey("SupplementId");
b1.HasIndex("BookId");
b1.ToTable("Supplement");
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.Navigation("Book");
});
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<int>("BookStatus")
.HasColumnType("INTEGER");
b1.Property<int?>("PdfStatus")
.HasColumnType("INTEGER");
b1.Property<string>("Tags")
.HasColumnType("TEXT");
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem");
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId")
.HasColumnType("INTEGER");
b2.Property<float>("OverallRating")
.HasColumnType("REAL");
b2.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b2.Property<float>("StoryRating")
.HasColumnType("REAL");
b2.HasKey("UserDefinedItemBookId");
b2.ToTable("UserDefinedItem");
b2.WithOwner()
.HasForeignKey("UserDefinedItemBookId");
});
b1.Navigation("Book");
b1.Navigation("Rating");
});
b.Navigation("Category");
b.Navigation("Rating");
b.Navigation("Supplements");
b.Navigation("UserDefinedItem");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("ContributorsLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Contributor", "Contributor")
.WithMany("BooksLink")
.HasForeignKey("ContributorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("Contributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.HasOne("DataLayer.Category", "ParentCategory")
.WithMany()
.HasForeignKey("ParentCategoryCategoryId");
b.Navigation("ParentCategory");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithOne()
.HasForeignKey("DataLayer.LibraryBook", "BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("SeriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Series", "Series")
.WithMany("BooksLink")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("Series");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Navigation("ContributorsLink");
b.Navigation("SeriesLink");
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Navigation("BooksLink");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Navigation("BooksLink");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,63 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace DataLayer.Migrations
{
public partial class RenameLibraryBooks : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Library_Books_BookId",
table: "Library");
migrationBuilder.DropPrimaryKey(
name: "PK_Library",
table: "Library");
migrationBuilder.RenameTable(
name: "Library",
newName: "LibraryBooks");
migrationBuilder.AddPrimaryKey(
name: "PK_LibraryBooks",
table: "LibraryBooks",
column: "BookId");
migrationBuilder.AddForeignKey(
name: "FK_LibraryBooks_Books_BookId",
table: "LibraryBooks",
column: "BookId",
principalTable: "Books",
principalColumn: "BookId",
onDelete: ReferentialAction.Cascade);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_LibraryBooks_Books_BookId",
table: "LibraryBooks");
migrationBuilder.DropPrimaryKey(
name: "PK_LibraryBooks",
table: "LibraryBooks");
migrationBuilder.RenameTable(
name: "LibraryBooks",
newName: "Library");
migrationBuilder.AddPrimaryKey(
name: "PK_Library",
table: "Library",
column: "BookId");
migrationBuilder.AddForeignKey(
name: "FK_Library_Books_BookId",
table: "Library",
column: "BookId",
principalTable: "Books",
principalColumn: "BookId",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@@ -14,7 +14,7 @@ namespace DataLayer.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.5");
.HasAnnotation("ProductVersion", "5.0.9");
modelBuilder.Entity("DataLayer.Book", b =>
{
@@ -28,6 +28,9 @@ namespace DataLayer.Migrations
b.Property<int>("CategoryId")
.HasColumnType("INTEGER");
b.Property<int>("ContentType")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DatePublished")
.HasColumnType("TEXT");
@@ -152,7 +155,7 @@ namespace DataLayer.Migrations
b.HasKey("BookId");
b.ToTable("Library");
b.ToTable("LibraryBooks");
});
modelBuilder.Entity("DataLayer.Series", b =>

View File

@@ -6,7 +6,7 @@ namespace DataLayer
{
// only library importing should use tracking. All else should be NoTracking.
// only library importing should directly query Book. All else should use LibraryBook
public static class LibraryQueries
public static class LibraryBookQueries
{
//// tracking is a bad idea for main grid. it prevents anything else from updating entities unless getting them from the grid
//public static List<LibraryBook> GetLibrary_Flat_WithTracking(this LibationContext context)
@@ -17,14 +17,14 @@ namespace DataLayer
public static List<LibraryBook> GetLibrary_Flat_NoTracking(this LibationContext context)
=> context
.Library
.LibraryBooks
.AsNoTrackingWithIdentityResolution()
.GetLibrary()
.ToList();
public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId)
=> context
.Library
.LibraryBooks
.AsNoTrackingWithIdentityResolution()
.GetLibraryBook(productId);

View File

@@ -1,57 +0,0 @@
FOR QUICK MIGRATION INSTRUCTIONS:
_DB_NOTES.txt
HOW TO CREATE: EF CORE PROJECT
==============================
example is for sqlite but the same works with MsSql
nuget
Microsoft.EntityFrameworkCore.Tools (needed for using Package Manager Console)
Microsoft.EntityFrameworkCore.Sqlite
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.1;netstandard2.1
run. error
SQLite Error 1: 'no such table: Blogs'.
set project "Set as StartUp Project"
Tools >> Nuget Package Manager >> Package Manager Console
default project: Examples\SQLite_NETCore2_0
PM> add-migration InitialCreate
PM> Update-Database
if add-migration xyz throws and error, don't take the error msg at face value. try again with add-migration xyz -verbose
new sqlite .db file created: Copy always/Copy if newer
or copy .db file to destination
relative:
optionsBuilder.UseSqlite("Data Source=blogging.db");
absolute (use fwd slashes):
optionsBuilder.UseSqlite("Data Source=C:/foo/bar/blogging.db");
REFERENCE ARTICLES
------------------
https://docs.microsoft.com/en-us/ef/core/get-started/netcore/new-db-sqlite
https://carlos.mendible.com/2016/07/11/step-by-step-dotnet-core-and-entity-framework-core/
https://www.benday.com/2017/12/19/ef-core-2-0-migrations-without-hard-coded-connection-strings/

View File

@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApiDTOs;
using AudibleApi.Common;
using DataLayer;
using InternalUtilities;
@@ -67,6 +67,8 @@ namespace DtoImporterService
{
var item = importItem.DtoItem;
var contentType = item.IsEpisodes ? DataLayer.ContentType.Episode : DataLayer.ContentType.Product;
// absence of authors is very rare, but possible
if (!item.Authors?.Any() ?? true)
item.Authors = new[] { new Person { Name = "", Asin = null } };
@@ -105,6 +107,7 @@ namespace DtoImporterService
item.TitleWithSubtitle,
item.Description,
item.LengthInMinutes,
contentType,
authors,
narrators,
category,
@@ -120,8 +123,8 @@ namespace DtoImporterService
book.UpdateBookDetails(item.IsAbridged, item.DatePublished);
if (!string.IsNullOrWhiteSpace(item.SupplementUrl))
book.AddSupplementDownloadUrl(item.SupplementUrl);
if (item.PdfUrl is not null)
book.AddSupplementDownloadUrl(item.PdfUrl.ToString());
return book;
}

View File

@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApiDTOs;
using AudibleApi.Common;
using DataLayer;
using InternalUtilities;

View File

@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApiDTOs;
using AudibleApi.Common;
using DataLayer;
using InternalUtilities;

View File

@@ -5,7 +5,6 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApiDTOs\AudibleApiDTOs.csproj" />
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
</ItemGroup>

View File

@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApiDTOs;
using AudibleApi.Common;
using DataLayer;
using InternalUtilities;
@@ -34,7 +34,7 @@ namespace DtoImporterService
//
// CURRENT SOLUTION: don't re-insert
var currentLibraryProductIds = DbContext.Library.Select(l => l.Book.AudibleProductId).ToList();
var currentLibraryProductIds = DbContext.LibraryBooks.Select(l => l.Book.AudibleProductId).ToList();
var newItems = importItems.Where(dto => !currentLibraryProductIds.Contains(dto.DtoItem.ProductId)).ToList();
foreach (var newItem in newItems)
@@ -43,11 +43,11 @@ namespace DtoImporterService
DbContext.Books.Local.Single(b => b.AudibleProductId == newItem.DtoItem.ProductId),
newItem.DtoItem.DateAdded,
newItem.AccountId);
DbContext.Library.Add(libraryBook);
DbContext.LibraryBooks.Add(libraryBook);
}
// needed for v3 => v4 upgrade
var toUpdate = DbContext.Library.Where(l => l.Account == null);
var toUpdate = DbContext.LibraryBooks.Where(l => l.Account == null);
foreach (var u in toUpdate)
{
var item = importItems.FirstOrDefault(ii => ii.DtoItem.ProductId == u.Book.AudibleProductId);

View File

@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApiDTOs;
using AudibleApi.Common;
using DataLayer;
using InternalUtilities;
@@ -29,7 +29,7 @@ namespace DtoImporterService
return qtyNew;
}
private void loadLocal_series(List<AudibleApiDTOs.Series> series)
private void loadLocal_series(List<AudibleApi.Common.Series> series)
{
var seriesIds = series.Select(s => s.SeriesId).ToList();
var localIds = DbContext.Series.Local.Select(s => s.AudibleSeriesId).ToList();
@@ -42,7 +42,7 @@ namespace DtoImporterService
DbContext.Series.Where(s => remainingSeriesIds.Contains(s.AudibleSeriesId)).ToList();
}
private int upsertSeries(List<AudibleApiDTOs.Series> requestedSeries)
private int upsertSeries(List<AudibleApi.Common.Series> requestedSeries)
{
var qtyNew = 0;

View File

@@ -16,22 +16,22 @@ namespace FileLiberator
public class DownloadDecryptBook : IAudioDecodable
{
private AaxcDownloadConverter aaxcDownloader;
private AaxcDownloadConverter aaxcDownloader;
public event EventHandler<TimeSpan> StreamingTimeRemaining;
public event EventHandler<Action<byte[]>> RequestCoverArt;
public event EventHandler<string> TitleDiscovered;
public event EventHandler<string> AuthorsDiscovered;
public event EventHandler<string> NarratorsDiscovered;
public event EventHandler<byte[]> CoverImageDiscovered;
public event EventHandler<string> StreamingBegin;
public event EventHandler<DownloadProgress> StreamingProgressChanged;
public event EventHandler<string> StreamingCompleted;
public event EventHandler<LibraryBook> Begin;
public event EventHandler<string> StatusUpdate;
public event EventHandler<LibraryBook> Completed;
public event EventHandler<TimeSpan> StreamingTimeRemaining;
public event EventHandler<Action<byte[]>> RequestCoverArt;
public event EventHandler<string> TitleDiscovered;
public event EventHandler<string> AuthorsDiscovered;
public event EventHandler<string> NarratorsDiscovered;
public event EventHandler<byte[]> CoverImageDiscovered;
public event EventHandler<string> StreamingBegin;
public event EventHandler<DownloadProgress> StreamingProgressChanged;
public event EventHandler<string> StreamingCompleted;
public event EventHandler<LibraryBook> Begin;
public event EventHandler<string> StatusUpdate;
public event EventHandler<LibraryBook> Completed;
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
public async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{
Begin?.Invoke(this, libraryBook);
@@ -47,13 +47,12 @@ namespace FileLiberator
return new StatusHandler { "Decrypt failed" };
// moves files and returns dest dir
_ = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
var moveResults = MoveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
if (!libraryBook.Book.Audio_Exists)
if (!moveResults.movedAudioFile)
return new StatusHandler { "Cannot find final audio file after decryption" };
// only need to update if success. if failure, it will remain at 0 == NotLiberated
ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Liberated);
libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
return new StatusHandler();
}
@@ -126,7 +125,7 @@ namespace FileLiberator
}
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
private void AaxcDownloader_RetrievedCoverArt(object sender, byte[] e)
{
if (e is null && Configuration.Instance.AllowLibationFixup)
{
@@ -143,10 +142,10 @@ namespace FileLiberator
{
TitleDiscovered?.Invoke(this, e.TitleSansUnabridged);
AuthorsDiscovered?.Invoke(this, e.FirstAuthor ?? "[unknown]");
NarratorsDiscovered?.Invoke(this, e.Narrator ?? "[unknown]");
NarratorsDiscovered?.Invoke(this, e.Narrator ?? "[unknown]");
}
private static string moveFilesToBooksDir(Book product, string outputAudioFilename)
private static (string destinationDir, bool movedAudioFile) MoveFilesToBooksDir(Book product, string outputAudioFilename)
{
// create final directory. move each file into it. MOVE AUDIO FILE LAST
// new dir: safetitle_limit50char + " [" + productId + "]"
@@ -161,6 +160,7 @@ namespace FileLiberator
// audio filename: safetitle_limit50char + " [" + productId + "]." + audio_ext
var audioFileName = FileUtility.GetValidFilename(destinationDir, product.Title, musicFileExt, product.AudibleProductId);
bool movedAudioFile = false;
foreach (var f in sortedFiles)
{
var dest
@@ -173,11 +173,13 @@ namespace FileLiberator
Cue.UpdateFileName(f, audioFileName);
File.Move(f.FullName, dest);
movedAudioFile |= AudibleFileStorage.Audio.IsFileTypeMatch(f);
}
AudibleFileStorage.Audio.Refresh();
return destinationDir;
return (destinationDir, movedAudioFile);
}
private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename)
@@ -200,26 +202,26 @@ namespace FileLiberator
}
private static void validate(LibraryBook libraryBook)
{
string errorString(string field)
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
{
string errorString(string field)
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
string errorTitle()
{
var title
= (libraryBook.Book.Title.Length > 53)
? $"{libraryBook.Book.Title.Truncate(50)}..."
: libraryBook.Book.Title;
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
return errorBookTitle;
};
string errorTitle()
{
var title
= (libraryBook.Book.Title.Length > 53)
? $"{libraryBook.Book.Title.Truncate(50)}..."
: libraryBook.Book.Title;
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
return errorBookTitle;
};
if (string.IsNullOrWhiteSpace(libraryBook.Account))
throw new Exception(errorString("Account"));
if (string.IsNullOrWhiteSpace(libraryBook.Account))
throw new Exception(errorString("Account"));
if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale))
throw new Exception(errorString("Locale"));
}
if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale))
throw new Exception(errorString("Locale"));
}
public bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists;

View File

@@ -20,11 +20,10 @@ namespace FileLiberator
public override async Task<StatusHandler> ProcessItemAsync(LibraryBook libraryBook)
{
var proposedDownloadFilePath = getProposedDownloadFilePath(libraryBook);
await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
var result = verifyDownload(libraryBook);
var actualDownloadedFilePath = await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
var result = verifyDownload(actualDownloadedFilePath);
var liberatedStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
ApplicationServices.LibraryCommands.UpdatePdf(libraryBook, liberatedStatus);
libraryBook.Book.UserDefinedItem.PdfStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
return result;
}
@@ -49,7 +48,7 @@ namespace FileLiberator
private static string getdownloadUrl(LibraryBook libraryBook)
=> libraryBook?.Book?.Supplements?.FirstOrDefault()?.Url;
private async Task downloadPdfAsync(LibraryBook libraryBook, string proposedDownloadFilePath)
private async Task<string> downloadPdfAsync(LibraryBook libraryBook, string proposedDownloadFilePath)
{
var api = await GetApiAsync(libraryBook);
var downloadUrl = await api.GetPdfDownloadLinkAsync(libraryBook.Book.AudibleProductId);
@@ -58,10 +57,12 @@ namespace FileLiberator
var actualDownloadedFilePath = await PerformDownloadAsync(
proposedDownloadFilePath,
(p) => client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, p));
return actualDownloadedFilePath;
}
private static StatusHandler verifyDownload(LibraryBook libraryBook)
=> !libraryBook.Book.PDF_Exists
private static StatusHandler verifyDownload(string actualDownloadedFilePath)
=> !File.Exists(actualDownloadedFilePath)
? new StatusHandler { "Downloaded PDF cannot be found" }
: new StatusHandler();
}

View File

@@ -5,9 +5,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
<ProjectReference Include="..\AaxDecrypter\AaxDecrypter.csproj" />
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
<ProjectReference Include="..\FileManager\FileManager.csproj" />
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />

View File

@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ApplicationServices;
using DataLayer;
using Dinah.Core;
using Dinah.Core.ErrorHandling;
@@ -18,10 +17,8 @@ namespace FileLiberator
// when used in foreach: stateful. deferred execution
public static IEnumerable<LibraryBook> GetValidLibraryBooks(this IProcessable processable)
=> DbContexts.GetContext()
.GetLibrary_Flat_NoTracking()
.Where(libraryBook => processable.Validate(libraryBook));
public static IEnumerable<LibraryBook> GetValidLibraryBooks(this IProcessable processable, IEnumerable<LibraryBook> library)
=> library.Where(libraryBook => processable.Validate(libraryBook));
public static async Task<StatusHandler> ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook, bool validate)
{

View File

@@ -5,13 +5,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.Core" Version="1.0.5.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
<PackageReference Include="Octokit" Version="0.50.0" />
<PackageReference Include="Polly" Version="7.2.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AudibleApi;
using AudibleApiDTOs;
using AudibleApi.Common;
using Dinah.Core;
using Polly;
using Polly.Retry;
@@ -59,25 +59,12 @@ namespace InternalUtilities
private static async Task<List<Item>> getItemsAsync(Api api, LibraryOptions.ResponseGroupOptions responseGroups)
{
var items = await api.GetAllLibraryItemsAsync(responseGroups);
#if DEBUG
//var itemsDebug = items.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
//System.IO.File.WriteAllText("library.json", itemsDebug);
#endif
// remove episode parents
items.RemoveAll(i => i.IsEpisodes);
#region // episode handling. doesn't quite work
// // add individual/children episodes
// var childIds = items
// .Where(i => i.Episodes)
// .SelectMany(ep => ep.Relationships)
// .Where(r => r.RelationshipToProduct == AudibleApiDTOs.RelationshipToProduct.Child && r.RelationshipType == AudibleApiDTOs.RelationshipType.Episode)
// .Select(c => c.Asin)
// .ToList();
// foreach (var childId in childIds)
// {
// var bookResult = await api.GetLibraryBookAsync(childId, AudibleApi.LibraryOptions.ResponseGroupOptions.ALL_OPTIONS);
// var bookItem = AudibleApiDTOs.LibraryDtoV10.FromJson(bookResult.ToString()).Item;
// items.Add(bookItem);
// }
#endregion
await manageEpisodesAsync(api, items);
var validators = new List<IValidator>();
validators.AddRange(getValidators());
@@ -91,6 +78,143 @@ namespace InternalUtilities
return items;
}
#region episodes and podcasts
private static async Task manageEpisodesAsync(Api api, List<Item> items)
{
// add podcasts and episodes to list. If fail, don't let it de-rail the rest of the import
try
{
// get parents
var parents = items.Where(i => i.IsEpisodes).ToList();
#if DEBUG
//var parentsDebug = parents.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
//System.IO.File.WriteAllText("parents.json", parentsDebug);
#endif
if (!parents.Any())
return;
Serilog.Log.Logger.Information($"{parents.Count} series of shows/podcasts found");
// remove episode parents. even if the following stuff fails, these will still be removed from the collection.
// also must happen before processing children because children abuses this flag
items.RemoveAll(i => i.IsEpisodes);
// add children
var children = await getEpisodesAsync(api, parents);
Serilog.Log.Logger.Information($"{children.Count} episodes of shows/podcasts found");
items.AddRange(children);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error adding podcasts and episodes");
}
}
private static async Task<List<Item>> getEpisodesAsync(Api api, List<Item> parents)
{
var results = new List<Item>();
foreach (var parent in parents)
{
var children = await getEpisodeChildrenAsync(api, parent);
foreach (var child in children)
{
// use parent's 'DateAdded'. DateAdded is just a convenience prop for: PurchaseDate.UtcDateTime
child.PurchaseDate = parent.PurchaseDate;
// parent is essentially a series
child.Series = new Series[]
{
new Series
{
Asin = parent.Asin,
Sequence = parent.Relationships.Single(r => r.Asin == child.Asin).Sort.ToString(),
Title = parent.TitleWithSubtitle
}
};
// overload (read: abuse) IsEpisodes flag
child.Relationships = new Relationship[]
{
new Relationship
{
RelationshipToProduct = RelationshipToProduct.Child,
RelationshipType = RelationshipType.Episode
}
};
}
results.AddRange(children);
}
return results;
}
private static async Task<List<Item>> getEpisodeChildrenAsync(Api api, Item parent)
{
var childrenIds = parent.Relationships
.Where(r => r.RelationshipToProduct == RelationshipToProduct.Child && r.RelationshipType == RelationshipType.Episode)
.Select(r => r.Asin)
.ToList();
// fetch children in batches
const int batchSize = 20;
var results = new List<Item>();
for (var i = 1; ; i++)
{
var idBatch = childrenIds.Skip((i - 1) * batchSize).Take(batchSize).ToList();
if (!idBatch.Any())
break;
List<Item> childrenBatch;
try
{
childrenBatch = await api.GetCatalogProductsAsync(idBatch, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS);
#if DEBUG
//var childrenBatchDebug = childrenBatch.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
//System.IO.File.WriteAllText($"children of {parent.Asin}.json", childrenBatchDebug);
#endif
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error fetching batch of episodes. {@DebugInfo}", new
{
ParentId = parent.Asin,
ParentTitle = parent.Title,
BatchNumber = i,
ChildIdBatch = idBatch
});
throw;
}
Serilog.Log.Logger.Debug($"Batch {i}: {childrenBatch.Count} results");
// the service returned no results. probably indicates an error. stop running batches
if (!childrenBatch.Any())
break;
results.AddRange(childrenBatch);
}
Serilog.Log.Logger.Debug("Parent episodes/podcasts series. Children found. {@DebugInfo}", new
{
ParentId = parent.Asin,
ParentTitle = parent.Title,
ChildCount = childrenIds.Count
});
if (childrenIds.Count != results.Count)
{
var ex = new ApplicationException($"Mis-match: Children defined by parent={childrenIds.Count}. Children returned by batches={results.Count}");
Serilog.Log.Logger.Error(ex, "Quantity of series episodes defined by parent does not match quantity returned by batch fetching.");
throw ex;
}
return results;
}
#endregion
private static List<IValidator> getValidators()
{
var type = typeof(IValidator);

View File

@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AudibleApiDTOs;
using AudibleApi.Common;
namespace InternalUtilities
{
@@ -76,9 +76,9 @@ namespace InternalUtilities
var distinct = items.GetSeriesDistinct();
if (distinct.Any(s => s.SeriesId is null))
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(AudibleApiDTOs.Series.SeriesId)}", nameof(items)));
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(AudibleApi.Common.Series.SeriesId)}", nameof(items)));
if (distinct.Any(s => s.SeriesName is null))
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(AudibleApiDTOs.Series.SeriesName)}", nameof(items)));
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(AudibleApi.Common.Series.SeriesName)}", nameof(items)));
return exceptions;
}

View File

@@ -1,5 +1,5 @@
using System;
using AudibleApiDTOs;
using AudibleApi.Common;
namespace InternalUtilities
{

View File

@@ -5,7 +5,10 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj" />
<PackageReference Include="AudibleApi" Version="1.1.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FileManager\FileManager.csproj" />
</ItemGroup>

View File

@@ -5,6 +5,7 @@ VisualStudioVersion = 16.0.28803.156
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solution Items", "{03C8835F-936C-4AF7-87AE-FF92BDBE8B9B}"
ProjectSection(SolutionItems) = preProject
__README - COLLABORATORS.txt = __README - COLLABORATORS.txt
__TODO.txt = __TODO.txt
_DB_NOTES.txt = _DB_NOTES.txt
REFERENCE.txt = REFERENCE.txt
@@ -34,59 +35,21 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3 Domain Internal Utilities
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationSearchEngine", "LibationSearchEngine\LibationSearchEngine.csproj", "{2E1F5DB4-40CC-4804-A893-5DCE0193E598}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0 Tests", "0 Tests", "{38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0 Demos and Examples", "0 Demos and Examples", "{F61184E7-2426-4A13-ACEF-5689928E2CE2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core.Demos", "..\Dinah.Core\_Demos\Dinah.Core.Demos\Dinah.Core.Demos.csproj", "{9F1AA3DE-962F-469B-82B2-46F93491389B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core.Tests", "..\Dinah.Core\_Tests\Dinah.Core.Tests\Dinah.Core.Tests.csproj", "{E874D000-AD3A-4629-AC65-7219C2C7C1F0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestCommon", "..\Dinah.Core\_Tests\TestCommon\TestCommon.csproj", "{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitAllRepos", "..\GitAllRepos\GitAllRepos\GitAllRepos.csproj", "{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApi", "..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj", "{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApi.Tests", "..\audible api\AudibleApi\_Tests\AudibleApi.Tests\AudibleApi.Tests.csproj", "{111420E2-D4F0-4068-B46A-C4B6DCC823DC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationWinForms", "LibationWinForms\LibationWinForms.csproj", "{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinFormsDesigner", "WinFormsDesigner\WinFormsDesigner.csproj", "{0807616A-A77A-4B08-A65A-1582B09E114B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core", "..\Dinah.Core\Dinah.Core\Dinah.Core.csproj", "{9E951521-2587-4FC6-AD26-FAA9179FB6C4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.EntityFrameworkCore", "..\Dinah.Core\Dinah.EntityFrameworkCore\Dinah.EntityFrameworkCore.csproj", "{1255D9BA-CE6E-42E4-A253-6376540B9661}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LuceneNet303r2", "..\LuceneNet303r2\LuceneNet303r2\LuceneNet303r2.csproj", "{35803735-B669-4090-9681-CC7F7FABDC71}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LuceneNet303r2.Tests", "..\LuceneNet303r2\LuceneNet303r2.Tests\LuceneNet303r2.Tests.csproj", "{5A7681A5-60D9-480B-9AC7-63E0812A2548}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DtoImporterService", "DtoImporterService\DtoImporterService.csproj", "{401865F5-1942-4713-B230-04544C0A97B0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApiDTOs", "..\audible api\AudibleApi\AudibleApiDTOs\AudibleApiDTOs.csproj", "{C03C5D65-3B7F-453B-972F-23950B7E0604}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApiDTOs.Tests", "..\audible api\AudibleApi\_Tests\AudibleApiDTOs.Tests\AudibleApiDTOs.Tests.csproj", "{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AudibleApiClientExample", "..\audible api\AudibleApi\_Demos\AudibleApiClientExample\AudibleApiClientExample.csproj", "{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplicationServices", "ApplicationServices\ApplicationServices.csproj", "{B95650EA-25F0-449E-BA5D-99126BC5D730}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.Core.WindowsDesktop", "..\Dinah.Core\Dinah.Core.WindowsDesktop\Dinah.Core.WindowsDesktop.csproj", "{059CE32C-9AD6-45E9-A166-790DFFB0B730}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationLauncher", "LibationLauncher\LibationLauncher.csproj", "{F3B04A3A-20C8-4582-A54A-715AF6A5D859}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0 Libation Tests", "0 Libation Tests", "{67E66E82-5532-4440-AFB3-9FB1DF9DEF53}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Tests", "_Tests", "{67E66E82-5532-4440-AFB3-9FB1DF9DEF53}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalUtilities.Tests", "_Tests\InternalUtilities.Tests\InternalUtilities.Tests.csproj", "{8447C956-B03E-4F59-9DD4-877793B849D9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationSearchEngine.Tests", "_Tests\LibationSearchEngine.Tests\LibationSearchEngine.Tests.csproj", "{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dinah.EntityFrameworkCore.Tests", "..\Dinah.Core\_Tests\Dinah.EntityFrameworkCore.Tests\Dinah.EntityFrameworkCore.Tests.csproj", "{6F5131A0-09AE-4707-B82B-5E53CB74688E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AAXClean", "..\AAXClean\AAXClean.csproj", "{94BEB7CC-511D-45AB-9F09-09BE858EE486}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hoopla", "Hoopla\Hoopla.csproj", "{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hoopla", "Hoopla\Hoopla.csproj", "{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -118,78 +81,18 @@ Global
{2E1F5DB4-40CC-4804-A893-5DCE0193E598}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E1F5DB4-40CC-4804-A893-5DCE0193E598}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E1F5DB4-40CC-4804-A893-5DCE0193E598}.Release|Any CPU.Build.0 = Release|Any CPU
{9F1AA3DE-962F-469B-82B2-46F93491389B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9F1AA3DE-962F-469B-82B2-46F93491389B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9F1AA3DE-962F-469B-82B2-46F93491389B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9F1AA3DE-962F-469B-82B2-46F93491389B}.Release|Any CPU.Build.0 = Release|Any CPU
{E874D000-AD3A-4629-AC65-7219C2C7C1F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E874D000-AD3A-4629-AC65-7219C2C7C1F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E874D000-AD3A-4629-AC65-7219C2C7C1F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E874D000-AD3A-4629-AC65-7219C2C7C1F0}.Release|Any CPU.Build.0 = Release|Any CPU
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994}.Release|Any CPU.Build.0 = Release|Any CPU
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4}.Release|Any CPU.Build.0 = Release|Any CPU
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60}.Release|Any CPU.Build.0 = Release|Any CPU
{111420E2-D4F0-4068-B46A-C4B6DCC823DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{111420E2-D4F0-4068-B46A-C4B6DCC823DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{111420E2-D4F0-4068-B46A-C4B6DCC823DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{111420E2-D4F0-4068-B46A-C4B6DCC823DC}.Release|Any CPU.Build.0 = Release|Any CPU
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6}.Release|Any CPU.Build.0 = Release|Any CPU
{0807616A-A77A-4B08-A65A-1582B09E114B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0807616A-A77A-4B08-A65A-1582B09E114B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0807616A-A77A-4B08-A65A-1582B09E114B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0807616A-A77A-4B08-A65A-1582B09E114B}.Release|Any CPU.Build.0 = Release|Any CPU
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E951521-2587-4FC6-AD26-FAA9179FB6C4}.Release|Any CPU.Build.0 = Release|Any CPU
{1255D9BA-CE6E-42E4-A253-6376540B9661}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1255D9BA-CE6E-42E4-A253-6376540B9661}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1255D9BA-CE6E-42E4-A253-6376540B9661}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1255D9BA-CE6E-42E4-A253-6376540B9661}.Release|Any CPU.Build.0 = Release|Any CPU
{35803735-B669-4090-9681-CC7F7FABDC71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{35803735-B669-4090-9681-CC7F7FABDC71}.Debug|Any CPU.Build.0 = Debug|Any CPU
{35803735-B669-4090-9681-CC7F7FABDC71}.Release|Any CPU.ActiveCfg = Release|Any CPU
{35803735-B669-4090-9681-CC7F7FABDC71}.Release|Any CPU.Build.0 = Release|Any CPU
{5A7681A5-60D9-480B-9AC7-63E0812A2548}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5A7681A5-60D9-480B-9AC7-63E0812A2548}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A7681A5-60D9-480B-9AC7-63E0812A2548}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5A7681A5-60D9-480B-9AC7-63E0812A2548}.Release|Any CPU.Build.0 = Release|Any CPU
{401865F5-1942-4713-B230-04544C0A97B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{401865F5-1942-4713-B230-04544C0A97B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{401865F5-1942-4713-B230-04544C0A97B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{401865F5-1942-4713-B230-04544C0A97B0}.Release|Any CPU.Build.0 = Release|Any CPU
{C03C5D65-3B7F-453B-972F-23950B7E0604}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C03C5D65-3B7F-453B-972F-23950B7E0604}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C03C5D65-3B7F-453B-972F-23950B7E0604}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C03C5D65-3B7F-453B-972F-23950B7E0604}.Release|Any CPU.Build.0 = Release|Any CPU
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD}.Release|Any CPU.Build.0 = Release|Any CPU
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6}.Release|Any CPU.Build.0 = Release|Any CPU
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B95650EA-25F0-449E-BA5D-99126BC5D730}.Release|Any CPU.Build.0 = Release|Any CPU
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Debug|Any CPU.Build.0 = Debug|Any CPU
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Release|Any CPU.ActiveCfg = Release|Any CPU
{059CE32C-9AD6-45E9-A166-790DFFB0B730}.Release|Any CPU.Build.0 = Release|Any CPU
{F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F3B04A3A-20C8-4582-A54A-715AF6A5D859}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -202,14 +105,6 @@ Global
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Release|Any CPU.Build.0 = Release|Any CPU
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F5131A0-09AE-4707-B82B-5E53CB74688E}.Release|Any CPU.Build.0 = Release|Any CPU
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Debug|Any CPU.Build.0 = Debug|Any CPU
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Release|Any CPU.ActiveCfg = Release|Any CPU
{94BEB7CC-511D-45AB-9F09-09BE858EE486}.Release|Any CPU.Build.0 = Release|Any CPU
{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -220,34 +115,17 @@ Global
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{8BD8E012-F44F-4EE2-A234-D66C14D5FE4B} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
{1AE65B61-9C05-4C80-ABFF-48F16E22FDF1} = {F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}
{59A10DF3-63EC-43F1-A3BF-4000CFA118D2} = {751093DD-5DBA-463E-ADBE-E05FAFB6983E}
{393B5B27-D15C-4F77-9457-FA14BA8F3C73} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
{06882742-27A6-4347-97D9-56162CEC9C11} = {F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}
{2E1F5DB4-40CC-4804-A893-5DCE0193E598} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
{9F1AA3DE-962F-469B-82B2-46F93491389B} = {F61184E7-2426-4A13-ACEF-5689928E2CE2}
{E874D000-AD3A-4629-AC65-7219C2C7C1F0} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
{FF12ADA0-8975-4E67-B6EA-4AC82E0C8994} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
{AD1FDDC9-8D2A-436A-8EED-91FD74E7C7B4} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
{7EA01F9C-E579-4B01-A3B9-733B49DD0B60} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
{111420E2-D4F0-4068-B46A-C4B6DCC823DC} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
{635F00E1-AAD1-45F7-BEB7-D909AD33B9F6} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{0807616A-A77A-4B08-A65A-1582B09E114B} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{9E951521-2587-4FC6-AD26-FAA9179FB6C4} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
{1255D9BA-CE6E-42E4-A253-6376540B9661} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
{35803735-B669-4090-9681-CC7F7FABDC71} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
{5A7681A5-60D9-480B-9AC7-63E0812A2548} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
{401865F5-1942-4713-B230-04544C0A97B0} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
{C03C5D65-3B7F-453B-972F-23950B7E0604} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
{6069D7F6-BEA0-4917-AFD4-4EB680CB0EDD} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
{282EEE16-F569-47E1-992F-C6DB8AEC7AA6} = {F61184E7-2426-4A13-ACEF-5689928E2CE2}
{B95650EA-25F0-449E-BA5D-99126BC5D730} = {41CDCC73-9B81-49DD-9570-C54406E852AF}
{059CE32C-9AD6-45E9-A166-790DFFB0B730} = {43E3ACB3-E0BC-4370-8DBB-E3720C8C8FD1}
{F3B04A3A-20C8-4582-A54A-715AF6A5D859} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{8447C956-B03E-4F59-9DD4-877793B849D9} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
{6F5131A0-09AE-4707-B82B-5E53CB74688E} = {38E6C6D9-963A-4C5B-89F4-F2F14885ADFD}
{94BEB7CC-511D-45AB-9F09-09BE858EE486} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
{D8F56E5A-3E65-41A6-B7E7-C4515A264B1F} = {7FBBB086-0807-4998-85BF-6D1A49C8AD05}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution

View File

@@ -13,7 +13,7 @@
<!-- <PublishSingleFile>true</PublishSingleFile> -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Version>5.5.2.1</Version>
<Version>5.6.3.1</Version>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@@ -8,7 +8,15 @@ using Newtonsoft.Json.Linq;
namespace LibationLauncher
{
/// <summary>for migrations only. directly manipulatings settings files without going through domain logic</summary>
/// <summary>
///
///
/// directly manipulates settings files without going through domain logic.
///
/// for migrations only. use with caution.
///
///
/// </summary>
internal static class UNSAFE_MigrationHelper
{
#region appsettings.json

View File

@@ -9,7 +9,10 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\LuceneNet303r2\LuceneNet303r2\LuceneNet303r2.csproj" />
<PackageReference Include="LuceneNet303r2" Version="3.0.3.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DataLayer\DataLayer.csproj" />
<ProjectReference Include="..\FileManager\FileManager.csproj" />
</ItemGroup>

View File

@@ -9,24 +9,24 @@ using Lucene.Net.Search;
namespace LibationSearchEngine
{
// field names are case specific and, due to StandardAnalyzer, content is case INspecific
public static class LuceneExtensions
internal static class LuceneExtensions
{
public static void AddRaw(this Document document, string name, string value)
internal static void AddRaw(this Document document, string name, string value)
=> document.Add(new Field(name, value, Field.Store.YES, Field.Index.NOT_ANALYZED));
public static void AddAnalyzed(this Document document, string name, string value)
internal static void AddAnalyzed(this Document document, string name, string value)
{
if (value != null)
document.Add(new Field(name.ToLowerInvariant(), value, Field.Store.YES, Field.Index.ANALYZED));
}
public static void AddNotAnalyzed(this Document document, string name, string value)
internal static void AddNotAnalyzed(this Document document, string name, string value)
=> document.Add(new Field(name.ToLowerInvariant(), value, Field.Store.YES, Field.Index.NOT_ANALYZED));
public static void AddBool(this Document document, string name, bool value)
internal static void AddBool(this Document document, string name, bool value)
=> document.Add(new Field(name.ToLowerInvariant(), value.ToString(), Field.Store.YES, Field.Index.ANALYZED_NO_NORMS));
public static Query GetQuery(this Analyzer analyzer, string defaultField, string searchString)
internal static Query GetQuery(this Analyzer analyzer, string defaultField, string searchString)
=> new QueryParser(SearchEngine.Version, defaultField.ToLowerInvariant(), analyzer).Parse(searchString);
// put all numbers, including dates, into this format:

View File

@@ -107,14 +107,14 @@ namespace LibationSearchEngine
= new ReadOnlyDictionary<string, Func<LibraryBook, bool>>(
new Dictionary<string, Func<LibraryBook, bool>>
{
["HasDownloads"] = lb => lb.Book.Supplements.Any(),
["HasDownload"] = lb => lb.Book.Supplements.Any(),
["Downloads"] = lb => lb.Book.Supplements.Any(),
["Download"] = lb => lb.Book.Supplements.Any(),
["HasPDFs"] = lb => lb.Book.Supplements.Any(),
["HasPDF"] = lb => lb.Book.Supplements.Any(),
["PDFs"] = lb => lb.Book.Supplements.Any(),
["PDF"] = lb => lb.Book.Supplements.Any(),
["HasDownloads"] = lb => lb.Book.HasPdf,
["HasDownload"] = lb => lb.Book.HasPdf,
["Downloads"] = lb => lb.Book.HasPdf,
["Download"] = lb => lb.Book.HasPdf,
["HasPDFs"] = lb => lb.Book.HasPdf,
["HasPDF"] = lb => lb.Book.HasPdf,
["PDFs"] = lb => lb.Book.HasPdf,
["PDF"] = lb => lb.Book.HasPdf,
["IsRated"] = lb => lb.Book.UserDefinedItem.Rating.OverallRating > 0f,
["Rated"] = lb => lb.Book.UserDefinedItem.Rating.OverallRating > 0f,
@@ -129,6 +129,11 @@ namespace LibationSearchEngine
["IsLiberated"] = lb => isLiberated(lb.Book),
["Liberated"] = lb => isLiberated(lb.Book),
["LiberatedError"] = lb => liberatedError(lb.Book),
["Podcast"] = lb => lb.Book.ContentType == ContentType.Episode,
["IsPodcast"] = lb => lb.Book.ContentType == ContentType.Episode,
["Episode"] = lb => lb.Book.ContentType == ContentType.Episode,
["IsEpisode"] = lb => lb.Book.ContentType == ContentType.Episode,
}
);
@@ -192,25 +197,9 @@ namespace LibationSearchEngine
#endregion
#region create and update index
/// <summary>
/// create new. ie: full re-index
/// </summary>
/// <param name="overwrite"></param>
public void CreateNewIndex(LibationContext context, bool overwrite = true)
/// <summary>create new. ie: full re-index</summary>
public void CreateNewIndex(IEnumerable<LibraryBook> library, bool overwrite = true)
{
// 300 titles: 200- 400 ms
// 1021 titles: 1777-2250 ms
var sw = System.Diagnostics.Stopwatch.StartNew();
var stamps = new List<long>();
void log() => stamps.Add(sw.ElapsedMilliseconds);
log();
var library = context.GetLibrary_Flat_NoTracking();
log();
// location of index/create the index
using var index = getIndex();
var exists = IndexReader.IndexExists(index);
@@ -224,8 +213,6 @@ namespace LibationSearchEngine
var doc = createBookIndexDocument(libraryBook);
ixWriter.AddDocument(doc);
}
log();
}
/// <summary>Long running. Use await Task.Run(() => UpdateBook(productId))</summary>

View File

@@ -50,24 +50,24 @@ namespace LibationWinForms.BookLiberation
public static class ProcessorAutomationController
{
public static async Task BackupSingleBookAsync(LibraryBook libraryBook, EventHandler<LibraryBook> completedAction = null)
public static async Task BackupSingleBookAsync(LibraryBook libraryBook)
{
Serilog.Log.Logger.Information($"Begin {nameof(BackupSingleBookAsync)} {{@DebugInfo}}", new { libraryBook?.Book?.AudibleProductId });
var logMe = LogMe.RegisterForm();
var backupBook = CreateBackupBook(completedAction, logMe);
var backupBook = CreateBackupBook(logMe);
// continue even if libraryBook is null. we'll display even that in the processing box
await new BackupSingle(logMe, backupBook, libraryBook).RunBackupAsync();
}
public static async Task BackupAllBooksAsync(EventHandler<LibraryBook> completedAction = null)
public static async Task BackupAllBooksAsync()
{
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync));
var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm);
var backupBook = CreateBackupBook(completedAction, logMe);
var backupBook = CreateBackupBook(logMe);
await new BackupLoop(logMe, backupBook, automatedBackupsForm).RunBackupAsync();
}
@@ -84,19 +84,19 @@ namespace LibationWinForms.BookLiberation
await new BackupLoop(logMe, convertBook, automatedBackupsForm).RunBackupAsync();
}
public static async Task BackupAllPdfsAsync(EventHandler<LibraryBook> completedAction = null)
public static async Task BackupAllPdfsAsync()
{
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllPdfsAsync));
var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm);
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe, completedAction);
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe);
await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync();
}
private static IProcessable CreateBackupBook(EventHandler<LibraryBook> completedAction, LogMe logMe)
private static IProcessable CreateBackupBook(LogMe logMe)
{
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe);
@@ -104,7 +104,6 @@ namespace LibationWinForms.BookLiberation
async void onDownloadDecryptBookCompleted(object sender, LibraryBook e)
{
await downloadPdf.TryProcessAsync(e);
completedAction(sender, e);
}
var downloadDecryptBook = CreateProcessable<DownloadDecryptBook, AudioDecryptForm>(logMe, onDownloadDecryptBookCompleted);
@@ -246,7 +245,7 @@ $@" Title: {libraryBook.Book.Title}
if (dialogResult == SkipResult)
{
ApplicationServices.LibraryCommands.UpdateBook(libraryBook, LiberatedStatus.Error);
libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Error;
LogMe.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}");
}
@@ -305,7 +304,7 @@ An error occurred while trying to process this book.
protected override async Task RunAsync()
{
// support for 'skip this time only' requires state. iterators provide this state for free. therefore: use foreach/iterator here
foreach (var libraryBook in Processable.GetValidLibraryBooks())
foreach (var libraryBook in Processable.GetValidLibraryBooks(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()))
{
var keepGoing = await ProcessOneAsync(libraryBook, validate: false);
if (!keepGoing)

View File

@@ -99,7 +99,7 @@
this.detailsTb.ReadOnly = true;
this.detailsTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.detailsTb.Size = new System.Drawing.Size(484, 202);
this.detailsTb.TabIndex = 0;
this.detailsTb.TabIndex = 1;
//
// tagsGb
//
@@ -110,7 +110,7 @@
this.tagsGb.Location = new System.Drawing.Point(12, 220);
this.tagsGb.Name = "tagsGb";
this.tagsGb.Size = new System.Drawing.Size(570, 73);
this.tagsGb.TabIndex = 1;
this.tagsGb.TabIndex = 0;
this.tagsGb.TabStop = false;
this.tagsGb.Text = "Edit Tags";
//

View File

@@ -77,7 +77,6 @@ Purchase Date: {_libraryBook.DateAdded.ToString("d")}
if (status == LiberatedStatus.Error)
this.bookLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.Error, Text = "Error" });
setDefaultComboBox(this.bookLiberatedCb, status);
}

View File

@@ -1,22 +1,20 @@
using System;
using ApplicationServices;
using DataLayer;
using Dinah.Core.DataBinding;
using InternalUtilities;
using LibationWinForms.Login;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Windows.Forms;
using ApplicationServices;
using DataLayer;
using Dinah.Core.DataBinding;
using InternalUtilities;
using LibationWinForms.Login;
namespace LibationWinForms.Dialogs
{
public partial class RemoveBooksDialog : Form
{
public bool BooksRemoved { get; private set; }
private Account[] _accounts { get; }
private readonly List<LibraryBook> _libraryBooks;
private readonly SortableBindingList<RemovableGridEntry> _removableGridEntries;
@@ -26,15 +24,14 @@ namespace LibationWinForms.Dialogs
public RemoveBooksDialog(params Account[] accounts)
{
_libraryBooks = DbContexts.GetContext().GetLibrary_Flat_NoTracking();
_libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
_accounts = accounts;
InitializeComponent();
_labelFormat = label1.Text;
_dataGridView.CellContentClick += (s, e) => _dataGridView.CommitEdit(DataGridViewDataErrorContexts.Commit);
_dataGridView.CellValueChanged += DataGridView1_CellValueChanged;
_dataGridView.BindingContextChanged += (s, e) => UpdateSelection();
_dataGridView.BindingContextChanged += _dataGridView_BindingContextChanged;
var orderedGridEntries = _libraryBooks
.Select(lb => new RemovableGridEntry(lb))
@@ -47,10 +44,10 @@ namespace LibationWinForms.Dialogs
_dataGridView.Enabled = false;
}
private void DataGridView1_CellValueChanged(object sender, DataGridViewCellEventArgs e)
private void _dataGridView_BindingContextChanged(object sender, EventArgs e)
{
if (e.ColumnIndex == 0)
UpdateSelection();
_dataGridView.Sort(_dataGridView.Columns[0], ListSortDirection.Descending);
UpdateSelection();
}
private async void RemoveBooksDialog_Shown(object sender, EventArgs e)
@@ -59,9 +56,9 @@ namespace LibationWinForms.Dialogs
return;
try
{
var rmovedBooks = await LibraryCommands.FindInactiveBooks((account) => new WinformResponder(account), _libraryBooks, _accounts);
var removedBooks = await LibraryCommands.FindInactiveBooks((account) => new WinformResponder(account), _libraryBooks, _accounts);
var removable = _removableGridEntries.Where(rge => rmovedBooks.Any(rb => rb.Book.AudibleProductId == rge.AudibleProductId));
var removable = _removableGridEntries.Where(rge => removedBooks.Any(rb => rb.Book.AudibleProductId == rge.AudibleProductId));
if (!removable.Any())
return;
@@ -84,7 +81,7 @@ namespace LibationWinForms.Dialogs
}
}
private void btnRemoveBooks_Click(object sender, EventArgs e)
private async void btnRemoveBooks_Click(object sender, EventArgs e)
{
var selectedBooks = SelectedEntries.ToList();
@@ -105,26 +102,18 @@ namespace LibationWinForms.Dialogs
if (result == DialogResult.Yes)
{
using var context = DbContexts.GetContext();
var libBooks = context.GetLibrary_Flat_NoTracking();
var removeLibraryBooks = libBooks.Where(lb => selectedBooks.Any(rge => rge.AudibleProductId == lb.Book.AudibleProductId)).ToList();
context.Library.RemoveRange(removeLibraryBooks);
context.SaveChanges();
var idsToRemove = selectedBooks.Select(rge => rge.AudibleProductId).ToList();
var removeLibraryBooks = await LibraryCommands.RemoveBooksAsync(idsToRemove);
foreach (var rEntry in selectedBooks)
_removableGridEntries.Remove(rEntry);
BooksRemoved = removeLibraryBooks.Count > 0;
UpdateSelection();
}
}
private void UpdateSelection()
{
_dataGridView.Sort(_dataGridView.Columns[0], ListSortDirection.Descending);
var selectedCount = SelectedCount;
label1.Text = string.Format(_labelFormat, selectedCount, selectedCount != 1 ? "s" : string.Empty);
btnRemoveBooks.Enabled = selectedCount > 0;

View File

@@ -173,18 +173,19 @@
this.removeLibraryBooksToolStripMenuItem.Name = "removeLibraryBooksToolStripMenuItem";
this.removeLibraryBooksToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.removeLibraryBooksToolStripMenuItem.Text = "Remove Library Books";
this.removeLibraryBooksToolStripMenuItem.Click += new System.EventHandler(this.removeLibraryBooksToolStripMenuItem_Click);
//
// removeAllAccountsToolStripMenuItem
//
this.removeAllAccountsToolStripMenuItem.Name = "removeAllAccountsToolStripMenuItem";
this.removeAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(157, 22);
this.removeAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.removeAllAccountsToolStripMenuItem.Text = "All Accounts";
this.removeAllAccountsToolStripMenuItem.Click += new System.EventHandler(this.removeAllAccountsToolStripMenuItem_Click);
//
// removeSomeAccountsToolStripMenuItem
//
this.removeSomeAccountsToolStripMenuItem.Name = "removeSomeAccountsToolStripMenuItem";
this.removeSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(157, 22);
this.removeSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.removeSomeAccountsToolStripMenuItem.Text = "Some Accounts";
this.removeSomeAccountsToolStripMenuItem.Click += new System.EventHandler(this.removeSomeAccountsToolStripMenuItem_Click);
//
@@ -201,23 +202,22 @@
// beginBookBackupsToolStripMenuItem
//
this.beginBookBackupsToolStripMenuItem.Name = "beginBookBackupsToolStripMenuItem";
this.beginBookBackupsToolStripMenuItem.Size = new System.Drawing.Size(284, 22);
this.beginBookBackupsToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
this.beginBookBackupsToolStripMenuItem.Text = "Begin &Book and PDF Backups: {0}";
this.beginBookBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginBookBackupsToolStripMenuItem_Click);
//
// beginPdfBackupsToolStripMenuItem
//
this.beginPdfBackupsToolStripMenuItem.Name = "beginPdfBackupsToolStripMenuItem";
this.beginPdfBackupsToolStripMenuItem.Size = new System.Drawing.Size(284, 22);
this.beginPdfBackupsToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
this.beginPdfBackupsToolStripMenuItem.Text = "Begin &PDF Only Backups: {0}";
this.beginPdfBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginPdfBackupsToolStripMenuItem_Click);
//
// convertAllM4bToMp3ToolStripMenuItem
//
this.convertAllM4bToMp3ToolStripMenuItem.Name = "convertAllM4bToMp3ToolStripMenuItem";
this.convertAllM4bToMp3ToolStripMenuItem.Size = new System.Drawing.Size(284, 22);
this.convertAllM4bToMp3ToolStripMenuItem.Text = "Convert all M4b to Mp3 [Long-running]";
this.convertAllM4bToMp3ToolStripMenuItem.Visible = true;
this.convertAllM4bToMp3ToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
this.convertAllM4bToMp3ToolStripMenuItem.Text = "Convert all M4b to Mp3 [Long-running]...";
this.convertAllM4bToMp3ToolStripMenuItem.Click += new System.EventHandler(this.convertAllM4bToMp3ToolStripMenuItem_Click);
//
// exportToolStripMenuItem
@@ -310,7 +310,7 @@
// springLbl
//
this.springLbl.Name = "springLbl";
this.springLbl.Size = new System.Drawing.Size(517, 17);
this.springLbl.Size = new System.Drawing.Size(548, 17);
this.springLbl.Spring = true;
//
// backupsCountsLbl

View File

@@ -1,4 +1,8 @@
using ApplicationServices;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using ApplicationServices;
using DataLayer;
using Dinah.Core;
using Dinah.Core.Drawing;
@@ -6,11 +10,6 @@ using Dinah.Core.Windows.Forms;
using FileManager;
using InternalUtilities;
using LibationWinForms.Dialogs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace LibationWinForms
{
@@ -31,9 +30,10 @@ namespace LibationWinForms
return;
// independent UI updates
this.Load += setBackupCountsAsync;
this.Load += (_, __) => RestoreSizeAndLocation();
this.Load += (_, __) => RefreshImportMenu();
this.Load += restoreSizeAndLocation;
this.Load += RefreshImportMenu;
LibraryCommands.LibrarySizeChanged += reloadGridAndUpdateBottomNumbers;
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
@@ -46,11 +46,12 @@ namespace LibationWinForms
if (this.DesignMode)
return;
reloadGrid();
// can't refactor into "this.Load => reloadGridAndUpdateBottomNumbers"
// because loadInitialQuickFilterState must follow it
reloadGridAndUpdateBottomNumbers();
// also applies filter. ONLY call AFTER loading grid
loadInitialQuickFilterState();
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
@@ -58,7 +59,7 @@ namespace LibationWinForms
SaveSizeAndLocation();
}
private void RestoreSizeAndLocation()
private void restoreSizeAndLocation(object _ = null, object __ = null)
{
var config = Configuration.Instance;
@@ -128,9 +129,7 @@ namespace LibationWinForms
config.MainFormIsMaximized = this.WindowState == FormWindowState.Maximized;
}
#region reload grid
private bool isProcessingGridSelect = false;
private void reloadGrid()
private void reloadGridAndUpdateBottomNumbers(object _ = null, object __ = null)
{
// suppressed filter while init'ing UI
var prev_isProcessingGridSelect = isProcessingGridSelect;
@@ -140,8 +139,11 @@ namespace LibationWinForms
// UI init complete. now we can apply filter
doFilter(lastGoodFilter);
setBackupCounts(null, null);
}
#region reload grid
private ProductsGrid currProductsGrid;
private void setGrid()
{
@@ -151,13 +153,11 @@ namespace LibationWinForms
{
gridPanel.Controls.Remove(currProductsGrid);
currProductsGrid.VisibleCountChanged -= setVisibleCount;
currProductsGrid.BackupCountsChanged -= setBackupCountsAsync;
currProductsGrid.Dispose();
}
currProductsGrid = new ProductsGrid { Dock = DockStyle.Fill };
currProductsGrid.VisibleCountChanged += setVisibleCount;
currProductsGrid.BackupCountsChanged += setBackupCountsAsync;
gridPanel.UIThread(() => gridPanel.Controls.Add(currProductsGrid));
currProductsGrid.Display();
}
@@ -170,28 +170,57 @@ namespace LibationWinForms
#endregion
#region bottom: backup counts
private async void setBackupCountsAsync(object _, object __)
{
LibraryCommands.LibraryStats libraryStats = null;
await Task.Run(() => libraryStats = LibraryCommands.GetCounts());
private System.ComponentModel.BackgroundWorker updateCountsBw;
private bool runBackupCountsAgain;
setBookBackupCounts(libraryStats.booksFullyBackedUp, libraryStats.booksDownloadedOnly, libraryStats.booksNoProgress);
setPdfBackupCounts(libraryStats.pdfsDownloaded, libraryStats.pdfsNotDownloaded);
}
private void setBookBackupCounts(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress)
private void setBackupCounts(object _, object __)
{
var backupsCountsLbl_Format = "BACKUPS: No progress: {0} Encrypted: {1} Fully backed up: {2}";
runBackupCountsAgain = true;
if (updateCountsBw is not null)
return;
updateCountsBw = new System.ComponentModel.BackgroundWorker();
updateCountsBw.DoWork += UpdateCountsBw_DoWork;
updateCountsBw.RunWorkerCompleted += UpdateCountsBw_RunWorkerCompleted;
updateCountsBw.RunWorkerAsync();
}
private void UpdateCountsBw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
while (runBackupCountsAgain)
{
runBackupCountsAgain = false;
var libraryStats = LibraryCommands.GetCounts();
e.Result = libraryStats;
}
updateCountsBw = null;
}
private void UpdateCountsBw_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
setBookBackupCounts(libraryStats);
setPdfBackupCounts(libraryStats);
}
private void setBookBackupCounts(LibraryCommands.LibraryStats libraryStats)
{
var backupsCountsLbl_Format = "BACKUPS: No progress: {0} In process: {1} Fully backed up: {2}";
// enable/disable export
var hasResults = 0 < (booksFullyBackedUp + booksDownloadedOnly + booksNoProgress);
var hasResults = 0 < (libraryStats.booksFullyBackedUp + libraryStats.booksDownloadedOnly + libraryStats.booksNoProgress + libraryStats.booksError);
exportLibraryToolStripMenuItem.Enabled = hasResults;
// update bottom numbers
var pending = booksNoProgress + booksDownloadedOnly;
var pending = libraryStats.booksNoProgress + libraryStats.booksDownloadedOnly;
var statusStripText
= !hasResults ? "No books. Begin by importing your library"
: pending > 0 ? string.Format(backupsCountsLbl_Format, booksNoProgress, booksDownloadedOnly, booksFullyBackedUp)
: $"All {"book".PluralizeWithCount(booksFullyBackedUp)} backed up";
: libraryStats.booksError > 0 ? string.Format(backupsCountsLbl_Format + " Errors: {3}", libraryStats.booksNoProgress, libraryStats.booksDownloadedOnly, libraryStats.booksFullyBackedUp, libraryStats.booksError)
: pending > 0 ? string.Format(backupsCountsLbl_Format, libraryStats.booksNoProgress, libraryStats.booksDownloadedOnly, libraryStats.booksFullyBackedUp)
: $"All {"book".PluralizeWithCount(libraryStats.booksFullyBackedUp)} backed up";
// update menu item
var menuItemText
@@ -204,26 +233,26 @@ namespace LibationWinForms
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Enabled = pending > 0);
menuStrip1.UIThread(() => beginBookBackupsToolStripMenuItem.Text = string.Format(beginBookBackupsToolStripMenuItem_format, menuItemText));
}
private void setPdfBackupCounts(int pdfsDownloaded, int pdfsNotDownloaded)
private void setPdfBackupCounts(LibraryCommands.LibraryStats libraryStats)
{
var pdfsCountsLbl_Format = "| PDFs: NOT d/l\'ed: {0} Downloaded: {1}";
// update bottom numbers
var hasResults = 0 < (pdfsNotDownloaded + pdfsDownloaded);
var hasResults = 0 < (libraryStats.pdfsNotDownloaded + libraryStats.pdfsDownloaded);
var statusStripText
= !hasResults ? ""
: pdfsNotDownloaded > 0 ? string.Format(pdfsCountsLbl_Format, pdfsNotDownloaded, pdfsDownloaded)
: $"| All {pdfsDownloaded} PDFs downloaded";
: libraryStats.pdfsNotDownloaded > 0 ? string.Format(pdfsCountsLbl_Format, libraryStats.pdfsNotDownloaded, libraryStats.pdfsDownloaded)
: $"| All {libraryStats.pdfsDownloaded} PDFs downloaded";
// update menu item
var menuItemText
= pdfsNotDownloaded > 0
? $"{pdfsNotDownloaded} remaining"
= libraryStats.pdfsNotDownloaded > 0
? $"{libraryStats.pdfsNotDownloaded} remaining"
: "All PDFs have been downloaded";
// update UI
statusStrip1.UIThread(() => pdfsCountsLbl.Text = statusStripText);
menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Enabled = pdfsNotDownloaded > 0);
menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Enabled = libraryStats.pdfsNotDownloaded > 0);
menuStrip1.UIThread(() => beginPdfBackupsToolStripMenuItem.Text = string.Format(beginPdfBackupsToolStripMenuItem_format, menuItemText));
}
#endregion
@@ -249,6 +278,7 @@ namespace LibationWinForms
}
private void filterBtn_Click(object sender, EventArgs e) => doFilter();
private bool isProcessingGridSelect = false;
private string lastGoodFilter = "";
private void doFilter(string filterString)
{
@@ -276,7 +306,7 @@ namespace LibationWinForms
#endregion
#region Import menu
public void RefreshImportMenu()
public void RefreshImportMenu(object _ = null, EventArgs __ = null)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var count = persister.AccountsSettings.Accounts.Count;
@@ -288,11 +318,6 @@ namespace LibationWinForms
removeLibraryBooksToolStripMenuItem.Visible = count != 0;
if (count == 1)
{
removeLibraryBooksToolStripMenuItem.Click += removeThisAccountToolStripMenuItem_Click;
}
removeSomeAccountsToolStripMenuItem.Visible = count > 1;
removeAllAccountsToolStripMenuItem.Visible = count > 1;
}
@@ -330,13 +355,22 @@ namespace LibationWinForms
scanLibraries(scanAccountsDialog.CheckedAccounts);
}
private void removeThisAccountToolStripMenuItem_Click(object sender, EventArgs e)
private void removeLibraryBooksToolStripMenuItem_Click(object sender, EventArgs e)
{
// if 0 accounts, this will not be visible
// if 1 account, run scanLibrariesRemovedBooks() on this account
// if multiple accounts, another menu set will open. do not run scanLibrariesRemovedBooks()
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault();
var accounts = persister.AccountsSettings.GetAll();
if (accounts.Count != 1)
return;
var firstAccount = accounts.Single();
scanLibrariesRemovedBooks(firstAccount);
}
// selectively remove books from all accounts
private void removeAllAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
@@ -344,6 +378,7 @@ namespace LibationWinForms
scanLibrariesRemovedBooks(allAccounts.ToArray());
}
// selectively remove books from some accounts
private void removeSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var scanAccountsDialog = new ScanAccountsDialog(this);
@@ -361,9 +396,6 @@ namespace LibationWinForms
{
using var dialog = new RemoveBooksDialog(accounts);
dialog.ShowDialog();
if (dialog.BooksRemoved)
reloadGrid();
}
private void scanLibraries(IEnumerable<Account> accounts) => scanLibraries(accounts.ToArray());
@@ -376,23 +408,30 @@ namespace LibationWinForms
var newAdded = dialog.NewBooksAdded;
MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
if (totalProcessed > 0)
reloadGrid();
}
#endregion
#region liberate menu
private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e)
=> await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync(updateGridRow);
=> await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync();
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)
=> await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync(updateGridRow);
=> await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync();
private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e)
=> await BookLiberation.ProcessorAutomationController.ConvertAllBooksAsync();
{
var result = MessageBox.Show(
"This converts all m4b titles in your library to mp3 files. Original files are not deleted."
+ "\r\nFor large libraries this will take a long time and will take up more disk space."
+ "\r\n\r\nContinue?"
+ "\r\n\r\n(To always download titles as mp3 instead of m4b, go to Settings: Download my books as .MP3 files)",
"Convert all M4b => Mp3?",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
if (result == DialogResult.Yes)
await BookLiberation.ProcessorAutomationController.ConvertAllBooksAsync();
}
private void updateGridRow(object _, LibraryBook libraryBook) => currProductsGrid.RefreshRow(libraryBook.Book.AudibleProductId);
#endregion
#region Export menu
@@ -486,6 +525,5 @@ namespace LibationWinForms
private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog();
#endregion
}
}

View File

@@ -12,6 +12,9 @@ using Dinah.Core.Drawing;
namespace LibationWinForms
{
/// <summary>
/// The View Model for a LibraryBook
/// </summary>
internal class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
{
#region implementation properties
@@ -26,12 +29,15 @@ namespace LibationWinForms
private Book Book => LibraryBook.Book;
private Image _cover;
private Action Refilter { get; }
public GridEntry(LibraryBook libraryBook)
public GridEntry(LibraryBook libraryBook, Action refilterOnChanged = null)
{
LibraryBook = libraryBook;
Refilter = refilterOnChanged;
_memberValues = CreateMemberValueDictionary();
//Get cover art. If it's default, subscribe to PictureCached
{
(bool isDefault, byte[] picture) = FileManager.PictureStorage.GetPicture(new FileManager.PictureDefinition(Book.PictureId, FileManager.PictureSize._80x80));
@@ -58,7 +64,7 @@ namespace LibationWinForms
Description = GetDescriptionDisplay(Book);
}
//DisplayTags and Liberate properties are live.
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
}
private void PictureStorage_PictureCached(object sender, FileManager.PictureCachedEventArgs e)
@@ -70,8 +76,78 @@ namespace LibationWinForms
}
}
#region Data Source properties
#region detect changes to the model, update the view, and save to database.
/// <summary>
/// This event handler receives notifications from the model that it has changed.
/// Save to the database and notify the view that it's changed.
/// </summary>
private void UserDefinedItem_ItemChanged(object sender, string itemName)
{
var udi = sender as UserDefinedItem;
if (udi.Book.AudibleProductId != Book.AudibleProductId)
return;
switch (itemName)
{
case nameof(udi.Tags):
{
Book.UserDefinedItem.Tags = udi.Tags;
NotifyPropertyChanged(nameof(DisplayTags));
}
break;
case nameof(udi.BookStatus):
{
Book.UserDefinedItem.BookStatus = udi.BookStatus;
NotifyPropertyChanged(nameof(Liberate));
}
break;
case nameof(udi.PdfStatus):
{
Book.UserDefinedItem.PdfStatus = udi.PdfStatus;
NotifyPropertyChanged(nameof(Liberate));
}
break;
}
if (!suspendCommit)
Commit();
}
private bool suspendCommit = false;
/// <summary>
/// Begin editing the model, suspending commits until <see cref="EndEdit"/> is called.
/// </summary>
public void BeginEdit() => suspendCommit = true;
/// <summary>
/// Save all edits to the database.
/// </summary>
public void EndEdit()
{
Commit();
suspendCommit = false;
}
private void Commit()
{
// We don't want LiberatedStatus.PartialDownload to be a persistent status.
// If display/icon status is PartialDownload then save NotLiberated to db then restore PartialDownload for display
var displayStatus = Book.UserDefinedItem.BookStatus;
var saveStatus = displayStatus == LiberatedStatus.PartialDownload ? LiberatedStatus.NotLiberated : displayStatus;
Book.UserDefinedItem.BookStatus = saveStatus;
LibraryCommands.UpdateUserDefinedItem(Book);
Book.UserDefinedItem.BookStatus = displayStatus;
Refilter?.Invoke();
}
#endregion
#region Model properties exposed to the view
public Image Cover
{
get
@@ -96,8 +172,23 @@ namespace LibationWinForms
public string Category { get; }
public string Misc { get; }
public string Description { get; }
public string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
public (LiberatedState, PdfState) Liberate => (LibraryCommands.Liberated_Status(Book), LibraryCommands.Pdf_Status(Book));
public string DisplayTags
{
get => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
set => Book.UserDefinedItem.Tags = value;
}
// these 2 values being in 1 field is the trick behind getting the liberated+pdf 'stoplight' icon to draw. See: LiberateDataGridViewImageButtonCell.Paint
public (LiberatedStatus BookStatus, LiberatedStatus? PdfStatus) Liberate
{
get => (LibraryCommands.Liberated_Status(LibraryBook.Book), LibraryCommands.Pdf_Status(LibraryBook.Book));
set
{
LibraryBook.Book.UserDefinedItem.BookStatus = value.BookStatus;
LibraryBook.Book.UserDefinedItem.PdfStatus = value.PdfStatus;
}
}
#endregion
#region Data Sorting
@@ -121,7 +212,7 @@ namespace LibationWinForms
{ nameof(Category), () => Category },
{ nameof(Misc), () => Misc },
{ nameof(DisplayTags), () => DisplayTags },
{ nameof(Liberate), () => Liberate.Item1 }
{ nameof(Liberate), () => Liberate.BookStatus }
};
// Instantiate comparers for every exposed member object type.
@@ -131,7 +222,7 @@ namespace LibationWinForms
{ typeof(int), new ObjectComparer<int>() },
{ typeof(float), new ObjectComparer<float>() },
{ typeof(DateTime), new ObjectComparer<DateTime>() },
{ typeof(LiberatedState), new ObjectComparer<LiberatedState>() },
{ typeof(LiberatedStatus), new ObjectComparer<LiberatedStatus>() },
};
public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
@@ -200,5 +291,11 @@ namespace LibationWinForms
}
#endregion
~GridEntry()
{
UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
FileManager.PictureStorage.PictureCached -= PictureStorage_PictureCached;
}
}
}

View File

@@ -13,7 +13,11 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core.WindowsDesktop\Dinah.Core.WindowsDesktop.csproj" />
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="1.0.2.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
</ItemGroup>

View File

@@ -3,6 +3,7 @@ using System;
using System.Drawing;
using System.Windows.Forms;
using System.Linq;
using DataLayer;
namespace LibationWinForms
{
@@ -20,9 +21,11 @@ namespace LibationWinForms
{
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
if (value is (LiberatedState liberatedState, PdfState pdfState))
if (value is (LiberatedStatus, LiberatedStatus) or (LiberatedStatus, null))
{
(string mouseoverText, Bitmap buttonImage) = GetLiberateDisplay(liberatedState, pdfState);
var (bookState, pdfState) = ((LiberatedStatus bookState, LiberatedStatus? pdfState))value;
(string mouseoverText, Bitmap buttonImage) = GetLiberateDisplay(bookState, pdfState);
DrawButtonImage(graphics, buttonImage, cellBounds);
@@ -30,29 +33,33 @@ namespace LibationWinForms
}
}
private static (string mouseoverText, Bitmap buttonImage) GetLiberateDisplay(LiberatedState liberatedStatus, PdfState pdfStatus)
private static (string mouseoverText, Bitmap buttonImage) GetLiberateDisplay(LiberatedStatus liberatedStatus, LiberatedStatus? pdfStatus)
{
if (liberatedStatus == LiberatedStatus.Error)
return ("Book downloaded ERROR", SystemIcons.Error.ToBitmap());
(string libState, string image_lib) = liberatedStatus switch
{
LiberatedState.Liberated => ("Liberated", "green"),
LiberatedState.PartialDownload => ("File has been at least\r\npartially downloaded", "yellow"),
LiberatedState.NotDownloaded => ("Book NOT downloaded", "red"),
LiberatedStatus.Liberated => ("Liberated", "green"),
LiberatedStatus.PartialDownload => ("File has been at least\r\npartially downloaded", "yellow"),
LiberatedStatus.NotLiberated => ("Book NOT downloaded", "red"),
_ => throw new Exception("Unexpected liberation state")
};
(string pdfState, string image_pdf) = pdfStatus switch
{
PdfState.Downloaded => ("\r\nPDF downloaded", "_pdf_yes"),
PdfState.NotDownloaded => ("\r\nPDF NOT downloaded", "_pdf_no"),
PdfState.NoPdf => ("", ""),
LiberatedStatus.Liberated => ("\r\nPDF downloaded", "_pdf_yes"),
LiberatedStatus.NotLiberated => ("\r\nPDF NOT downloaded", "_pdf_no"),
LiberatedStatus.Error => ("\r\nPDF downloaded ERROR", "_pdf_no"),
null => ("", ""),
_ => throw new Exception("Unexpected PDF state")
};
var mouseoverText = libState + pdfState;
if (liberatedStatus == LiberatedState.NotDownloaded ||
liberatedStatus == LiberatedState.PartialDownload ||
pdfStatus == PdfState.NotDownloaded)
if (liberatedStatus == LiberatedStatus.NotLiberated ||
liberatedStatus == LiberatedStatus.PartialDownload ||
pdfStatus == LiberatedStatus.NotLiberated)
mouseoverText += "\r\nClick to complete";
var buttonImage = (Bitmap)Properties.Resources.ResourceManager.GetObject($"liberate_{image_lib}{image_pdf}");

View File

@@ -29,7 +29,6 @@ namespace LibationWinForms
public partial class ProductsGrid : UserControl
{
public event EventHandler<int> VisibleCountChanged;
public event EventHandler BackupCountsChanged;
// alias
private DataGridView _dataGridView => gridEntryDataGridView;
@@ -86,7 +85,7 @@ namespace LibationWinForms
}
// else: liberate
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook, (_, __) => RefreshRow(libraryBook.Book.AudibleProductId));
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(libraryBook);
}
private void Details_Click(GridEntry liveGridEntry)
@@ -95,15 +94,12 @@ namespace LibationWinForms
if (bookDetailsForm.ShowDialog() != DialogResult.OK)
return;
var qtyChanges = LibraryCommands.UpdateUserDefinedItem(liveGridEntry.LibraryBook.Book, bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
if (qtyChanges == 0)
return;
liveGridEntry.BeginEdit();
//Re-apply filters
Filter();
liveGridEntry.DisplayTags = bookDetailsForm.NewTags;
liveGridEntry.Liberate = (bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
//Update whole GridEntry row
liveGridEntry.NotifyPropertyChanged();
liveGridEntry.EndEdit();
}
#endregion
@@ -120,8 +116,7 @@ namespace LibationWinForms
//
// transform into sorted GridEntry.s BEFORE binding
//
using var context = DbContexts.GetContext();
var lib = context.GetLibrary_Flat_NoTracking();
var lib = DbContexts.GetLibrary_Flat_NoTracking();
// if no data. hide all columns. return
if (!lib.Any())
@@ -132,7 +127,7 @@ namespace LibationWinForms
}
var orderedGridEntries = lib
.Select(lb => new GridEntry(lb)).ToList()
.Select(lb => new GridEntry(lb, Filter)).ToList()
// default load order
.OrderByDescending(ge => (DateTime)ge.GetMemberValue(nameof(ge.PurchaseDate)))
//// more advanced example: sort by author, then series, then title
@@ -146,21 +141,6 @@ namespace LibationWinForms
// FILTER
Filter();
BackupCountsChanged?.Invoke(this, EventArgs.Empty);
}
public void RefreshRow(string productId)
{
var liveGridEntry = getGridEntry((ge) => ge.AudibleProductId == productId);
// update GridEntry Liberate cell
liveGridEntry?.NotifyPropertyChanged(nameof(liveGridEntry.Liberate));
// needed in case filtering by -IsLiberated and it gets changed to Liberated. want to immediately show the change
Filter();
BackupCountsChanged?.Invoke(this, EventArgs.Empty);
}
#endregion
@@ -195,12 +175,7 @@ namespace LibationWinForms
#endregion
#region DataGridView Macro
private GridEntry getGridEntry(Func<GridEntry, bool> predicate)
=> ((SortableBindingList<GridEntry>)gridEntryBindingSource.DataSource).FirstOrDefault(predicate);
private GridEntry getGridEntry(int rowIndex) => _dataGridView.GetBoundItem<GridEntry>(rowIndex);
#endregion
}
}

View File

@@ -1,25 +1,9 @@
-- begin VERSIONING ---------------------------------------------------------------------------------------------------------------------
https://github.com/rmcrackan/Libation/releases
v3.1.8 : Experimental: add Australia to locale options
v3.1.7 : Improved logging
v3.1.6 : Bugfix: some series indexes/sequences formats cause library not to import
v3.1.5 : Bugfix: some series indexes/sequences could cause library not to import
v3.1.4 : Bugfix: IsAuthorNarrated was returning no books
v3.1.3 : fix weirdness with build number
v3.1.2 : minor bug fixes
pre-github versions:
v3.1.1 : Check if upgrade available on github
v3.1.0 : FIRST PUBLIC RELEASE
v3.1-beta.11 : Improved configuration and settings file management. Configurable logging
v3.1-beta.10 : New feature: clicking Liberate button on a liberated item navigates to that audio file
v3.1-beta.9 : New feature: liberate individual book
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
v3.1-beta.3 : fixed known performance issue: Full-screen grid is slow to respond loading when books aren't liberated
v3.1-beta.2 : fixed known performance issue: Tag add/edit
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
@@ -29,29 +13,6 @@ v2 : new library page scraping. still chrome cookies. all decryption is handled
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 project > Publish...
click Publish
OPTION 2: cmd line
change dir to folder containing project
cd C:\[full...path]\Libation\LibationWinForms
this will use the parameters specified in csproj
dotnet publish -c Release
OPTION 3: cmd line, custom
open csproj
remove: PublishTrimmed, PublishReadyToRun, RuntimeIdentifier
run customized publish. examples:
publish all platforms
dotnet publish -c Release
publish win64 platform only
dotnet publish -r win-x64 -c Release
publish win64 platform, single-file
dotnet publish -r win-x64 -c Release
-- end HOW TO PUBLISH ---------------------------------------------------------------------------------------------------------------------
-- begin IMAGES/ICONS ---------------------------------------------------------------------------------------------------------------------
edit tags icon images from:
icons8.com
@@ -60,32 +21,9 @@ edit tags icon images from:
'edit' icon: https://www.iconfinder.com/icons/383147/edit_icon
-- end IMAGES/ICONS ---------------------------------------------------------------------------------------------------------------------
-- begin AUDIBLE DETAILS ---------------------------------------------------------------------------------------------------------------------
alternate book id (eg BK_RAND_006061) is called 'sku' , 'sku_lite' , 'prod_id' , 'product_id' in different parts of the site
-- end AUDIBLE DETAILS ---------------------------------------------------------------------------------------------------------------------
-- begin SOLUTION LAYOUT ---------------------------------------------------------------------------------------------------------------------
do NOT combine jsons for
- audible-scraped persistence: library, book details
- libation-generated persistence: FileLocations.json
- user-defined persistence: BookTags.json
-- end SOLUTION LAYOUT ---------------------------------------------------------------------------------------------------------------------
-- begin EF CORE ---------------------------------------------------------------------------------------------------------------------
transaction notes
-----------------
// https://msdn.microsoft.com/en-us/data/dn456843.aspx
// Rollback is called by transaction Dispose(). No need to call it explicitly
using var dbContext = new LibationContext();
using var dbContextTransaction = dbContext.Database.BeginTransaction();
refreshAction(dbContext, productItems);
dbContext.SaveChanges();
dbContextTransaction.Commit();
aggregate root is transactional boundary
// //context.Database.CurrentTransaction
//var dbTransaction = Microsoft.EntityFrameworkCore.Storage.DbContextTransactionExtensions.GetDbTransaction(context.Database.CurrentTransaction);
// // test with and without : using TransactionScope scope = new TransactionScope();
//System.Transactions.Transaction.Current.TransactionCompleted += (sender, e) => { };
// also : https://docs.microsoft.com/en-us/dotnet/api/system.transactions.transaction.enlistvolatile
-- end EF CORE ---------------------------------------------------------------------------------------------------------------------

View File

@@ -1,345 +0,0 @@
namespace WinFormsDesigner
{
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
this.gridPanel = new System.Windows.Forms.Panel();
this.filterHelpBtn = new System.Windows.Forms.Button();
this.filterBtn = new System.Windows.Forms.Button();
this.filterSearchTb = new System.Windows.Forms.TextBox();
this.menuStrip1 = new System.Windows.Forms.MenuStrip();
this.importToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.noAccountsYetAddAccountToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfAllAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfSomeAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.liberateToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.beginBookBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.beginPdfBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exportToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.quickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.firstFilterIsDefaultToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.editQuickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.accountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.basicSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.advancedSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
this.visibleCountLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.springLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.backupsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.pdfsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.addFilterBtn = new System.Windows.Forms.Button();
this.exportLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.menuStrip1.SuspendLayout();
this.statusStrip1.SuspendLayout();
this.SuspendLayout();
//
// gridPanel
//
this.gridPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.gridPanel.Location = new System.Drawing.Point(12, 56);
this.gridPanel.Name = "gridPanel";
this.gridPanel.Size = new System.Drawing.Size(839, 386);
this.gridPanel.TabIndex = 5;
//
// filterHelpBtn
//
this.filterHelpBtn.Location = new System.Drawing.Point(12, 27);
this.filterHelpBtn.Name = "filterHelpBtn";
this.filterHelpBtn.Size = new System.Drawing.Size(22, 23);
this.filterHelpBtn.TabIndex = 3;
this.filterHelpBtn.Text = "?";
this.filterHelpBtn.UseVisualStyleBackColor = true;
//
// filterBtn
//
this.filterBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.filterBtn.Location = new System.Drawing.Point(776, 27);
this.filterBtn.Name = "filterBtn";
this.filterBtn.Size = new System.Drawing.Size(75, 23);
this.filterBtn.TabIndex = 2;
this.filterBtn.Text = "Filter";
this.filterBtn.UseVisualStyleBackColor = true;
//
// filterSearchTb
//
this.filterSearchTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.filterSearchTb.Location = new System.Drawing.Point(186, 29);
this.filterSearchTb.Name = "filterSearchTb";
this.filterSearchTb.Size = new System.Drawing.Size(584, 20);
this.filterSearchTb.TabIndex = 1;
//
// menuStrip1
//
this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.importToolStripMenuItem,
this.liberateToolStripMenuItem,
this.exportToolStripMenuItem,
this.quickFiltersToolStripMenuItem,
this.settingsToolStripMenuItem});
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
this.menuStrip1.Name = "menuStrip1";
this.menuStrip1.Size = new System.Drawing.Size(863, 24);
this.menuStrip1.TabIndex = 0;
this.menuStrip1.Text = "menuStrip1";
//
// importToolStripMenuItem
//
this.importToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.noAccountsYetAddAccountToolStripMenuItem,
this.scanLibraryToolStripMenuItem,
this.scanLibraryOfAllAccountsToolStripMenuItem,
this.scanLibraryOfSomeAccountsToolStripMenuItem});
this.importToolStripMenuItem.Name = "importToolStripMenuItem";
this.importToolStripMenuItem.Size = new System.Drawing.Size(55, 20);
this.importToolStripMenuItem.Text = "&Import";
//
// noAccountsYetAddAccountToolStripMenuItem
//
this.noAccountsYetAddAccountToolStripMenuItem.Name = "noAccountsYetAddAccountToolStripMenuItem";
this.noAccountsYetAddAccountToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.noAccountsYetAddAccountToolStripMenuItem.Text = "No accounts yet. A&dd Account...";
//
// scanLibraryToolStripMenuItem
//
this.scanLibraryToolStripMenuItem.Name = "scanLibraryToolStripMenuItem";
this.scanLibraryToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryToolStripMenuItem.Text = "Scan &Library";
//
// scanLibraryOfAllAccountsToolStripMenuItem
//
this.scanLibraryOfAllAccountsToolStripMenuItem.Name = "scanLibraryOfAllAccountsToolStripMenuItem";
this.scanLibraryOfAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryOfAllAccountsToolStripMenuItem.Text = "Scan Library of &All Accounts";
//
// scanLibraryOfSomeAccountsToolStripMenuItem
//
this.scanLibraryOfSomeAccountsToolStripMenuItem.Name = "scanLibraryOfSomeAccountsToolStripMenuItem";
this.scanLibraryOfSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryOfSomeAccountsToolStripMenuItem.Text = "Scan Library of &Some Accounts...";
//
// liberateToolStripMenuItem
//
this.liberateToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.beginBookBackupsToolStripMenuItem,
this.beginPdfBackupsToolStripMenuItem});
this.liberateToolStripMenuItem.Name = "liberateToolStripMenuItem";
this.liberateToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
this.liberateToolStripMenuItem.Text = "&Liberate";
//
// beginBookBackupsToolStripMenuItem
//
this.beginBookBackupsToolStripMenuItem.Name = "beginBookBackupsToolStripMenuItem";
this.beginBookBackupsToolStripMenuItem.Size = new System.Drawing.Size(248, 22);
this.beginBookBackupsToolStripMenuItem.Text = "Begin &Book and PDF Backups: {0}";
//
// beginPdfBackupsToolStripMenuItem
//
this.beginPdfBackupsToolStripMenuItem.Name = "beginPdfBackupsToolStripMenuItem";
this.beginPdfBackupsToolStripMenuItem.Size = new System.Drawing.Size(248, 22);
this.beginPdfBackupsToolStripMenuItem.Text = "Begin &PDF Only Backups: {0}";
//
// exportToolStripMenuItem
//
this.exportToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.exportLibraryToolStripMenuItem});
this.exportToolStripMenuItem.Name = "exportToolStripMenuItem";
this.exportToolStripMenuItem.Size = new System.Drawing.Size(53, 20);
this.exportToolStripMenuItem.Text = "E&xport";
//
// quickFiltersToolStripMenuItem
//
this.quickFiltersToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.firstFilterIsDefaultToolStripMenuItem,
this.editQuickFiltersToolStripMenuItem,
this.toolStripSeparator1});
this.quickFiltersToolStripMenuItem.Name = "quickFiltersToolStripMenuItem";
this.quickFiltersToolStripMenuItem.Size = new System.Drawing.Size(84, 20);
this.quickFiltersToolStripMenuItem.Text = "Quick &Filters";
//
// firstFilterIsDefaultToolStripMenuItem
//
this.firstFilterIsDefaultToolStripMenuItem.Name = "firstFilterIsDefaultToolStripMenuItem";
this.firstFilterIsDefaultToolStripMenuItem.Size = new System.Drawing.Size(256, 22);
this.firstFilterIsDefaultToolStripMenuItem.Text = "Start Libation with 1st filter &Default";
//
// editQuickFiltersToolStripMenuItem
//
this.editQuickFiltersToolStripMenuItem.Name = "editQuickFiltersToolStripMenuItem";
this.editQuickFiltersToolStripMenuItem.Size = new System.Drawing.Size(256, 22);
this.editQuickFiltersToolStripMenuItem.Text = "&Edit quick filters...";
//
// toolStripSeparator1
//
this.toolStripSeparator1.Name = "toolStripSeparator1";
this.toolStripSeparator1.Size = new System.Drawing.Size(253, 6);
//
// settingsToolStripMenuItem
//
this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.accountsToolStripMenuItem,
this.basicSettingsToolStripMenuItem,
this.advancedSettingsToolStripMenuItem});
this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
this.settingsToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
this.settingsToolStripMenuItem.Text = "&Settings";
//
// accountsToolStripMenuItem
//
this.accountsToolStripMenuItem.Name = "accountsToolStripMenuItem";
this.accountsToolStripMenuItem.Size = new System.Drawing.Size(181, 22);
this.accountsToolStripMenuItem.Text = "&Accounts...";
//
// basicSettingsToolStripMenuItem
//
this.basicSettingsToolStripMenuItem.Name = "basicSettingsToolStripMenuItem";
this.basicSettingsToolStripMenuItem.Size = new System.Drawing.Size(181, 22);
this.basicSettingsToolStripMenuItem.Text = "&Basic Settings...";
//
// advancedSettingsToolStripMenuItem
//
this.advancedSettingsToolStripMenuItem.Name = "advancedSettingsToolStripMenuItem";
this.advancedSettingsToolStripMenuItem.Size = new System.Drawing.Size(181, 22);
this.advancedSettingsToolStripMenuItem.Text = "Ad&vanced Settings...";
//
// statusStrip1
//
this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.visibleCountLbl,
this.springLbl,
this.backupsCountsLbl,
this.pdfsCountsLbl});
this.statusStrip1.Location = new System.Drawing.Point(0, 445);
this.statusStrip1.Name = "statusStrip1";
this.statusStrip1.Size = new System.Drawing.Size(863, 22);
this.statusStrip1.TabIndex = 6;
this.statusStrip1.Text = "statusStrip1";
//
// visibleCountLbl
//
this.visibleCountLbl.Name = "visibleCountLbl";
this.visibleCountLbl.Size = new System.Drawing.Size(61, 17);
this.visibleCountLbl.Text = "Visible: {0}";
//
// springLbl
//
this.springLbl.Name = "springLbl";
this.springLbl.Size = new System.Drawing.Size(233, 17);
this.springLbl.Spring = true;
//
// backupsCountsLbl
//
this.backupsCountsLbl.Name = "backupsCountsLbl";
this.backupsCountsLbl.Size = new System.Drawing.Size(336, 17);
this.backupsCountsLbl.Text = "BACKUPS: No progress: {0} Encrypted: {1} Fully backed up: {2}";
//
// pdfsCountsLbl
//
this.pdfsCountsLbl.Name = "pdfsCountsLbl";
this.pdfsCountsLbl.Size = new System.Drawing.Size(218, 17);
this.pdfsCountsLbl.Text = "| PDFs: NOT d/l\'ed: {0} Downloaded: {1}";
//
// addFilterBtn
//
this.addFilterBtn.Location = new System.Drawing.Point(40, 27);
this.addFilterBtn.Name = "addFilterBtn";
this.addFilterBtn.Size = new System.Drawing.Size(140, 23);
this.addFilterBtn.TabIndex = 4;
this.addFilterBtn.Text = "Add To Quick Filters";
this.addFilterBtn.UseVisualStyleBackColor = true;
//
// exportLibraryToolStripMenuItem
//
this.exportLibraryToolStripMenuItem.Name = "exportLibraryToolStripMenuItem";
this.exportLibraryToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
this.exportLibraryToolStripMenuItem.Text = "E&xport Library...";
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(863, 467);
this.Controls.Add(this.filterBtn);
this.Controls.Add(this.addFilterBtn);
this.Controls.Add(this.filterSearchTb);
this.Controls.Add(this.filterHelpBtn);
this.Controls.Add(this.statusStrip1);
this.Controls.Add(this.gridPanel);
this.Controls.Add(this.menuStrip1);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MainMenuStrip = this.menuStrip1;
this.Name = "Form1";
this.Text = "Libation: Liberate your Library";
this.menuStrip1.ResumeLayout(false);
this.menuStrip1.PerformLayout();
this.statusStrip1.ResumeLayout(false);
this.statusStrip1.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Panel gridPanel;
private System.Windows.Forms.MenuStrip menuStrip1;
private System.Windows.Forms.ToolStripMenuItem importToolStripMenuItem;
private System.Windows.Forms.StatusStrip statusStrip1;
private System.Windows.Forms.ToolStripStatusLabel springLbl;
private System.Windows.Forms.ToolStripStatusLabel visibleCountLbl;
private System.Windows.Forms.ToolStripMenuItem liberateToolStripMenuItem;
private System.Windows.Forms.ToolStripStatusLabel backupsCountsLbl;
private System.Windows.Forms.ToolStripMenuItem beginBookBackupsToolStripMenuItem;
private System.Windows.Forms.ToolStripStatusLabel pdfsCountsLbl;
private System.Windows.Forms.ToolStripMenuItem beginPdfBackupsToolStripMenuItem;
private System.Windows.Forms.TextBox filterSearchTb;
private System.Windows.Forms.Button filterBtn;
private System.Windows.Forms.Button filterHelpBtn;
private System.Windows.Forms.ToolStripMenuItem settingsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem scanLibraryToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem quickFiltersToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem firstFilterIsDefaultToolStripMenuItem;
private System.Windows.Forms.Button addFilterBtn;
private System.Windows.Forms.ToolStripMenuItem editQuickFiltersToolStripMenuItem;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
private System.Windows.Forms.ToolStripMenuItem basicSettingsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem advancedSettingsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem accountsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem scanLibraryOfAllAccountsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem scanLibraryOfSomeAccountsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem noAccountsYetAddAccountToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem exportToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem exportLibraryToolStripMenuItem;
}
}

View File

@@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WinFormsDesigner
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
namespace WinFormsDesigner
{
internal class GridEntry
{
[Browsable(false)]
public string Tags { get; set; }
[Browsable(false)]
public IEnumerable<string> TagsEnumerated { get; set; }
[Browsable(false)]
public string Download_Status { get; set; }
public Image Cover { get; set; }
public string Title { get; set; }
public string Authors { get; set; }
public string Narrators { get; set; }
public int Length { get; set; }
public string Series { get; set; }
public string Description { get; set; }
public string Category { get; set; }
public string Product_Rating { get; set; }
public DateTime? Purchase_Date { get; set; }
public string My_Rating { get; set; }
public string Misc { get; set; }
}
}

View File

@@ -1,191 +0,0 @@
namespace WinFormsDesigner
{
partial class ProductsGrid
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.gridEntryBindingSource = new System.Windows.Forms.BindingSource(this.components);
this.gridEntryDataGridView = new System.Windows.Forms.DataGridView();
this.dataGridViewImageColumn1 = new System.Windows.Forms.DataGridViewImageColumn();
this.dataGridViewTextBoxColumn1 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn2 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn3 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn4 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn5 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn6 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn7 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn8 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn9 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn10 = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.dataGridViewTextBoxColumn11 = new System.Windows.Forms.DataGridViewTextBoxColumn();
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).BeginInit();
this.SuspendLayout();
//
// gridEntryBindingSource
//
this.gridEntryBindingSource.DataSource = typeof(WinFormsDesigner.GridEntry);
//
// gridEntryDataGridView
//
this.gridEntryDataGridView.AutoGenerateColumns = false;
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.dataGridViewImageColumn1,
this.dataGridViewTextBoxColumn1,
this.dataGridViewTextBoxColumn2,
this.dataGridViewTextBoxColumn3,
this.dataGridViewTextBoxColumn4,
this.dataGridViewTextBoxColumn5,
this.dataGridViewTextBoxColumn6,
this.dataGridViewTextBoxColumn7,
this.dataGridViewTextBoxColumn8,
this.dataGridViewTextBoxColumn9,
this.dataGridViewTextBoxColumn10,
this.dataGridViewTextBoxColumn11});
this.gridEntryDataGridView.DataSource = this.gridEntryBindingSource;
this.gridEntryDataGridView.Location = new System.Drawing.Point(54, 58);
this.gridEntryDataGridView.Name = "gridEntryDataGridView";
this.gridEntryDataGridView.Size = new System.Drawing.Size(300, 220);
this.gridEntryDataGridView.TabIndex = 0;
//
// dataGridViewImageColumn1
//
this.dataGridViewImageColumn1.DataPropertyName = "Cover";
this.dataGridViewImageColumn1.HeaderText = "Cover";
this.dataGridViewImageColumn1.Name = "dataGridViewImageColumn1";
this.dataGridViewImageColumn1.ReadOnly = true;
//
// dataGridViewTextBoxColumn1
//
this.dataGridViewTextBoxColumn1.DataPropertyName = "Title";
this.dataGridViewTextBoxColumn1.HeaderText = "Title";
this.dataGridViewTextBoxColumn1.Name = "dataGridViewTextBoxColumn1";
this.dataGridViewTextBoxColumn1.ReadOnly = true;
//
// dataGridViewTextBoxColumn2
//
this.dataGridViewTextBoxColumn2.DataPropertyName = "Authors";
this.dataGridViewTextBoxColumn2.HeaderText = "Authors";
this.dataGridViewTextBoxColumn2.Name = "dataGridViewTextBoxColumn2";
this.dataGridViewTextBoxColumn2.ReadOnly = true;
//
// dataGridViewTextBoxColumn3
//
this.dataGridViewTextBoxColumn3.DataPropertyName = "Narrators";
this.dataGridViewTextBoxColumn3.HeaderText = "Narrators";
this.dataGridViewTextBoxColumn3.Name = "dataGridViewTextBoxColumn3";
this.dataGridViewTextBoxColumn3.ReadOnly = true;
//
// dataGridViewTextBoxColumn4
//
this.dataGridViewTextBoxColumn4.DataPropertyName = "Length";
this.dataGridViewTextBoxColumn4.HeaderText = "Length";
this.dataGridViewTextBoxColumn4.Name = "dataGridViewTextBoxColumn4";
this.dataGridViewTextBoxColumn4.ReadOnly = true;
//
// dataGridViewTextBoxColumn5
//
this.dataGridViewTextBoxColumn5.DataPropertyName = "Series";
this.dataGridViewTextBoxColumn5.HeaderText = "Series";
this.dataGridViewTextBoxColumn5.Name = "dataGridViewTextBoxColumn5";
this.dataGridViewTextBoxColumn5.ReadOnly = true;
//
// dataGridViewTextBoxColumn6
//
this.dataGridViewTextBoxColumn6.DataPropertyName = "Description";
this.dataGridViewTextBoxColumn6.HeaderText = "Description";
this.dataGridViewTextBoxColumn6.Name = "dataGridViewTextBoxColumn6";
this.dataGridViewTextBoxColumn6.ReadOnly = true;
//
// dataGridViewTextBoxColumn7
//
this.dataGridViewTextBoxColumn7.DataPropertyName = "Category";
this.dataGridViewTextBoxColumn7.HeaderText = "Category";
this.dataGridViewTextBoxColumn7.Name = "dataGridViewTextBoxColumn7";
this.dataGridViewTextBoxColumn7.ReadOnly = true;
//
// dataGridViewTextBoxColumn8
//
this.dataGridViewTextBoxColumn8.DataPropertyName = "Product_Rating";
this.dataGridViewTextBoxColumn8.HeaderText = "Product_Rating";
this.dataGridViewTextBoxColumn8.Name = "dataGridViewTextBoxColumn8";
this.dataGridViewTextBoxColumn8.ReadOnly = true;
//
// dataGridViewTextBoxColumn9
//
this.dataGridViewTextBoxColumn9.DataPropertyName = "Purchase_Date";
this.dataGridViewTextBoxColumn9.HeaderText = "Purchase_Date";
this.dataGridViewTextBoxColumn9.Name = "dataGridViewTextBoxColumn9";
this.dataGridViewTextBoxColumn9.ReadOnly = true;
//
// dataGridViewTextBoxColumn10
//
this.dataGridViewTextBoxColumn10.DataPropertyName = "My_Rating";
this.dataGridViewTextBoxColumn10.HeaderText = "My_Rating";
this.dataGridViewTextBoxColumn10.Name = "dataGridViewTextBoxColumn10";
this.dataGridViewTextBoxColumn10.ReadOnly = true;
//
// dataGridViewTextBoxColumn11
//
this.dataGridViewTextBoxColumn11.DataPropertyName = "Misc";
this.dataGridViewTextBoxColumn11.HeaderText = "Misc";
this.dataGridViewTextBoxColumn11.Name = "dataGridViewTextBoxColumn11";
this.dataGridViewTextBoxColumn11.ReadOnly = true;
//
// ProductsGrid
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.gridEntryDataGridView);
this.Name = "ProductsGrid";
this.Size = new System.Drawing.Size(434, 329);
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.BindingSource gridEntryBindingSource;
private System.Windows.Forms.DataGridView gridEntryDataGridView;
private System.Windows.Forms.DataGridViewImageColumn dataGridViewImageColumn1;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn1;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn2;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn3;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn4;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn5;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn6;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn7;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn8;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn9;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn10;
private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumn11;
}
}

View File

@@ -1,27 +0,0 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace WinFormsDesigner
{
// INSTRUCTIONS TO UPDATE DATA_GRID_VIEW
// - delete current DataGridView
// - view > other windows > data sources
// - refresh
// OR
// - Add New Data Source
// Object. Next
// WinFormsDesigner
// AudibleDTO
// GridEntry
// - go to Design view
// - click on Data Sources > ProductItem. drowdown: DataGridView
// - drag/drop ProductItem on design surface
public partial class ProductsGrid : UserControl
{
public ProductsGrid()
{
InitializeComponent();
}
}
}

View File

@@ -1,123 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="gridEntryBindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root>

View File

@@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WinFormsDesigner
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
}

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This file is automatically generated by Visual Studio .Net. It is
used to store generic object data source configuration information.
Renaming the file extension or editing the content of this file may
cause the file to be unrecognizable by the program.
-->
<GenericObjectDataSource DisplayName="GridEntry" Version="1.0" xmlns="urn:schemas-microsoft-com:xml-msdatasource">
<TypeInfo>WinFormsDesigner.GridEntry, WinFormsDesigner, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null</TypeInfo>
</GenericObjectDataSource>

View File

@@ -1,72 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{0807616A-A77A-4B08-A65A-1582B09E114B}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>WinFormsDesigner</RootNamespace>
<AssemblyName>WinFormsDesigner</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<TargetFrameworkProfile />
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>
</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
</ItemGroup>
<ItemGroup>
<Compile Include="Form1.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Form1.Designer.cs">
<DependentUpon>Form1.cs</DependentUpon>
</Compile>
<Compile Include="ProductsGrid.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Include="ProductsGrid.Designer.cs">
<DependentUpon>ProductsGrid.cs</DependentUpon>
</Compile>
<Compile Include="GridEntry.cs" />
<Compile Include="Program.cs" />
<EmbeddedResource Include="Form1.resx">
<DependentUpon>Form1.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="ProductsGrid.resx">
<DependentUpon>ProductsGrid.cs</DependentUpon>
</EmbeddedResource>
<None Include="Properties\DataSources\WinFormsDesigner.GridEntry.datasource" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@@ -1,45 +1,42 @@
Logging/Debugging (EF CORE)
===========================
Once you configure logging on a DbContext instance it will be enabled on all instances of that DbContext type
using var context = new MyContext();
context.ConfigureLogging(s => System.Diagnostics.Debug.WriteLine(s)); // write to Visual Studio "Output" tab
//context.ConfigureLogging(s => Console.WriteLine(s));
see comments at top of file:
Dinah.EntityFrameworkCore\DbContextLoggingExtensions.cs
LocalDb
=======
only works if LocalDb is separately installed on host box
SSMS db connection: (LocalDb)\MSSQLLocalDB
eg: Server=(localdb)\mssqllocaldb;Database=DataLayer.LibationContext;Integrated Security=true;
LocalDb database files live at:
C:\Users\[user]\DataLayer.LibationContext.mdf
C:\Users\[user]\DataLayer.LibationContext_log.ldf
Migrations
==========
Visual Studio, EF Core
----------------------
Migrations, quick
=================
View > Other Windows > Package Manager Console
Default project: DataLayer
Startup project: DataLayer
since we have mult contexts, must use -context:
Add-Migration MyComment -context LibationContext
Update-Database -context LibationContext
Add-Migration MyComment -context LibationContext
Update-Database -context LibationContext
Startup project: reset to prev. eg: LibationLauncher
ERROR
=====
Add-Migration : The term 'Add-Migration' is not recognized as the name of a cmdlet, function, script file, or operable program
SOLUTION
--------
add nuget pkg: Microsoft.EntityFrameworkCore.Tools
Migrations, detailed
====================
if only 1 context present, can omit -context arg:
Add-Migration MyComment
Update-Database
SQLite
======
SQLite does not support all migrations (schema changes) due to limitations in SQLite
delete db before running Update-Database?
Migrations, errors
=================
if add-migration xyz throws and error, don't take the error msg at face value. try again with add-migration xyz -verbose
ERROR: Add-Migration : The term 'Add-Migration' is not recognized as the name of a cmdlet, function, script file, or operable program
SOLUTION: add nuget pkg: Microsoft.EntityFrameworkCore.Tools
SqLite config
=============
relative:
optionsBuilder.UseSqlite("Data Source=blogging.db");
absolute (use fwd slashes):
optionsBuilder.UseSqlite("Data Source=C:/foo/bar/blogging.db");
Logging/Debugging (EF CORE)
===========================
Once you configure logging on a DbContext instance it will be enabled on all instances of that DbContext type
using var context = new MyContext();
context.ConfigureLogging(s => System.Diagnostics.Debug.WriteLine(s)); // write to Visual Studio "Output" tab
//context.ConfigureLogging(s => Console.WriteLine(s));
see comments at top of file:
Dinah.EntityFrameworkCore\DbContextLoggingExtensions.cs

View File

@@ -18,11 +18,6 @@ using Moq;
using Moq.Protected;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using TestAudibleApiCommon;
using TestCommon;
using static AuthorizationShared.Shared;
using static AuthorizationShared.Shared.AccessTokenTemporality;
using static TestAudibleApiCommon.ComputedTestValues;
namespace AccountsTests
{
@@ -429,7 +424,7 @@ namespace AccountsTests
var exists = accountsSettings.Upsert("cng", "us");
exists.AccountName.Should().Be("foo");
orig.Should().IsSameOrEqualTo(exists);
orig.Should().BeSameAs(exists);
}
}

View File

@@ -7,18 +7,18 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.3" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.3" />
<PackageReference Include="coverlet.collector" Version="3.0.3">
<PackageReference Include="FluentAssertions" Version="6.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.6" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.6" />
<PackageReference Include="coverlet.collector" Version="3.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\audible api\AudibleApi\_Tests\AudibleApi.Tests\AudibleApi.Tests.csproj" />
<ProjectReference Include="..\..\..\Dinah.Core\_Tests\TestCommon\TestCommon.csproj" />
<ProjectReference Include="..\..\InternalUtilities\InternalUtilities.csproj" />
</ItemGroup>

View File

@@ -7,17 +7,18 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.3" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.3" />
<PackageReference Include="coverlet.collector" Version="3.0.3">
<PackageReference Include="FluentAssertions" Version="6.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.6" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.6" />
<PackageReference Include="coverlet.collector" Version="3.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Dinah.Core\_Tests\TestCommon\TestCommon.csproj" />
<ProjectReference Include="..\..\LibationSearchEngine\LibationSearchEngine.csproj" />
</ItemGroup>

View File

@@ -16,7 +16,6 @@ using Moq;
using Moq.Protected;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using TestCommon;
namespace SearchEngineTests
{

View File

@@ -0,0 +1,54 @@
Thank you for looking at the code for Libation. Here are some guidelines and expectations for contributors.
IDEALS
* Libation has always been and will always be free and open source.
* I work on this as time permits; this is part of the reason Libation is free. I work as much or as little as I feel like. You should feel empowered to do likewise.
* I welcome collaboration, ideas, and helpful criticism. At the end of the day though, the final call is mine. If you disagree, you're welcome to fork the repo. (git terminology is weird. I didn't want to tell you to go fork yourself, but here we are.)
* I believe deeply in respecting user privacy.
- As little personally identifiable info is retained as possible. Log files even contain masked user names.
- Passwords are never stored; only tokens are.
- Passwords are never displayed or logged. Metadata is allowed. Eg: logging debug info like password length can be useful to identify nulls or blank fields.
- Nothing is transmitted back to me unless explicitly initiated by the user. So far this isn't done at all, but I could imagine a future feature where the user could choose to transmit log files.
- The rules are looser for Verbose mode logging and the user is appropriately warned.
* Elephant in the room: piracy. It's impossible to remove DRM without addressing this. I 100% do not endorse or condone piracy. I believe in personal choice and personal responsibility. The user should have the choice of how to access, use, store, manipulate, and backup the content they have purchased. It is their responsibility how they handle this freedom.
STRUCTURE
* Folders in the solution are numbered. Eg: "4 Domain (db)"
* All projects should only refer to other projects in the same folder or to projects in folders with smaller numbers.
* 1 Core Libraries
This is code which has roughly equivilent priority and knowledge as the BCL. In practice, if code is this universal then it doesn't live here long and is instead moved into Dinah.Core.
* 2 Utilities (domain ignorant)
Stand-alone libraries with no knowledge of anything having to do with Libation or other programs. In theory any of these should be able to one day be converted to a nuget pkg
* 3 Domain Internal Utilities (db ignorant)
Can have knowledge of Libation concepts. Cannot access the database.
* 4 Domain (db)
All database access
* 5 Domain Utilities (db aware)
This is often where database, domain-ignorant util.s, and non-database capabilities come together to provide the domain-conscious access points
* 6 Application
UI and application launcher
CODING GUIDELINES
* Follow existing coding style when possible
* No changing things just to be 'cleaner'. You can clean up as you go but 'style wars' serve no one and do not enhance the product. If you go through and change all the namespaces for consistency, I'm going to reject the PR
* I have no tabs vs spaces preference. Do whichever one you want
IDIOSYNCRASIES
* Libation has been a labor of love for years. This passion project has also been a 'forever project' and thus a place to test out new technologies, styles, and ideas. Consistency is not guaranteed. Legacy code is a certainty.
BEING NOTIFIED OF CHANGES
If you're interested in being notified of changes, I recommend subscribing to the github rss feeds:
https://github.com/rmcrackan/Dinah.Core/commits/master.atom
https://github.com/rmcrackan/AudibleApi/commits/master.atom
https://github.com/rmcrackan/Libation/commits/master.atom