mirror of
https://github.com/rmcrackan/Libation.git
synced 2025-12-24 06:28:02 -05:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
726b36de4d | ||
|
|
abd00ff1df | ||
|
|
7b966f6962 | ||
|
|
c0e955d5ef | ||
|
|
bc6f53c8ea | ||
|
|
cefab86ce1 | ||
|
|
249a2f3b59 | ||
|
|
0e9f2c7681 | ||
|
|
d25c32ff45 | ||
|
|
642a500f87 | ||
|
|
0e2469db64 | ||
|
|
9aa4ef70af | ||
|
|
1812fc2c7c | ||
|
|
f9849abb7b | ||
|
|
9cfe8ee6ca | ||
|
|
44e2cef18c | ||
|
|
4dc29affc3 | ||
|
|
2df38706f7 | ||
|
|
f30e9dae6f | ||
|
|
50843e5102 | ||
|
|
a13b00d520 | ||
|
|
b5ebe3db23 | ||
|
|
40f3e4503b | ||
|
|
0d93243b66 | ||
|
|
59c3845d21 | ||
|
|
a3ee3c2881 | ||
|
|
e971d34948 | ||
|
|
2b3f67fb99 | ||
|
|
4509b8c8eb | ||
|
|
2e40bebd7d | ||
|
|
dfc4121ab0 | ||
|
|
3648607d4d | ||
|
|
b22c35f841 | ||
|
|
2795690199 | ||
|
|
b1f92343cf | ||
|
|
9e1d657f60 | ||
|
|
389761355d |
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="16.0.0" />
|
||||
<PackageReference Include="NPOI" Version="2.5.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApiDTOs\AudibleApiDTOs.csproj" />
|
||||
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj" />
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using DtoImporterService;
|
||||
using InternalUtilities;
|
||||
using Serilog;
|
||||
@@ -24,7 +25,7 @@ namespace ApplicationServices
|
||||
var totalCount = importItems.Count;
|
||||
Log.Logger.Information($"GetAllLibraryItems: Total count {totalCount}");
|
||||
|
||||
var newCount = await getNewCountAsync(importItems);
|
||||
var newCount = await importIntoDbAsync(importItems);
|
||||
Log.Logger.Information($"Import: New count {newCount}");
|
||||
|
||||
await Task.Run(() => SearchEngineCommands.FullReIndex());
|
||||
@@ -32,6 +33,23 @@ namespace ApplicationServices
|
||||
|
||||
return (totalCount, newCount);
|
||||
}
|
||||
catch (AudibleApi.Authentication.LoginFailedException lfEx)
|
||||
{
|
||||
lfEx.MoveResponseBodyFile(FileManager.Configuration.Instance.LibationFiles);
|
||||
|
||||
// nuget Serilog.Exceptions would automatically log custom properties
|
||||
// However, it comes with a scary warning when used with EntityFrameworkCore which I'm not yet ready to implement:
|
||||
// https://github.com/RehanSaeed/Serilog.Exceptions
|
||||
// work-around: use 3rd param. don't just put exception object in 3rd param -- info overload: stack trace, etc
|
||||
Log.Logger.Error(lfEx, "Error importing library. Login failed. {@DebugInfo}", new {
|
||||
lfEx.RequestUrl,
|
||||
ResponseStatusCodeNumber = (int)lfEx.ResponseStatusCode,
|
||||
ResponseStatusCodeDesc = lfEx.ResponseStatusCode,
|
||||
lfEx.ResponseInputFields,
|
||||
lfEx.ResponseBodyFilePath
|
||||
});
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Error importing library");
|
||||
@@ -61,21 +79,18 @@ namespace ApplicationServices
|
||||
|
||||
private static async Task<List<ImportItem>> scanAccountAsync(Api api, Account account)
|
||||
{
|
||||
Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||
ArgumentValidator.EnsureNotNull(account, nameof(account));
|
||||
|
||||
var localeName = account.Locale?.Name;
|
||||
Log.Logger.Information("ImportLibraryAsync. {@DebugInfo}", new
|
||||
{
|
||||
account.AccountName,
|
||||
account.AccountId,
|
||||
LocaleName = localeName,
|
||||
Account = account?.MaskedLogEntry ?? "[null]"
|
||||
});
|
||||
|
||||
var dtoItems = await AudibleApiActions.GetLibraryValidatedAsync(api);
|
||||
return dtoItems.Select(d => new ImportItem { DtoItem = d, AccountId = account.AccountId, LocaleName = localeName }).ToList();
|
||||
return dtoItems.Select(d => new ImportItem { DtoItem = d, AccountId = account.AccountId, LocaleName = account.Locale?.Name }).ToList();
|
||||
}
|
||||
|
||||
private static async Task<int> getNewCountAsync(List<ImportItem> importItems)
|
||||
private static async Task<int> importIntoDbAsync(List<ImportItem> importItems)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
var libraryImporter = new LibraryImporter(context);
|
||||
|
||||
268
ApplicationServices/UNTESTED/LibraryExporter.cs
Normal file
268
ApplicationServices/UNTESTED/LibraryExporter.cs
Normal file
@@ -0,0 +1,268 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CsvHelper;
|
||||
using CsvHelper.Configuration.Attributes;
|
||||
using DataLayer;
|
||||
using NPOI.XSSF.UserModel;
|
||||
using Serilog;
|
||||
|
||||
namespace ApplicationServices
|
||||
{
|
||||
public class ExportDto
|
||||
{
|
||||
public static string GetName(string fieldName)
|
||||
{
|
||||
var property = typeof(ExportDto).GetProperty(fieldName);
|
||||
var attribute = property.GetCustomAttributes(typeof(NameAttribute), true)[0];
|
||||
var description = (NameAttribute)attribute;
|
||||
var text = description.Names;
|
||||
return text[0];
|
||||
}
|
||||
|
||||
[Name("Account")]
|
||||
public string Account { get; set; }
|
||||
|
||||
[Name("Date Added to library")]
|
||||
public DateTime DateAdded { get; set; }
|
||||
|
||||
[Name("Audible Product Id")]
|
||||
public string AudibleProductId { get; set; }
|
||||
|
||||
[Name("Locale")]
|
||||
public string Locale { get; set; }
|
||||
|
||||
[Name("Title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
[Name("Authors")]
|
||||
public string AuthorNames { get; set; }
|
||||
|
||||
[Name("Narrators")]
|
||||
public string NarratorNames { get; set; }
|
||||
|
||||
[Name("Length In Minutes")]
|
||||
public int LengthInMinutes { get; set; }
|
||||
|
||||
[Name("Publisher")]
|
||||
public string Publisher { get; set; }
|
||||
|
||||
[Name("Pdf url")]
|
||||
public string PdfUrl { get; set; }
|
||||
|
||||
[Name("Series Names")]
|
||||
public string SeriesNames { get; set; }
|
||||
|
||||
[Name("Series Order")]
|
||||
public string SeriesOrder { get; set; }
|
||||
|
||||
[Name("Community Rating: Overall")]
|
||||
public float? CommunityRatingOverall { get; set; }
|
||||
|
||||
[Name("Community Rating: Performance")]
|
||||
public float? CommunityRatingPerformance { get; set; }
|
||||
|
||||
[Name("Community Rating: Story")]
|
||||
public float? CommunityRatingStory { get; set; }
|
||||
|
||||
[Name("Cover Id")]
|
||||
public string PictureId { get; set; }
|
||||
|
||||
[Name("Is Abridged?")]
|
||||
public bool IsAbridged { get; set; }
|
||||
|
||||
[Name("Date Published")]
|
||||
public DateTime? DatePublished { get; set; }
|
||||
|
||||
[Name("Categories")]
|
||||
public string CategoriesNames { get; set; }
|
||||
|
||||
[Name("My Rating: Overall")]
|
||||
public float? MyRatingOverall { get; set; }
|
||||
|
||||
[Name("My Rating: Performance")]
|
||||
public float? MyRatingPerformance { get; set; }
|
||||
|
||||
[Name("My Rating: Story")]
|
||||
public float? MyRatingStory { get; set; }
|
||||
|
||||
[Name("My Libation Tags")]
|
||||
public string MyLibationTags { get; set; }
|
||||
}
|
||||
public static class LibToDtos
|
||||
{
|
||||
public static List<ExportDto> ToDtos(this IEnumerable<LibraryBook> library)
|
||||
=> library.Select(a => new ExportDto
|
||||
{
|
||||
Account = a.Account,
|
||||
DateAdded = a.DateAdded,
|
||||
AudibleProductId = a.Book.AudibleProductId,
|
||||
Locale = a.Book.Locale,
|
||||
Title = a.Book.Title,
|
||||
AuthorNames = a.Book.AuthorNames,
|
||||
NarratorNames = a.Book.NarratorNames,
|
||||
LengthInMinutes = a.Book.LengthInMinutes,
|
||||
Publisher = a.Book.Publisher,
|
||||
PdfUrl = a.Book.Supplements?.FirstOrDefault()?.Url,
|
||||
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,
|
||||
CommunityRatingPerformance = a.Book.Rating?.PerformanceRating,
|
||||
CommunityRatingStory = a.Book.Rating?.StoryRating,
|
||||
PictureId = a.Book.PictureId,
|
||||
IsAbridged = a.Book.IsAbridged,
|
||||
DatePublished = a.Book.DatePublished,
|
||||
CategoriesNames = a.Book.CategoriesNames.Any() ? a.Book.CategoriesNames.Aggregate((a, b) => $"{a}, {b}") : "",
|
||||
MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating,
|
||||
MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating,
|
||||
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,
|
||||
MyLibationTags = a.Book.UserDefinedItem.Tags
|
||||
}).ToList();
|
||||
}
|
||||
public static class LibraryExporter
|
||||
{
|
||||
public static void ToCsv(string saveFilePath)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
var dtos = context.GetLibrary_Flat_NoTracking().ToDtos();
|
||||
|
||||
if (!dtos.Any())
|
||||
return;
|
||||
|
||||
using var writer = new System.IO.StreamWriter(saveFilePath);
|
||||
using var csv = new CsvWriter(writer, System.Globalization.CultureInfo.CurrentCulture);
|
||||
|
||||
csv.WriteHeader(typeof(ExportDto));
|
||||
csv.NextRecord();
|
||||
csv.WriteRecords(dtos);
|
||||
}
|
||||
|
||||
public static void ToJson(string saveFilePath)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
var dtos = context.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 workbook = new XSSFWorkbook();
|
||||
var sheet = workbook.CreateSheet("Library");
|
||||
|
||||
var detailSubtotalFont = workbook.CreateFont();
|
||||
detailSubtotalFont.IsBold = true;
|
||||
|
||||
var detailSubtotalCellStyle = workbook.CreateCellStyle();
|
||||
detailSubtotalCellStyle.SetFont(detailSubtotalFont);
|
||||
|
||||
// headers
|
||||
var rowIndex = 0;
|
||||
var row = sheet.CreateRow(rowIndex);
|
||||
|
||||
var columns = new[] {
|
||||
nameof (ExportDto.Account),
|
||||
nameof (ExportDto.DateAdded),
|
||||
nameof (ExportDto.AudibleProductId),
|
||||
nameof (ExportDto.Locale),
|
||||
nameof (ExportDto.Title),
|
||||
nameof (ExportDto.AuthorNames),
|
||||
nameof (ExportDto.NarratorNames),
|
||||
nameof (ExportDto.LengthInMinutes),
|
||||
nameof (ExportDto.Publisher),
|
||||
nameof (ExportDto.PdfUrl),
|
||||
nameof (ExportDto.SeriesNames),
|
||||
nameof (ExportDto.SeriesOrder),
|
||||
nameof (ExportDto.CommunityRatingOverall),
|
||||
nameof (ExportDto.CommunityRatingPerformance),
|
||||
nameof (ExportDto.CommunityRatingStory),
|
||||
nameof (ExportDto.PictureId),
|
||||
nameof (ExportDto.IsAbridged),
|
||||
nameof (ExportDto.DatePublished),
|
||||
nameof (ExportDto.CategoriesNames),
|
||||
nameof (ExportDto.MyRatingOverall),
|
||||
nameof (ExportDto.MyRatingPerformance),
|
||||
nameof (ExportDto.MyRatingStory),
|
||||
nameof (ExportDto.MyLibationTags)
|
||||
};
|
||||
var col = 0;
|
||||
foreach (var c in columns)
|
||||
{
|
||||
var cell = row.CreateCell(col++);
|
||||
var name = ExportDto.GetName(c);
|
||||
cell.SetCellValue(name);
|
||||
cell.CellStyle = detailSubtotalCellStyle;
|
||||
}
|
||||
|
||||
var dateFormat = workbook.CreateDataFormat();
|
||||
var dateStyle = workbook.CreateCellStyle();
|
||||
dateStyle.DataFormat = dateFormat.GetFormat("MM/dd/yyyy HH:mm:ss");
|
||||
|
||||
rowIndex++;
|
||||
|
||||
// Add data rows
|
||||
foreach (var dto in dtos)
|
||||
{
|
||||
col = 0;
|
||||
|
||||
row = sheet.CreateRow(rowIndex);
|
||||
|
||||
row.CreateCell(col++).SetCellValue(dto.Account);
|
||||
|
||||
var dateAddedCell = row.CreateCell(col++);
|
||||
dateAddedCell.CellStyle = dateStyle;
|
||||
dateAddedCell.SetCellValue(dto.DateAdded);
|
||||
|
||||
row.CreateCell(col++).SetCellValue(dto.AudibleProductId);
|
||||
row.CreateCell(col++).SetCellValue(dto.Locale);
|
||||
row.CreateCell(col++).SetCellValue(dto.Title);
|
||||
row.CreateCell(col++).SetCellValue(dto.AuthorNames);
|
||||
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.SeriesNames);
|
||||
row.CreateCell(col++).SetCellValue(dto.SeriesOrder);
|
||||
|
||||
col = createCell(row, col, dto.CommunityRatingOverall);
|
||||
col = createCell(row, col, dto.CommunityRatingPerformance);
|
||||
col = createCell(row, col, dto.CommunityRatingStory);
|
||||
|
||||
row.CreateCell(col++).SetCellValue(dto.PictureId);
|
||||
row.CreateCell(col++).SetCellValue(dto.IsAbridged);
|
||||
|
||||
var datePubCell = row.CreateCell(col++);
|
||||
datePubCell.CellStyle = dateStyle;
|
||||
if (dto.DatePublished.HasValue)
|
||||
datePubCell.SetCellValue(dto.DatePublished.Value);
|
||||
else
|
||||
datePubCell.SetCellValue("");
|
||||
|
||||
row.CreateCell(col++).SetCellValue(dto.CategoriesNames);
|
||||
|
||||
col = createCell(row, col, dto.MyRatingOverall);
|
||||
col = createCell(row, col, dto.MyRatingPerformance);
|
||||
col = createCell(row, col, dto.MyRatingStory);
|
||||
|
||||
row.CreateCell(col++).SetCellValue(dto.MyLibationTags);
|
||||
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
using var fileData = new System.IO.FileStream(saveFilePath, System.IO.FileMode.Create);
|
||||
workbook.Write(fileData);
|
||||
}
|
||||
private static int createCell(NPOI.SS.UserModel.IRow row, int col, float? nullableFloat)
|
||||
{
|
||||
if (nullableFloat.HasValue)
|
||||
row.CreateCell(col++).SetCellValue(nullableFloat.Value);
|
||||
else
|
||||
row.CreateCell(col++).SetCellValue("");
|
||||
return col;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.IO;
|
||||
using System;
|
||||
using System.IO;
|
||||
using DataLayer;
|
||||
using LibationSearchEngine;
|
||||
|
||||
@@ -12,31 +13,43 @@ namespace ApplicationServices
|
||||
engine.CreateNewIndex();
|
||||
}
|
||||
|
||||
public static SearchResultSet Search(string searchString)
|
||||
public static SearchResultSet Search(string searchString) => performSearchEngineFunc_safe(e =>
|
||||
e.Search(searchString)
|
||||
);
|
||||
|
||||
public static void UpdateBookTags(Book book) => performSearchEngineAction_safe(e =>
|
||||
e.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags)
|
||||
);
|
||||
|
||||
public static void UpdateIsLiberated(Book book) => performSearchEngineAction_safe(e =>
|
||||
e.UpdateIsLiberated(book.AudibleProductId)
|
||||
);
|
||||
|
||||
private static void performSearchEngineAction_safe(Action<SearchEngine> action)
|
||||
{
|
||||
var engine = new SearchEngine(DbContexts.GetContext());
|
||||
try
|
||||
{
|
||||
return engine.Search(searchString);
|
||||
action(engine);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
FullReIndex();
|
||||
return engine.Search(searchString);
|
||||
action(engine);
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateBookTags(Book book)
|
||||
private static T performSearchEngineFunc_safe<T>(Func<SearchEngine, T> action)
|
||||
{
|
||||
var engine = new SearchEngine(DbContexts.GetContext());
|
||||
try
|
||||
{
|
||||
engine.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags);
|
||||
return action(engine);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
FullReIndex();
|
||||
engine.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags);
|
||||
return action(engine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp3.1;netstandard2.1</TargetFrameworks>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -12,13 +12,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.7">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.7">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -62,15 +62,27 @@ namespace FileLiberator
|
||||
if (outputAudioFilename == null)
|
||||
return new StatusHandler { "Decrypt failed" };
|
||||
|
||||
moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
|
||||
var destinationDir = moveFilesToBooksDir(libraryBook.Book, outputAudioFilename);
|
||||
|
||||
Dinah.Core.IO.FileExt.SafeDelete(aaxFilename);
|
||||
var config = Configuration.Instance;
|
||||
if (config.RetainAaxFiles)
|
||||
{
|
||||
var newAaxFilename = FileUtility.GetValidFilename(
|
||||
destinationDir,
|
||||
Path.GetFileNameWithoutExtension(aaxFilename),
|
||||
"aax");
|
||||
File.Move(aaxFilename, newAaxFilename);
|
||||
}
|
||||
else
|
||||
{
|
||||
Dinah.Core.IO.FileExt.SafeDelete(aaxFilename);
|
||||
}
|
||||
|
||||
var statusHandler = new StatusHandler();
|
||||
var finalAudioExists = AudibleFileStorage.Audio.Exists(libraryBook.Book.AudibleProductId);
|
||||
if (!finalAudioExists)
|
||||
statusHandler.AddError("Cannot find final audio file after decryption");
|
||||
return statusHandler;
|
||||
return new StatusHandler { "Cannot find final audio file after decryption" };
|
||||
|
||||
return new StatusHandler();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -120,12 +132,12 @@ namespace FileLiberator
|
||||
}
|
||||
}
|
||||
|
||||
private static void moveFilesToBooksDir(Book product, string outputAudioFilename)
|
||||
private static string moveFilesToBooksDir(Book product, string outputAudioFilename)
|
||||
{
|
||||
// create final directory. move each file into it. MOVE AUDIO FILE LAST
|
||||
// new dir: safetitle_limit50char + " [" + productId + "]"
|
||||
|
||||
var destinationDir = getDestDir(product);
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestDir(product.Title, product.AudibleProductId);
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
|
||||
var sortedFiles = getProductFilesSorted(product, outputAudioFilename);
|
||||
@@ -142,19 +154,9 @@ namespace FileLiberator
|
||||
|
||||
File.Move(f.FullName, dest);
|
||||
}
|
||||
}
|
||||
|
||||
private static string getDestDir(Book product)
|
||||
{
|
||||
// to prevent the paths from getting too long, we don't need after the 1st ":" for the folder
|
||||
var underscoreIndex = product.Title.IndexOf(':');
|
||||
var titleDir
|
||||
= underscoreIndex < 4
|
||||
? product.Title
|
||||
: product.Title.Substring(0, underscoreIndex);
|
||||
var finalDir = FileUtility.GetValidFilename(AudibleFileStorage.BooksDirectory, titleDir, null, product.AudibleProductId);
|
||||
return finalDir;
|
||||
}
|
||||
return destinationDir;
|
||||
}
|
||||
|
||||
private static List<FileInfo> getProductFilesSorted(Book product, string outputAudioFilename)
|
||||
{
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace FileLiberator
|
||||
{
|
||||
validate(libraryBook);
|
||||
|
||||
var api = await AudibleApiActions.GetApiAsync(libraryBook.Account, libraryBook.Book.Locale);
|
||||
var api = await GetApiAsync(libraryBook);
|
||||
|
||||
var actualFilePath = await PerformDownloadAsync(
|
||||
tempAaxFilename,
|
||||
@@ -72,7 +72,7 @@ namespace FileLiberator
|
||||
libraryBook.Book.Title,
|
||||
libraryBook.Book.AudibleProductId,
|
||||
libraryBook.Book.Locale,
|
||||
libraryBook.Account,
|
||||
Account = libraryBook.Account?.ToMask() ?? "[empty]",
|
||||
tempAaxFilename,
|
||||
actualFilePath,
|
||||
length,
|
||||
|
||||
@@ -31,17 +31,24 @@ namespace FileLiberator
|
||||
private static string getProposedDownloadFilePath(LibraryBook libraryBook)
|
||||
{
|
||||
// if audio file exists, get it's dir. else return base Book dir
|
||||
var destinationDir =
|
||||
// this is safe b/c GetDirectoryName(null) == null
|
||||
Path.GetDirectoryName(AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId))
|
||||
?? AudibleFileStorage.PDF.StorageDirectory;
|
||||
var existingPath = Path.GetDirectoryName(AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId));
|
||||
var file = getdownloadUrl(libraryBook);
|
||||
|
||||
return Path.Combine(destinationDir, Path.GetFileName(getdownloadUrl(libraryBook)));
|
||||
if (existingPath != null)
|
||||
return Path.Combine(existingPath, Path.GetFileName(file));
|
||||
|
||||
var full = FileUtility.GetValidFilename(
|
||||
AudibleFileStorage.PDF.StorageDirectory,
|
||||
libraryBook.Book.Title,
|
||||
Path.GetExtension(file),
|
||||
libraryBook.Book.AudibleProductId);
|
||||
return full;
|
||||
}
|
||||
|
||||
private async Task downloadPdfAsync(LibraryBook libraryBook, string proposedDownloadFilePath)
|
||||
{
|
||||
var downloadUrl = getdownloadUrl(libraryBook);
|
||||
var api = await GetApiAsync(libraryBook);
|
||||
var downloadUrl = await api.GetPdfDownloadLinkAsync(libraryBook.Book.AudibleProductId);
|
||||
|
||||
var client = new HttpClient();
|
||||
var actualDownloadedFilePath = await PerformDownloadAsync(
|
||||
|
||||
@@ -38,6 +38,9 @@ namespace FileLiberator
|
||||
}
|
||||
}
|
||||
|
||||
protected static Task<AudibleApi.Api> GetApiAsync(LibraryBook libraryBook)
|
||||
=> InternalUtilities.AudibleApiActions.GetApiAsync(libraryBook.Account, libraryBook.Book.Locale);
|
||||
|
||||
protected async Task<string> PerformDownloadAsync(string proposedDownloadFilePath, Func<Progress<DownloadProgress>, Task<string>> func)
|
||||
{
|
||||
var progress = new Progress<DownloadProgress>();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
|
||||
namespace FileLiberator
|
||||
@@ -15,64 +17,47 @@ namespace FileLiberator
|
||||
//
|
||||
|
||||
|
||||
/// <summary>Process the first valid product. Create default context</summary>
|
||||
/// <returns>Returns either the status handler from the process, or null if all books have been processed</returns>
|
||||
public static async Task<StatusHandler> ProcessFirstValidAsync(this IProcessable processable)
|
||||
{
|
||||
var libraryBook = processable.getNextValidBook();
|
||||
if (libraryBook == null)
|
||||
return null;
|
||||
// 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));
|
||||
|
||||
return await processBookAsync(processable, libraryBook);
|
||||
}
|
||||
|
||||
/// <summary>Process the first valid product. Create default context</summary>
|
||||
/// <returns>Returns either the status handler from the process, or null if all books have been processed</returns>
|
||||
public static async Task<StatusHandler> ProcessSingleAsync(this IProcessable processable, string productId)
|
||||
public static LibraryBook GetSingleLibraryBook(string productId)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
var libraryBook = context
|
||||
.Library
|
||||
.GetLibrary()
|
||||
.SingleOrDefault(lb => lb.Book.AudibleProductId == productId);
|
||||
return libraryBook;
|
||||
}
|
||||
|
||||
if (libraryBook == null)
|
||||
return null;
|
||||
public static async Task<StatusHandler> ProcessSingleAsync(this IProcessable processable, LibraryBook libraryBook)
|
||||
{
|
||||
if (!processable.Validate(libraryBook))
|
||||
return new StatusHandler { "Validation failed" };
|
||||
|
||||
return await processBookAsync(processable, libraryBook);
|
||||
return await processable.ProcessBookAsync_NoValidation(libraryBook);
|
||||
}
|
||||
|
||||
private static async Task<StatusHandler> processBookAsync(IProcessable processable, LibraryBook libraryBook)
|
||||
public static async Task<StatusHandler> ProcessBookAsync_NoValidation(this IProcessable processable, LibraryBook libraryBook)
|
||||
{
|
||||
Serilog.Log.Information("Begin " + nameof(processBookAsync) + " {@DebugInfo}", new
|
||||
Serilog.Log.Logger.Information("Begin " + nameof(ProcessBookAsync_NoValidation) + " {@DebugInfo}", new
|
||||
{
|
||||
libraryBook.Book.Title,
|
||||
libraryBook.Book.AudibleProductId,
|
||||
libraryBook.Book.Locale,
|
||||
libraryBook.Account
|
||||
Account = libraryBook.Account?.ToMask() ?? "[empty]"
|
||||
});
|
||||
|
||||
// this should never happen. check anyway. ProcessFirstValidAsync returning null is the signal that we're done. we can't let another IProcessable accidentally send this command
|
||||
var status = await processable.ProcessAsync(libraryBook);
|
||||
if (status == null)
|
||||
throw new Exception("Processable should never return a null status");
|
||||
var status
|
||||
= (await processable.ProcessAsync(libraryBook))
|
||||
?? new StatusHandler { "Processable should never return a null status" };
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
private static LibraryBook getNextValidBook(this IProcessable processable)
|
||||
{
|
||||
var libraryBooks = DbContexts.GetContext().GetLibrary_Flat_NoTracking();
|
||||
|
||||
foreach (var libraryBook in libraryBooks)
|
||||
if (processable.Validate(libraryBook))
|
||||
return libraryBook;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static async Task<StatusHandler> TryProcessAsync(this IProcessable processable, LibraryBook libraryBook)
|
||||
=> processable.Validate(libraryBook)
|
||||
? await processable.ProcessAsync(libraryBook)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -11,76 +11,77 @@ namespace FileManager
|
||||
// could add images here, but for now images are stored in a well-known location
|
||||
public enum FileType { Unknown, Audio, AAX, PDF }
|
||||
|
||||
/// <summary>
|
||||
/// Files are large. File contents are never read by app.
|
||||
/// Paths are varied.
|
||||
/// Files are written during download/decrypt/backup/liberate.
|
||||
/// Paths are read at app launch and during download/decrypt/backup/liberate.
|
||||
/// Many files are often looked up at once
|
||||
/// </summary>
|
||||
public sealed class AudibleFileStorage : Enumeration<AudibleFileStorage>
|
||||
{
|
||||
#region static
|
||||
public static AudibleFileStorage Audio { get; }
|
||||
public static AudibleFileStorage AAX { get; }
|
||||
public static AudibleFileStorage PDF { get; }
|
||||
/// <summary>
|
||||
/// Files are large. File contents are never read by app.
|
||||
/// Paths are varied.
|
||||
/// Files are written during download/decrypt/backup/liberate.
|
||||
/// Paths are read at app launch and during download/decrypt/backup/liberate.
|
||||
/// Many files are often looked up at once
|
||||
/// </summary>
|
||||
public abstract class AudibleFileStorage : Enumeration<AudibleFileStorage>
|
||||
{
|
||||
public abstract string[] Extensions { get; }
|
||||
public abstract string StorageDirectory { get; }
|
||||
|
||||
public static string DownloadsInProgress { get; }
|
||||
public static string DecryptInProgress { get; }
|
||||
public static string BooksDirectory => Configuration.Instance.Books;
|
||||
// not customizable. don't move to config
|
||||
public static string DownloadsFinal { get; }
|
||||
= new DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("DownloadsFinal").FullName;
|
||||
#region static
|
||||
public static AudioFileStorage Audio { get; } = new AudioFileStorage();
|
||||
public static AudibleFileStorage AAX { get; } = new AaxFileStorage();
|
||||
public static AudibleFileStorage PDF { get; } = new PdfFileStorage();
|
||||
|
||||
static AudibleFileStorage()
|
||||
public static string DownloadsInProgress
|
||||
{
|
||||
#region init DecryptInProgress
|
||||
if (!Configuration.Instance.DecryptInProgressEnum.In("WinTemp", "LibationFiles"))
|
||||
Configuration.Instance.DecryptInProgressEnum = "WinTemp";
|
||||
var M4bRootDir
|
||||
= Configuration.Instance.DecryptInProgressEnum == "WinTemp" // else "LibationFiles"
|
||||
? Configuration.WinTemp
|
||||
: Configuration.Instance.LibationFiles;
|
||||
DecryptInProgress = Path.Combine(M4bRootDir, "DecryptInProgress");
|
||||
Directory.CreateDirectory(DecryptInProgress);
|
||||
#endregion
|
||||
get
|
||||
{
|
||||
if (!Configuration.Instance.DownloadsInProgressEnum.In("WinTemp", "LibationFiles"))
|
||||
Configuration.Instance.DownloadsInProgressEnum = "WinTemp";
|
||||
var AaxRootDir
|
||||
= Configuration.Instance.DownloadsInProgressEnum == "WinTemp"
|
||||
? Configuration.WinTemp
|
||||
: Configuration.Instance.LibationFiles;
|
||||
|
||||
#region init DownloadsInProgress
|
||||
if (!Configuration.Instance.DownloadsInProgressEnum.In("WinTemp", "LibationFiles"))
|
||||
Configuration.Instance.DownloadsInProgressEnum = "WinTemp";
|
||||
var AaxRootDir
|
||||
= Configuration.Instance.DownloadsInProgressEnum == "WinTemp" // else "LibationFiles"
|
||||
? Configuration.WinTemp
|
||||
: Configuration.Instance.LibationFiles;
|
||||
DownloadsInProgress = Path.Combine(AaxRootDir, "DownloadsInProgress");
|
||||
Directory.CreateDirectory(DownloadsInProgress);
|
||||
#endregion
|
||||
return Directory.CreateDirectory(Path.Combine(AaxRootDir, "DownloadsInProgress")).FullName;
|
||||
}
|
||||
}
|
||||
|
||||
#region init BooksDirectory
|
||||
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
|
||||
Configuration.Instance.Books = Path.Combine(Configuration.Instance.LibationFiles, "Books");
|
||||
Directory.CreateDirectory(Configuration.Instance.Books);
|
||||
#endregion
|
||||
public static string DecryptInProgress
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Configuration.Instance.DecryptInProgressEnum.In("WinTemp", "LibationFiles"))
|
||||
Configuration.Instance.DecryptInProgressEnum = "WinTemp";
|
||||
|
||||
// must do this in static ctor, not w/inline properties
|
||||
// static properties init before static ctor so these dir.s would still be null
|
||||
Audio = new AudibleFileStorage(FileType.Audio, BooksDirectory, "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac");
|
||||
AAX = new AudibleFileStorage(FileType.AAX, DownloadsFinal, "aax");
|
||||
PDF = new AudibleFileStorage(FileType.PDF, BooksDirectory, "pdf", "zip");
|
||||
var M4bRootDir
|
||||
= Configuration.Instance.DecryptInProgressEnum == "WinTemp"
|
||||
? Configuration.WinTemp
|
||||
: Configuration.Instance.LibationFiles;
|
||||
|
||||
return Directory.CreateDirectory(Path.Combine(M4bRootDir, "DecryptInProgress")).FullName;
|
||||
}
|
||||
}
|
||||
|
||||
// not customizable. don't move to config
|
||||
public static string DownloadsFinal => new DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("DownloadsFinal").FullName;
|
||||
|
||||
public static string BooksDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
|
||||
Configuration.Instance.Books = Path.Combine(Configuration.Instance.LibationFiles, "Books");
|
||||
return Directory.CreateDirectory(Configuration.Instance.Books).FullName;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region instance
|
||||
public FileType FileType => (FileType)Value;
|
||||
|
||||
public string StorageDirectory => DisplayName;
|
||||
|
||||
private IEnumerable<string> extensions_noDots { get; }
|
||||
private string extAggr { get; }
|
||||
|
||||
private AudibleFileStorage(FileType fileType, string storageDirectory, params string[] extensions) : base((int)fileType, storageDirectory)
|
||||
protected AudibleFileStorage(FileType fileType) : base((int)fileType, fileType.ToString())
|
||||
{
|
||||
extensions_noDots = extensions.Select(ext => ext.Trim('.')).ToList();
|
||||
extensions_noDots = Extensions.Select(ext => ext.Trim('.')).ToList();
|
||||
extAggr = extensions_noDots.Aggregate((a, b) => $"{a}|{b}");
|
||||
}
|
||||
|
||||
@@ -90,30 +91,89 @@ namespace FileManager
|
||||
/// - a directory name has the product id and an audio file is immediately inside
|
||||
/// - any audio filename contains the product id
|
||||
/// </summary>
|
||||
public bool Exists(string productId)
|
||||
=> GetPath(productId) != null;
|
||||
public bool Exists(string productId) => GetPath(productId) != null;
|
||||
|
||||
public string GetPath(string productId)
|
||||
{
|
||||
{
|
||||
var cachedFile = FilePathCache.GetPath(productId, FileType);
|
||||
if (cachedFile != null)
|
||||
return cachedFile;
|
||||
}
|
||||
var cachedFile = FilePathCache.GetPath(productId, FileType);
|
||||
if (cachedFile != null)
|
||||
return cachedFile;
|
||||
|
||||
var firstOrNull =
|
||||
var firstOrNull =
|
||||
Directory
|
||||
.EnumerateFiles(StorageDirectory, "*.*", SearchOption.AllDirectories)
|
||||
.FirstOrDefault(s => Regex.IsMatch(s, $@"{productId}.*?\.({extAggr})$", RegexOptions.IgnoreCase));
|
||||
|
||||
if (firstOrNull is null)
|
||||
return null;
|
||||
|
||||
FilePathCache.Upsert(productId, FileType, firstOrNull);
|
||||
return firstOrNull;
|
||||
}
|
||||
|
||||
public string GetDestDir(string title, string asin)
|
||||
{
|
||||
// to prevent the paths from getting too long, we don't need after the 1st ":" for the folder
|
||||
var underscoreIndex = title.IndexOf(':');
|
||||
var titleDir
|
||||
= underscoreIndex < 4
|
||||
? title
|
||||
: title.Substring(0, underscoreIndex);
|
||||
var finalDir = FileUtility.GetValidFilename(StorageDirectory, titleDir, null, asin);
|
||||
return finalDir;
|
||||
}
|
||||
|
||||
public bool IsFileTypeMatch(FileInfo fileInfo)
|
||||
=> extensions_noDots.ContainsInsensative(fileInfo.Extension.Trim('.'));
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class AudioFileStorage : AudibleFileStorage
|
||||
{
|
||||
public const string SKIP_FILE_EXT = "libhack";
|
||||
|
||||
public override string[] Extensions { get; } = new[] { "m4b", "mp3", "aac", "mp4", "m4a", "ogg", "flac", SKIP_FILE_EXT };
|
||||
|
||||
// we always want to use the latest config value, therefore
|
||||
// - DO use 'get' arrow "=>"
|
||||
// - do NOT use assign "="
|
||||
public override string StorageDirectory => BooksDirectory;
|
||||
|
||||
public AudioFileStorage() : base(FileType.Audio) { }
|
||||
|
||||
public string CreateSkipFile(string title, string asin, string contents = null)
|
||||
{
|
||||
var destinationDir = GetDestDir(title, asin);
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
|
||||
var path = FileUtility.GetValidFilename(destinationDir, title, SKIP_FILE_EXT, asin);
|
||||
File.WriteAllText(path, contents ?? string.Empty);
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
public class AaxFileStorage : AudibleFileStorage
|
||||
{
|
||||
public override string[] Extensions { get; } = new[] { "aax" };
|
||||
|
||||
// we always want to use the latest config value, therefore
|
||||
// - DO use 'get' arrow "=>"
|
||||
// - do NOT use assign "="
|
||||
public override string StorageDirectory => DownloadsFinal;
|
||||
|
||||
public AaxFileStorage() : base(FileType.AAX) { }
|
||||
}
|
||||
|
||||
public class PdfFileStorage : AudibleFileStorage
|
||||
{
|
||||
public override string[] Extensions { get; } = new[] { "pdf", "zip" };
|
||||
|
||||
// we always want to use the latest config value, therefore
|
||||
// - DO use 'get' arrow "=>"
|
||||
// - do NOT use assign "="
|
||||
public override string StorageDirectory => BooksDirectory;
|
||||
|
||||
public PdfFileStorage() : base(FileType.PDF) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,13 @@ namespace FileManager
|
||||
set => persistentDictionary.Set(nameof(DecryptInProgressEnum), value);
|
||||
}
|
||||
|
||||
[Description("Retain .aax files after decrypting?")]
|
||||
public bool RetainAaxFiles
|
||||
{
|
||||
get => persistentDictionary.Get<bool>(nameof(RetainAaxFiles));
|
||||
set => persistentDictionary.Set(nameof(RetainAaxFiles), value);
|
||||
}
|
||||
|
||||
// note: any potential file manager static ctors can't compensate if storage dir is changed at run time via settings. this is partly bad architecture. but the side effect is desirable. if changing LibationFiles location: restart app
|
||||
|
||||
// singleton stuff
|
||||
|
||||
@@ -40,8 +40,13 @@ namespace FileManager
|
||||
return stringCache[propertyName];
|
||||
}
|
||||
|
||||
public T Get<T>(string propertyName) where T : class
|
||||
=> GetObject(propertyName) is T obj ? obj : default;
|
||||
public T Get<T>(string propertyName)
|
||||
{
|
||||
var o = GetObject(propertyName);
|
||||
if (o is null) return default;
|
||||
if (o is JToken jt) return jt.Value<T>();
|
||||
return (T)o;
|
||||
}
|
||||
|
||||
public object GetObject(string propertyName)
|
||||
{
|
||||
|
||||
@@ -93,5 +93,11 @@ namespace InternalUtilities
|
||||
}
|
||||
|
||||
public override string ToString() => $"{AccountId} - {Locale?.Name ?? "[empty]"}";
|
||||
|
||||
public string MaskedLogEntry => @$"AccountId={mask(AccountId)}|AccountName={mask(AccountName)}|Locale={Locale?.Name ?? "[empty]"}";
|
||||
private static string mask(string str)
|
||||
=> str is null ? "[null]"
|
||||
: str == string.Empty ? "[empty]"
|
||||
: str.ToMask();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AudibleApi;
|
||||
using AudibleApiDTOs;
|
||||
using Dinah.Core;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
|
||||
@@ -16,7 +17,7 @@ namespace InternalUtilities
|
||||
{
|
||||
Serilog.Log.Logger.Information("GetApiAsync. {@DebugInfo}", new
|
||||
{
|
||||
username,
|
||||
Username = username.ToMask(),
|
||||
LocaleName = localeName,
|
||||
});
|
||||
return EzApiCreator.GetApiAsync(
|
||||
@@ -31,7 +32,7 @@ namespace InternalUtilities
|
||||
{
|
||||
Serilog.Log.Logger.Information("GetApiAsync. {@DebugInfo}", new
|
||||
{
|
||||
AccountId = account?.AccountId ?? "[empty]",
|
||||
Account = account?.MaskedLogEntry ?? "[null]",
|
||||
LocaleName = account?.Locale?.Name
|
||||
});
|
||||
return EzApiCreator.GetApiAsync(
|
||||
|
||||
@@ -86,6 +86,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0 Libation Tests", "0 Libat
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InternalUtilities.Tests", "_Tests\InternalUtilities.Tests\InternalUtilities.Tests.csproj", "{8447C956-B03E-4F59-9DD4-877793B849D9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibationSearchEngine.Tests", "_Tests\LibationSearchEngine.Tests\LibationSearchEngine.Tests.csproj", "{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -208,6 +210,10 @@ Global
|
||||
{8447C956-B03E-4F59-9DD4-877793B849D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8447C956-B03E-4F59-9DD4-877793B849D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8447C956-B03E-4F59-9DD4-877793B849D9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C5B21768-C7C9-4FCB-AC1E-187B223D5A98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{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
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -242,6 +248,7 @@ Global
|
||||
{E7EFD64D-6630-4426-B09C-B6862A92E3FD} = {F0CBB7A7-D3FB-41FF-8F47-CF3F6A592249}
|
||||
{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}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon>libation.ico</ApplicationIcon>
|
||||
<AssemblyName>Libation</AssemblyName>
|
||||
@@ -13,7 +13,7 @@
|
||||
<!-- <PublishSingleFile>true</PublishSingleFile> -->
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
|
||||
<Version>4.0.1.0</Version>
|
||||
<Version>4.2.2.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -27,7 +27,8 @@ namespace LibationLauncher
|
||||
|
||||
AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
|
||||
migrate_v3_to_v4();
|
||||
migrate_to_v4_0_0();
|
||||
migrate_to_v4_0_3(); // add setting for whether to delete/retain aax
|
||||
|
||||
ensureLoggingConfig();
|
||||
ensureSerilogConfig();
|
||||
@@ -83,7 +84,7 @@ namespace LibationLauncher
|
||||
#region v3 => v4 migration
|
||||
static string AccountsSettingsFileLegacy30 => Path.Combine(Configuration.Instance.LibationFiles, "IdentityTokens.json");
|
||||
|
||||
private static void migrate_v3_to_v4()
|
||||
private static void migrate_to_v4_0_0()
|
||||
{
|
||||
migrateLegacyIdentityFile();
|
||||
|
||||
@@ -205,6 +206,27 @@ namespace LibationLauncher
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region migrate_to_v4_0_3 add setting for whether to delete/retain aax
|
||||
private static void migrate_to_v4_0_3()
|
||||
{
|
||||
if (!File.Exists(Configuration.Instance.SettingsFilePath))
|
||||
return;
|
||||
|
||||
// use JObject to remove decrypt key and locale from Settings.json
|
||||
var settingsContents = File.ReadAllText(Configuration.Instance.SettingsFilePath);
|
||||
var jObj = JObject.Parse(settingsContents);
|
||||
|
||||
var jRetainAaxFiles = jObj.Property("RetainAaxFiles");
|
||||
if (jRetainAaxFiles is null)
|
||||
{
|
||||
jObj.Add("RetainAaxFiles", false);
|
||||
|
||||
var newContents = jObj.ToString(Formatting.Indented);
|
||||
File.WriteAllText(Configuration.Instance.SettingsFilePath, newContents);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
private static string defaultLoggingLevel { get; } = "Information";
|
||||
private static void ensureLoggingConfig()
|
||||
{
|
||||
@@ -301,8 +323,8 @@ namespace LibationLauncher
|
||||
.AddJsonFile(config.SettingsFilePath)
|
||||
.Build();
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(configuration)
|
||||
.CreateLogger();
|
||||
.ReadFrom.Configuration(configuration)
|
||||
.CreateLogger();
|
||||
|
||||
//// MANUAL HARD CODED
|
||||
//Log.Logger = new LoggerConfiguration()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -42,5 +42,56 @@ namespace LibationSearchEngine
|
||||
// positive look behind: beginning space { [ :
|
||||
// positive look ahead: end space ] }
|
||||
public static Regex NumbersRegex { get; } = new Regex(@"(?<=^|\s|\{|\[|:)(\d+\.?\d*)(?=$|\s|\]|\})", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// proper bools are single keywords which are turned into keyword:True
|
||||
/// if bordered by colons or inside brackets, they are not stand-alone bool keywords
|
||||
/// the negative lookbehind and lookahead patterns prevent bugs where a bool keyword is also a user-defined tag:
|
||||
/// [israted]
|
||||
/// parseTag => tags:israted
|
||||
/// replaceBools => tags:israted:True
|
||||
/// or
|
||||
/// [israted]
|
||||
/// replaceBools => israted:True
|
||||
/// parseTag => [israted:True]
|
||||
/// also don't want to apply :True where the value already exists:
|
||||
/// israted:false => israted:false:True
|
||||
///
|
||||
/// despite using parans, lookahead and lookbehind are zero-length assertions which do not capture. therefore the bool search keyword is still $1 since it's the first and only capture
|
||||
/// </summary>
|
||||
private static string boolPattern_parameterized { get; }
|
||||
= @"
|
||||
(?<! # begin negative lookbehind
|
||||
[ # begin char set
|
||||
: # colon
|
||||
\[ # open bracket, escaped
|
||||
] # end char set
|
||||
\s* # optional space
|
||||
) # end negative lookbehind
|
||||
|
||||
\b # word boundary
|
||||
({0}) # captured bool search keyword. this is the $1 reference used in regex.Replace
|
||||
\b # word boundary
|
||||
|
||||
(?! # begin negative lookahead
|
||||
\s* # optional space
|
||||
[ # begin char set
|
||||
: # colon
|
||||
\] # close bracket, escaped
|
||||
] # end char set
|
||||
) # end negative lookahead
|
||||
";
|
||||
private static Dictionary<string, Regex> boolRegexDic { get; } = new Dictionary<string, Regex>();
|
||||
public static Regex GetBoolRegex(string boolSearch)
|
||||
{
|
||||
if (boolRegexDic.TryGetValue(boolSearch, out var regex))
|
||||
return regex;
|
||||
|
||||
var boolPattern = string.Format(boolPattern_parameterized, boolSearch);
|
||||
regex = new Regex(boolPattern, RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
boolRegexDic.Add(boolSearch, regex);
|
||||
|
||||
return regex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,16 @@ namespace LibationSearchEngine
|
||||
|
||||
public const string _ID_ = "_ID_";
|
||||
public const string TAGS = "tags";
|
||||
// special field for each book which includes all major parts of the book's metadata. enables non-targetting searching
|
||||
public const string ALL = "all";
|
||||
|
||||
private static ReadOnlyDictionary<string, Func<LibraryBook, string>> idIndexRules { get; }
|
||||
// the workaround which allows displaying all books when query is empty
|
||||
public const string ALL_QUERY = "*:*";
|
||||
|
||||
public SearchEngine(LibationContext context) => this.context = context;
|
||||
|
||||
#region index rules
|
||||
private static ReadOnlyDictionary<string, Func<LibraryBook, string>> idIndexRules { get; }
|
||||
= new ReadOnlyDictionary<string, Func<LibraryBook, string>>(
|
||||
new Dictionary<string, Func<LibraryBook, string>>
|
||||
{
|
||||
@@ -38,6 +45,7 @@ namespace LibationSearchEngine
|
||||
["ASIN"] = lb => lb.Book.AudibleProductId
|
||||
}
|
||||
);
|
||||
|
||||
private static ReadOnlyDictionary<string, Func<LibraryBook, string>> stringIndexRules { get; }
|
||||
= new ReadOnlyDictionary<string, Func<LibraryBook, string>>(
|
||||
new Dictionary<string, Func<LibraryBook, string>>
|
||||
@@ -98,6 +106,7 @@ namespace LibationSearchEngine
|
||||
["MyRating"] = lb => lb.Book.UserDefinedItem.Rating.OverallRating.ToLuceneString()
|
||||
}
|
||||
);
|
||||
|
||||
private static ReadOnlyDictionary<string, Func<LibraryBook, bool>> boolIndexRules { get; }
|
||||
= new ReadOnlyDictionary<string, Func<LibraryBook, bool>>(
|
||||
new Dictionary<string, Func<LibraryBook, bool>>
|
||||
@@ -114,12 +123,17 @@ namespace LibationSearchEngine
|
||||
["IsRated"] = lb => lb.Book.UserDefinedItem.Rating.OverallRating > 0f,
|
||||
["Rated"] = lb => lb.Book.UserDefinedItem.Rating.OverallRating > 0f,
|
||||
|
||||
["IsAuthorNarrated"] = lb => isAuthorNarrated(lb),
|
||||
["AuthorNarrated"] = lb => isAuthorNarrated(lb),
|
||||
["IsAuthorNarrated"] = isAuthorNarrated,
|
||||
["AuthorNarrated"] = isAuthorNarrated,
|
||||
|
||||
[nameof(Book.IsAbridged)] = lb => lb.Book.IsAbridged,
|
||||
["Abridged"] = lb => lb.Book.IsAbridged,
|
||||
});
|
||||
|
||||
// this will only be evaluated at time of re-index. ie: state of files moved later will be out of sync until next re-index
|
||||
["IsLiberated"] = lb => isLiberated(lb.Book.AudibleProductId),
|
||||
["Liberated"] = lb => isLiberated(lb.Book.AudibleProductId),
|
||||
}
|
||||
);
|
||||
|
||||
private static bool isAuthorNarrated(LibraryBook lb)
|
||||
{
|
||||
@@ -128,6 +142,8 @@ namespace LibationSearchEngine
|
||||
return authors.Intersect(narrators).Any();
|
||||
}
|
||||
|
||||
private static bool isLiberated(string id) => AudibleFileStorage.Audio.Exists(id);
|
||||
|
||||
// use these common fields in the "all" default search field
|
||||
private static IEnumerable<Func<LibraryBook, string>> allFieldIndexRules { get; }
|
||||
= new List<Func<LibraryBook, string>>
|
||||
@@ -137,8 +153,10 @@ namespace LibationSearchEngine
|
||||
stringIndexRules[nameof(Book.AuthorNames)],
|
||||
stringIndexRules[nameof(Book.NarratorNames)]
|
||||
};
|
||||
#endregion
|
||||
|
||||
public static IEnumerable<string> GetSearchIdFields()
|
||||
#region get search fields. used for display in help
|
||||
public static IEnumerable<string> GetSearchIdFields()
|
||||
{
|
||||
foreach (var key in idIndexRules.Keys)
|
||||
yield return key;
|
||||
@@ -173,11 +191,13 @@ namespace LibationSearchEngine
|
||||
foreach (var key in numberIndexRules.Keys)
|
||||
yield return key;
|
||||
}
|
||||
#endregion
|
||||
|
||||
private Directory getIndex() => FSDirectory.Open(SearchEngineDirectory);
|
||||
|
||||
public SearchEngine(LibationContext context) => this.context = context;
|
||||
|
||||
#region create and update index
|
||||
/// <summary>
|
||||
/// create new. ie: full re-index
|
||||
/// </summary>
|
||||
/// <param name="overwrite"></param>
|
||||
public void CreateNewIndex(bool overwrite = true)
|
||||
{
|
||||
// 300 products
|
||||
@@ -211,6 +231,22 @@ namespace LibationSearchEngine
|
||||
log();
|
||||
}
|
||||
|
||||
/// <summary>Long running. Use await Task.Run(() => UpdateBook(productId))</summary>
|
||||
public void UpdateBook(string productId)
|
||||
{
|
||||
var libraryBook = context.GetLibraryBook_Flat_NoTracking(productId);
|
||||
var term = new Term(_ID_, productId);
|
||||
|
||||
var document = createBookIndexDocument(libraryBook);
|
||||
var createNewIndex = false;
|
||||
|
||||
using var index = getIndex();
|
||||
using var analyzer = new StandardAnalyzer(Version);
|
||||
using var ixWriter = new IndexWriter(index, analyzer, createNewIndex, IndexWriter.MaxFieldLength.UNLIMITED);
|
||||
ixWriter.DeleteDocuments(term);
|
||||
ixWriter.AddDocument(document);
|
||||
}
|
||||
|
||||
private static Document createBookIndexDocument(LibraryBook libraryBook)
|
||||
{
|
||||
var doc = new Document();
|
||||
@@ -248,77 +284,100 @@ namespace LibationSearchEngine
|
||||
return doc;
|
||||
}
|
||||
|
||||
/// <summary>Long running. Use await Task.Run(() => UpdateBook(productId))</summary>
|
||||
public void UpdateBook(string productId)
|
||||
{
|
||||
var libraryBook = context.GetLibraryBook_Flat_NoTracking(productId);
|
||||
var term = new Term(_ID_, productId);
|
||||
// update single document entry
|
||||
// all fields, including 'tags' are case-specific
|
||||
public void UpdateTags(string productId, string tags) => updateAnalyzedField(productId, TAGS, tags);
|
||||
|
||||
var document = createBookIndexDocument(libraryBook);
|
||||
var createNewIndex = false;
|
||||
// all fields are case-specific
|
||||
private static void updateAnalyzedField(string productId, string fieldName, string newValue)
|
||||
=> updateDocument(
|
||||
productId,
|
||||
d =>
|
||||
{
|
||||
// fields are key value pairs. MULTIPLE FIELDS CAN POTENTIALLY HAVE THE SAME KEY.
|
||||
// ie: must remove old before adding new else will create unwanted duplicates.
|
||||
d.RemoveField(fieldName);
|
||||
d.AddAnalyzed(fieldName, newValue);
|
||||
});
|
||||
|
||||
using var index = getIndex();
|
||||
using var analyzer = new StandardAnalyzer(Version);
|
||||
using var ixWriter = new IndexWriter(index, analyzer, createNewIndex, IndexWriter.MaxFieldLength.UNLIMITED);
|
||||
ixWriter.DeleteDocuments(term);
|
||||
ixWriter.AddDocument(document);
|
||||
}
|
||||
// update single document entry
|
||||
public void UpdateIsLiberated(string productId)
|
||||
=> updateDocument(
|
||||
productId,
|
||||
d =>
|
||||
{
|
||||
// fields are key value pairs. MULTIPLE FIELDS CAN POTENTIALLY HAVE THE SAME KEY.
|
||||
// ie: must remove old before adding new else will create unwanted duplicates.
|
||||
var v = isLiberated(productId);
|
||||
d.RemoveField("IsLiberated");
|
||||
d.AddBool("IsLiberated", v);
|
||||
d.RemoveField("Liberated");
|
||||
d.AddBool("Liberated", v);
|
||||
});
|
||||
|
||||
public void UpdateTags(string productId, string tags)
|
||||
private static void updateDocument(string productId, Action<Document> action)
|
||||
{
|
||||
var productTerm = new Term(_ID_, productId);
|
||||
|
||||
using var index = getIndex();
|
||||
using var index = getIndex();
|
||||
|
||||
// get existing document
|
||||
using var searcher = new IndexSearcher(index);
|
||||
var query = new TermQuery(productTerm);
|
||||
var docs = searcher.Search(query, 1);
|
||||
var scoreDoc = docs.ScoreDocs.SingleOrDefault();
|
||||
if (scoreDoc == null)
|
||||
throw new Exception("document not found");
|
||||
var document = searcher.Doc(scoreDoc.Doc);
|
||||
// get existing document
|
||||
using var searcher = new IndexSearcher(index);
|
||||
var query = new TermQuery(productTerm);
|
||||
var docs = searcher.Search(query, 1);
|
||||
var scoreDoc = docs.ScoreDocs.SingleOrDefault();
|
||||
if (scoreDoc == null)
|
||||
throw new Exception("document not found");
|
||||
var document = searcher.Doc(scoreDoc.Doc);
|
||||
|
||||
|
||||
// update document entry with new tags
|
||||
// fields are key value pairs and MULTIPLE FIELDS CAN HAVE THE SAME KEY. must remove old before adding new
|
||||
// REMEMBER: all fields, including 'tags' are case-specific
|
||||
document.RemoveField(TAGS);
|
||||
document.AddAnalyzed(TAGS, tags);
|
||||
// perform update
|
||||
action(document);
|
||||
|
||||
// update index
|
||||
var createNewIndex = false;
|
||||
using var analyzer = new StandardAnalyzer(Version);
|
||||
using var ixWriter = new IndexWriter(index, analyzer, createNewIndex, IndexWriter.MaxFieldLength.UNLIMITED);
|
||||
ixWriter.UpdateDocument(productTerm, document, analyzer);
|
||||
}
|
||||
|
||||
// update index
|
||||
var createNewIndex = false;
|
||||
using var analyzer = new StandardAnalyzer(Version);
|
||||
using var ixWriter = new IndexWriter(index, analyzer, createNewIndex, IndexWriter.MaxFieldLength.UNLIMITED);
|
||||
ixWriter.UpdateDocument(productTerm, document, analyzer);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region search
|
||||
public SearchResultSet Search(string searchString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(searchString))
|
||||
searchString = "*:*";
|
||||
Serilog.Log.Logger.Debug("original search string: {@DebugInfo}", new { searchString });
|
||||
searchString = FormatSearchQuery(searchString);
|
||||
Serilog.Log.Logger.Debug("formatted search string: {@DebugInfo}", new { searchString });
|
||||
|
||||
#region apply formatting
|
||||
searchString = parseTag(searchString);
|
||||
var results = generalSearch(searchString);
|
||||
Serilog.Log.Logger.Debug("Hit(s): {@DebugInfo}", new { count = results.Docs.Count() });
|
||||
displayResults(results);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public static string FormatSearchQuery(string searchString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(searchString))
|
||||
return ALL_QUERY;
|
||||
|
||||
searchString = replaceBools(searchString);
|
||||
|
||||
searchString = parseTag(searchString);
|
||||
|
||||
// in ranges " TO " must be uppercase
|
||||
searchString = searchString.Replace(" to ", " TO ");
|
||||
|
||||
searchString = padNumbers(searchString);
|
||||
|
||||
searchString = lowerFieldNames(searchString);
|
||||
#endregion
|
||||
|
||||
var results = generalSearch(searchString);
|
||||
|
||||
displayResults(results);
|
||||
|
||||
return results;
|
||||
return searchString;
|
||||
}
|
||||
|
||||
private static string parseTag(string tagSearchString)
|
||||
#region format query string
|
||||
private static string parseTag(string tagSearchString)
|
||||
{
|
||||
var allMatches = LuceneRegex
|
||||
.TagRegex
|
||||
@@ -337,9 +396,10 @@ namespace LibationSearchEngine
|
||||
|
||||
private static string replaceBools(string searchString)
|
||||
{
|
||||
// negative look-ahead for optional spaces then colon. don't want to double-up. eg:"israted:false" => "israted:false:True"
|
||||
foreach (var boolSearch in boolIndexRules.Keys)
|
||||
searchString = Regex.Replace(searchString, $@"\b({boolSearch})\b(?!\s*:)", @"$1:True", RegexOptions.IgnoreCase);
|
||||
searchString =
|
||||
LuceneRegex.GetBoolRegex(boolSearch)
|
||||
.Replace(searchString, @"$1:True");
|
||||
|
||||
return searchString;
|
||||
}
|
||||
@@ -376,13 +436,10 @@ namespace LibationSearchEngine
|
||||
|
||||
return searchString;
|
||||
}
|
||||
|
||||
public int MaxSearchResultsToReturn { get; set; } = 999;
|
||||
#endregion
|
||||
|
||||
private SearchResultSet generalSearch(string searchString)
|
||||
{
|
||||
Console.WriteLine($"searchString: {searchString}");
|
||||
|
||||
var defaultField = ALL;
|
||||
|
||||
using var index = getIndex();
|
||||
@@ -403,14 +460,14 @@ namespace LibationSearchEngine
|
||||
boolQuery.Add(new MatchAllDocsQuery(), Occur.MUST);
|
||||
}
|
||||
|
||||
Console.WriteLine($" query: {query}");
|
||||
|
||||
var docs = searcher
|
||||
.Search(query, MaxSearchResultsToReturn)
|
||||
.Search(query, searcher.MaxDoc + 1)
|
||||
.ScoreDocs
|
||||
.Select(ds => new ScoreDocExplicit(searcher.Doc(ds.Doc), ds.Score))
|
||||
.ToList();
|
||||
return new SearchResultSet(query.ToString(), docs);
|
||||
var queryString = query.ToString();
|
||||
Serilog.Log.Logger.Debug("query: {@DebugInfo}", new { queryString });
|
||||
return new SearchResultSet(queryString, docs);
|
||||
}
|
||||
|
||||
private IEnumerable<Occur> getOccurs_recurs(BooleanQuery query)
|
||||
@@ -430,7 +487,6 @@ namespace LibationSearchEngine
|
||||
|
||||
private void displayResults(SearchResultSet docs)
|
||||
{
|
||||
Console.WriteLine($"Hit(s): {docs.Docs.Count()}");
|
||||
//for (int i = 0; i < docs.Docs.Count(); i++)
|
||||
//{
|
||||
// var sde = docs.Docs.First();
|
||||
@@ -438,13 +494,14 @@ namespace LibationSearchEngine
|
||||
// Document doc = sde.Doc;
|
||||
// float score = sde.Score;
|
||||
|
||||
// Console.WriteLine($"{(i + 1)}) score={score}. Fields:");
|
||||
// Serilog.Log.Logger.Debug($"{(i + 1)}) score={score}. Fields:");
|
||||
// var allFields = doc.GetFields();
|
||||
// foreach (var f in allFields)
|
||||
// Console.WriteLine($" [{f.Name}]={f.StringValue}");
|
||||
// Serilog.Log.Logger.Debug($" [{f.Name}]={f.StringValue}");
|
||||
//}
|
||||
|
||||
//Console.WriteLine();
|
||||
}
|
||||
#endregion
|
||||
|
||||
private static Directory getIndex() => FSDirectory.Open(SearchEngineDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon>libation.ico</ApplicationIcon>
|
||||
|
||||
@@ -27,9 +27,11 @@
|
||||
<Compile Update="UNTESTED\Dialogs\LibationFilesDialog.Designer.cs">
|
||||
<DependentUpon>LibationFilesDialog.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="UNTESTED\Dialogs\ScanAccountsDialog.cs">
|
||||
<SubType>Form</SubType>
|
||||
<Compile Update="UNTESTED\Dialogs\Login\ApprovalNeededDialog.cs" />
|
||||
<Compile Update="UNTESTED\Dialogs\Login\ApprovalNeededDialog.Designer.cs">
|
||||
<DependentUpon>ApprovalNeededDialog.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="UNTESTED\Dialogs\ScanAccountsDialog.cs" />
|
||||
<Compile Update="UNTESTED\Dialogs\ScanAccountsDialog.Designer.cs">
|
||||
<DependentUpon>ScanAccountsDialog.cs</DependentUpon>
|
||||
</Compile>
|
||||
|
||||
@@ -24,24 +24,13 @@ namespace LibationWinForms.BookLiberation
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public void AppendError(Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Automated backup: error");
|
||||
appendText("ERROR: " + ex.Message);
|
||||
}
|
||||
public void AppendText(string text)
|
||||
{
|
||||
Serilog.Log.Logger.Information($"Automated backup: {text}");
|
||||
appendText(text);
|
||||
}
|
||||
private void appendText(string text)
|
||||
public void WriteLine(string text)
|
||||
=> logTb.UIThread(() => logTb.AppendText($"{DateTime.Now} {text}{Environment.NewLine}"));
|
||||
|
||||
public void FinalizeUI()
|
||||
{
|
||||
keepGoingCb.Enabled = false;
|
||||
logTb.AppendText("");
|
||||
AppendText("DONE");
|
||||
}
|
||||
|
||||
private void AutomatedBackupsForm_FormClosing(object sender, FormClosingEventArgs e) => keepGoingCb.Checked = false;
|
||||
|
||||
@@ -17,15 +17,15 @@ namespace LibationWinForms.BookLiberation
|
||||
// thread-safe UI updates
|
||||
public void UpdateFilename(string title) => filenameLbl.UIThread(() => filenameLbl.Text = title);
|
||||
|
||||
public void DownloadProgressChanged(long BytesReceived, long TotalBytesToReceive)
|
||||
public void DownloadProgressChanged(long BytesReceived, long? TotalBytesToReceive)
|
||||
{
|
||||
// this won't happen with download file. it will happen with download string
|
||||
if (TotalBytesToReceive < 0)
|
||||
if (!TotalBytesToReceive.HasValue || TotalBytesToReceive.Value <= 0)
|
||||
return;
|
||||
|
||||
progressLbl.UIThread(() => progressLbl.Text = $"{BytesReceived:#,##0} of {TotalBytesToReceive:#,##0}");
|
||||
progressLbl.UIThread(() => progressLbl.Text = $"{BytesReceived:#,##0} of {TotalBytesToReceive.Value:#,##0}");
|
||||
|
||||
var d = double.Parse(BytesReceived.ToString()) / double.Parse(TotalBytesToReceive.ToString()) * 100.0;
|
||||
var d = double.Parse(BytesReceived.ToString()) / double.Parse(TotalBytesToReceive.Value.ToString()) * 100.0;
|
||||
var i = int.Parse(Math.Truncate(d).ToString());
|
||||
progressBar1.UIThread(() => progressBar1.Value = i);
|
||||
|
||||
|
||||
@@ -1,30 +1,69 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using FileLiberator;
|
||||
|
||||
namespace LibationWinForms.BookLiberation
|
||||
{
|
||||
// decouple serilog and form. include convenience factory method
|
||||
public class LogMe
|
||||
{
|
||||
public event EventHandler<string> LogInfo;
|
||||
public event EventHandler<string> LogErrorString;
|
||||
public event EventHandler<(Exception, string)> LogError;
|
||||
|
||||
public static LogMe RegisterForm(AutomatedBackupsForm form)
|
||||
{
|
||||
var logMe = new LogMe();
|
||||
|
||||
logMe.LogInfo += (_, text) => Serilog.Log.Logger.Information($"Automated backup: {text}");
|
||||
logMe.LogInfo += (_, text) => form.WriteLine(text);
|
||||
|
||||
logMe.LogErrorString += (_, text) => Serilog.Log.Logger.Error(text);
|
||||
logMe.LogErrorString += (_, text) => form.WriteLine(text);
|
||||
|
||||
logMe.LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error");
|
||||
logMe.LogError += (_, tuple) =>
|
||||
{
|
||||
form.WriteLine(tuple.Item2 ?? "Automated backup: error");
|
||||
form.WriteLine("ERROR: " + tuple.Item1.Message);
|
||||
};
|
||||
|
||||
return logMe;
|
||||
}
|
||||
|
||||
public void Info(string text) => LogInfo?.Invoke(this, text);
|
||||
public void Error(string text) => LogErrorString?.Invoke(this, text);
|
||||
public void Error(Exception ex, string text = null) => LogError?.Invoke(this, (ex, text));
|
||||
}
|
||||
|
||||
public static class ProcessorAutomationController
|
||||
{
|
||||
public static async Task BackupSingleBookAsync(string productId, EventHandler<LibraryBook> completedAction = null)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin " + nameof(BackupSingleBookAsync) + " {@DebugInfo}", new { productId });
|
||||
|
||||
var backupBook = getWiredUpBackupBook(completedAction);
|
||||
|
||||
var automatedBackupsForm = attachToBackupsForm(backupBook);
|
||||
(AutomatedBackupsForm automatedBackupsForm, LogMe logMe) = attachToBackupsForm(backupBook);
|
||||
automatedBackupsForm.KeepGoingVisible = false;
|
||||
|
||||
await runSingleBackupAsync(backupBook, automatedBackupsForm, productId);
|
||||
var libraryBook = IProcessableExt.GetSingleLibraryBook(productId);
|
||||
// continue even if libraryBook is null. we'll display even that in the processing box
|
||||
await new BackupSingle(logMe, backupBook, automatedBackupsForm, libraryBook).RunBackupAsync();
|
||||
}
|
||||
|
||||
public static async Task BackupAllBooksAsync(EventHandler<LibraryBook> completedAction = null)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync));
|
||||
|
||||
var backupBook = getWiredUpBackupBook(completedAction);
|
||||
|
||||
var automatedBackupsForm = attachToBackupsForm(backupBook);
|
||||
|
||||
await runBackupLoopAsync(backupBook, automatedBackupsForm);
|
||||
(AutomatedBackupsForm automatedBackupsForm, LogMe logMe) = attachToBackupsForm(backupBook);
|
||||
await new BackupLoop(logMe, backupBook, automatedBackupsForm).RunBackupAsync();
|
||||
}
|
||||
|
||||
private static BackupBook getWiredUpBackupBook(EventHandler<LibraryBook> completedAction)
|
||||
@@ -33,7 +72,17 @@ namespace LibationWinForms.BookLiberation
|
||||
|
||||
backupBook.DownloadBook.Begin += (_, __) => wireUpEvents(backupBook.DownloadBook);
|
||||
backupBook.DecryptBook.Begin += (_, __) => wireUpEvents(backupBook.DecryptBook);
|
||||
backupBook.DownloadPdf.Begin += (_, __) => wireUpEvents(backupBook.DownloadPdf);
|
||||
backupBook.DownloadPdf.Begin += (_, __) => wireUpEvents(backupBook.DownloadPdf);
|
||||
|
||||
// must occur before completedAction. A common use case is:
|
||||
// - filter by -liberated
|
||||
// - liberate only that book
|
||||
// completedAction is to refresh grid
|
||||
// - want to see that book disappear from grid
|
||||
// also for this to work, updateIsLiberated can NOT be async
|
||||
backupBook.DownloadBook.Completed += updateIsLiberated;
|
||||
backupBook.DecryptBook.Completed += updateIsLiberated;
|
||||
backupBook.DownloadPdf.Completed += updateIsLiberated;
|
||||
|
||||
if (completedAction != null)
|
||||
{
|
||||
@@ -45,22 +94,25 @@ namespace LibationWinForms.BookLiberation
|
||||
return backupBook;
|
||||
}
|
||||
|
||||
private static AutomatedBackupsForm attachToBackupsForm(BackupBook backupBook)
|
||||
private static void updateIsLiberated(object sender, LibraryBook e) => ApplicationServices.SearchEngineCommands.UpdateIsLiberated(e.Book);
|
||||
|
||||
private static (AutomatedBackupsForm, LogMe) attachToBackupsForm(BackupBook backupBook)
|
||||
{
|
||||
#region create form
|
||||
#region create form and logger
|
||||
var automatedBackupsForm = new AutomatedBackupsForm();
|
||||
var logMe = LogMe.RegisterForm(automatedBackupsForm);
|
||||
#endregion
|
||||
|
||||
#region define how model actions will affect form behavior
|
||||
void downloadBookBegin(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Download Step, Begin: {libraryBook.Book}");
|
||||
void statusUpdate(object _, string str) => automatedBackupsForm.AppendText("- " + str);
|
||||
void downloadBookCompleted(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Download Step, Completed: {libraryBook.Book}");
|
||||
void decryptBookBegin(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Decrypt Step, Begin: {libraryBook.Book}");
|
||||
void downloadBookBegin(object _, LibraryBook libraryBook) => logMe.Info($"Download Step, Begin: {libraryBook.Book}");
|
||||
void statusUpdate(object _, string str) => logMe.Info("- " + str);
|
||||
void downloadBookCompleted(object _, LibraryBook libraryBook) => logMe.Info($"Download Step, Completed: {libraryBook.Book}");
|
||||
void decryptBookBegin(object _, LibraryBook libraryBook) => logMe.Info($"Decrypt Step, Begin: {libraryBook.Book}");
|
||||
// extra line after book is completely finished
|
||||
void decryptBookCompleted(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Decrypt Step, Completed: {libraryBook.Book}{Environment.NewLine}");
|
||||
void downloadPdfBegin(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"PDF Step, Begin: {libraryBook.Book}");
|
||||
void decryptBookCompleted(object _, LibraryBook libraryBook) => logMe.Info($"Decrypt Step, Completed: {libraryBook.Book}{Environment.NewLine}");
|
||||
void downloadPdfBegin(object _, LibraryBook libraryBook) => logMe.Info($"PDF Step, Begin: {libraryBook.Book}");
|
||||
// extra line after book is completely finished
|
||||
void downloadPdfCompleted(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"PDF Step, Completed: {libraryBook.Book}{Environment.NewLine}");
|
||||
void downloadPdfCompleted(object _, LibraryBook libraryBook) => logMe.Info($"PDF Step, Completed: {libraryBook.Book}{Environment.NewLine}");
|
||||
#endregion
|
||||
|
||||
#region subscribe new form to model's events
|
||||
@@ -91,16 +143,17 @@ namespace LibationWinForms.BookLiberation
|
||||
};
|
||||
#endregion
|
||||
|
||||
return automatedBackupsForm;
|
||||
return (automatedBackupsForm, logMe);
|
||||
}
|
||||
|
||||
public static async Task BackupAllPdfsAsync(EventHandler<LibraryBook> completedAction = null)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllPdfsAsync));
|
||||
|
||||
var downloadPdf = getWiredUpDownloadPdf(completedAction);
|
||||
|
||||
var automatedBackupsForm = attachToBackupsForm(downloadPdf);
|
||||
|
||||
await runBackupLoopAsync(downloadPdf, automatedBackupsForm);
|
||||
(AutomatedBackupsForm automatedBackupsForm, LogMe logMe) = attachToBackupsForm(downloadPdf);
|
||||
await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync();
|
||||
}
|
||||
|
||||
private static DownloadPdf getWiredUpDownloadPdf(EventHandler<LibraryBook> completedAction)
|
||||
@@ -126,7 +179,7 @@ namespace LibationWinForms.BookLiberation
|
||||
downloadDialog.UpdateFilename(str);
|
||||
downloadDialog.Show();
|
||||
};
|
||||
downloadFile.DownloadProgressChanged += (_, progress) => downloadDialog.DownloadProgressChanged(progress.BytesReceived, progress.TotalBytesToReceive.Value);
|
||||
downloadFile.DownloadProgressChanged += (_, progress) => downloadDialog.DownloadProgressChanged(progress.BytesReceived, progress.TotalBytesToReceive);
|
||||
downloadFile.DownloadCompleted += (_, __) => downloadDialog.Close();
|
||||
|
||||
await downloadFile.PerformDownloadFileAsync(url, destination);
|
||||
@@ -161,7 +214,7 @@ namespace LibationWinForms.BookLiberation
|
||||
void fileDownloadCompleted(object _, string __) => downloadDialog.Close();
|
||||
|
||||
void downloadProgressChanged(object _, Dinah.Core.Net.Http.DownloadProgress progress)
|
||||
=> downloadDialog.DownloadProgressChanged(progress.BytesReceived, progress.TotalBytesToReceive.Value);
|
||||
=> downloadDialog.DownloadProgressChanged(progress.BytesReceived, progress.TotalBytesToReceive);
|
||||
|
||||
void unsubscribe(object _ = null, EventArgs __ = null)
|
||||
{
|
||||
@@ -244,17 +297,18 @@ namespace LibationWinForms.BookLiberation
|
||||
#endregion
|
||||
}
|
||||
|
||||
private static AutomatedBackupsForm attachToBackupsForm(IDownloadableProcessable downloadable)
|
||||
private static (AutomatedBackupsForm, LogMe) attachToBackupsForm(IDownloadableProcessable downloadable)
|
||||
{
|
||||
#region create form
|
||||
#region create form and logger
|
||||
var automatedBackupsForm = new AutomatedBackupsForm();
|
||||
var logMe = LogMe.RegisterForm(automatedBackupsForm);
|
||||
#endregion
|
||||
|
||||
#region define how model actions will affect form behavior
|
||||
void begin(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Begin: {libraryBook.Book}");
|
||||
void statusUpdate(object _, string str) => automatedBackupsForm.AppendText("- " + str);
|
||||
void begin(object _, LibraryBook libraryBook) => logMe.Info($"Begin: {libraryBook.Book}");
|
||||
void statusUpdate(object _, string str) => logMe.Info("- " + str);
|
||||
// extra line after book is completely finished
|
||||
void completed(object _, LibraryBook libraryBook) => automatedBackupsForm.AppendText($"Completed: {libraryBook.Book}{Environment.NewLine}");
|
||||
void completed(object _, LibraryBook libraryBook) => logMe.Info($"Completed: {libraryBook.Book}{Environment.NewLine}");
|
||||
#endregion
|
||||
|
||||
#region subscribe new form to model's events
|
||||
@@ -273,73 +327,150 @@ namespace LibationWinForms.BookLiberation
|
||||
};
|
||||
#endregion
|
||||
|
||||
return automatedBackupsForm;
|
||||
return (automatedBackupsForm, logMe);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class BackupRunner
|
||||
{
|
||||
protected LogMe LogMe { get; }
|
||||
protected IProcessable Processable { get; }
|
||||
protected AutomatedBackupsForm AutomatedBackupsForm { get; }
|
||||
|
||||
protected BackupRunner(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm)
|
||||
{
|
||||
LogMe = logMe;
|
||||
Processable = processable;
|
||||
AutomatedBackupsForm = automatedBackupsForm;
|
||||
}
|
||||
|
||||
// automated backups looper feels like a composible IProcessable: logic, UI, begin + process child + end
|
||||
// however the process step doesn't follow the pattern: Validate(product) + Process(product)
|
||||
private static async Task runBackupLoopAsync(IProcessable processable, AutomatedBackupsForm automatedBackupsForm)
|
||||
protected abstract Task RunAsync();
|
||||
|
||||
protected abstract string SkipDialogText { get; }
|
||||
protected abstract MessageBoxButtons SkipDialogButtons { get; }
|
||||
protected abstract DialogResult CreateSkipFileResult { get; }
|
||||
|
||||
public async Task RunBackupAsync()
|
||||
{
|
||||
automatedBackupsForm.Show();
|
||||
AutomatedBackupsForm.Show();
|
||||
|
||||
try
|
||||
{
|
||||
var shouldContinue = true;
|
||||
while (shouldContinue)
|
||||
{
|
||||
var statusHandler = await processable.ProcessFirstValidAsync();
|
||||
shouldContinue = validateStatus(statusHandler, automatedBackupsForm);
|
||||
}
|
||||
await RunAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
automatedBackupsForm.AppendError(ex);
|
||||
LogMe.Error(ex);
|
||||
}
|
||||
|
||||
automatedBackupsForm.FinalizeUI();
|
||||
AutomatedBackupsForm.FinalizeUI();
|
||||
LogMe.Info("DONE");
|
||||
}
|
||||
|
||||
private static async Task runSingleBackupAsync(IProcessable processable, AutomatedBackupsForm automatedBackupsForm, string productId)
|
||||
protected async Task<bool> ProcessOneAsync(Func<LibraryBook, Task<StatusHandler>> func, LibraryBook libraryBook)
|
||||
{
|
||||
automatedBackupsForm.Show();
|
||||
string logMessage;
|
||||
|
||||
try
|
||||
{
|
||||
var statusHandler = await processable.ProcessSingleAsync(productId);
|
||||
validateStatus(statusHandler, automatedBackupsForm);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
automatedBackupsForm.AppendError(ex);
|
||||
}
|
||||
var statusHandler = await func(libraryBook);
|
||||
|
||||
automatedBackupsForm.FinalizeUI();
|
||||
}
|
||||
if (statusHandler.IsSuccess)
|
||||
return true;
|
||||
|
||||
private static bool validateStatus(StatusHandler statusHandler, AutomatedBackupsForm automatedBackupsForm)
|
||||
{
|
||||
if (statusHandler == null)
|
||||
{
|
||||
automatedBackupsForm.AppendText("Done. All books have been processed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (statusHandler.HasErrors)
|
||||
{
|
||||
automatedBackupsForm.AppendText("ERROR. All books have not been processed. Most recent valid book: processing failed");
|
||||
foreach (var errorMessage in statusHandler.Errors)
|
||||
automatedBackupsForm.AppendText(errorMessage);
|
||||
return false;
|
||||
LogMe.Error(errorMessage);
|
||||
|
||||
logMessage = statusHandler.Errors.Aggregate((a, b) => $"{a}\r\n{b}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogMe.Error(ex);
|
||||
|
||||
logMessage = ex.Message + "\r\n|\r\n" + ex.StackTrace;
|
||||
}
|
||||
|
||||
if (!automatedBackupsForm.KeepGoing)
|
||||
{
|
||||
if (automatedBackupsForm.KeepGoingVisible && !automatedBackupsForm.KeepGoingChecked)
|
||||
automatedBackupsForm.AppendText("'Keep going' is unchecked");
|
||||
LogMe.Error("ERROR. All books have not been processed. Most recent book: processing failed");
|
||||
|
||||
var dialogResult = MessageBox.Show(SkipDialogText, "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question);
|
||||
|
||||
if (dialogResult == DialogResult.Abort)
|
||||
return false;
|
||||
|
||||
if (dialogResult == CreateSkipFileResult)
|
||||
{
|
||||
var path = FileManager.AudibleFileStorage.Audio.CreateSkipFile(libraryBook.Book.Title, libraryBook.Book.AudibleProductId, logMessage);
|
||||
LogMe.Info($@"
|
||||
Created new 'skip' file
|
||||
[{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}
|
||||
{path}
|
||||
".Trim());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
class BackupSingle : BackupRunner
|
||||
{
|
||||
private LibraryBook _libraryBook { get; }
|
||||
|
||||
protected override string SkipDialogText => @"
|
||||
An error occurred while trying to process this book. Skip this book permanently?
|
||||
|
||||
- Click YES to skip this book permanently.
|
||||
|
||||
- Click NO to skip the book this time only. We'll try again later.
|
||||
".Trim();
|
||||
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.YesNo;
|
||||
protected override DialogResult CreateSkipFileResult => DialogResult.Yes;
|
||||
|
||||
public BackupSingle(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm, LibraryBook libraryBook)
|
||||
: base(logMe, processable, automatedBackupsForm)
|
||||
{
|
||||
_libraryBook = libraryBook;
|
||||
}
|
||||
|
||||
protected override async Task RunAsync()
|
||||
{
|
||||
if (_libraryBook is not null)
|
||||
await ProcessOneAsync(Processable.ProcessSingleAsync, _libraryBook);
|
||||
}
|
||||
}
|
||||
class BackupLoop : BackupRunner
|
||||
{
|
||||
protected override string SkipDialogText => @"
|
||||
An error occurred while trying to process this book
|
||||
|
||||
- ABORT: stop processing books.
|
||||
|
||||
- RETRY: retry this book later. Just skip it for now. Continue processing books. (Will try this book again later.)
|
||||
|
||||
- IGNORE: Permanently ignore this book. Continue processing books. (Will not try this book again later.)
|
||||
".Trim();
|
||||
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore;
|
||||
protected override DialogResult CreateSkipFileResult => DialogResult.Ignore;
|
||||
|
||||
public BackupLoop(LogMe logMe, IProcessable processable, AutomatedBackupsForm automatedBackupsForm)
|
||||
: base(logMe, processable, automatedBackupsForm) { }
|
||||
|
||||
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())
|
||||
{
|
||||
var keepGoing = await ProcessOneAsync(Processable.ProcessBookAsync_NoValidation, libraryBook);
|
||||
if (!keepGoing)
|
||||
return;
|
||||
|
||||
if (!AutomatedBackupsForm.KeepGoing)
|
||||
{
|
||||
if (AutomatedBackupsForm.KeepGoingVisible && !AutomatedBackupsForm.KeepGoingChecked)
|
||||
LogMe.Info("'Keep going' is unchecked");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
LogMe.Info("Done. All books have been processed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ namespace LibationWinForms.Dialogs
|
||||
if (string.IsNullOrWhiteSpace(dto.AccountId))
|
||||
throw new Exception("Please enter an account id for all accounts");
|
||||
if (string.IsNullOrWhiteSpace(dto.LocaleName))
|
||||
throw new Exception("Please select a local name for all accounts");
|
||||
throw new Exception("Please select a locale (i.e.: country or region) for all accounts");
|
||||
|
||||
var acct = accountsSettings.Upsert(dto.AccountId, dto.LocaleName);
|
||||
acct.LibraryScan = dto.LibraryScan;
|
||||
|
||||
78
LibationWinForms/UNTESTED/Dialogs/Login/ApprovalNeededDialog.Designer.cs
generated
Normal file
78
LibationWinForms/UNTESTED/Dialogs/Login/ApprovalNeededDialog.Designer.cs
generated
Normal file
@@ -0,0 +1,78 @@
|
||||
namespace LibationWinForms.Dialogs.Login
|
||||
{
|
||||
partial class ApprovalNeededDialog
|
||||
{
|
||||
/// <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()
|
||||
{
|
||||
this.approvedBtn = new System.Windows.Forms.Button();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// approvedBtn
|
||||
//
|
||||
this.approvedBtn.Location = new System.Drawing.Point(15, 25);
|
||||
this.approvedBtn.Name = "approvedBtn";
|
||||
this.approvedBtn.Size = new System.Drawing.Size(79, 23);
|
||||
this.approvedBtn.TabIndex = 1;
|
||||
this.approvedBtn.Text = "Approved";
|
||||
this.approvedBtn.UseVisualStyleBackColor = true;
|
||||
this.approvedBtn.Click += new System.EventHandler(this.approvedBtn_Click);
|
||||
//
|
||||
// label1
|
||||
//
|
||||
this.label1.AutoSize = true;
|
||||
this.label1.Location = new System.Drawing.Point(12, 9);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(104, 13);
|
||||
this.label1.TabIndex = 0;
|
||||
this.label1.Text = "Click after approving";
|
||||
//
|
||||
// ApprovalNeededDialog
|
||||
//
|
||||
this.AcceptButton = this.approvedBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(149, 60);
|
||||
this.Controls.Add(this.label1);
|
||||
this.Controls.Add(this.approvedBtn);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "ApprovalNeededDialog";
|
||||
this.ShowIcon = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Approval Needed";
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
private System.Windows.Forms.Button approvedBtn;
|
||||
private System.Windows.Forms.Label label1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms.Dialogs.Login
|
||||
{
|
||||
public partial class ApprovalNeededDialog : Form
|
||||
{
|
||||
public ApprovalNeededDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void approvedBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
DialogResult = DialogResult.OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,5 +37,11 @@ namespace LibationWinForms.Login
|
||||
return (dialog.Email, dialog.Password);
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
public void ShowApprovalNeeded()
|
||||
{
|
||||
using var dialog = new ApprovalNeededDialog();
|
||||
dialog.ShowDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
27
LibationWinForms/UNTESTED/Form1.Designer.cs
generated
27
LibationWinForms/UNTESTED/Form1.Designer.cs
generated
@@ -35,12 +35,14 @@
|
||||
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();
|
||||
@@ -56,6 +58,7 @@
|
||||
this.pdfsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.addFilterBtn = new System.Windows.Forms.Button();
|
||||
this.noAccountsYetAddAccountToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.exportLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.menuStrip1.SuspendLayout();
|
||||
this.statusStrip1.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
@@ -106,6 +109,7 @@
|
||||
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);
|
||||
@@ -125,6 +129,12 @@
|
||||
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";
|
||||
@@ -169,6 +179,14 @@
|
||||
this.beginPdfBackupsToolStripMenuItem.Text = "Begin &PDF Only Backups: {0}";
|
||||
this.beginPdfBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginPdfBackupsToolStripMenuItem_Click);
|
||||
//
|
||||
// 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[] {
|
||||
@@ -283,6 +301,13 @@
|
||||
this.noAccountsYetAddAccountToolStripMenuItem.Text = "No accounts yet. A&dd Account...";
|
||||
this.noAccountsYetAddAccountToolStripMenuItem.Click += new System.EventHandler(this.noAccountsYetAddAccountToolStripMenuItem_Click);
|
||||
//
|
||||
// exportLibraryToolStripMenuItem
|
||||
//
|
||||
this.exportLibraryToolStripMenuItem.Name = "exportLibraryToolStripMenuItem";
|
||||
this.exportLibraryToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
|
||||
this.exportLibraryToolStripMenuItem.Text = "E&xport Library...";
|
||||
this.exportLibraryToolStripMenuItem.Click += new System.EventHandler(this.exportLibraryToolStripMenuItem_Click);
|
||||
//
|
||||
// Form1
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
@@ -338,5 +363,7 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +130,9 @@ namespace LibationWinForms
|
||||
var downloadedOnly = results.Count(r => r == AudioFileState.aax);
|
||||
var noProgress = results.Count(r => r == AudioFileState.none);
|
||||
|
||||
// enable/disable export
|
||||
exportLibraryToolStripMenuItem.Enabled = results.Any();
|
||||
|
||||
// update bottom numbers
|
||||
var pending = noProgress + downloadedOnly;
|
||||
var statusStripText
|
||||
@@ -299,6 +302,45 @@ namespace LibationWinForms
|
||||
private void updateGridRow(object _, LibraryBook libraryBook) => currProductsGrid.RefreshRow(libraryBook.Book.AudibleProductId);
|
||||
#endregion
|
||||
|
||||
#region Export menu
|
||||
private void exportLibraryToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var saveFileDialog = new SaveFileDialog
|
||||
{
|
||||
Title = "Where to export Library",
|
||||
Filter = "Excel Workbook (*.xlsx)|*.xlsx|CSV files (*.csv)|*.csv|JSON files (*.json)|*.json" // + "|All files (*.*)|*.*"
|
||||
};
|
||||
|
||||
if (saveFileDialog.ShowDialog() != DialogResult.OK)
|
||||
return;
|
||||
|
||||
// FilterIndex is 1-based, NOT 0-based
|
||||
switch (saveFileDialog.FilterIndex)
|
||||
{
|
||||
case 1: // xlsx
|
||||
default:
|
||||
LibraryExporter.ToXlsx(saveFileDialog.FileName);
|
||||
break;
|
||||
case 2: // csv
|
||||
LibraryExporter.ToCsv(saveFileDialog.FileName);
|
||||
break;
|
||||
case 3: // json
|
||||
LibraryExporter.ToJson(saveFileDialog.FileName);
|
||||
break;
|
||||
}
|
||||
|
||||
MessageBox.Show("Library exported to:\r\n" + saveFileDialog.FileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error attempting to export library");
|
||||
MessageBox.Show("Error attempting to export your library. Error message:\r\n\r\n" + ex.Message, "Error exporting", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region quick filters menu
|
||||
private void loadInitialQuickFilterState()
|
||||
{
|
||||
|
||||
@@ -201,6 +201,9 @@ namespace LibationWinForms
|
||||
// update cells incl Liberate button text
|
||||
dataGridView.InvalidateRow(rowId);
|
||||
|
||||
// needed in case filtering by -IsLiberated and it gets changed to Liberated. want to immediately show the change
|
||||
filter();
|
||||
|
||||
BackupCountsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
@@ -396,8 +399,6 @@ namespace LibationWinForms
|
||||
}
|
||||
currencyManager.ResumeBinding();
|
||||
VisibleCountChanged?.Invoke(this, dataGridView.AsEnumerable().Count(r => r.Visible));
|
||||
|
||||
var luceneSearchString_debug = searchResults.SearchString;
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
- [Download your books -- DRM-free!](#download-your-books----drm-free)
|
||||
- [Download PDF attachments](#download-pdf-attachments)
|
||||
- [Details of downloaded files](#details-of-downloaded-files)
|
||||
- [Export your library](#export-your-library)
|
||||
3. [Searching and filtering](#searching-and-filtering)
|
||||
- [Tags](#tags)
|
||||
- [Searches](#searches)
|
||||
@@ -144,6 +145,12 @@ When you set up Libation, you'll specify a Books directory. Libation looks insid
|
||||
* .cue: this is a file which logs where chapter breaks occur. Many tools are able to use this if you want to split your book into files along chapter lines.
|
||||
* .nfo: This is just some general info about the book and includes some technical stats about the audiofile.
|
||||
|
||||
### Export your library
|
||||
|
||||

|
||||
|
||||
Export your library to Excel, CSV, or JSON
|
||||
|
||||
## Searching and filtering
|
||||
|
||||
### Tags
|
||||
|
||||
77
WinFormsDesigner/Dialogs/Login/ApprovalNeededDialog.Designer.cs
generated
Normal file
77
WinFormsDesigner/Dialogs/Login/ApprovalNeededDialog.Designer.cs
generated
Normal file
@@ -0,0 +1,77 @@
|
||||
namespace WinFormsDesigner.Dialogs.Login
|
||||
{
|
||||
partial class ApprovalNeededDialog
|
||||
{
|
||||
/// <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()
|
||||
{
|
||||
this.approvedBtn = new System.Windows.Forms.Button();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// approvedBtn
|
||||
//
|
||||
this.approvedBtn.Location = new System.Drawing.Point(15, 25);
|
||||
this.approvedBtn.Name = "approvedBtn";
|
||||
this.approvedBtn.Size = new System.Drawing.Size(79, 23);
|
||||
this.approvedBtn.TabIndex = 1;
|
||||
this.approvedBtn.Text = "Approved";
|
||||
this.approvedBtn.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// label1
|
||||
//
|
||||
this.label1.AutoSize = true;
|
||||
this.label1.Location = new System.Drawing.Point(12, 9);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(104, 13);
|
||||
this.label1.TabIndex = 0;
|
||||
this.label1.Text = "Click after approving";
|
||||
//
|
||||
// ApprovalNeededDialog
|
||||
//
|
||||
this.AcceptButton = this.approvedBtn;
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(149, 60);
|
||||
this.Controls.Add(this.label1);
|
||||
this.Controls.Add(this.approvedBtn);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "ApprovalNeededDialog";
|
||||
this.ShowIcon = false;
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "Approval Needed";
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
private System.Windows.Forms.Button approvedBtn;
|
||||
private System.Windows.Forms.Label label1;
|
||||
}
|
||||
}
|
||||
13
WinFormsDesigner/Dialogs/Login/ApprovalNeededDialog.cs
Normal file
13
WinFormsDesigner/Dialogs/Login/ApprovalNeededDialog.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace WinFormsDesigner.Dialogs.Login
|
||||
{
|
||||
public partial class ApprovalNeededDialog : Form
|
||||
{
|
||||
public ApprovalNeededDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,120 +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>
|
||||
</root>
|
||||
@@ -1,120 +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>
|
||||
</root>
|
||||
@@ -5,8 +5,6 @@ namespace WinFormsDesigner.Dialogs.Login
|
||||
{
|
||||
public partial class _2faCodeDialog : Form
|
||||
{
|
||||
public string NewTags { get; private set; }
|
||||
|
||||
public _2faCodeDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -1,120 +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>
|
||||
</root>
|
||||
29
WinFormsDesigner/Form1.Designer.cs
generated
29
WinFormsDesigner/Form1.Designer.cs
generated
@@ -35,12 +35,14 @@
|
||||
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();
|
||||
@@ -55,7 +57,7 @@
|
||||
this.backupsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.pdfsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.addFilterBtn = new System.Windows.Forms.Button();
|
||||
this.noAccountsYetAddAccountToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.exportLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.menuStrip1.SuspendLayout();
|
||||
this.statusStrip1.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
@@ -103,6 +105,7 @@
|
||||
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);
|
||||
@@ -122,6 +125,12 @@
|
||||
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";
|
||||
@@ -161,6 +170,14 @@
|
||||
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[] {
|
||||
@@ -262,11 +279,11 @@
|
||||
this.addFilterBtn.Text = "Add To Quick Filters";
|
||||
this.addFilterBtn.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// noAccountsYetAddAccountToolStripMenuItem
|
||||
// exportLibraryToolStripMenuItem
|
||||
//
|
||||
this.noAccountsYetAddAccountToolStripMenuItem.Name = "noAccountsYetAddAccountToolStripMenuItem";
|
||||
this.noAccountsYetAddAccountToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
|
||||
this.noAccountsYetAddAccountToolStripMenuItem.Text = "No accounts yet. A&dd Account...";
|
||||
this.exportLibraryToolStripMenuItem.Name = "exportLibraryToolStripMenuItem";
|
||||
this.exportLibraryToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
|
||||
this.exportLibraryToolStripMenuItem.Text = "E&xport Library...";
|
||||
//
|
||||
// Form1
|
||||
//
|
||||
@@ -322,5 +339,7 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,12 @@
|
||||
<Compile Include="Dialogs\LibationFilesDialog.Designer.cs">
|
||||
<DependentUpon>LibationFilesDialog.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Dialogs\Login\ApprovalNeededDialog.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Dialogs\Login\ApprovalNeededDialog.Designer.cs">
|
||||
<DependentUpon>ApprovalNeededDialog.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Dialogs\Login\AudibleLoginDialog.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
@@ -176,15 +182,6 @@
|
||||
<EmbeddedResource Include="Dialogs\LibationFilesDialog.resx">
|
||||
<DependentUpon>LibationFilesDialog.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Dialogs\Login\AudibleLoginDialog.resx">
|
||||
<DependentUpon>AudibleLoginDialog.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Dialogs\Login\CaptchaDialog.resx">
|
||||
<DependentUpon>CaptchaDialog.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Dialogs\Login\_2faCodeDialog.resx">
|
||||
<DependentUpon>_2faCodeDialog.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Dialogs\ScanAccountsDialog.resx">
|
||||
<DependentUpon>ScanAccountsDialog.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon />
|
||||
<StartupObject />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<RootNamespace>ffmpeg_decrypt</RootNamespace>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net5.0-windows</TargetFramework>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.1.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="1.3.0">
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</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">
|
||||
<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>
|
||||
|
||||
</Project>
|
||||
39
_Tests/LibationSearchEngine.Tests/SearchEngineTests.cs
Normal file
39
_Tests/LibationSearchEngine.Tests/SearchEngineTests.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Dinah.Core;
|
||||
using FluentAssertions;
|
||||
using FluentAssertions.Common;
|
||||
using LibationSearchEngine;
|
||||
using Microsoft.VisualStudio.TestPlatform.Common.Filtering;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
using Moq.Protected;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using TestCommon;
|
||||
|
||||
namespace SearchEngineTests
|
||||
{
|
||||
[TestClass]
|
||||
public class FormatSearchQuery
|
||||
{
|
||||
[TestMethod]
|
||||
[DataRow(null, SearchEngine.ALL_QUERY)]
|
||||
[DataRow("", SearchEngine.ALL_QUERY)]
|
||||
[DataRow(" ", SearchEngine.ALL_QUERY)]
|
||||
[DataRow("israted", "israted:True")]
|
||||
[DataRow("israted:True", "israted:True")]
|
||||
[DataRow("isRated:false", "israted:false")]
|
||||
[DataRow("[israted]", "tags:israted")]
|
||||
[DataRow("1 to 10", "00000001.00 TO 00000010.00")]
|
||||
[DataRow("19990101 to 20001231", "19990101.00 TO 20001231.00")]
|
||||
public void FormattingTest(string input, string output)
|
||||
=> SearchEngine.FormatSearchQuery(input).Should().Be(output);
|
||||
}
|
||||
}
|
||||
BIN
images/Export.png
Normal file
BIN
images/Export.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Reference in New Issue
Block a user