Compare commits

...

13 Commits

Author SHA1 Message Date
Robert McRackan
a13b00d520 - better logging for LoginFailedException
- upgrade nuget pkg.s
2020-10-08 11:48:48 -04:00
Robert McRackan
b5ebe3db23 increm version 2020-10-07 17:48:30 -04:00
Robert McRackan
40f3e4503b Version # 2020-10-02 16:22:42 -04:00
Robert McRackan
0d93243b66 Obscure account names in logs 2020-10-02 16:10:35 -04:00
Robert McRackan
59c3845d21 Standardize logging 2020-10-02 09:35:58 -04:00
Robert McRackan
a3ee3c2881 v4.0.9 2020-10-01 12:35:16 -04:00
Robert McRackan
e971d34948 Bug fix: downloading PDFs without also Liberating books -- post-download verification step was failing 2020-09-22 15:43:13 -04:00
Robert McRackan
2b3f67fb99 Merge branch 'master' of https://github.com/rmcrackan/Libation 2020-09-21 13:10:45 -04:00
Robert McRackan
4509b8c8eb Audible whack-a-mole: they changed how to download pdfs 2020-09-21 13:10:36 -04:00
rmcrackan
2e40bebd7d Update README.md 2020-09-11 22:12:25 -04:00
Robert McRackan
dfc4121ab0 add pics for readme 2020-09-11 22:09:44 -04:00
Robert McRackan
3648607d4d export to xlsx 2020-09-11 21:56:56 -04:00
Robert McRackan
b22c35f841 new feature: json export 2020-09-11 21:07:20 -04:00
17 changed files with 185 additions and 101 deletions

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="15.0.5" />
<PackageReference Include="CsvHelper" Version="15.0.6" />
<PackageReference Include="NPOI" Version="2.5.1" />
</ItemGroup>

View File

@@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using AudibleApi;
using DataLayer;
using Dinah.Core;
using DtoImporterService;
using InternalUtilities;
using Serilog;
@@ -32,6 +33,24 @@ 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.RequestInputFields,
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,13 +80,13 @@ 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,
AccountName = account.AccountName.ToMask(),
AccountId = account.AccountId.ToMask(),
LocaleName = localeName,
});

View File

@@ -11,6 +11,15 @@ 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; }
@@ -40,9 +49,9 @@ namespace ApplicationServices
[Name("Pdf url")]
public string PdfUrl { get; set; }
[Name("Series Names")]
public string SeriesNames{ get; set; }
public string SeriesNames { get; set; }
[Name("Series Order")]
public string SeriesOrder { get; set; }
@@ -58,10 +67,10 @@ namespace ApplicationServices
[Name("Cover Id")]
public string PictureId { get; set; }
[Name("Is Abridged?")]
public bool IsAbridged { get; set; }
[Name("Date Published")]
public DateTime? DatePublished { get; set; }
@@ -112,16 +121,6 @@ namespace ApplicationServices
}
public static class LibraryExporter
{
private static List<acct> GetAccts()
{
var ia = true;
var userAccounts = new List<acct>();
for (var i = 0; i < 7; i++)
userAccounts.Add(new acct { UserName = "u" + i, Email = "e" + i, CreationDate = DateTime.Now.AddDays(-i * 2), LastLoginDate = DateTime.Now.AddDays(-i), IsApproved = (ia = !ia), Comment = "c [ ] * % , ' \" \\ \n " + i });
return userAccounts;
}
public static void ToCsv(string saveFilePath)
{
using var context = DbContexts.GetContext();
@@ -138,16 +137,19 @@ namespace ApplicationServices
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 library = context.GetLibrary_Flat_NoTracking();
}
public static void TEST_ToXlsx(string saveFilePath)
{
// https://steemit.com/utopian-io/@haig/how-to-create-excel-spreadsheets-using-npoi
var dtos = context.GetLibrary_Flat_NoTracking().ToDtos();
var workbook = new XSSFWorkbook();
var sheet = workbook.CreateSheet("Library");
@@ -162,71 +164,105 @@ namespace ApplicationServices
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(0);
cell.SetCellValue("Username");
cell.CellStyle = detailSubtotalCellStyle;
}
{
var cell = row.CreateCell(1);
cell.SetCellValue("Email");
cell.CellStyle = detailSubtotalCellStyle;
}
{
var cell = row.CreateCell(2);
cell.SetCellValue("Joined");
cell.CellStyle = detailSubtotalCellStyle;
}
{
var cell = row.CreateCell(3);
cell.SetCellValue("Last Login");
cell.CellStyle = detailSubtotalCellStyle;
}
{
var cell = row.CreateCell(4);
cell.SetCellValue("Approved?");
cell.CellStyle = detailSubtotalCellStyle;
}
{
var cell = row.CreateCell(5);
cell.SetCellValue("Comments");
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 (acct account in GetAccts())
foreach (var dto in dtos)
{
col = 0;
row = sheet.CreateRow(rowIndex);
row.CreateCell(0).SetCellValue(account.UserName);
row.CreateCell(1).SetCellValue(account.Email);
row.CreateCell(2).SetCellValue(account.CreationDate.ToShortDateString());
row.CreateCell(3).SetCellValue(account.LastLoginDate.ToShortDateString());
row.CreateCell(4).SetCellValue(account.IsApproved);
row.CreateCell(5).SetCellValue(account.Comment);
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);
}
class acct
private static int createCell(NPOI.SS.UserModel.IRow row, int col, float? nullableFloat)
{
public string UserName { get; set; }
public string Email { get; set; }
public DateTime CreationDate { get; set; }
public DateTime LastLoginDate { get; set; }
public bool IsApproved { get; set; }
public string Comment { get; set; }
}
public static void ToJson(string saveFilePath)
{
using var context = DbContexts.GetContext();
var library = context.GetLibrary_Flat_NoTracking();
if (nullableFloat.HasValue)
row.CreateCell(col++).SetCellValue(nullableFloat.Value);
else
row.CreateCell(col++).SetCellValue("");
return col;
}
}
}

View File

@@ -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,

View File

@@ -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(

View File

@@ -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>();

View File

@@ -46,7 +46,7 @@ namespace FileLiberator
private static async Task<StatusHandler> processBookAsync(IProcessable processable, LibraryBook libraryBook)
{
Serilog.Log.Information("Begin " + nameof(processBookAsync) + " {@DebugInfo}", new
Serilog.Log.Logger.Information("Begin " + nameof(processBookAsync) + " {@DebugInfo}", new
{
libraryBook.Book.Title,
libraryBook.Book.AudibleProductId,

View File

@@ -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]",
AccountId = account?.AccountId.ToMask() ?? "[empty]",
LocaleName = account?.Locale?.Name
});
return EzApiCreator.GetApiAsync(

View File

@@ -13,7 +13,7 @@
<!-- <PublishSingleFile>true</PublishSingleFile> -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Version>4.0.4.4</Version>
<Version>4.0.12.4</Version>
</PropertyGroup>
<ItemGroup>

View File

@@ -323,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()

View File

@@ -27,9 +27,7 @@
<Compile Update="UNTESTED\Dialogs\LibationFilesDialog.Designer.cs">
<DependentUpon>LibationFilesDialog.cs</DependentUpon>
</Compile>
<Compile Update="UNTESTED\Dialogs\ScanAccountsDialog.cs">
<SubType>Form</SubType>
</Compile>
<Compile Update="UNTESTED\Dialogs\ScanAccountsDialog.cs" />
<Compile Update="UNTESTED\Dialogs\ScanAccountsDialog.Designer.cs">
<DependentUpon>ScanAccountsDialog.cs</DependentUpon>
</Compile>

View File

@@ -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);

View File

@@ -10,6 +10,8 @@ namespace LibationWinForms.BookLiberation
{
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);
@@ -20,6 +22,8 @@ namespace LibationWinForms.BookLiberation
public static async Task BackupAllBooksAsync(EventHandler<LibraryBook> completedAction = null)
{
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync));
var backupBook = getWiredUpBackupBook(completedAction);
var automatedBackupsForm = attachToBackupsForm(backupBook);
@@ -96,6 +100,8 @@ namespace LibationWinForms.BookLiberation
public static async Task BackupAllPdfsAsync(EventHandler<LibraryBook> completedAction = null)
{
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllPdfsAsync));
var downloadPdf = getWiredUpDownloadPdf(completedAction);
var automatedBackupsForm = attachToBackupsForm(downloadPdf);
@@ -126,7 +132,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 +167,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)
{

View File

@@ -310,25 +310,32 @@ namespace LibationWinForms
var saveFileDialog = new SaveFileDialog
{
Title = "Where to export Library",
Filter = "CSV files (*.csv)|*.csv|All files (*.*)|*.*"
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: // csv
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.Error(ex, "Error attempting to export library");
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);
}
}

View File

@@ -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](images/Export.png)
Export your library to Excel, CSV, or JSON
## Searching and filtering
### Tags

View File

@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.2" />
<PackageReference Include="coverlet.collector" Version="1.3.0">

BIN
images/Export.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB