mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-13 00:09:22 -05:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6df47d4d9c | ||
|
|
0103c879f2 | ||
|
|
79e2bca8fc | ||
|
|
7bbc681767 | ||
|
|
c95dccd246 | ||
|
|
94cf665be7 | ||
|
|
7d38874257 | ||
|
|
659f793eb8 | ||
|
|
6f7cd4d5b5 | ||
|
|
068f37319f | ||
|
|
dc58a101af | ||
|
|
7b68415b02 | ||
|
|
1514de54da | ||
|
|
804bac5c4c | ||
|
|
3fa805d51f | ||
|
|
1eff725125 | ||
|
|
af1b1a70ae | ||
|
|
042f2e135a | ||
|
|
930fc3da58 | ||
|
|
90e8d03590 | ||
|
|
ee908a4f13 | ||
|
|
70ec31303b | ||
|
|
319d547aa0 | ||
|
|
a59e42e7c9 | ||
|
|
af2e89dd1e | ||
|
|
b2c5884e11 | ||
|
|
11d9cdefe2 | ||
|
|
54485ae150 | ||
|
|
4bd641ee50 | ||
|
|
6e56297434 | ||
|
|
e1f59eadbd | ||
|
|
cfda065219 |
@@ -1 +1,3 @@
|
||||
# This page has been moved to https://getlibation.com/docs/advanced/advanced
|
||||
# This page has been moved to https://getlibation.com/docs/advanced/advanced
|
||||
|
||||
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/advanced/advanced.md))
|
||||
@@ -1 +1,3 @@
|
||||
# This page has been moved to https://getlibation.com/docs/features/audio-file-formats
|
||||
# This page has been moved to https://getlibation.com/docs/features/audio-file-formats
|
||||
|
||||
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/features/audio-file-formats.md))
|
||||
@@ -1 +1,3 @@
|
||||
# This page has been moved to https://getlibation.com/docs/installation/docker
|
||||
# This page has been moved to https://getlibation.com/docs/installation/docker
|
||||
|
||||
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/installation/docker.md))
|
||||
@@ -1 +1,3 @@
|
||||
# This page has been moved to https://getlibation.com/docs/frequently-asked-questions
|
||||
# This page has been moved to https://getlibation.com/docs/frequently-asked-questions
|
||||
|
||||
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/frequently-asked-questions.md))
|
||||
@@ -1 +1,3 @@
|
||||
# This page has been moved to https://getlibation.com/docs/getting-started
|
||||
# This page has been moved to https://getlibation.com/docs/getting-started
|
||||
|
||||
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/getting-started.md))
|
||||
@@ -1 +1,3 @@
|
||||
# This page has been moved to https://getlibation.com/docs/installation/linux
|
||||
# This page has been moved to https://getlibation.com/docs/installation/linux
|
||||
|
||||
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/installation/linux.md))
|
||||
@@ -1 +1,3 @@
|
||||
# This page has been moved to https://getlibation.com/docs/installation/mac
|
||||
# This page has been moved to https://getlibation.com/docs/installation/mac
|
||||
|
||||
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/installation/mac.md))
|
||||
@@ -1 +1,3 @@
|
||||
# This page has been moved to https://getlibation.com/docs/development/nix-linux-setup
|
||||
# This page has been moved to https://getlibation.com/docs/development/nix-linux-setup
|
||||
|
||||
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/development/nix-linux-setup.md))
|
||||
@@ -1 +1,3 @@
|
||||
# This page has been moved to https://getlibation.com/docs/features/naming-templates
|
||||
# This page has been moved to https://getlibation.com/docs/features/naming-templates
|
||||
|
||||
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/features/naming-templates.md))
|
||||
@@ -1 +1,3 @@
|
||||
# This page has been moved to https://getlibation.com/docs/features/searching-and-filtering
|
||||
# This page has been moved to https://getlibation.com/docs/features/searching-and-filtering
|
||||
|
||||
([page in github](https://github.com/rmcrackan/Libation/blob/master/docs/features/searching-and-filtering.md))
|
||||
@@ -13,7 +13,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean.Codecs" Version="2.1.2.1" />
|
||||
<PackageReference Include="AAXClean.Codecs" Version="2.1.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -40,58 +40,14 @@ namespace AaxDecrypter
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
https://github.com/rmcrackan/Libation/pull/127#issuecomment-939088489
|
||||
|
||||
If the chapter truly is empty, that is, 0 audio frames in length, then yes it is ignored.
|
||||
If the chapter is shorter than 3 seconds long but still has some audio frames, those frames are combined with the following chapter and not split into a new file.
|
||||
|
||||
I also implemented file naming by chapter title. When 2 or more consecutive chapters are combined, the first of the combined chapter's title is used in the file name. For example, given an audiobook with the following chapters:
|
||||
|
||||
00:00:00 - 00:00:02 | Part 1
|
||||
00:00:02 - 00:35:00 | Chapter 1
|
||||
00:35:02 - 01:02:00 | Chapter 2
|
||||
01:02:00 - 01:02:02 | Part 2
|
||||
01:02:02 - 01:41:00 | Chapter 3
|
||||
01:41:00 - 02:05:00 | Chapter 4
|
||||
|
||||
The book will be split into the following files:
|
||||
|
||||
00:00:00 - 00:35:00 | Book - 01 - Part 1.m4b
|
||||
00:35:00 - 01:02:00 | Book - 02 - Chapter 2.m4b
|
||||
01:02:00 - 01:41:00 | Book - 03 - Part 2.m4b
|
||||
01:41:00 - 02:05:00 | Book - 04 - Chapter 4.m4b
|
||||
|
||||
That naming may not be desirable for everyone, but it's an easy change to instead use the last of the combined chapter's title in the file name.
|
||||
*/
|
||||
protected async override Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
||||
{
|
||||
if (AaxFile is null) return false;
|
||||
var chapters = DownloadOptions.ChapterInfo.Chapters;
|
||||
|
||||
// Ensure split files are at least minChapterLength in duration.
|
||||
var splitChapters = new ChapterInfo(DownloadOptions.ChapterInfo.StartOffset);
|
||||
|
||||
var runningTotal = TimeSpan.Zero;
|
||||
string title = "";
|
||||
|
||||
for (int i = 0; i < chapters.Count; i++)
|
||||
{
|
||||
if (runningTotal == TimeSpan.Zero)
|
||||
title = chapters[i].Title;
|
||||
|
||||
runningTotal += chapters[i].Duration;
|
||||
|
||||
if (runningTotal >= minChapterLength)
|
||||
{
|
||||
splitChapters.AddChapter(title, runningTotal);
|
||||
runningTotal = TimeSpan.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
await (AaxConversion = decryptMultiAsync(AaxFile, splitChapters));
|
||||
await (AaxConversion = decryptMultiAsync(AaxFile, DownloadOptions.ChapterInfo));
|
||||
|
||||
if (AaxConversion.IsCompletedSuccessfully)
|
||||
await moveMoovToBeginning(AaxFile, workingFileStream?.Name);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Version>13.1.0.1</Version>
|
||||
<Version>13.1.2.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="14.0.0" />
|
||||
|
||||
@@ -121,7 +121,7 @@ namespace AppScaffolding
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
Log.Logger.Warning(ex, "Could not delete SQLite WAL file: {@WalFile}", walFile);
|
||||
Log.Logger.Warning(ex, "Could not delete SQLite WAL file: {WalFile}", walFile);
|
||||
}
|
||||
}
|
||||
if (File.Exists(shmFile))
|
||||
@@ -132,7 +132,7 @@ namespace AppScaffolding
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Warning(ex, "Could not delete SQLite SHM file: {@ShmFile}", shmFile);
|
||||
Log.Logger.Warning(ex, "Could not delete SQLite SHM file: {ShmFile}", shmFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="33.1.0" />
|
||||
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
||||
<PackageReference Include="CsvHelper" Version="33.1.0" PrivateAssets="all" />
|
||||
<PackageReference Include="ClosedXML" Version="0.105.0" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
142
Source/ApplicationServices/ExportDto.cs
Normal file
142
Source/ApplicationServices/ExportDto.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using CsvHelper.Configuration.Attributes;
|
||||
using DataLayer;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace ApplicationServices;
|
||||
|
||||
internal class ExportDto(LibraryBook libBook)
|
||||
{
|
||||
[Name("Account")]
|
||||
public string Account { get; } = libBook.Account;
|
||||
|
||||
[Name("Date Added to library")]
|
||||
public DateTime DateAdded { get; } = libBook.DateAdded;
|
||||
|
||||
[Name("Is Audible Plus?")]
|
||||
public bool IsAudiblePlus { get; } = libBook.IsAudiblePlus;
|
||||
|
||||
[Name("Absent from last scan?")]
|
||||
public bool AbsentFromLastScan { get; } = libBook.AbsentFromLastScan;
|
||||
|
||||
[Name("Audible Product Id")]
|
||||
public string AudibleProductId { get; } = libBook.Book.AudibleProductId;
|
||||
|
||||
[Name("Locale")]
|
||||
public string Locale { get; } = libBook.Book.Locale;
|
||||
|
||||
[Name("Title")]
|
||||
public string Title { get; } = libBook.Book.Title;
|
||||
|
||||
[Name("Subtitle")]
|
||||
public string Subtitle { get; } = libBook.Book.Subtitle;
|
||||
|
||||
[Name("Authors")]
|
||||
public string AuthorNames { get; } = libBook.Book.AuthorNames;
|
||||
|
||||
[Name("Narrators")]
|
||||
public string NarratorNames { get; } = libBook.Book.NarratorNames;
|
||||
|
||||
[Name("Length In Minutes")]
|
||||
public int LengthInMinutes { get; } = libBook.Book.LengthInMinutes;
|
||||
|
||||
[Name("Description")]
|
||||
public string Description { get; } = libBook.Book.Description;
|
||||
|
||||
[Name("Publisher")]
|
||||
public string Publisher { get; } = libBook.Book.Publisher;
|
||||
|
||||
[Name("Has PDF")]
|
||||
public bool HasPdf { get; } = libBook.Book.HasPdf;
|
||||
|
||||
[Name("Series Names")]
|
||||
public string SeriesNames { get; } = libBook.Book.SeriesNames();
|
||||
|
||||
[Name("Series Order")]
|
||||
public string SeriesOrder { get; } = libBook.Book.SeriesLink?.Any() is true ? string.Join(", ", libBook.Book.SeriesLink.Select(sl => $"{sl.Order} : {sl.Series.Name}")) : "";
|
||||
|
||||
[Name("Community Rating: Overall")]
|
||||
public float? CommunityRatingOverall { get; } = ZeroIsNull(libBook.Book.Rating?.OverallRating);
|
||||
|
||||
[Name("Community Rating: Performance")]
|
||||
public float? CommunityRatingPerformance { get; } = ZeroIsNull(libBook.Book.Rating?.PerformanceRating);
|
||||
|
||||
[Name("Community Rating: Story")]
|
||||
public float? CommunityRatingStory { get; } = ZeroIsNull(libBook.Book.Rating?.StoryRating);
|
||||
|
||||
[Name("Cover Id")]
|
||||
public string PictureId { get; } = libBook.Book.PictureId;
|
||||
|
||||
[Name("Cover Id Large")]
|
||||
public string PictureLarge { get; } = libBook.Book.PictureLarge;
|
||||
|
||||
[Name("Is Abridged?")]
|
||||
public bool IsAbridged { get; } = libBook.Book.IsAbridged;
|
||||
|
||||
[Name("Date Published")]
|
||||
public DateTime? DatePublished { get; } = libBook.Book.DatePublished;
|
||||
|
||||
[Name("Categories")]
|
||||
public string CategoriesNames { get; } = string.Join("; ", libBook.Book.LowestCategoryNames());
|
||||
|
||||
[Name("My Rating: Overall")]
|
||||
public float? MyRatingOverall { get; } = ZeroIsNull(libBook.Book.UserDefinedItem.Rating.OverallRating);
|
||||
|
||||
[Name("My Rating: Performance")]
|
||||
public float? MyRatingPerformance { get; } = ZeroIsNull(libBook.Book.UserDefinedItem.Rating.PerformanceRating);
|
||||
|
||||
[Name("My Rating: Story")]
|
||||
public float? MyRatingStory { get; } = ZeroIsNull(libBook.Book.UserDefinedItem.Rating.StoryRating);
|
||||
|
||||
[Name("My Libation Tags")]
|
||||
public string MyLibationTags { get; } = libBook.Book.UserDefinedItem.Tags;
|
||||
|
||||
[Name("Book Liberated Status")]
|
||||
public string BookStatus { get; } = libBook.Book.UserDefinedItem.BookStatus.ToString();
|
||||
|
||||
[Name("PDF Liberated Status")]
|
||||
public string? PdfStatus { get; } = libBook.Book.UserDefinedItem.PdfStatus.ToString();
|
||||
|
||||
[Name("Content Type")]
|
||||
public string ContentType { get; } = libBook.Book.ContentType.ToString();
|
||||
|
||||
[Name("Language")]
|
||||
public string Language { get; } = libBook.Book.Language;
|
||||
|
||||
[Name("Last Downloaded")]
|
||||
public DateTime? LastDownloaded { get; } = libBook.Book.UserDefinedItem.LastDownloaded;
|
||||
|
||||
[Name("Last Downloaded Version")]
|
||||
public string? LastDownloadedVersion { get; } = libBook.Book.UserDefinedItem.LastDownloadedVersion?.ToString();
|
||||
|
||||
[Name("Is Finished?")]
|
||||
public bool IsFinished { get; } = libBook.Book.UserDefinedItem.IsFinished;
|
||||
|
||||
[Name("Is Spatial?")]
|
||||
public bool IsSpatial { get; } = libBook.Book.IsSpatial;
|
||||
|
||||
[Name("Included Until")]
|
||||
public DateTime? IncludedUntil { get; } = libBook.IncludedUntil;
|
||||
|
||||
[Name("Last Downloaded File Version")]
|
||||
public string? LastDownloadedFileVersion { get; } = libBook.Book.UserDefinedItem.LastDownloadedFileVersion;
|
||||
|
||||
[Ignore /* csv ignore */]
|
||||
public AudioFormat? LastDownloadedFormat { get; } = libBook.Book.UserDefinedItem.LastDownloadedFormat;
|
||||
|
||||
[Name("Last Downloaded Codec"), JsonIgnore]
|
||||
public string CodecString => LastDownloadedFormat?.CodecString ?? "";
|
||||
|
||||
[Name("Last Downloaded Sample rate"), JsonIgnore]
|
||||
public int? SampleRate => LastDownloadedFormat?.SampleRate;
|
||||
|
||||
[Name("Last Downloaded Audio Channels"), JsonIgnore]
|
||||
public int? ChannelCount => LastDownloadedFormat?.ChannelCount;
|
||||
|
||||
[Name("Last Downloaded Bitrate"), JsonIgnore]
|
||||
public int? BitRate => LastDownloadedFormat?.BitRate;
|
||||
|
||||
private static float? ZeroIsNull(float? value) => value is 0 ? null : value;
|
||||
}
|
||||
9
Source/ApplicationServices/ISearchEngine.cs
Normal file
9
Source/ApplicationServices/ISearchEngine.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using LibationSearchEngine;
|
||||
|
||||
#nullable enable
|
||||
namespace ApplicationServices;
|
||||
|
||||
public interface ISearchEngine
|
||||
{
|
||||
SearchResultSet? GetSearchResultSet(string? searchString);
|
||||
}
|
||||
@@ -220,6 +220,8 @@ namespace ApplicationServices
|
||||
{
|
||||
book.AbsentFromLastScan = false;
|
||||
}
|
||||
book.SetIncludedUntil(importItem.DtoItem.GetExpirationDate());
|
||||
book.SetIsAudiblePlus(importItem.DtoItem.IsAyce is true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,328 +3,79 @@ using CsvHelper;
|
||||
using CsvHelper.Configuration.Attributes;
|
||||
using DataLayer;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace ApplicationServices
|
||||
#nullable enable
|
||||
namespace ApplicationServices;
|
||||
|
||||
public static class LibraryExporter
|
||||
{
|
||||
public class ExportDto
|
||||
public static void ToCsv(string saveFilePath, IEnumerable<LibraryBook>? libraryBooks = null)
|
||||
{
|
||||
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];
|
||||
}
|
||||
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
|
||||
var dtos = libraryBooks.ToDtos();
|
||||
if (dtos.Count == 0)
|
||||
return;
|
||||
|
||||
[Name("Account")]
|
||||
public string Account { get; set; }
|
||||
using var csv = new CsvWriter(new System.IO.StreamWriter(saveFilePath), CultureInfo.CurrentCulture);
|
||||
csv.WriteHeader(typeof(ExportDto));
|
||||
csv.NextRecord();
|
||||
csv.WriteRecords(dtos); }
|
||||
|
||||
[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("Subtitle")]
|
||||
public string Subtitle { 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("Description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[Name("Publisher")]
|
||||
public string Publisher { get; set; }
|
||||
|
||||
[Name("Has PDF")]
|
||||
public bool HasPdf { 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; }
|
||||
|
||||
[Name("Book Liberated Status")]
|
||||
public string BookStatus { get; set; }
|
||||
|
||||
[Name("PDF Liberated Status")]
|
||||
public string PdfStatus { get; set; }
|
||||
|
||||
[Name("Content Type")]
|
||||
public string ContentType { get; set; }
|
||||
|
||||
[Name("Language")]
|
||||
public string Language { get; set; }
|
||||
|
||||
[Name("Last Downloaded")]
|
||||
public DateTime? LastDownloaded { get; set; }
|
||||
|
||||
[Name("Last Downloaded Version")]
|
||||
public string LastDownloadedVersion { get; set; }
|
||||
|
||||
[Name("Is Finished?")]
|
||||
public bool IsFinished { get; set; }
|
||||
|
||||
[Name("Is Spatial?")]
|
||||
public bool IsSpatial { get; set; }
|
||||
|
||||
[Name("Included Until")]
|
||||
public DateTime? IncludedUntil { get; set; }
|
||||
|
||||
[Name("Last Downloaded File Version")]
|
||||
public string LastDownloadedFileVersion { get; set; }
|
||||
|
||||
[Ignore /* csv ignore */]
|
||||
public AudioFormat LastDownloadedFormat { get; set; }
|
||||
|
||||
[Name("Last Downloaded Codec"), JsonIgnore]
|
||||
public string CodecString => LastDownloadedFormat?.CodecString ?? "";
|
||||
|
||||
[Name("Last Downloaded Sample rate"), JsonIgnore]
|
||||
public int? SampleRate => LastDownloadedFormat?.SampleRate;
|
||||
|
||||
[Name("Last Downloaded Audio Channels"), JsonIgnore]
|
||||
public int? ChannelCount => LastDownloadedFormat?.ChannelCount;
|
||||
|
||||
[Name("Last Downloaded Bitrate"), JsonIgnore]
|
||||
public int? BitRate => LastDownloadedFormat?.BitRate;
|
||||
public static void ToJson(string saveFilePath, IEnumerable<LibraryBook>? libraryBooks = null)
|
||||
{
|
||||
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
|
||||
var dtos = libraryBooks.ToDtos();
|
||||
var serializer = new JsonSerializer();
|
||||
using var writer = new JsonTextWriter(new System.IO.StreamWriter(saveFilePath)) { Formatting = Formatting.Indented };
|
||||
serializer.Serialize(writer, dtos);
|
||||
}
|
||||
|
||||
public static class LibToDtos
|
||||
public static void ToXlsx(string saveFilePath, IEnumerable<LibraryBook>? libraryBooks = null)
|
||||
{
|
||||
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,
|
||||
Subtitle = a.Book.Subtitle,
|
||||
AuthorNames = a.Book.AuthorNames,
|
||||
NarratorNames = a.Book.NarratorNames,
|
||||
LengthInMinutes = a.Book.LengthInMinutes,
|
||||
Description = a.Book.Description,
|
||||
Publisher = a.Book.Publisher,
|
||||
HasPdf = a.Book.HasPdf,
|
||||
SeriesNames = a.Book.SeriesNames(),
|
||||
SeriesOrder = a.Book.SeriesLink.Any() ? a.Book.SeriesLink?.Select(sl => $"{sl.Order} : {sl.Series.Name}").Aggregate((a, b) => $"{a}, {b}") : "",
|
||||
CommunityRatingOverall = a.Book.Rating?.OverallRating.ZeroIsNull(),
|
||||
CommunityRatingPerformance = a.Book.Rating?.PerformanceRating.ZeroIsNull(),
|
||||
CommunityRatingStory = a.Book.Rating?.StoryRating.ZeroIsNull(),
|
||||
PictureId = a.Book.PictureId,
|
||||
IsAbridged = a.Book.IsAbridged,
|
||||
DatePublished = a.Book.DatePublished,
|
||||
CategoriesNames = string.Join("; ", a.Book.LowestCategoryNames()),
|
||||
MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating.ZeroIsNull(),
|
||||
MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating.ZeroIsNull(),
|
||||
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating.ZeroIsNull(),
|
||||
MyLibationTags = a.Book.UserDefinedItem.Tags,
|
||||
BookStatus = a.Book.UserDefinedItem.BookStatus.ToString(),
|
||||
PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString(),
|
||||
ContentType = a.Book.ContentType.ToString(),
|
||||
Language = a.Book.Language,
|
||||
LastDownloaded = a.Book.UserDefinedItem.LastDownloaded,
|
||||
LastDownloadedVersion = a.Book.UserDefinedItem.LastDownloadedVersion?.ToString() ?? "",
|
||||
IsFinished = a.Book.UserDefinedItem.IsFinished,
|
||||
IsSpatial = a.Book.IsSpatial,
|
||||
IncludedUntil = a.IncludedUntil,
|
||||
LastDownloadedFileVersion = a.Book.UserDefinedItem.LastDownloadedFileVersion ?? "",
|
||||
LastDownloadedFormat = a.Book.UserDefinedItem.LastDownloadedFormat
|
||||
}).ToList();
|
||||
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
|
||||
var dtos = libraryBooks.ToDtos();
|
||||
|
||||
private static float? ZeroIsNull(this float value) => value is 0 ? null : value;
|
||||
}
|
||||
public static class LibraryExporter
|
||||
{
|
||||
public static void ToCsv(string saveFilePath)
|
||||
using var workbook = new XLWorkbook();
|
||||
var sheet = workbook.AddWorksheet("Library");
|
||||
var columns = typeof(ExportDto).GetProperties().Where(p => p.GetCustomAttribute<NameAttribute>() is not null).ToArray();
|
||||
|
||||
// headers
|
||||
var currentRow = sheet.FirstRow();
|
||||
var currentCell = currentRow.FirstCell();
|
||||
foreach (var column in columns)
|
||||
{
|
||||
var dtos = DbContexts.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);
|
||||
currentCell.Value = GetColumnName(column);
|
||||
currentCell.Style.Font.Bold = true;
|
||||
currentCell = currentCell.CellRight();
|
||||
}
|
||||
|
||||
public static void ToJson(string saveFilePath)
|
||||
var dateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern + " HH:mm:ss";
|
||||
|
||||
// Add data rows
|
||||
foreach (var dto in dtos)
|
||||
{
|
||||
var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos();
|
||||
var json = JsonConvert.SerializeObject(dtos, Formatting.Indented);
|
||||
System.IO.File.WriteAllText(saveFilePath, json);
|
||||
}
|
||||
currentRow = currentRow.RowBelow();
|
||||
currentCell = currentRow.FirstCell();
|
||||
|
||||
public static void ToXlsx(string saveFilePath)
|
||||
{
|
||||
var dtos = DbContexts.GetLibrary_Flat_NoTracking().ToDtos();
|
||||
|
||||
using var workbook = new XLWorkbook();
|
||||
var sheet = workbook.AddWorksheet("Library");
|
||||
|
||||
|
||||
// headers
|
||||
var columns = new[] {
|
||||
nameof(ExportDto.Account),
|
||||
nameof(ExportDto.DateAdded),
|
||||
nameof(ExportDto.AudibleProductId),
|
||||
nameof(ExportDto.Locale),
|
||||
nameof(ExportDto.Title),
|
||||
nameof(ExportDto.Subtitle),
|
||||
nameof(ExportDto.AuthorNames),
|
||||
nameof(ExportDto.NarratorNames),
|
||||
nameof(ExportDto.LengthInMinutes),
|
||||
nameof(ExportDto.Description),
|
||||
nameof(ExportDto.Publisher),
|
||||
nameof(ExportDto.HasPdf),
|
||||
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),
|
||||
nameof(ExportDto.BookStatus),
|
||||
nameof(ExportDto.PdfStatus),
|
||||
nameof(ExportDto.ContentType),
|
||||
nameof(ExportDto.Language),
|
||||
nameof(ExportDto.LastDownloaded),
|
||||
nameof(ExportDto.LastDownloadedVersion),
|
||||
nameof(ExportDto.IsFinished),
|
||||
nameof(ExportDto.IsSpatial),
|
||||
nameof(ExportDto.IncludedUntil),
|
||||
nameof(ExportDto.LastDownloadedFileVersion),
|
||||
nameof(ExportDto.CodecString),
|
||||
nameof(ExportDto.SampleRate),
|
||||
nameof(ExportDto.ChannelCount),
|
||||
nameof(ExportDto.BitRate)
|
||||
};
|
||||
|
||||
int rowIndex = 1, col = 1;
|
||||
var headerRow = sheet.Row(rowIndex++);
|
||||
foreach (var c in columns)
|
||||
foreach (var column in columns)
|
||||
{
|
||||
var headerCell = headerRow.Cell(col++);
|
||||
headerCell.Value = ExportDto.GetName(c);
|
||||
headerCell.Style.Font.Bold = true;
|
||||
var value = column.GetValue(dto);
|
||||
currentCell.Value = XLCellValue.FromObject(value);
|
||||
currentCell.Style.DateFormat.Format = currentCell.DataType is XLDataType.DateTime ? dateFormat : string.Empty;
|
||||
currentCell = currentCell.CellRight();
|
||||
}
|
||||
|
||||
var dateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern + " HH:mm:ss";
|
||||
|
||||
// Add data rows
|
||||
foreach (var dto in dtos)
|
||||
{
|
||||
col = 1;
|
||||
var row = sheet.Row(rowIndex++);
|
||||
|
||||
row.Cell(col++).Value = dto.Account;
|
||||
row.Cell(col++).SetDate(dto.DateAdded, dateFormat);
|
||||
row.Cell(col++).Value = dto.AudibleProductId;
|
||||
row.Cell(col++).Value = dto.Locale;
|
||||
row.Cell(col++).Value = dto.Title;
|
||||
row.Cell(col++).Value = dto.Subtitle;
|
||||
row.Cell(col++).Value = dto.AuthorNames;
|
||||
row.Cell(col++).Value = dto.NarratorNames;
|
||||
row.Cell(col++).Value = dto.LengthInMinutes;
|
||||
row.Cell(col++).Value = dto.Description;
|
||||
row.Cell(col++).Value = dto.Publisher;
|
||||
row.Cell(col++).Value = dto.HasPdf;
|
||||
row.Cell(col++).Value = dto.SeriesNames;
|
||||
row.Cell(col++).Value = dto.SeriesOrder;
|
||||
row.Cell(col++).Value = dto.CommunityRatingOverall;
|
||||
row.Cell(col++).Value = dto.CommunityRatingPerformance;
|
||||
row.Cell(col++).Value = dto.CommunityRatingStory;
|
||||
row.Cell(col++).Value = dto.PictureId;
|
||||
row.Cell(col++).Value = dto.IsAbridged;
|
||||
row.Cell(col++).SetDate(dto.DatePublished, dateFormat);
|
||||
row.Cell(col++).Value = dto.CategoriesNames;
|
||||
row.Cell(col++).Value = dto.MyRatingOverall;
|
||||
row.Cell(col++).Value = dto.MyRatingPerformance;
|
||||
row.Cell(col++).Value = dto.MyRatingStory;
|
||||
row.Cell(col++).Value = dto.MyLibationTags;
|
||||
row.Cell(col++).Value = dto.BookStatus;
|
||||
row.Cell(col++).Value = dto.PdfStatus;
|
||||
row.Cell(col++).Value = dto.ContentType;
|
||||
row.Cell(col++).Value = dto.Language;
|
||||
row.Cell(col++).SetDate(dto.LastDownloaded, dateFormat);
|
||||
row.Cell(col++).Value = dto.LastDownloadedVersion;
|
||||
row.Cell(col++).Value = dto.IsFinished;
|
||||
row.Cell(col++).Value = dto.IsSpatial;
|
||||
row.Cell(col++).Value = dto.IncludedUntil;
|
||||
row.Cell(col++).Value = dto.LastDownloadedFileVersion;
|
||||
row.Cell(col++).Value = dto.CodecString;
|
||||
row.Cell(col++).Value = dto.SampleRate;
|
||||
row.Cell(col++).Value = dto.ChannelCount;
|
||||
row.Cell(col++).Value = dto.BitRate;
|
||||
}
|
||||
|
||||
workbook.SaveAs(saveFilePath);
|
||||
}
|
||||
|
||||
private static void SetDate(this IXLCell cell, DateTime? value, string dateFormat)
|
||||
{
|
||||
cell.Value = value;
|
||||
cell.Style.DateFormat.Format = dateFormat;
|
||||
}
|
||||
workbook.SaveAs(saveFilePath);
|
||||
}
|
||||
|
||||
private static List<ExportDto> ToDtos(this IEnumerable<LibraryBook> library)
|
||||
=> library.Select(a => new ExportDto(a)).ToList();
|
||||
|
||||
private static string GetColumnName(PropertyInfo property)
|
||||
=> property.GetCustomAttribute<NameAttribute>()?.Names?.FirstOrDefault() ?? property.Name;
|
||||
}
|
||||
|
||||
16
Source/ApplicationServices/MainSearchEngine.cs
Normal file
16
Source/ApplicationServices/MainSearchEngine.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using LibationSearchEngine;
|
||||
|
||||
#nullable enable
|
||||
namespace ApplicationServices;
|
||||
|
||||
/// <summary>
|
||||
/// The main search engine used Libation.
|
||||
/// Acts as an adapter to SearchEngineCommands.Search()
|
||||
/// </summary>
|
||||
public class MainSearchEngine : ISearchEngine
|
||||
{
|
||||
public static MainSearchEngine Instance { get; } = new MainSearchEngine();
|
||||
private MainSearchEngine() { }
|
||||
public SearchResultSet? GetSearchResultSet(string? searchString)
|
||||
=> string.IsNullOrEmpty(searchString) ? null : SearchEngineCommands.Search(searchString);
|
||||
}
|
||||
45
Source/ApplicationServices/TempSearchEngine.cs
Normal file
45
Source/ApplicationServices/TempSearchEngine.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using LibationSearchEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
#nullable enable
|
||||
namespace ApplicationServices;
|
||||
|
||||
/// <summary>
|
||||
/// A temporary search engine created in InProgress/TempSearchEngine
|
||||
/// Used for Trash Bin searches to avoid interfering with the main search engine
|
||||
/// </summary>
|
||||
public class TempSearchEngine : ISearchEngine
|
||||
{
|
||||
public static string SearchEnginePath { get; }
|
||||
= System.IO.Path.Combine(Configuration.Instance.InProgress, nameof(TempSearchEngine));
|
||||
private SearchEngine SearchEngine { get; } = new SearchEngine(SearchEnginePath);
|
||||
|
||||
public bool ReindexSearchEngine(IEnumerable<LibraryBook> books)
|
||||
{
|
||||
try
|
||||
{
|
||||
SearchEngine.CreateNewIndex(books, overwrite: true);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public SearchResultSet? GetSearchResultSet(string? searchString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(searchString))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return SearchEngine.Search(searchString);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,6 @@ public class ContributorSanitizer : ISanitizer
|
||||
private static Person[]? SanitizePersonArray(Person?[]? contributors)
|
||||
=> contributors
|
||||
?.OfType<Person>()
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c.Asin) && !string.IsNullOrWhiteSpace(c.Name))
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c.Name))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="10.1.2.1" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.33.2" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.33.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
24
Source/AudibleUtilities/Extensions.cs
Normal file
24
Source/AudibleUtilities/Extensions.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using AudibleApi.Common;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace AudibleUtilities;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
extension(Item item)
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines when your audible plus or free book will expire from your library
|
||||
/// plan.IsAyce from underlying AudibleApi project determines the plans to look at, first plan found is used.
|
||||
/// In some cases current date is later than end date so exclude.
|
||||
/// </summary>
|
||||
/// <returns>The DateTime that this title will become unavailable, otherwise null</returns>
|
||||
public DateTime? GetExpirationDate()
|
||||
=> item.Plans
|
||||
?.Where(p => p.IsAyce)
|
||||
.Select(p => p.EndDate)
|
||||
.FirstOrDefault(end => end.HasValue && end.Value.Year is not (2099 or 9999) && end.Value.LocalDateTime >= DateTime.Now)
|
||||
?.DateTime;
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,11 @@
|
||||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
499
Source/DataLayer.Postgres/Migrations/20260107224301_AddIsAudiblePlus.Designer.cs
generated
Normal file
499
Source/DataLayer.Postgres/Migrations/20260107224301_AddIsAudiblePlus.Designer.cs
generated
Normal file
@@ -0,0 +1,499 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DataLayer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DataLayer.Postgres.Migrations
|
||||
{
|
||||
[DbContext(typeof(LibationContext))]
|
||||
[Migration("20260107224301_AddIsAudiblePlus")]
|
||||
partial class AddIsAudiblePlus
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("CategoryCategoryLadder", b =>
|
||||
{
|
||||
b.Property<int>("_categoriesCategoryId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("_categoryLaddersCategoryLadderId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("_categoriesCategoryId", "_categoryLaddersCategoryLadderId");
|
||||
|
||||
b.HasIndex("_categoryLaddersCategoryLadderId");
|
||||
|
||||
b.ToTable("CategoryCategoryLadder");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("BookId"));
|
||||
|
||||
b.Property<string>("AudibleProductId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("ContentType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("DatePublished")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsAbridged")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsSpatial")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("LengthInMinutes")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Locale")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PictureId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PictureLarge")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Subtitle")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.HasIndex("AudibleProductId");
|
||||
|
||||
b.ToTable("Books");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookCategory", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("CategoryLadderId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("BookId", "CategoryLadderId");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("CategoryLadderId");
|
||||
|
||||
b.ToTable("BookCategory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ContributorId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<byte>("Order")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.HasKey("BookId", "ContributorId", "Role");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("ContributorId");
|
||||
|
||||
b.ToTable("BookContributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.Property<int>("CategoryId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("CategoryId"));
|
||||
|
||||
b.Property<string>("AudibleCategoryId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("CategoryId");
|
||||
|
||||
b.HasIndex("AudibleCategoryId");
|
||||
|
||||
b.ToTable("Categories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.CategoryLadder", b =>
|
||||
{
|
||||
b.Property<int>("CategoryLadderId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("CategoryLadderId"));
|
||||
|
||||
b.HasKey("CategoryLadderId");
|
||||
|
||||
b.ToTable("CategoryLadders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Property<int>("ContributorId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("ContributorId"));
|
||||
|
||||
b.Property<string>("AudibleContributorId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("ContributorId");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.ToTable("Contributors");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
ContributorId = -1,
|
||||
Name = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("AbsentFromLastScan")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Account")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("DateAdded")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("IncludedUntil")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<bool>("IsAudiblePlus")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.ToTable("LibraryBooks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("SeriesId"));
|
||||
|
||||
b.Property<string>("AudibleSeriesId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("SeriesId");
|
||||
|
||||
b.HasIndex("AudibleSeriesId");
|
||||
|
||||
b.ToTable("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Order")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("SeriesId", "BookId");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("SeriesBook");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CategoryCategoryLadder", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("_categoriesCategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.CategoryLadder", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("_categoryLaddersCategoryLadderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.Property<float>("OverallRating")
|
||||
.HasColumnType("real");
|
||||
|
||||
b1.Property<float>("PerformanceRating")
|
||||
.HasColumnType("real");
|
||||
|
||||
b1.Property<float>("StoryRating")
|
||||
.HasColumnType("real");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("Books");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("BookId");
|
||||
});
|
||||
|
||||
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
|
||||
{
|
||||
b1.Property<int>("SupplementId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("SupplementId"));
|
||||
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.Property<string>("Url")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("SupplementId");
|
||||
|
||||
b1.HasIndex("BookId");
|
||||
|
||||
b1.ToTable("Supplement");
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.Navigation("Book");
|
||||
});
|
||||
|
||||
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.Property<int>("BookStatus")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.Property<bool>("IsFinished")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<DateTime?>("LastDownloaded")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b1.Property<string>("LastDownloadedFileVersion")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<long?>("LastDownloadedFormat")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("LastDownloadedVersion")
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.Property<int?>("PdfStatus")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.Property<string>("Tags")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("UserDefinedItem", (string)null);
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
|
||||
{
|
||||
b2.Property<int>("UserDefinedItemBookId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b2.Property<float>("OverallRating")
|
||||
.HasColumnType("real");
|
||||
|
||||
b2.Property<float>("PerformanceRating")
|
||||
.HasColumnType("real");
|
||||
|
||||
b2.Property<float>("StoryRating")
|
||||
.HasColumnType("real");
|
||||
|
||||
b2.HasKey("UserDefinedItemBookId");
|
||||
|
||||
b2.ToTable("UserDefinedItem");
|
||||
|
||||
b2.WithOwner()
|
||||
.HasForeignKey("UserDefinedItemBookId");
|
||||
});
|
||||
|
||||
b1.Navigation("Book");
|
||||
|
||||
b1.Navigation("Rating")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
b.Navigation("Rating");
|
||||
|
||||
b.Navigation("Supplements");
|
||||
|
||||
b.Navigation("UserDefinedItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookCategory", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("CategoriesLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.CategoryLadder", "CategoryLadder")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("CategoryLadderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("CategoryLadder");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("ContributorsLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Contributor", "Contributor")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("ContributorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Contributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithOne()
|
||||
.HasForeignKey("DataLayer.LibraryBook", "BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("SeriesLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Series", "Series")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Navigation("CategoriesLink");
|
||||
|
||||
b.Navigation("ContributorsLink");
|
||||
|
||||
b.Navigation("SeriesLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.CategoryLadder", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DataLayer.Postgres.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddIsAudiblePlus : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Tags",
|
||||
table: "UserDefinedItem",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "real",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "real",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "real",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsAudiblePlus",
|
||||
table: "LibraryBooks",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsAudiblePlus",
|
||||
table: "LibraryBooks");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Tags",
|
||||
table: "UserDefinedItem",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "real",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real");
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "real",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real");
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "real",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "real");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ namespace DataLayer.Postgres.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.8")
|
||||
.HasAnnotation("ProductVersion", "10.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
@@ -210,6 +210,9 @@ namespace DataLayer.Postgres.Migrations
|
||||
b.Property<DateTime?>("IncludedUntil")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<bool>("IsAudiblePlus")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
@@ -351,6 +354,7 @@ namespace DataLayer.Postgres.Migrations
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.Property<string>("Tags")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
@@ -384,7 +388,8 @@ namespace DataLayer.Postgres.Migrations
|
||||
|
||||
b1.Navigation("Book");
|
||||
|
||||
b1.Navigation("Rating");
|
||||
b1.Navigation("Rating")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
b.Navigation("Rating");
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
482
Source/DataLayer.Sqlite/Migrations/20260107224303_AddIsAudiblePlus.Designer.cs
generated
Normal file
482
Source/DataLayer.Sqlite/Migrations/20260107224303_AddIsAudiblePlus.Designer.cs
generated
Normal file
@@ -0,0 +1,482 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DataLayer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
[DbContext(typeof(LibationContext))]
|
||||
[Migration("20260107224303_AddIsAudiblePlus")]
|
||||
partial class AddIsAudiblePlus
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
|
||||
|
||||
modelBuilder.Entity("CategoryCategoryLadder", b =>
|
||||
{
|
||||
b.Property<int>("_categoriesCategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("_categoryLaddersCategoryLadderId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("_categoriesCategoryId", "_categoryLaddersCategoryLadderId");
|
||||
|
||||
b.HasIndex("_categoryLaddersCategoryLadderId");
|
||||
|
||||
b.ToTable("CategoryCategoryLadder");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleProductId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ContentType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("DatePublished")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsAbridged")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsSpatial")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("LengthInMinutes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Locale")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PictureId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PictureLarge")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Subtitle")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.HasIndex("AudibleProductId");
|
||||
|
||||
b.ToTable("Books");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookCategory", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CategoryLadderId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("BookId", "CategoryLadderId");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("CategoryLadderId");
|
||||
|
||||
b.ToTable("BookCategory");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ContributorId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<byte>("Order")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("BookId", "ContributorId", "Role");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("ContributorId");
|
||||
|
||||
b.ToTable("BookContributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Category", b =>
|
||||
{
|
||||
b.Property<int>("CategoryId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleCategoryId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("CategoryId");
|
||||
|
||||
b.HasIndex("AudibleCategoryId");
|
||||
|
||||
b.ToTable("Categories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.CategoryLadder", b =>
|
||||
{
|
||||
b.Property<int>("CategoryLadderId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("CategoryLadderId");
|
||||
|
||||
b.ToTable("CategoryLadders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Property<int>("ContributorId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleContributorId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("ContributorId");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.ToTable("Contributors");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
ContributorId = -1,
|
||||
Name = ""
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("AbsentFromLastScan")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Account")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("IncludedUntil")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsAudiblePlus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("BookId");
|
||||
|
||||
b.ToTable("LibraryBooks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AudibleSeriesId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SeriesId");
|
||||
|
||||
b.HasIndex("AudibleSeriesId");
|
||||
|
||||
b.ToTable("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.Property<int>("SeriesId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Order")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("SeriesId", "BookId");
|
||||
|
||||
b.HasIndex("BookId");
|
||||
|
||||
b.HasIndex("SeriesId");
|
||||
|
||||
b.ToTable("SeriesBook");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CategoryCategoryLadder", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Category", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("_categoriesCategoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.CategoryLadder", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("_categoryLaddersCategoryLadderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<float>("OverallRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.Property<float>("PerformanceRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.Property<float>("StoryRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("Books");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("BookId");
|
||||
});
|
||||
|
||||
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
|
||||
{
|
||||
b1.Property<int>("SupplementId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Url")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("SupplementId");
|
||||
|
||||
b1.HasIndex("BookId");
|
||||
|
||||
b1.ToTable("Supplement");
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.Navigation("Book");
|
||||
});
|
||||
|
||||
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
|
||||
{
|
||||
b1.Property<int>("BookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<int>("BookStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<bool>("IsFinished")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<DateTime?>("LastDownloaded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<string>("LastDownloadedFileVersion")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<long?>("LastDownloadedFormat")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("LastDownloadedVersion")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.Property<int?>("PdfStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Tags")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
|
||||
b1.ToTable("UserDefinedItem", (string)null);
|
||||
|
||||
b1.WithOwner("Book")
|
||||
.HasForeignKey("BookId");
|
||||
|
||||
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
|
||||
{
|
||||
b2.Property<int>("UserDefinedItemBookId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b2.Property<float>("OverallRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.Property<float>("PerformanceRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.Property<float>("StoryRating")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b2.HasKey("UserDefinedItemBookId");
|
||||
|
||||
b2.ToTable("UserDefinedItem");
|
||||
|
||||
b2.WithOwner()
|
||||
.HasForeignKey("UserDefinedItemBookId");
|
||||
});
|
||||
|
||||
b1.Navigation("Book");
|
||||
|
||||
b1.Navigation("Rating")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
b.Navigation("Rating");
|
||||
|
||||
b.Navigation("Supplements");
|
||||
|
||||
b.Navigation("UserDefinedItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookCategory", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("CategoriesLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.CategoryLadder", "CategoryLadder")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("CategoryLadderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("CategoryLadder");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("ContributorsLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Contributor", "Contributor")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("ContributorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Contributor");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithOne()
|
||||
.HasForeignKey("DataLayer.LibraryBook", "BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||
{
|
||||
b.HasOne("DataLayer.Book", "Book")
|
||||
.WithMany("SeriesLink")
|
||||
.HasForeignKey("BookId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("DataLayer.Series", "Series")
|
||||
.WithMany("BooksLink")
|
||||
.HasForeignKey("SeriesId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("Series");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Book", b =>
|
||||
{
|
||||
b.Navigation("CategoriesLink");
|
||||
|
||||
b.Navigation("ContributorsLink");
|
||||
|
||||
b.Navigation("SeriesLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.CategoryLadder", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Contributor", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DataLayer.Series", b =>
|
||||
{
|
||||
b.Navigation("BooksLink");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DataLayer.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddIsAudiblePlus : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Tags",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "REAL",
|
||||
nullable: false,
|
||||
defaultValue: 0f,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsAudiblePlus",
|
||||
table: "LibraryBooks",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsAudiblePlus",
|
||||
table: "LibraryBooks");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Tags",
|
||||
table: "UserDefinedItem",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_StoryRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "REAL",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL");
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_PerformanceRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "REAL",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL");
|
||||
|
||||
migrationBuilder.AlterColumn<float>(
|
||||
name: "Rating_OverallRating",
|
||||
table: "UserDefinedItem",
|
||||
type: "REAL",
|
||||
nullable: true,
|
||||
oldClrType: typeof(float),
|
||||
oldType: "REAL");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace DataLayer.Migrations
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.8");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
|
||||
|
||||
modelBuilder.Entity("CategoryCategoryLadder", b =>
|
||||
{
|
||||
@@ -197,6 +197,9 @@ namespace DataLayer.Migrations
|
||||
b.Property<DateTime?>("IncludedUntil")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsAudiblePlus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -334,6 +337,7 @@ namespace DataLayer.Migrations
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b1.Property<string>("Tags")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b1.HasKey("BookId");
|
||||
@@ -367,7 +371,8 @@ namespace DataLayer.Migrations
|
||||
|
||||
b1.Navigation("Book");
|
||||
|
||||
b1.Navigation("Rating");
|
||||
b1.Navigation("Rating")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
b.Navigation("Rating");
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
<PackageReference Include="Dinah.Core" Version="10.0.0.1" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="10.0.0.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace DataLayer
|
||||
public bool AbsentFromLastScan { get; set; }
|
||||
|
||||
public DateTime? IncludedUntil { get; private set; }
|
||||
public bool IsAudiblePlus { get; set; }
|
||||
private LibraryBook() { }
|
||||
public LibraryBook(Book book, DateTime dateAdded, string account)
|
||||
{
|
||||
@@ -28,6 +29,7 @@ namespace DataLayer
|
||||
|
||||
public void SetAccount(string account) => Account = account;
|
||||
public void SetIncludedUntil(DateTime? includedUntil) => IncludedUntil = includedUntil;
|
||||
public void SetIsAudiblePlus(bool isAudiblePlus) => IsAudiblePlus = isAudiblePlus;
|
||||
public override string ToString() => $"{DateAdded:d} {Book}";
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,11 @@ using System.Text;
|
||||
namespace DataLayer;
|
||||
public class MockLibraryBook : LibraryBook
|
||||
{
|
||||
protected MockLibraryBook(Book book, DateTime dateAdded, string account, DateTime? includedUntil)
|
||||
protected MockLibraryBook(Book book, DateTime dateAdded, string account, DateTime? includedUntil, bool isAudiblePlus)
|
||||
: base(book, dateAdded, account)
|
||||
{
|
||||
SetIncludedUntil(includedUntil);
|
||||
SetIsAudiblePlus(isAudiblePlus);
|
||||
}
|
||||
|
||||
public MockLibraryBook AddSeries(string seriesName, int order)
|
||||
@@ -76,6 +77,7 @@ public class MockLibraryBook : LibraryBook
|
||||
DateTime? dateAdded = null,
|
||||
DateTime? datePublished = null,
|
||||
DateTime? includedUntil = null,
|
||||
bool isAudiblePlus = false,
|
||||
string title = "Mock Book Title",
|
||||
string subtitle = "Mock Book Subtitle",
|
||||
string description = "This is a mock book description.",
|
||||
@@ -115,7 +117,8 @@ public class MockLibraryBook : LibraryBook
|
||||
book,
|
||||
dateAdded ?? DateTime.Now,
|
||||
account,
|
||||
includedUntil)
|
||||
includedUntil,
|
||||
isAudiblePlus)
|
||||
{
|
||||
AbsentFromLastScan = absetFromLastScan
|
||||
};
|
||||
|
||||
@@ -47,7 +47,8 @@ namespace DataLayer
|
||||
=> context
|
||||
.LibraryBooks
|
||||
.AsNoTrackingWithIdentityResolution()
|
||||
.Where(lb => lb.IsDeleted)
|
||||
//Return all parents so the trash bin grid can show podcasts beneath their parents
|
||||
.Where(lb => lb.IsDeleted || lb.Book.ContentType == ContentType.Parent)
|
||||
.getLibrary()
|
||||
.ToList();
|
||||
|
||||
|
||||
@@ -18,6 +18,14 @@ namespace DtoImporterService
|
||||
private SeriesImporter seriesImporter { get; }
|
||||
private CategoryImporter categoryImporter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether <see cref="BookImporter"/> loaded every Book from the <seealso cref="LibationContext"/> during import.
|
||||
/// If true, the DbContext was queried for all Books, rather than just those being imported.
|
||||
/// If means that all <see cref="LibraryBook"/> objects in the DbContext will have their <see cref="LibraryBook.Book"/> property populated.
|
||||
/// If false, only those Books being imported were loaded, and some <see cref="LibraryBook"/> objects will have a null <see cref="LibraryBook.Book"/> property for books not included in the import set.
|
||||
/// </summary>
|
||||
internal bool LoadedEntireLibrary {get; private set; }
|
||||
|
||||
public BookImporter(LibationContext context) : base(context)
|
||||
{
|
||||
contributorImporter = new ContributorImporter(DbContext);
|
||||
@@ -56,6 +64,7 @@ namespace DtoImporterService
|
||||
.ToArray()
|
||||
.Where(b => productIds.Contains(b.AudibleProductId))
|
||||
.ToDictionarySafe(b => b.AudibleProductId);
|
||||
LoadedEntireLibrary = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -69,16 +78,16 @@ namespace DtoImporterService
|
||||
{
|
||||
var qtyNew = 0;
|
||||
|
||||
foreach (var item in importItems)
|
||||
{
|
||||
if (!Cache.TryGetValue(item.DtoItem.ProductId, out var book))
|
||||
foreach (var item in importItems)
|
||||
{
|
||||
book = createNewBook(item);
|
||||
qtyNew++;
|
||||
}
|
||||
if (!Cache.TryGetValue(item.DtoItem.ProductId, out var book))
|
||||
{
|
||||
book = createNewBook(item);
|
||||
qtyNew++;
|
||||
}
|
||||
|
||||
updateBook(item, book);
|
||||
}
|
||||
updateBook(item, book);
|
||||
}
|
||||
|
||||
return qtyNew;
|
||||
}
|
||||
@@ -160,6 +169,14 @@ namespace DtoImporterService
|
||||
{
|
||||
var item = importItem.DtoItem;
|
||||
|
||||
// Replacing narrators only became necessary to correct a bug introduced in 13.1.0
|
||||
// which would no import narrators with null ASINs. Thus, affected books had the
|
||||
// author listed as the narrators. This can probably be removed in the future.
|
||||
// Bug went live in 13.1.0 on 2026/01/02. Today is 2026/01/08.
|
||||
var narrators = item.Narrators?.DistinctBy(a => a.Name).Select(n => contributorImporter.Cache[n.Name]).ToArray();
|
||||
if (narrators is not null && narrators.Length > 0)
|
||||
book.ReplaceNarrators(narrators);
|
||||
|
||||
book.UpdateLengthInMinutes(item.LengthInMinutes);
|
||||
|
||||
// Update the book titles, since formatting can change
|
||||
|
||||
@@ -87,7 +87,7 @@ namespace DtoImporterService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding category ladder. {@DebugInfo}", categoryList);
|
||||
Serilog.Log.Logger.Error(ex, "Error adding category ladder.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,138 +6,135 @@ using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
|
||||
namespace DtoImporterService
|
||||
namespace DtoImporterService;
|
||||
|
||||
public class LibraryBookImporter : ItemsImporterBase
|
||||
{
|
||||
public class LibraryBookImporter : ItemsImporterBase
|
||||
protected override IValidator Validator => new LibraryValidator();
|
||||
|
||||
private BookImporter bookImporter { get; }
|
||||
|
||||
public LibraryBookImporter(LibationContext context) : base(context)
|
||||
{
|
||||
protected override IValidator Validator => new LibraryValidator();
|
||||
|
||||
private BookImporter bookImporter { get; }
|
||||
|
||||
public LibraryBookImporter(LibationContext context) : base(context)
|
||||
{
|
||||
bookImporter = new BookImporter(DbContext);
|
||||
}
|
||||
|
||||
protected override int DoImport(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
bookImporter.Import(importItems);
|
||||
|
||||
var qtyNew = upsertLibraryBooks(importItems);
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private int upsertLibraryBooks(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
// technically, we should be able to have duplicate books from separate accounts.
|
||||
// this would violate the current pk and would be difficult to deal with elsewhere:
|
||||
// - what to show in the grid
|
||||
// - which to consider liberated
|
||||
//
|
||||
// sqlite cannot alter pk. the work around is an extensive headache
|
||||
// - update: now possible in .net5/efcore5
|
||||
//
|
||||
// currently, inserting LibraryBook will throw error if the same book is in multiple accounts for the same region.
|
||||
//
|
||||
// CURRENT SOLUTION: don't re-insert
|
||||
|
||||
|
||||
//When Books are upserted during the BookImporter run, they are linked to their LibraryBook in the DbContext
|
||||
//instance. If a LibraryBook has a null book here, that means it's Book was not imported during by BookImporter.
|
||||
//There should never be duplicates, but this is defensive.
|
||||
var existingEntries = DbContext.LibraryBooks.AsEnumerable().Where(l => l.Book is not null).ToDictionarySafe(l => l.Book.AudibleProductId);
|
||||
|
||||
//If importItems are contains duplicates by asin, keep the Item that's "available"
|
||||
var uniqueImportItems = ToDictionarySafe(importItems, dto => dto.DtoItem.ProductId, tieBreak);
|
||||
|
||||
int qtyNew = 0;
|
||||
|
||||
foreach (var item in uniqueImportItems.Values)
|
||||
{
|
||||
if (existingEntries.TryGetValue(item.DtoItem.ProductId, out LibraryBook existing))
|
||||
{
|
||||
if (existing.Account != item.AccountId)
|
||||
{
|
||||
//Book is absent from the existing LibraryBook's account. Use the alternate account.
|
||||
existing.SetAccount(item.AccountId);
|
||||
}
|
||||
|
||||
existing.AbsentFromLastScan = isUnavailable(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing = new LibraryBook(
|
||||
bookImporter.Cache[item.DtoItem.ProductId],
|
||||
item.DtoItem.DateAdded,
|
||||
item.AccountId)
|
||||
{
|
||||
AbsentFromLastScan = isUnavailable(item)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
DbContext.LibraryBooks.Add(existing);
|
||||
qtyNew++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding library book. {@DebugInfo}", new { existing.Book, existing.Account });
|
||||
}
|
||||
}
|
||||
|
||||
existing.SetIncludedUntil(GetExpirationDate(item));
|
||||
}
|
||||
|
||||
var scannedAccounts = importItems.Select(i => i.AccountId).Distinct().ToList();
|
||||
|
||||
//If an existing Book wasn't found in the import, the owning LibraryBook's Book will be null.
|
||||
//Only change AbsentFromLastScan for LibraryBooks of accounts that were scanned.
|
||||
foreach (var nullBook in DbContext.LibraryBooks.AsEnumerable().Where(lb => lb.Book is null && lb.Account.In(scannedAccounts)))
|
||||
nullBook.AbsentFromLastScan = true;
|
||||
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private static Dictionary<TKey, TSource> ToDictionarySafe<TKey, TSource>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TSource, TSource> tieBreaker)
|
||||
{
|
||||
var dictionary = new Dictionary<TKey, TSource>();
|
||||
|
||||
foreach (TSource newItem in source)
|
||||
{
|
||||
TKey key = keySelector(newItem);
|
||||
|
||||
dictionary[key]
|
||||
= dictionary.TryGetValue(key, out TSource existingItem)
|
||||
? tieBreaker(existingItem, newItem)
|
||||
: newItem;
|
||||
}
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
private static ImportItem tieBreak(ImportItem item1, ImportItem item2)
|
||||
=> isUnavailable(item1) && !isUnavailable(item2) ? item2 : item1;
|
||||
|
||||
private static bool isUnavailable(ImportItem item)
|
||||
=> isFutureRelease(item) || isPlusTitleUnavailable(item);
|
||||
|
||||
private static bool isFutureRelease(ImportItem item)
|
||||
=> item.DtoItem.IssueDate is DateTimeOffset dt && dt > DateTimeOffset.UtcNow;
|
||||
|
||||
private static bool isPlusTitleUnavailable(ImportItem item)
|
||||
=> item.DtoItem.ContentType is null
|
||||
|| (item.DtoItem.IsAyce is true && item.DtoItem.Plans?.Any(p => p.IsAyce) is not true);
|
||||
|
||||
/// <summary>
|
||||
/// Determines when your audible plus or free book will expire from your library
|
||||
/// plan.IsAyce from underlying AudibleApi project determines the plans to look at, first plan found is used.
|
||||
/// In some cases current date is later than end date so exclude.
|
||||
/// </summary>
|
||||
/// <returns>The DateTime that this title will become unavailable, otherwise null</returns>
|
||||
private static DateTime? GetExpirationDate(ImportItem item)
|
||||
=> item.DtoItem.Plans
|
||||
?.Where(p => p.IsAyce)
|
||||
.Select(p => p.EndDate)
|
||||
.FirstOrDefault(end => end.HasValue && end.Value.Year is not (2099 or 9999) && end.Value.LocalDateTime >= DateTime.Now)
|
||||
?.DateTime;
|
||||
bookImporter = new BookImporter(DbContext);
|
||||
}
|
||||
|
||||
protected override int DoImport(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
bookImporter.Import(importItems);
|
||||
|
||||
var qtyNew = upsertLibraryBooks(importItems);
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private int upsertLibraryBooks(IEnumerable<ImportItem> importItems)
|
||||
{
|
||||
// technically, we should be able to have duplicate books from separate accounts.
|
||||
// this would violate the current pk and would be difficult to deal with elsewhere:
|
||||
// - what to show in the grid
|
||||
// - which to consider liberated
|
||||
//
|
||||
// sqlite cannot alter pk. the work around is an extensive headache
|
||||
// - update: now possible in .net5/efcore5
|
||||
//
|
||||
// currently, inserting LibraryBook will throw error if the same book is in multiple accounts for the same region.
|
||||
//
|
||||
// CURRENT SOLUTION: don't re-insert
|
||||
|
||||
|
||||
//When Books are upserted during the BookImporter run, they are linked to their LibraryBook in the DbContext
|
||||
//instance. If a LibraryBook has a null book here, that means it's Book was not imported during by BookImporter.
|
||||
//There should never be duplicates, but this is defensive.
|
||||
var existingEntries = DbContext.LibraryBooks.AsEnumerable().Where(l => l.Book is not null).ToDictionarySafe(l => l.Book.AudibleProductId);
|
||||
|
||||
//If importItems are contains duplicates by asin, keep the Item that's "available"
|
||||
var uniqueImportItems = ToDictionarySafe(importItems, dto => dto.DtoItem.ProductId, tieBreak);
|
||||
|
||||
int qtyNew = 0;
|
||||
foreach (var item in uniqueImportItems.Values)
|
||||
{
|
||||
if (existingEntries.TryGetValue(item.DtoItem.ProductId, out LibraryBook existing))
|
||||
{
|
||||
if (existing.Account != item.AccountId)
|
||||
{
|
||||
//Book is absent from the existing LibraryBook's account. Use the alternate account.
|
||||
existing.SetAccount(item.AccountId);
|
||||
}
|
||||
|
||||
existing.AbsentFromLastScan = isUnavailable(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing = new LibraryBook(
|
||||
bookImporter.Cache[item.DtoItem.ProductId],
|
||||
item.DtoItem.DateAdded,
|
||||
item.AccountId)
|
||||
{
|
||||
AbsentFromLastScan = isUnavailable(item)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
DbContext.LibraryBooks.Add(existing);
|
||||
qtyNew++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error adding library book. {@DebugInfo}", new { existing.Book, existing.Account });
|
||||
}
|
||||
}
|
||||
|
||||
existing.SetIncludedUntil(item.DtoItem.GetExpirationDate());
|
||||
existing.SetIsAudiblePlus(item.DtoItem.IsAyce is true);
|
||||
}
|
||||
|
||||
var scannedAccounts = importItems.Select(i => i.AccountId).Distinct().ToHashSet();
|
||||
var allInScannedAccounts = DbContext.LibraryBooks.Where(lb => scannedAccounts.Contains(lb.Account)).ToArray();
|
||||
|
||||
if (bookImporter.LoadedEntireLibrary)
|
||||
{
|
||||
//If the entire library was loaded, we can be sure that all existing LibraryBooks have their Book property populated.
|
||||
//Find LibraryBooks which have a Book but weren't found in the import, and mark them as absent.
|
||||
foreach (var absentBook in allInScannedAccounts.Where(lb => !uniqueImportItems.ContainsKey(lb.Book.AudibleProductId)))
|
||||
absentBook.AbsentFromLastScan = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
//If an existing Book wasn't found in the import, the owning LibraryBook's Book will be null.
|
||||
//Only change AbsentFromLastScan for LibraryBooks of accounts that were scanned.
|
||||
foreach (var nullBook in allInScannedAccounts.Where(lb => lb.Book is null && lb.Account.In(scannedAccounts)))
|
||||
nullBook.AbsentFromLastScan = true;
|
||||
}
|
||||
|
||||
return qtyNew;
|
||||
}
|
||||
|
||||
private static Dictionary<TKey, TSource> ToDictionarySafe<TKey, TSource>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TSource, TSource> tieBreaker)
|
||||
{
|
||||
var dictionary = new Dictionary<TKey, TSource>();
|
||||
|
||||
foreach (TSource newItem in source)
|
||||
{
|
||||
TKey key = keySelector(newItem);
|
||||
|
||||
dictionary[key]
|
||||
= dictionary.TryGetValue(key, out TSource existingItem)
|
||||
? tieBreaker(existingItem, newItem)
|
||||
: newItem;
|
||||
}
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
private static ImportItem tieBreak(ImportItem item1, ImportItem item2)
|
||||
=> isUnavailable(item1) && !isUnavailable(item2) ? item2 : item1;
|
||||
|
||||
private static bool isUnavailable(ImportItem item)
|
||||
=> isFutureRelease(item) || isPlusTitleUnavailable(item);
|
||||
|
||||
private static bool isFutureRelease(ImportItem item)
|
||||
=> item.DtoItem.IssueDate is DateTimeOffset dt && dt > DateTimeOffset.UtcNow;
|
||||
|
||||
private static bool isPlusTitleUnavailable(ImportItem item)
|
||||
=> item.DtoItem.ContentType is null
|
||||
|| (item.DtoItem.IsAyce is true && item.DtoItem.Plans?.Any(p => p.IsAyce) is not true);
|
||||
}
|
||||
@@ -67,7 +67,7 @@ public static class AudioFormatDecoder
|
||||
var mpegSize = mp3File.Length - mp3File.Position;
|
||||
if (mpegSize < 64)
|
||||
{
|
||||
Serilog.Log.Logger.Warning("Remaining file length is too short to contain any mp3 frames. {@File}", mp3Filename);
|
||||
Serilog.Log.Logger.Warning("Remaining file length is too short to contain any mp3 frames. {File}", mp3Filename);
|
||||
return AudioFormat.Default;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ public static class AudioFormatDecoder
|
||||
|
||||
if (layerDesc is not Layer.Layer_3)
|
||||
{
|
||||
Serilog.Log.Logger.Warning("Could not read mp3 data from {@layerVersion} file.", layerDesc.ToString());
|
||||
Serilog.Log.Logger.Warning("Could not read mp3 data from {layerVersion} file.", layerDesc);
|
||||
return AudioFormat.Default;
|
||||
}
|
||||
|
||||
|
||||
@@ -322,7 +322,7 @@ namespace FileLiberator
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Failure to determine output audio format should not be considered a failure to download the book
|
||||
Serilog.Log.Logger.Error(ex, "Error determining output audio format for {@Book}. File = '{@audioFile}'", options.LibraryBook, firstAudioFile);
|
||||
Serilog.Log.Logger.Error(ex, "Error determining output audio format for {@Book}. File = '{@audioFile}'", options.LibraryBook.LogFriendly(), firstAudioFile);
|
||||
return AudioFormat.Default;
|
||||
}
|
||||
|
||||
@@ -427,7 +427,7 @@ namespace FileLiberator
|
||||
{
|
||||
//Failure to download cover art should not be considered a failure to download the book
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
Serilog.Log.Logger.Error(ex, "Error downloading cover art for {@Book} to {@metadataFile}.", options.LibraryBook, coverPath);
|
||||
Serilog.Log.Logger.Error(ex, "Error downloading cover art for {@Book} to {coverPath}.", options.LibraryBook.LogFriendly(), coverPath);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -476,7 +476,7 @@ namespace FileLiberator
|
||||
{
|
||||
//Failure to download records should not be considered a failure to download the book
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
Serilog.Log.Logger.Error(ex, "Error downloading clips and bookmarks for {@Book} to {@recordsPath}.", options.LibraryBook, recordsPath);
|
||||
Serilog.Log.Logger.Error(ex, "Error downloading clips and bookmarks for {@Book} to {recordsPath}.", options.LibraryBook.LogFriendly(), recordsPath);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -512,7 +512,7 @@ namespace FileLiberator
|
||||
{
|
||||
//Failure to download metadata should not be considered a failure to download the book
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
Serilog.Log.Logger.Error(ex, "Error downloading metdatat of {@Book} to {@metadataFile}.", options.LibraryBook, metadataPath);
|
||||
Serilog.Log.Logger.Error(ex, "Error downloading metadata of {@Book} to {metadataFile}.", options.LibraryBook.LogFriendly(), metadataPath);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -523,12 +523,12 @@ namespace FileLiberator
|
||||
{
|
||||
Serilog.Log.Verbose("Getting destination directory for {@Book}", libraryBook.LogFriendly());
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook, Configuration);
|
||||
Serilog.Log.Verbose("Got destination directory for {@Book}. {@Directory}", libraryBook.LogFriendly(), destinationDir);
|
||||
Serilog.Log.Verbose("Got destination directory for {@Book}. {Directory}", libraryBook.LogFriendly(), destinationDir);
|
||||
if (!Directory.Exists(destinationDir))
|
||||
{
|
||||
Serilog.Log.Verbose("Creating destination {@Directory}", destinationDir);
|
||||
Serilog.Log.Verbose("Creating destination {Directory}", destinationDir);
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
Serilog.Log.Verbose("Created destination {@Directory}", destinationDir);
|
||||
Serilog.Log.Verbose("Created destination {Directory}", destinationDir);
|
||||
}
|
||||
return destinationDir;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ public partial class DownloadOptions
|
||||
}
|
||||
else if (metadata.ContentReference != license.ContentMetadata.ContentReference)
|
||||
{
|
||||
Serilog.Log.Logger.Warning("Metadata ContentReference does not match License ContentReference with drm_type = {@DrmType}. {@Metadata}. {@License} ",
|
||||
Serilog.Log.Logger.Warning("Metadata ContentReference does not match License ContentReference with drm_type = {DrmType}. {@Metadata}. {@License} ",
|
||||
license.DrmType,
|
||||
metadata.ContentReference,
|
||||
license.ContentMetadata.ContentReference);
|
||||
@@ -111,7 +111,7 @@ public partial class DownloadOptions
|
||||
if (canUseWidevine)
|
||||
Serilog.Log.Logger.Warning("Unable to get a Widevine CDM. Falling back to ADRM.");
|
||||
else
|
||||
Serilog.Log.Logger.Warning("Account {@account} is not registered as an android device, so content will not be downloaded with Widevine DRM. Remove and re-add the account in Libation to fix.", libraryBook.Account.ToMask());
|
||||
Serilog.Log.Logger.Warning("Account {account} is not registered as an android device, so content will not be downloaded with Widevine DRM. Remove and re-add the account in Libation to fix.", libraryBook.Account.ToMask());
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
@@ -170,17 +170,6 @@ public partial class DownloadOptions
|
||||
/// </summary>
|
||||
public static DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, LicenseInfo licInfo)
|
||||
{
|
||||
long chapterStartMs
|
||||
= config.StripAudibleBrandAudio
|
||||
? licInfo.ContentMetadata.ChapterInfo.BrandIntroDurationMs
|
||||
: 0;
|
||||
|
||||
var dlOptions = new DownloadOptions(config, libraryBook, licInfo)
|
||||
{
|
||||
ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(chapterStartMs)),
|
||||
RuntimeLength = TimeSpan.FromMilliseconds(licInfo.ContentMetadata.ChapterInfo.RuntimeLengthMs),
|
||||
};
|
||||
|
||||
var titleConcat = config.CombineNestedChapterTitles ? ": " : null;
|
||||
var chapters
|
||||
= flattenChapters(licInfo.ContentMetadata.ChapterInfo.Chapters, titleConcat)
|
||||
@@ -190,18 +179,22 @@ public partial class DownloadOptions
|
||||
if (config.MergeOpeningAndEndCredits)
|
||||
combineCredits(chapters);
|
||||
|
||||
if (config.StripAudibleBrandAudio)
|
||||
stripBranding(chapters, licInfo.ContentMetadata.ChapterInfo.BrandIntroDurationMs, licInfo.ContentMetadata.ChapterInfo.BrandOutroDurationMs);
|
||||
|
||||
if (config.SplitFilesByChapter)
|
||||
combineShortChapters(chapters, config.MinimumFileDuration * 1000);
|
||||
|
||||
var dlOptions = new DownloadOptions(config, libraryBook, licInfo)
|
||||
{
|
||||
ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(chapters[0].StartOffsetMs)),
|
||||
RuntimeLength = TimeSpan.FromMilliseconds(licInfo.ContentMetadata.ChapterInfo.RuntimeLengthMs),
|
||||
};
|
||||
|
||||
//Build AAXClean.ChapterInfo
|
||||
for (int i = 0; i < chapters.Count; i++)
|
||||
{
|
||||
var chapter = chapters[i];
|
||||
long chapLenMs = chapter.LengthMs;
|
||||
|
||||
if (i == 0)
|
||||
chapLenMs -= chapterStartMs;
|
||||
|
||||
if (config.StripAudibleBrandAudio && i == chapters.Count - 1)
|
||||
chapLenMs -= licInfo.ContentMetadata.ChapterInfo.BrandOutroDurationMs;
|
||||
|
||||
dlOptions.ChapterInfo.AddChapter(chapter.Title, TimeSpan.FromMilliseconds(chapLenMs));
|
||||
dlOptions.ChapterInfo.AddChapter(chapters[i].Title, TimeSpan.FromMilliseconds(chapters[i].LengthMs));
|
||||
}
|
||||
|
||||
return dlOptions;
|
||||
@@ -349,6 +342,50 @@ public partial class DownloadOptions
|
||||
return chaps;
|
||||
}
|
||||
|
||||
/*
|
||||
https://github.com/rmcrackan/Libation/pull/127#issuecomment-939088489
|
||||
|
||||
If the chapter truly is empty, that is, 0 audio frames in length, then yes it is ignored.
|
||||
If the chapter is shorter than minChapterLength but still has some audio frames, those
|
||||
frames are combined with the following chapter and not split into a new file.
|
||||
|
||||
When 2 or more consecutive chapters are combined, chapter titles are concatenated
|
||||
with a apace in between. For example, given an audiobook with the following chapters:
|
||||
|
||||
00:00:00 - 00:00:02 | Part 1
|
||||
00:00:02 - 00:35:00 | Chapter 1
|
||||
00:35:02 - 01:02:00 | Chapter 2
|
||||
01:02:00 - 01:02:02 | Part 2
|
||||
01:02:02 - 01:41:00 | Chapter 3
|
||||
01:41:00 - 02:05:00 | Chapter 4
|
||||
|
||||
The book will be split into the following files:
|
||||
|
||||
00:00:00 - 00:35:00 | Book - 01 - Part 1 Chapter 1.m4b
|
||||
00:35:00 - 01:02:00 | Book - 02 - Chapter 2.m4b
|
||||
01:02:00 - 01:41:00 | Book - 03 - Part 2.m4b
|
||||
01:41:00 - 02:05:00 | Book - 04 - Chapter 4.m4b
|
||||
*/
|
||||
public static void combineShortChapters(List<Chapter> chapters, long minChapterLengthMs)
|
||||
{
|
||||
for (int i = 0; i < chapters.Count; i++)
|
||||
{
|
||||
while (chapters[i].LengthMs < minChapterLengthMs && chapters.Count > i + 1)
|
||||
{
|
||||
chapters[i].Title += " " + chapters[i + 1].Title;
|
||||
chapters[i].LengthMs += chapters[i + 1].LengthMs;
|
||||
chapters.RemoveAt(i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void stripBranding(List<Chapter> chapters, long introMs, long outroMs)
|
||||
{
|
||||
chapters[0].LengthMs -= introMs;
|
||||
chapters[0].StartOffsetMs += introMs;
|
||||
chapters[^1].LengthMs -= outroMs;
|
||||
}
|
||||
|
||||
public static void combineCredits(IList<Chapter> chapters)
|
||||
{
|
||||
if (chapters.Count > 1 && chapters[0].Title == "Opening Credits")
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace FileManager
|
||||
if (!Directory.Exists(directoryName))
|
||||
return false;
|
||||
|
||||
Serilog.Log.Logger.Debug("Testing write permissions for directory: {@DirectoryName}", directoryName);
|
||||
Serilog.Log.Logger.Debug("Testing write permissions for directory: {DirectoryName}", directoryName);
|
||||
var testFilePath = Path.Combine(directoryName, Guid.NewGuid().ToString());
|
||||
return CanWriteFile(testFilePath);
|
||||
}
|
||||
@@ -55,9 +55,9 @@ namespace FileManager
|
||||
{
|
||||
try
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Testing ability to write filename: {@filename}", filename);
|
||||
Serilog.Log.Logger.Debug("Testing ability to write filename: {filename}", filename);
|
||||
File.WriteAllBytes(filename, []);
|
||||
Serilog.Log.Logger.Debug("Deleting test file after successful write: {@filename}", filename);
|
||||
Serilog.Log.Logger.Debug("Deleting test file after successful write: {filename}", filename);
|
||||
try
|
||||
{
|
||||
FileUtility.SaferDelete(filename);
|
||||
@@ -65,13 +65,13 @@ namespace FileManager
|
||||
catch (Exception ex)
|
||||
{
|
||||
//An error deleting the file doesn't constitute a write failure.
|
||||
Serilog.Log.Logger.Debug(ex, "Error deleting test file: {@filename}", filename);
|
||||
Serilog.Log.Logger.Debug(ex, "Error deleting test file: {filename}", filename);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Debug(ex, "Error writing test file: {@filename}", filename);
|
||||
Serilog.Log.Logger.Debug(ex, "Error writing test file: {filename}", filename);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ namespace FileManager
|
||||
File.WriteAllText(Filepath, endContents);
|
||||
success = true;
|
||||
}
|
||||
Serilog.Log.Logger.Information("Removed property. {@DebugInfo}", propertyName);
|
||||
Serilog.Log.Logger.Information("Removed property. {propertyName}", propertyName);
|
||||
}
|
||||
catch { }
|
||||
|
||||
|
||||
@@ -70,11 +70,11 @@
|
||||
<TrimmableAssembly Include="Avalonia.Themes.Default" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.9" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.10" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.9" />
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.10" />
|
||||
<PackageReference Include="ReactiveUI.Avalonia" Version="11.3.8" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.9" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.10" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />
|
||||
|
||||
@@ -97,7 +97,7 @@ public class TrashBinViewModel : ViewModelBase, IDisposable
|
||||
|
||||
public void Reload()
|
||||
{
|
||||
var deletedBooks = DbContexts.GetDeletedLibraryBooks();
|
||||
var deletedBooks = DbContexts.GetDeletedLibraryBooks().Where(lb => lb.Book.ContentType is not ContentType.Parent);
|
||||
|
||||
DeletedBooks.Clear();
|
||||
DeletedBooks.AddRange(deletedBooks.Select(lb => new CheckBoxViewModel { Item = lb }));
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace HangoverWinForms
|
||||
deletedCbl.Items.Clear();
|
||||
List<LibraryBook> deletedBooks = DbContexts.GetDeletedLibraryBooks();
|
||||
|
||||
foreach (var lb in deletedBooks)
|
||||
foreach (var lb in deletedBooks.Where(lb => lb.Book.ContentType is not ContentType.Parent))
|
||||
deletedCbl.Items.Add(lb);
|
||||
|
||||
setLabel();
|
||||
|
||||
@@ -101,6 +101,29 @@
|
||||
<Setter Property="BorderThickness" Value="2" />
|
||||
</Style>
|
||||
</Style>
|
||||
|
||||
<Style Selector="NumericUpDown.SmallNumericUpDown">
|
||||
<Setter Property="Height" Value="{DynamicResource TextControlThemeMinHeight}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Style Selector="^ /template/ ButtonSpinner#PART_Spinner">
|
||||
<Style Selector="^ RepeatButton#PART_IncreaseButton">
|
||||
<Setter Property="MinWidth" Value="24" />
|
||||
<Style Selector="^ > PathIcon">
|
||||
<Setter Property="Data">
|
||||
M0,0 l8,-10 l8,10 Z
|
||||
</Setter>
|
||||
</Style>
|
||||
</Style>
|
||||
<Style Selector="^ RepeatButton#PART_DecreaseButton">
|
||||
<Setter Property="MinWidth" Value="24" />
|
||||
<Style Selector="^ > PathIcon">
|
||||
<Setter Property="Data">
|
||||
M0,0 l8,10 l8,-10 Z
|
||||
</Setter>
|
||||
</Style>
|
||||
</Style>
|
||||
</Style>
|
||||
</Style>
|
||||
</Application.Styles>
|
||||
|
||||
<NativeMenu.Menu>
|
||||
|
||||
@@ -1,119 +1,121 @@
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using LibationUiBase.GridView;
|
||||
using Avalonia.Input;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace LibationAvalonia.Controls
|
||||
namespace LibationAvalonia.Controls;
|
||||
|
||||
public class DataGridCellContextMenu<TContext> where TContext : class
|
||||
{
|
||||
internal static class DataGridContextMenus
|
||||
public static DataGridCellContextMenu<TContext>? Create(ContextMenu? contextMenu)
|
||||
{
|
||||
public static event EventHandler<DataGridCellContextMenuStripNeededEventArgs>? CellContextMenuStripNeeded;
|
||||
private static readonly ContextMenu ContextMenu = new();
|
||||
public static readonly AvaloniaList<Control> MenuItems = new();
|
||||
private static readonly PropertyInfo OwningColumnProperty;
|
||||
private static readonly PropertyInfo OwningGridProperty;
|
||||
|
||||
static DataGridContextMenus()
|
||||
DataGrid? grid = null;
|
||||
DataGridCell? cell = null;
|
||||
var parent = contextMenu?.Parent;
|
||||
while (parent is not null && grid is null)
|
||||
{
|
||||
ContextMenu.ItemsSource = MenuItems;
|
||||
OwningColumnProperty = typeof(DataGridCell).GetProperty("OwningColumn", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
?? throw new InvalidOperationException("Could not find OwningColumn property on DataGridCell");
|
||||
OwningGridProperty = typeof(DataGridColumn).GetProperty("OwningGrid", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
?? throw new InvalidOperationException("Could not find OwningGrid property on DataGridColumn");
|
||||
grid ??= parent as DataGrid;
|
||||
cell ??= parent as DataGridCell;
|
||||
|
||||
parent = parent.Parent;
|
||||
}
|
||||
|
||||
public static void AttachContextMenu(this DataGridCell cell)
|
||||
if (grid is null || cell is null || cell.Tag is not DataGridColumn column || contextMenu!.DataContext is not TContext clickedEntry)
|
||||
return null;
|
||||
|
||||
var allSelected = grid.SelectedItems.OfType<TContext>().ToArray();
|
||||
var clickedIndex = Array.IndexOf(allSelected, clickedEntry);
|
||||
if (clickedIndex == -1)
|
||||
{
|
||||
if (cell is not null && cell.ContextMenu is null)
|
||||
{
|
||||
cell.ContextRequested += Cell_ContextRequested;
|
||||
cell.ContextMenu = ContextMenu;
|
||||
}
|
||||
//User didn't right-click on a selected cell
|
||||
grid.SelectedItem = clickedEntry;
|
||||
allSelected = [clickedEntry];
|
||||
}
|
||||
else if (clickedIndex > 0)
|
||||
{
|
||||
//Ensure the clicked entry is first in the list
|
||||
(allSelected[0], allSelected[clickedIndex]) = (allSelected[clickedIndex], allSelected[0]);
|
||||
}
|
||||
|
||||
private static void Cell_ContextRequested(object? sender, ContextRequestedEventArgs e)
|
||||
return new DataGridCellContextMenu<TContext>(contextMenu, grid, column, allSelected);
|
||||
}
|
||||
|
||||
public string CellClipboardContents
|
||||
{
|
||||
get
|
||||
{
|
||||
if (sender is DataGridCell cell &&
|
||||
cell.DataContext is GridEntry clickedEntry &&
|
||||
OwningColumnProperty.GetValue(cell) is DataGridColumn column &&
|
||||
OwningGridProperty.GetValue(column) is DataGrid grid)
|
||||
var lines = GetClipboardLines(getClickedCell: true);
|
||||
return lines.Count >= 1 ? lines[0] : string.Empty;
|
||||
}
|
||||
}
|
||||
public string GetRowClipboardContents() => string.Join(Environment.NewLine, GetClipboardLines(false));
|
||||
|
||||
public ContextMenu ContextMenu { get; }
|
||||
public DataGrid Grid { get; }
|
||||
public DataGridColumn Column { get; }
|
||||
public TContext[] RowItems { get; }
|
||||
public AvaloniaList<Control> ContextMenuItems { get; }
|
||||
|
||||
private DataGridCellContextMenu(ContextMenu contextMenu, DataGrid grid, DataGridColumn column, TContext[] rowItems)
|
||||
{
|
||||
Grid = grid;
|
||||
Column = column;
|
||||
RowItems = rowItems;
|
||||
ContextMenu = contextMenu;
|
||||
ContextMenuItems = contextMenu.ItemsSource as AvaloniaList<Control> ?? new();
|
||||
contextMenu.ItemsSource = ContextMenuItems;
|
||||
ContextMenuItems.Clear();
|
||||
}
|
||||
|
||||
private List<string> GetClipboardLines(bool getClickedCell)
|
||||
{
|
||||
if (RowItems is null || RowItems.Length == 0)
|
||||
return [];
|
||||
|
||||
List<string> lines = [];
|
||||
Grid.CopyingRowClipboardContent += Grid_CopyingRowClipboardContent;
|
||||
Grid.RaiseEvent(GetCopyEventArgs());
|
||||
Grid.CopyingRowClipboardContent -= Grid_CopyingRowClipboardContent;
|
||||
return lines;
|
||||
|
||||
void Grid_CopyingRowClipboardContent(object? sender, DataGridRowClipboardEventArgs e)
|
||||
{
|
||||
if (getClickedCell)
|
||||
{
|
||||
var allSelected = grid.SelectedItems.OfType<GridEntry>().ToArray();
|
||||
var clickedIndex = Array.IndexOf(allSelected, clickedEntry);
|
||||
if (clickedIndex == -1)
|
||||
if (e.IsColumnHeadersRow)
|
||||
return;
|
||||
var cellContent = e.ClipboardRowContent.FirstOrDefault(c => c.Column == Column);
|
||||
if (cellContent.Column is not null)
|
||||
{
|
||||
//User didn't right-click on a selected cell
|
||||
grid.SelectedItem = clickedEntry;
|
||||
allSelected = [clickedEntry];
|
||||
lines.Add(cellContent.Content?.ToString() ?? string.Empty);
|
||||
}
|
||||
else if (clickedIndex > 0)
|
||||
{
|
||||
//Ensure the clicked entry is first in the list
|
||||
(allSelected[0], allSelected[clickedIndex]) = (allSelected[clickedIndex], allSelected[0]);
|
||||
}
|
||||
|
||||
var args = new DataGridCellContextMenuStripNeededEventArgs
|
||||
{
|
||||
Column = column,
|
||||
Grid = grid,
|
||||
GridEntries = allSelected,
|
||||
ContextMenu = ContextMenu
|
||||
};
|
||||
|
||||
args.ContextMenuItems.Clear();
|
||||
CellContextMenuStripNeeded?.Invoke(sender, args);
|
||||
e.Handled = args.ContextMenuItems.Count == 0;
|
||||
}
|
||||
else if (e.Item == RowItems[0])
|
||||
lines.Insert(1, FormatClipboardRowContent(e));
|
||||
else
|
||||
e.Handled = true;
|
||||
lines.Add(FormatClipboardRowContent(e));
|
||||
|
||||
//Clear so that the DataGrid copy implementation doesn't set the clipboard
|
||||
e.ClipboardRowContent.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public class DataGridCellContextMenuStripNeededEventArgs
|
||||
private static KeyEventArgs GetCopyEventArgs() => new()
|
||||
{
|
||||
private static readonly MethodInfo GetCellValueMethod;
|
||||
static DataGridCellContextMenuStripNeededEventArgs()
|
||||
{
|
||||
GetCellValueMethod = typeof(DataGridColumn).GetMethod("GetCellValue", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||
?? throw new InvalidOperationException("Could not find GetCellValue method on DataGridColumn");
|
||||
}
|
||||
Key = Key.C,
|
||||
KeyModifiers = KeyModifiers.Control,
|
||||
Route = Avalonia.Interactivity.RoutingStrategies.Bubble,
|
||||
PhysicalKey = PhysicalKey.C,
|
||||
KeySymbol = "c",
|
||||
KeyDeviceType = KeyDeviceType.Keyboard,
|
||||
RoutedEvent = InputElement.KeyDownEvent
|
||||
};
|
||||
|
||||
private static string GetCellValue(DataGridColumn column, object item)
|
||||
=> GetCellValueMethod.Invoke(column, new object[] { item, column.ClipboardContentBinding })?.ToString() ?? "";
|
||||
private string FormatClipboardRowContent(DataGridRowClipboardEventArgs e)
|
||||
=> string.Join("\t", e.ClipboardRowContent.Select(c => RemoveLineBreaks(c.Content?.ToString())));
|
||||
private static string RemoveLineBreaks(string? text)
|
||||
=> text?.Replace("\r\n", " ").Replace('\r', ' ').Replace('\n', ' ') ?? "";
|
||||
|
||||
public string CellClipboardContents => GetCellValue(Column, GridEntries[0]);
|
||||
public string GetRowClipboardContents()
|
||||
{
|
||||
if (GridEntries is null || GridEntries.Length == 0)
|
||||
return string.Empty;
|
||||
else if (GridEntries.Length == 1)
|
||||
return HeaderNames + Environment.NewLine + GetRowClipboardContents(GridEntries[0]);
|
||||
else
|
||||
return string.Join(Environment.NewLine, GridEntries.Select(GetRowClipboardContents).Prepend(HeaderNames));
|
||||
}
|
||||
|
||||
private string HeaderNames
|
||||
=> string.Join("\t",
|
||||
Grid.Columns
|
||||
.Where(c => c.IsVisible)
|
||||
.OrderBy(c => c.DisplayIndex)
|
||||
.Select(c => RemoveLineBreaks(c.Header.ToString() ?? "")));
|
||||
|
||||
private static string RemoveLineBreaks(string text)
|
||||
=> text.Replace("\r\n", "").Replace('\r', ' ').Replace('\n', ' ');
|
||||
|
||||
private string GetRowClipboardContents(GridEntry gridEntry)
|
||||
{
|
||||
var contents = Grid.Columns.Where(c => c.IsVisible).OrderBy(c => c.DisplayIndex).Select(c => RemoveLineBreaks(GetCellValue(c, gridEntry))).ToArray();
|
||||
return string.Join("\t", contents);
|
||||
}
|
||||
|
||||
public required DataGrid Grid { get; init; }
|
||||
public required DataGridColumn Column { get; init; }
|
||||
public required GridEntry[] GridEntries { get; init; }
|
||||
public required ContextMenu ContextMenu { get; init; }
|
||||
public AvaloniaList<Control> ContextMenuItems => DataGridContextMenus.MenuItems;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace LibationAvalonia.Controls
|
||||
IsEditingMode = false
|
||||
};
|
||||
|
||||
cell?.AttachContextMenu();
|
||||
cell.Tag = this;
|
||||
|
||||
if (!IsReadOnly)
|
||||
ToolTip.SetTip(myRatingElement, "Click to change ratings");
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace LibationAvalonia.Controls
|
||||
{
|
||||
protected override Control GenerateElement(DataGridCell cell, object dataItem)
|
||||
{
|
||||
cell?.AttachContextMenu();
|
||||
cell.Tag = this;
|
||||
return base.GenerateElement(cell, dataItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
xmlns:vm="clr-namespace:LibationAvalonia.ViewModels.Settings"
|
||||
x:DataType="vm:AudioSettingsVM"
|
||||
x:CompileBindings="True"
|
||||
x:Class="LibationAvalonia.Controls.Settings.Audio">
|
||||
|
||||
<Grid
|
||||
@@ -38,43 +39,43 @@
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Text="{CompiledBinding FileDownloadQualityText}" />
|
||||
Text="{Binding FileDownloadQualityText}" />
|
||||
|
||||
<controls:WheelComboBox
|
||||
Margin="5,0,0,0"
|
||||
Grid.Column="1"
|
||||
ItemsSource="{CompiledBinding DownloadQualities}"
|
||||
SelectedItem="{CompiledBinding FileDownloadQuality}"/>
|
||||
ItemsSource="{Binding DownloadQualities}"
|
||||
SelectedItem="{Binding FileDownloadQuality}"/>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<CheckBox
|
||||
ToolTip.Tip="{CompiledBinding UseWidevineTip}"
|
||||
ToolTip.Tip="{Binding UseWidevineTip}"
|
||||
IsCheckedChanged="UseWidevine_IsCheckedChanged"
|
||||
IsChecked="{CompiledBinding UseWidevine, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding UseWidevineText}" />
|
||||
IsChecked="{Binding UseWidevine, Mode=TwoWay}">
|
||||
<TextBlock Text="{Binding UseWidevineText}" />
|
||||
</CheckBox>
|
||||
<CheckBox
|
||||
Grid.Column="1"
|
||||
ToolTip.Tip="{CompiledBinding Request_xHE_AACTip}"
|
||||
IsEnabled="{CompiledBinding UseWidevine}"
|
||||
IsChecked="{CompiledBinding Request_xHE_AAC, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding Request_xHE_AACText}" />
|
||||
ToolTip.Tip="{Binding Request_xHE_AACTip}"
|
||||
IsEnabled="{Binding UseWidevine}"
|
||||
IsChecked="{Binding Request_xHE_AAC, Mode=TwoWay}">
|
||||
<TextBlock Text="{Binding Request_xHE_AACText}" />
|
||||
</CheckBox>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<CheckBox
|
||||
ToolTip.Tip="{CompiledBinding RequestSpatialTip}"
|
||||
IsEnabled="{CompiledBinding UseWidevine}"
|
||||
IsChecked="{CompiledBinding RequestSpatial, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding RequestSpatialText}" />
|
||||
ToolTip.Tip="{Binding RequestSpatialTip}"
|
||||
IsEnabled="{Binding UseWidevine}"
|
||||
IsChecked="{Binding RequestSpatial, Mode=TwoWay}">
|
||||
<TextBlock Text="{Binding RequestSpatialText}" />
|
||||
</CheckBox>
|
||||
<Grid
|
||||
Grid.Column="1"
|
||||
ColumnDefinitions="Auto,Auto"
|
||||
VerticalAlignment="Top"
|
||||
ToolTip.Tip="{CompiledBinding SpatialAudioCodecTip}">
|
||||
ToolTip.Tip="{Binding SpatialAudioCodecTip}">
|
||||
<Grid.IsEnabled>
|
||||
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||
<MultiBinding.Bindings>
|
||||
@@ -92,78 +93,100 @@
|
||||
Margin="5,0,0,0"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
ItemsSource="{CompiledBinding SpatialAudioCodecs}"
|
||||
SelectedItem="{CompiledBinding SpatialAudioCodec}"/>
|
||||
ItemsSource="{Binding SpatialAudioCodecs}"
|
||||
SelectedItem="{Binding SpatialAudioCodec}"/>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding CreateCueSheet, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding CreateCueSheetText}" />
|
||||
<CheckBox IsChecked="{Binding CreateCueSheet, Mode=TwoWay}">
|
||||
<TextBlock Text="{Binding CreateCueSheetText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding DownloadCoverArt, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding DownloadCoverArtText}" />
|
||||
<CheckBox IsChecked="{Binding DownloadCoverArt, Mode=TwoWay}">
|
||||
<TextBlock Text="{Binding DownloadCoverArtText}" />
|
||||
</CheckBox>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<CheckBox IsChecked="{CompiledBinding DownloadClipsBookmarks, Mode=TwoWay}">
|
||||
<CheckBox IsChecked="{Binding DownloadClipsBookmarks, Mode=TwoWay}">
|
||||
<TextBlock Text="Download Clips, Notes and Bookmarks as" />
|
||||
</CheckBox>
|
||||
|
||||
<controls:WheelComboBox
|
||||
Margin="5,0,0,0"
|
||||
Grid.Column="1"
|
||||
IsEnabled="{CompiledBinding DownloadClipsBookmarks}"
|
||||
ItemsSource="{CompiledBinding ClipBookmarkFormats}"
|
||||
SelectedItem="{CompiledBinding ClipBookmarkFormat}"/>
|
||||
IsEnabled="{Binding DownloadClipsBookmarks}"
|
||||
ItemsSource="{Binding ClipBookmarkFormats}"
|
||||
SelectedItem="{Binding ClipBookmarkFormat}"/>
|
||||
</Grid>
|
||||
|
||||
<CheckBox
|
||||
IsChecked="{CompiledBinding RetainAaxFile, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding RetainAaxFileTip}">
|
||||
<TextBlock Text="{CompiledBinding RetainAaxFileText}" />
|
||||
IsChecked="{Binding RetainAaxFile, Mode=TwoWay}"
|
||||
ToolTip.Tip="{Binding RetainAaxFileTip}">
|
||||
<TextBlock Text="{Binding RetainAaxFileText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox
|
||||
IsChecked="{CompiledBinding MergeOpeningAndEndCredits, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding MergeOpeningAndEndCreditsTip}">
|
||||
<TextBlock Text="{CompiledBinding MergeOpeningEndCreditsText}" />
|
||||
IsChecked="{Binding MergeOpeningAndEndCredits, Mode=TwoWay}"
|
||||
ToolTip.Tip="{Binding MergeOpeningAndEndCreditsTip}">
|
||||
<TextBlock Text="{Binding MergeOpeningEndCreditsText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox
|
||||
ToolTip.Tip="{CompiledBinding CombineNestedChapterTitlesTip}"
|
||||
IsChecked="{CompiledBinding CombineNestedChapterTitles, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding CombineNestedChapterTitlesText}" />
|
||||
ToolTip.Tip="{Binding CombineNestedChapterTitlesTip}"
|
||||
IsChecked="{Binding CombineNestedChapterTitles, Mode=TwoWay}">
|
||||
<TextBlock Text="{Binding CombineNestedChapterTitlesText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox
|
||||
ToolTip.Tip="{CompiledBinding AllowLibationFixupTip}"
|
||||
IsChecked="{CompiledBinding AllowLibationFixup, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding AllowLibationFixupText}" />
|
||||
ToolTip.Tip="{Binding AllowLibationFixupTip}"
|
||||
IsChecked="{Binding AllowLibationFixup, Mode=TwoWay}">
|
||||
<TextBlock Text="{Binding AllowLibationFixupText}" />
|
||||
</CheckBox>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="1"
|
||||
Label="Audiobook Fix-ups"
|
||||
IsEnabled="{CompiledBinding AllowLibationFixup}">
|
||||
IsEnabled="{Binding AllowLibationFixup}">
|
||||
|
||||
<StackPanel Orientation="Vertical">
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding SplitFilesByChapter, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding SplitFilesByChapterText}" />
|
||||
<Grid
|
||||
Margin="0,0,0,5"
|
||||
Grid.ColumnDefinitions="Auto,Auto"
|
||||
Grid.RowDefinitions="Auto,Auto">
|
||||
|
||||
<CheckBox Margin="0" Grid.ColumnSpan="2" IsChecked="{Binding SplitFilesByChapter, Mode=TwoWay}">
|
||||
<TextBlock Grid.ColumnSpan="2" Text="{Binding SplitFilesByChapterText}" />
|
||||
</CheckBox>
|
||||
|
||||
<TextBlock Grid.Row="1" Margin="15,0" VerticalAlignment="Center" ToolTip.Tip="{Binding MinimumFileDurationTip}" Text="{Binding MinimumFileDurationText}" />
|
||||
|
||||
<NumericUpDown
|
||||
Classes="SmallNumericUpDown"
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
ToolTip.Tip="{Binding MinimumFileDurationTip}"
|
||||
MinWidth="100"
|
||||
Minimum="0"
|
||||
Maximum="120"
|
||||
Increment="1"
|
||||
FormatString="N0"
|
||||
ParsingNumberStyle="Integer"
|
||||
IsEnabled="{Binding SplitFilesByChapter}"
|
||||
Value="{Binding MinimumFileDuration, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
|
||||
<CheckBox
|
||||
IsChecked="{Binding StripAudibleBrandAudio, Mode=TwoWay}"
|
||||
ToolTip.Tip="{Binding StripAudibleBrandAudioTip}">
|
||||
<TextBlock Text="{Binding StripAudibleBrandingText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox
|
||||
IsChecked="{CompiledBinding StripAudibleBrandAudio, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding StripAudibleBrandAudioTip}">
|
||||
<TextBlock Text="{CompiledBinding StripAudibleBrandingText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox
|
||||
IsChecked="{CompiledBinding StripUnabridged, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding StripUnabridgedTip}">
|
||||
<TextBlock Text="{CompiledBinding StripUnabridgedText}" />
|
||||
IsChecked="{Binding StripUnabridged, Mode=TwoWay}"
|
||||
ToolTip.Tip="{Binding StripUnabridgedTip}">
|
||||
<TextBlock Text="{Binding StripUnabridgedText}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
@@ -178,24 +201,24 @@
|
||||
Margin="10,0,0,0">
|
||||
|
||||
<RadioButton
|
||||
IsChecked="{CompiledBinding !DecryptToLossy, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding DecryptToLossyTip}">
|
||||
IsChecked="{Binding !DecryptToLossy, Mode=TwoWay}"
|
||||
ToolTip.Tip="{Binding DecryptToLossyTip}">
|
||||
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock
|
||||
Text="Download my books in the original audio format (Lossless)" />
|
||||
<CheckBox
|
||||
IsEnabled="{CompiledBinding !DecryptToLossy}"
|
||||
IsChecked="{CompiledBinding MoveMoovToBeginning, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding MoveMoovToBeginningTip}">
|
||||
<TextBlock Text="{CompiledBinding MoveMoovToBeginningText}" />
|
||||
IsEnabled="{Binding !DecryptToLossy}"
|
||||
IsChecked="{Binding MoveMoovToBeginning, Mode=TwoWay}"
|
||||
ToolTip.Tip="{Binding MoveMoovToBeginningTip}">
|
||||
<TextBlock Text="{Binding MoveMoovToBeginningText}" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
</RadioButton>
|
||||
|
||||
<RadioButton
|
||||
IsChecked="{CompiledBinding DecryptToLossy, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding DecryptToLossyTip}">
|
||||
IsChecked="{Binding DecryptToLossy, Mode=TwoWay}"
|
||||
ToolTip.Tip="{Binding DecryptToLossyTip}">
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="Download my books as .MP3 files (transcode if necessary)" />
|
||||
@@ -203,7 +226,7 @@
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Column="1"
|
||||
IsEnabled="{CompiledBinding DecryptToLossy}"
|
||||
IsEnabled="{Binding DecryptToLossy}"
|
||||
Label="Mp3 Encoding Options">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*">
|
||||
@@ -220,21 +243,21 @@
|
||||
<RadioButton
|
||||
Margin="5"
|
||||
Content="Bitrate"
|
||||
IsChecked="{CompiledBinding LameTargetBitrate, Mode=TwoWay}"/>
|
||||
IsChecked="{Binding LameTargetBitrate, Mode=TwoWay}"/>
|
||||
|
||||
<RadioButton
|
||||
Grid.Column="1"
|
||||
Margin="5"
|
||||
Content="Quality"
|
||||
IsChecked="{CompiledBinding !LameTargetBitrate, Mode=TwoWay}"/>
|
||||
IsChecked="{Binding !LameTargetBitrate, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
</controls:GroupBox>
|
||||
|
||||
<CheckBox
|
||||
HorizontalAlignment="Right"
|
||||
Grid.Column="1"
|
||||
IsChecked="{CompiledBinding LameDownsampleMono, Mode=TwoWay}"
|
||||
ToolTip.Tip="{CompiledBinding LameDownsampleMonoTip}">
|
||||
IsChecked="{Binding LameDownsampleMono, Mode=TwoWay}"
|
||||
ToolTip.Tip="{Binding LameDownsampleMonoTip}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
@@ -249,8 +272,8 @@
|
||||
<controls:WheelComboBox
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{CompiledBinding SampleRates}"
|
||||
SelectedItem="{CompiledBinding SelectedSampleRate, Mode=TwoWay}"/>
|
||||
ItemsSource="{Binding SampleRates}"
|
||||
SelectedItem="{Binding SelectedSampleRate, Mode=TwoWay}"/>
|
||||
|
||||
<TextBlock Margin="0,0,0,5" Grid.Column="2" Text="Encoder Quality:" />
|
||||
|
||||
@@ -258,23 +281,23 @@
|
||||
Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{CompiledBinding EncoderQualities}"
|
||||
SelectedItem="{CompiledBinding SelectedEncoderQuality, Mode=TwoWay}"/>
|
||||
ItemsSource="{Binding EncoderQualities}"
|
||||
SelectedItem="{Binding SelectedEncoderQuality, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="2"
|
||||
Margin="0,5"
|
||||
Label="Bitrate"
|
||||
IsEnabled="{CompiledBinding LameTargetBitrate}" >
|
||||
IsEnabled="{Binding LameTargetBitrate}" >
|
||||
|
||||
<StackPanel>
|
||||
<Grid ColumnDefinitions="*,25,Auto">
|
||||
|
||||
<Slider
|
||||
Grid.Column="0"
|
||||
IsEnabled="{CompiledBinding !LameMatchSource}"
|
||||
Value="{CompiledBinding LameBitrate, Mode=TwoWay}"
|
||||
IsEnabled="{Binding !LameMatchSource}"
|
||||
Value="{Binding LameBitrate, Mode=TwoWay}"
|
||||
Minimum="16"
|
||||
Maximum="320"
|
||||
IsSnapToTickEnabled="True" TickFrequency="16"
|
||||
@@ -283,7 +306,7 @@
|
||||
|
||||
<Slider.Styles>
|
||||
<Style Selector="Slider /template/ Thumb">
|
||||
<Setter Property="ToolTip.Tip" Value="{CompiledBinding $parent[Slider].Value, Mode=OneWay, StringFormat='\{0:f0\} Kbps'}" />
|
||||
<Setter Property="ToolTip.Tip" Value="{Binding $parent[Slider].Value, Mode=OneWay, StringFormat='\{0:f0\} Kbps'}" />
|
||||
<Setter Property="ToolTip.Placement" Value="Top" />
|
||||
<Setter Property="ToolTip.VerticalOffset" Value="-10" />
|
||||
<Setter Property="ToolTip.HorizontalOffset" Value="-30" />
|
||||
@@ -294,7 +317,7 @@
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
Text="{CompiledBinding LameBitrate}" />
|
||||
Text="{Binding LameBitrate}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
@@ -306,7 +329,7 @@
|
||||
|
||||
<CheckBox
|
||||
Grid.Column="0"
|
||||
IsChecked="{CompiledBinding LameConstantBitrate, Mode=TwoWay}">
|
||||
IsChecked="{Binding LameConstantBitrate, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
@@ -317,7 +340,7 @@
|
||||
<CheckBox
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
IsChecked="{CompiledBinding LameMatchSource, Mode=TwoWay}">
|
||||
IsChecked="{Binding LameMatchSource, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
@@ -332,7 +355,7 @@
|
||||
Grid.Row="3"
|
||||
Margin="0,5"
|
||||
Label="Quality"
|
||||
IsEnabled="{CompiledBinding !LameTargetBitrate}">
|
||||
IsEnabled="{Binding !LameTargetBitrate}">
|
||||
|
||||
<Grid
|
||||
ColumnDefinitions="*,Auto,25"
|
||||
@@ -341,7 +364,7 @@
|
||||
<Slider
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Value="{CompiledBinding LameVBRQuality, Mode=TwoWay}"
|
||||
Value="{Binding LameVBRQuality, Mode=TwoWay}"
|
||||
Minimum="0"
|
||||
Maximum="9"
|
||||
IsSnapToTickEnabled="True" TickFrequency="1"
|
||||
@@ -349,7 +372,7 @@
|
||||
TickPlacement="Outside">
|
||||
<Slider.Styles>
|
||||
<Style Selector="Slider /template/ Thumb">
|
||||
<Setter Property="ToolTip.Tip" Value="{CompiledBinding $parent[Slider].Value, Mode=OneWay, StringFormat='V\{0:f0\}'}" />
|
||||
<Setter Property="ToolTip.Tip" Value="{Binding $parent[Slider].Value, Mode=OneWay, StringFormat='V\{0:f0\}'}" />
|
||||
<Setter Property="ToolTip.Placement" Value="Top" />
|
||||
<Setter Property="ToolTip.VerticalOffset" Value="-10" />
|
||||
<Setter Property="ToolTip.HorizontalOffset" Value="-30" />
|
||||
@@ -363,7 +386,7 @@
|
||||
Orientation="Horizontal">
|
||||
|
||||
<TextBlock Text="V" />
|
||||
<TextBlock Text="{CompiledBinding LameVBRQuality}" />
|
||||
<TextBlock Text="{Binding LameVBRQuality}" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
@@ -397,8 +420,8 @@
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="0,10,0,0"
|
||||
IsEnabled="{CompiledBinding SplitFilesByChapter}"
|
||||
Label="{CompiledBinding ChapterTitleTemplateText}">
|
||||
IsEnabled="{Binding SplitFilesByChapter}"
|
||||
Label="{Binding ChapterTitleTemplateText}">
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,8" >
|
||||
|
||||
@@ -406,7 +429,7 @@
|
||||
Grid.Column="0"
|
||||
FontSize="14"
|
||||
IsReadOnly="True"
|
||||
Text="{CompiledBinding ChapterTitleTemplate}" />
|
||||
Text="{Binding ChapterTitleTemplate}" />
|
||||
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="450"
|
||||
xmlns:uibase="clr-namespace:LibationUiBase;assembly=LibationUiBase"
|
||||
x:DataType="uibase:LocatedAudiobooksViewModel"
|
||||
x:CompileBindings="True"
|
||||
Width="600" Height="450"
|
||||
x:Class="LibationAvalonia.Dialogs.LocateAudiobooksDialog"
|
||||
Title="Locate Audiobooks"
|
||||
@@ -13,17 +16,34 @@
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
|
||||
<TextBlock Text="IDs Found: " />
|
||||
<TextBlock Text="{Binding FoundAsins}" />
|
||||
<TextBlock Text="{Binding FoundAsinCount}" />
|
||||
</StackPanel>
|
||||
<ListBox Margin="0,5,0,0" Grid.Row="1" Grid.ColumnSpan="2" Name="foundAudiobooksLB" ItemsSource="{Binding FoundFiles}" AutoScrollToSelectedItem="true">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Grid.Column="0" Margin="0,0,10,0" Text="{Binding Item1}" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding Item2}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
<DataGrid
|
||||
Margin="0,5,0,0"
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
IsReadOnly="True"
|
||||
CanUserSortColumns="True"
|
||||
CanUserResizeColumns="True"
|
||||
GridLinesVisibility="All"
|
||||
DoubleTapped="foundFilesDataGrid_DoubleTapped"
|
||||
Name="foundFilesDataGrid"
|
||||
ItemsSource="{Binding FoundFiles}">
|
||||
<DataGrid.Styles>
|
||||
<Style Selector="DataGridCell TextBlock">
|
||||
<Setter Property="ToolTip.Tip" Value="Double-click to open containing folder."/>
|
||||
</Style>
|
||||
</DataGrid.Styles>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn
|
||||
Header="Found ID"
|
||||
Width="Auto"
|
||||
Binding="{Binding ID}" />
|
||||
<DataGridTextColumn
|
||||
Header="Found File"
|
||||
Width="*"
|
||||
Binding="{Binding FileName}" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
@@ -1,41 +1,39 @@
|
||||
using ApplicationServices;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform.Storage;
|
||||
using DataLayer;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
using LibationUiBase;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class LocateAudiobooksDialog : DialogWindow
|
||||
{
|
||||
private event EventHandler<FilePathCache.CacheEntry>? FileFound;
|
||||
private readonly CancellationTokenSource tokenSource = new();
|
||||
private readonly List<string> foundAsins = new();
|
||||
private readonly LocatedAudiobooksViewModel _viewModel;
|
||||
public LocateAudiobooksDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
DataContext = _viewModel = new();
|
||||
var list = new AvaloniaList<FoundAudiobook>();
|
||||
DataContext = _viewModel = new(list);
|
||||
list.CollectionChanged += (_, _) => foundFilesDataGrid.ScrollIntoView(list[^1], foundFilesDataGrid.Columns[0]);
|
||||
this.RestoreSizeAndLocation(Configuration.Instance);
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
_viewModel.FoundFiles.Add(new("[0000001]", "Filename 1.m4b"));
|
||||
_viewModel.FoundFiles.Add(new("[0000002]", "Filename 2.m4b"));
|
||||
_viewModel.AddFoundFile(new("0000000001", FileType.Audio, "Filename 1.m4b"));
|
||||
_viewModel.AddFoundFile(new("0000000002", FileType.Audio, "Filename 2.m4b"));
|
||||
}
|
||||
else
|
||||
{
|
||||
Opened += LocateAudiobooksDialog_Opened;
|
||||
FileFound += LocateAudiobooks_FileFound;
|
||||
Closing += LocateAudiobooksDialog_Closing;
|
||||
}
|
||||
}
|
||||
@@ -49,19 +47,6 @@ namespace LibationAvalonia.Dialogs
|
||||
this.SaveSizeAndLocation(Configuration.Instance);
|
||||
}
|
||||
|
||||
private void LocateAudiobooks_FileFound(object? sender, FilePathCache.CacheEntry e)
|
||||
{
|
||||
var newItem = new Tuple<string, string>($"[{e.Id}]", Path.GetFileName(e.Path));
|
||||
_viewModel.FoundFiles.Add(newItem);
|
||||
foundAudiobooksLB.SelectedItem = newItem;
|
||||
|
||||
if (!foundAsins.Any(asin => asin == e.Id))
|
||||
{
|
||||
foundAsins.Add(e.Id);
|
||||
_viewModel.FoundAsins = foundAsins.Count;
|
||||
}
|
||||
}
|
||||
|
||||
private async void LocateAudiobooksDialog_Opened(object? sender, EventArgs e)
|
||||
{
|
||||
var folderPicker = new FolderPickerOpenOptions
|
||||
@@ -76,37 +61,18 @@ namespace LibationAvalonia.Dialogs
|
||||
if (selectedFolder is null || !Directory.Exists(selectedFolder))
|
||||
{
|
||||
await CancelAndCloseAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await foreach (var book in AudioFileStorage.FindAudiobooksAsync(selectedFolder, tokenSource.Token))
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
FilePathCache.Insert(book);
|
||||
|
||||
var lb = DbContexts.GetLibraryBook_Flat_NoTracking(book.Id);
|
||||
if (lb is not null && lb.Book?.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated)
|
||||
await lb.UpdateBookStatusAsync(LiberatedStatus.Liberated);
|
||||
|
||||
tokenSource.Token.ThrowIfCancellationRequested();
|
||||
FileFound?.Invoke(this, book);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Error adding found audiobook file to Libation. {@audioFile}", book);
|
||||
}
|
||||
await _viewModel.FindAndAddBooksAsync(selectedFolder, tokenSource.Token);
|
||||
await MessageBox.Show(this, $"Libation has found {_viewModel.FoundAsinCount} unique audiobooks and added them to its database. ", $"Found {_viewModel.FoundAsinCount} Audiobooks");
|
||||
}
|
||||
}
|
||||
|
||||
await MessageBox.Show(this, $"Libation has found {foundAsins.Count} unique audiobooks and added them to its database. ", $"Found {foundAsins.Count} Audiobooks");
|
||||
await SaveAndCloseAsync();
|
||||
private void foundFilesDataGrid_DoubleTapped(object? sender, Avalonia.Input.TappedEventArgs e)
|
||||
{
|
||||
if (sender is DataGrid dg && dg.SelectedItem is FoundAudiobook foundAudiobook)
|
||||
Go.To.File(foundAudiobook.Entry.Path);
|
||||
}
|
||||
}
|
||||
|
||||
public class LocatedAudiobooksViewModel : ViewModelBase
|
||||
{
|
||||
public AvaloniaList<Tuple<string, string>> FoundFiles { get; } = new();
|
||||
public int FoundAsins { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,49 +4,72 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="630" d:DesignHeight="480"
|
||||
x:Class="LibationAvalonia.Dialogs.TrashBinDialog"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
MinWidth="630" MinHeight="480"
|
||||
Width="630" Height="480"
|
||||
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
|
||||
xmlns:views="clr-namespace:LibationAvalonia.Views"
|
||||
x:DataType="dialogs:TrashBinViewModel"
|
||||
x:CompileBindings="True"
|
||||
MinWidth="680" MinHeight="480"
|
||||
Width="680" Height="480"
|
||||
Title="Trash Bin"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Icon="/Assets/libation.ico">
|
||||
|
||||
<Grid
|
||||
RowDefinitions="Auto,*,Auto">
|
||||
<Grid Margin="5"
|
||||
RowDefinitions="Auto,Auto,*,Auto">
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Margin="5"
|
||||
Text="Check books you want to permanently delete from or restore to Libation" />
|
||||
<TextBlock Text="Check books you want to permanently delete from or restore to Libation"/>
|
||||
|
||||
<controls:CheckedListBox
|
||||
Grid.Row="1"
|
||||
Margin="5,0,5,0"
|
||||
BorderThickness="1"
|
||||
BorderBrush="Gray"
|
||||
IsEnabled="{Binding ControlsEnabled}"
|
||||
Items="{Binding DeletedBooks}" />
|
||||
<Grid Margin="0,5" Grid.Row="1" Grid.ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock VerticalAlignment="Center" Text="Search Deleted Books:"/>
|
||||
<TextBox Name="searchTb" Margin="5,0" Grid.Column="1" Text="{Binding FilterString}" IsEnabled="{Binding ControlsEnabled}">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Command="{Binding FilterBtnAsync}" Gesture="Enter" />
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<Button Classes="SaveButton" Grid.Column="2" Command="{Binding FilterBtnAsync}" IsEnabled="{Binding ControlsEnabled}" VerticalAlignment="Stretch" Content="Filter" />
|
||||
</Grid>
|
||||
|
||||
<views:ProductsDisplay
|
||||
Grid.Row="2"
|
||||
DisableContextMenu="True"
|
||||
DisableColumnCustomization="True"
|
||||
IsEnabled="{Binding $parent.((dialogs:TrashBinViewModel)DataContext).ControlsEnabled}"
|
||||
DataContext="{Binding ProductsDisplay}" />
|
||||
|
||||
<Grid
|
||||
Grid.Row="2"
|
||||
Margin="5"
|
||||
ColumnDefinitions="Auto,Auto,*,Auto">
|
||||
Margin="0,5,0,0"
|
||||
Grid.Row="3"
|
||||
ColumnDefinitions="Auto,Auto,Auto,Auto,*,Auto">
|
||||
|
||||
<CheckBox
|
||||
IsEnabled="{Binding ControlsEnabled}"
|
||||
IsThreeState="True"
|
||||
Margin="0,0,20,0"
|
||||
Margin="0,0,14,0"
|
||||
IsChecked="{Binding EverythingChecked}"
|
||||
Content="Everything" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="0,0,14,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding CheckedCountText}" />
|
||||
|
||||
<CheckBox
|
||||
Grid.Column="2"
|
||||
IsEnabled="{Binding ControlsEnabled}"
|
||||
IsThreeState="True"
|
||||
Margin="0,0,15,0"
|
||||
IsChecked="{Binding AudiblePlusChecked}"
|
||||
Content="Audible Plus Books" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="3"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding AudiblePlusCheckedCountText}" />
|
||||
|
||||
<Button
|
||||
IsEnabled="{Binding ControlsEnabled}"
|
||||
Grid.Column="2"
|
||||
Grid.Column="4"
|
||||
Margin="0,0,20,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Stretch"
|
||||
@@ -56,7 +79,7 @@
|
||||
|
||||
<Button
|
||||
IsEnabled="{Binding ControlsEnabled}"
|
||||
Grid.Column="3"
|
||||
Grid.Column="5"
|
||||
Command="{Binding PermanentlyDeleteCheckedAsync}" >
|
||||
<TextBlock
|
||||
TextAlignment="Center"
|
||||
|
||||
@@ -1,140 +1,159 @@
|
||||
using ApplicationServices;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using DataLayer;
|
||||
using LibationAvalonia.Controls;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
namespace LibationAvalonia.Dialogs;
|
||||
|
||||
public partial class TrashBinDialog : DialogWindow
|
||||
{
|
||||
public partial class TrashBinDialog : DialogWindow
|
||||
private TrashBinViewModel VM { get; }
|
||||
public TrashBinDialog()
|
||||
{
|
||||
public TrashBinDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new TrashBinViewModel();
|
||||
InitializeComponent();
|
||||
SaveOnEnter = false;
|
||||
ControlToFocusOnShow = searchTb;
|
||||
DataContext = VM = new TrashBinViewModel();
|
||||
|
||||
this.Closing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
|
||||
|
||||
KeyBindings.Add(new Avalonia.Input.KeyBinding
|
||||
{
|
||||
Gesture = new Avalonia.Input.KeyGesture(Avalonia.Input.Key.Escape),
|
||||
Command = ReactiveCommand.Create(Close)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class TrashBinViewModel : ViewModelBase, IDisposable
|
||||
{
|
||||
public AvaloniaList<CheckBoxViewModel> DeletedBooks { get; }
|
||||
public string CheckedCountText => $"Checked : {_checkedBooksCount} of {_totalBooksCount}";
|
||||
public bool ControlsEnabled { get => field; set => this.RaiseAndSetIfChanged(ref field, value); } = true;
|
||||
|
||||
private bool? everythingChecked = false;
|
||||
public bool? EverythingChecked
|
||||
{
|
||||
get => everythingChecked;
|
||||
set
|
||||
{
|
||||
everythingChecked = value ?? false;
|
||||
|
||||
if (everythingChecked is true)
|
||||
CheckAll();
|
||||
else if (everythingChecked is false)
|
||||
UncheckAll();
|
||||
}
|
||||
}
|
||||
|
||||
private int _totalBooksCount = 0;
|
||||
private int _checkedBooksCount = -1;
|
||||
public int CheckedBooksCount
|
||||
{
|
||||
get => _checkedBooksCount;
|
||||
set
|
||||
{
|
||||
if (_checkedBooksCount != value)
|
||||
{
|
||||
_checkedBooksCount = value;
|
||||
this.RaisePropertyChanged(nameof(CheckedCountText));
|
||||
}
|
||||
|
||||
everythingChecked
|
||||
= _checkedBooksCount == 0 || _totalBooksCount == 0 ? false
|
||||
: _checkedBooksCount == _totalBooksCount ? true
|
||||
: null;
|
||||
|
||||
this.RaisePropertyChanged(nameof(EverythingChecked));
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<LibraryBook> CheckedBooks => DeletedBooks.Where(i => i.IsChecked).Select(i => i.Item).Cast<LibraryBook>();
|
||||
|
||||
public TrashBinViewModel()
|
||||
{
|
||||
DeletedBooks = new()
|
||||
{
|
||||
ResetBehavior = ResetBehavior.Remove
|
||||
};
|
||||
|
||||
tracker = DeletedBooks.TrackItemPropertyChanged(CheckboxPropertyChanged);
|
||||
Reload();
|
||||
}
|
||||
|
||||
public void CheckAll()
|
||||
{
|
||||
foreach (var item in DeletedBooks)
|
||||
item.IsChecked = true;
|
||||
}
|
||||
|
||||
public void UncheckAll()
|
||||
{
|
||||
foreach (var item in DeletedBooks)
|
||||
item.IsChecked = false;
|
||||
}
|
||||
|
||||
public async Task RestoreCheckedAsync()
|
||||
{
|
||||
ControlsEnabled = false;
|
||||
var qtyChanges = await CheckedBooks.RestoreBooksAsync();
|
||||
if (qtyChanges > 0)
|
||||
Reload();
|
||||
ControlsEnabled = true;
|
||||
}
|
||||
|
||||
public async Task PermanentlyDeleteCheckedAsync()
|
||||
{
|
||||
ControlsEnabled = false;
|
||||
var qtyChanges = await CheckedBooks.PermanentlyDeleteBooksAsync();
|
||||
if (qtyChanges > 0)
|
||||
Reload();
|
||||
ControlsEnabled = true;
|
||||
}
|
||||
|
||||
private void Reload()
|
||||
{
|
||||
var deletedBooks = DbContexts.GetDeletedLibraryBooks();
|
||||
|
||||
DeletedBooks.Clear();
|
||||
DeletedBooks.AddRange(deletedBooks.Select(lb => new CheckBoxViewModel { Item = lb }));
|
||||
|
||||
_totalBooksCount = DeletedBooks.Count;
|
||||
CheckedBooksCount = 0;
|
||||
}
|
||||
|
||||
private IDisposable tracker;
|
||||
private void CheckboxPropertyChanged(Tuple<object?, PropertyChangedEventArgs> e)
|
||||
{
|
||||
if (e.Item2.PropertyName == nameof(CheckBoxViewModel.IsChecked))
|
||||
CheckedBooksCount = DeletedBooks.Count(b => b.IsChecked);
|
||||
}
|
||||
|
||||
public void Dispose() => tracker?.Dispose();
|
||||
Closing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
|
||||
Loaded += async (_, _) => await VM.InitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class TrashBinViewModel : ViewModelBase
|
||||
{
|
||||
private TempSearchEngine SearchEngine { get; } = new();
|
||||
public ProductsDisplayViewModel ProductsDisplay { get; }
|
||||
public string? CheckedCountText { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string? AudiblePlusCheckedCountText { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public bool ControlsEnabled { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string? FilterString { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
|
||||
private bool? m_everythingChecked = false;
|
||||
private bool? m_audiblePlusChecked = false;
|
||||
public bool? EverythingChecked
|
||||
{
|
||||
get => m_everythingChecked;
|
||||
set
|
||||
{
|
||||
m_everythingChecked = value ?? false;
|
||||
SetVisibleChecked(_ => true, m_everythingChecked.Value);
|
||||
}
|
||||
}
|
||||
public bool? AudiblePlusChecked
|
||||
{
|
||||
get => m_audiblePlusChecked;
|
||||
set
|
||||
{
|
||||
m_audiblePlusChecked = value ?? false;
|
||||
SetVisibleChecked(e => e.IsAudiblePlus, m_audiblePlusChecked.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public TrashBinViewModel()
|
||||
{
|
||||
ProductsDisplay = new() { SearchEngine = SearchEngine };
|
||||
ProductsDisplay.RemovableCountChanged += (_, _) => UpdateCounts();
|
||||
ProductsDisplay.VisibleCountChanged += (_, _) => UpdateCounts();
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
var deletedBooks = GetDeletedLibraryBooks();
|
||||
SearchEngine.ReindexSearchEngine(deletedBooks);
|
||||
await ProductsDisplay.BindToGridAsync(deletedBooks);
|
||||
await ProductsDisplay.ScanAndRemoveBooksAsync();
|
||||
ControlsEnabled = true;
|
||||
}
|
||||
|
||||
private async Task ReloadAsync()
|
||||
{
|
||||
var deletedBooks = GetDeletedLibraryBooks();
|
||||
SearchEngine.ReindexSearchEngine(deletedBooks);
|
||||
await ProductsDisplay.UpdateGridAsync(deletedBooks);
|
||||
}
|
||||
|
||||
public void SetVisibleChecked(Func<LibraryBook, bool> predicate, bool isChecked)
|
||||
{
|
||||
ProductsDisplay.GetVisibleGridEntries().Where(e => predicate(e.LibraryBook)).ForEach(i => i.Remove = isChecked);
|
||||
}
|
||||
|
||||
private IEnumerable<LibraryBook> GetCheckedBooks() => ProductsDisplay.GetVisibleGridEntries().Where(i => i.Remove is true).Select(i => i.LibraryBook);
|
||||
|
||||
private void UpdateCounts()
|
||||
{
|
||||
var visible = ProductsDisplay.GetVisibleGridEntries().ToArray();
|
||||
var plusVisibleCount = visible.Count(e => e.LibraryBook.IsAudiblePlus);
|
||||
|
||||
var checkedCount = visible.Count(e => e.Remove is true);
|
||||
var plusCheckedCount = visible.Count(e => e.LibraryBook.IsAudiblePlus && e.Remove is true);
|
||||
|
||||
CheckedCountText = $"Checked: {checkedCount} of {visible.Length}";
|
||||
AudiblePlusCheckedCountText = $"Checked: {plusCheckedCount} of {plusVisibleCount}";
|
||||
|
||||
bool? everythingChecked = checkedCount == 0 || visible.Length == 0 ? false
|
||||
: checkedCount == visible.Length ? true
|
||||
: null;
|
||||
|
||||
bool? audiblePlusChecked = plusCheckedCount == 0 || plusVisibleCount == 0 ? false
|
||||
: plusCheckedCount == plusVisibleCount ? true
|
||||
: null;
|
||||
|
||||
this.RaiseAndSetIfChanged(ref m_everythingChecked, everythingChecked, nameof(EverythingChecked));
|
||||
this.RaiseAndSetIfChanged(ref m_audiblePlusChecked, audiblePlusChecked, nameof(AudiblePlusChecked));
|
||||
}
|
||||
|
||||
public async Task FilterBtnAsync()
|
||||
{
|
||||
var lastGood = ProductsDisplay.FilterString;
|
||||
try
|
||||
{
|
||||
await ProductsDisplay.Filter(FilterString);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await ProductsDisplay.Filter(lastGood);
|
||||
FilterString = lastGood;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RestoreCheckedAsync()
|
||||
{
|
||||
ControlsEnabled = false;
|
||||
var qtyChanges = await GetCheckedBooks().RestoreBooksAsync();
|
||||
if (qtyChanges > 0)
|
||||
await ReloadAsync();
|
||||
ControlsEnabled = true;
|
||||
}
|
||||
|
||||
public async Task PermanentlyDeleteCheckedAsync()
|
||||
{
|
||||
ControlsEnabled = false;
|
||||
var qtyChanges = await GetCheckedBooks().PermanentlyDeleteBooksAsync();
|
||||
if (qtyChanges > 0)
|
||||
await ReloadAsync();
|
||||
ControlsEnabled = true;
|
||||
}
|
||||
|
||||
private static List<LibraryBook> GetDeletedLibraryBooks()
|
||||
{
|
||||
#if DEBUG
|
||||
if (Avalonia.Controls.Design.IsDesignMode)
|
||||
{
|
||||
return [
|
||||
MockLibraryBook.CreateBook(title: "Mock Audible Plus Library Book 4", isAudiblePlus: true),
|
||||
MockLibraryBook.CreateBook(title: "Mock Audible Plus Library Book 3", isAudiblePlus: true),
|
||||
MockLibraryBook.CreateBook(title: "Mock Library Book 2"),
|
||||
MockLibraryBook.CreateBook(title: "Mock Library Book 1"),
|
||||
];
|
||||
}
|
||||
#endif
|
||||
return DbContexts.GetDeletedLibraryBooks();
|
||||
}
|
||||
}
|
||||
@@ -73,12 +73,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Controls.ColorPicker" Version="11.3.9" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.9" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.9" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.9" />
|
||||
<PackageReference Include="Avalonia.Controls.ColorPicker" Version="11.3.10" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.10" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.10" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.10" />
|
||||
<PackageReference Include="ReactiveUI.Avalonia" Version="11.3.8" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.9" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.10" />
|
||||
<PackageReference Include="WebViewControlAvaloniaFree" Version="11.3.15" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public Task? BindToGridTask { get; set; }
|
||||
public ProcessQueueViewModel ProcessQueue { get; } = new ProcessQueueViewModel();
|
||||
public ProductsDisplayViewModel ProductsDisplay { get; } = new ProductsDisplayViewModel();
|
||||
public ProductsDisplayViewModel ProductsDisplay { get; } = new() { SearchEngine = MainSearchEngine.Instance };
|
||||
|
||||
public double? DownloadProgress { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
|
||||
|
||||
@@ -34,18 +34,12 @@ namespace LibationAvalonia.ViewModels
|
||||
public bool RemoveColumnVisible { get => field; private set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
|
||||
public List<LibraryBook> GetVisibleBookEntries()
|
||||
=> FilteredInGridEntries?
|
||||
.OfType<LibraryBookEntry>()
|
||||
.Select(lbe => lbe.LibraryBook)
|
||||
.ToList()
|
||||
?? SOURCE
|
||||
.OfType<LibraryBookEntry>()
|
||||
.Select(lbe => lbe.LibraryBook)
|
||||
.ToList();
|
||||
=> GetVisibleGridEntries().Select(lbe => lbe.LibraryBook).ToList();
|
||||
|
||||
private IEnumerable<LibraryBookEntry> GetAllBookEntries()
|
||||
=> SOURCE
|
||||
.BookEntries();
|
||||
public IEnumerable<LibraryBookEntry> GetVisibleGridEntries()
|
||||
=> (FilteredInGridEntries as IEnumerable<GridEntry> ?? SOURCE).OfType<LibraryBookEntry>();
|
||||
|
||||
private IEnumerable<LibraryBookEntry> GetAllBookEntries() => SOURCE.BookEntries();
|
||||
|
||||
public ProductsDisplayViewModel()
|
||||
{
|
||||
@@ -53,6 +47,8 @@ namespace LibationAvalonia.ViewModels
|
||||
VisibleCountChanged?.Invoke(this, 0);
|
||||
}
|
||||
|
||||
public ISearchEngine? SearchEngine { get; set; }
|
||||
|
||||
private static readonly System.Reflection.MethodInfo? SetFlagsMethod;
|
||||
|
||||
/// <summary>
|
||||
@@ -120,7 +116,8 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
|
||||
//Create the filtered-in list before adding entries to GridEntries to avoid a refresh or UI action
|
||||
FilteredInGridEntries = geList.Union(seriesEntries.SelectMany(s => s.Children)).FilterEntries(FilterString);
|
||||
var searchResultSet = SearchEngine?.GetSearchResultSet(FilterString);
|
||||
FilteredInGridEntries = geList.Union(seriesEntries.SelectMany(s => s.Children)).FilterEntries(searchResultSet);
|
||||
|
||||
// Adding SOURCE to the DataGridViewCollection _after_ building the SOURCE list
|
||||
//Saves ~500 ms on a library of ~4500 books.
|
||||
@@ -315,7 +312,8 @@ namespace LibationAvalonia.ViewModels
|
||||
if (SOURCE.Count == 0)
|
||||
return;
|
||||
|
||||
FilteredInGridEntries = SOURCE.FilterEntries(searchString);
|
||||
var results = SearchEngine?.GetSearchResultSet(searchString);
|
||||
FilteredInGridEntries = SOURCE.FilterEntries(results);
|
||||
|
||||
await refreshGrid();
|
||||
}
|
||||
@@ -334,7 +332,9 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
private async void SearchEngineCommands_SearchEngineUpdated(object? sender, EventArgs? e)
|
||||
{
|
||||
var filterResults = SOURCE.FilterEntries(FilterString);
|
||||
|
||||
var searchResultSet = SearchEngine?.GetSearchResultSet(FilterString);
|
||||
var filterResults = SOURCE.FilterEntries(searchResultSet);
|
||||
|
||||
if (FilteredInGridEntries.SearchSetsDiffer(filterResults))
|
||||
{
|
||||
@@ -454,7 +454,7 @@ namespace LibationAvalonia.ViewModels
|
||||
#endregion
|
||||
|
||||
#region Column Widths
|
||||
|
||||
public bool DisablePersistColumnWidths { get; set; }
|
||||
public DataGridLength TitleWidth { get => getColumnWidth("Title", 200); set => setColumnWidth("Title", value); }
|
||||
public DataGridLength AuthorsWidth { get => getColumnWidth("Authors", 100); set => setColumnWidth("Authors", value); }
|
||||
public DataGridLength NarratorsWidth { get => getColumnWidth("Narrators", 100); set => setColumnWidth("Narrators", value); }
|
||||
@@ -480,6 +480,7 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
private void setColumnWidth(string columnName, DataGridLength width, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
if (DisablePersistColumnWidths) return;
|
||||
var dictionary = Configuration.Instance.GridColumnsWidths;
|
||||
|
||||
var newValue = (int)width.DisplayValue;
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
DownloadClipsBookmarks = config.DownloadClipsBookmarks;
|
||||
ClipBookmarkFormat = config.ClipsBookmarksFileFormat;
|
||||
SplitFilesByChapter = config.SplitFilesByChapter;
|
||||
MinimumFileDuration = config.MinimumFileDuration;
|
||||
MergeOpeningAndEndCredits = config.MergeOpeningAndEndCredits;
|
||||
StripAudibleBrandAudio = config.StripAudibleBrandAudio;
|
||||
StripUnabridged = config.StripUnabridged;
|
||||
@@ -71,6 +72,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
config.DownloadClipsBookmarks = DownloadClipsBookmarks;
|
||||
config.ClipsBookmarksFileFormat = ClipBookmarkFormat;
|
||||
config.SplitFilesByChapter = SplitFilesByChapter;
|
||||
config.MinimumFileDuration = MinimumFileDuration;
|
||||
config.MergeOpeningAndEndCredits = MergeOpeningAndEndCredits;
|
||||
config.StripAudibleBrandAudio = StripAudibleBrandAudio;
|
||||
config.StripUnabridged = StripUnabridged;
|
||||
@@ -118,6 +120,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public string DownloadCoverArtText { get; } = Configuration.GetDescription(nameof(Configuration.DownloadCoverArt));
|
||||
public string RetainAaxFileText { get; } = Configuration.GetDescription(nameof(Configuration.RetainAaxFile));
|
||||
public string SplitFilesByChapterText { get; } = Configuration.GetDescription(nameof(Configuration.SplitFilesByChapter));
|
||||
public string MinimumFileDurationText { get; } = Configuration.GetDescription(nameof(Configuration.MinimumFileDuration));
|
||||
public string MergeOpeningEndCreditsText { get; } = Configuration.GetDescription(nameof(Configuration.MergeOpeningAndEndCredits));
|
||||
public string StripAudibleBrandingText { get; } = Configuration.GetDescription(nameof(Configuration.StripAudibleBrandAudio));
|
||||
public string StripUnabridgedText { get; } = Configuration.GetDescription(nameof(Configuration.StripUnabridged));
|
||||
@@ -146,6 +149,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public string StripUnabridgedTip => Configuration.GetHelpText(nameof(StripUnabridged));
|
||||
public bool DecryptToLossy { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string DecryptToLossyTip => Configuration.GetHelpText(nameof(DecryptToLossy));
|
||||
public string MinimumFileDurationTip => Configuration.GetHelpText(nameof(MinimumFileDuration));
|
||||
public bool MoveMoovToBeginning { get; set; }
|
||||
|
||||
public bool LameDownsampleMono { get; set; } = Design.IsDesignMode;
|
||||
@@ -153,6 +157,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public bool LameConstantBitrate { get; set; } = Design.IsDesignMode;
|
||||
|
||||
public bool SplitFilesByChapter { get => field; set { this.RaiseAndSetIfChanged(ref field, value); } }
|
||||
public int MinimumFileDuration { get => field; set { this.RaiseAndSetIfChanged(ref field, value); } }
|
||||
public bool LameTargetBitrate { get => field; set { this.RaiseAndSetIfChanged(ref field, value); } }
|
||||
public bool LameMatchSource { get => field; set { this.RaiseAndSetIfChanged(ref field, value); } }
|
||||
public int LameBitrate { get => field; set { this.RaiseAndSetIfChanged(ref field, value); } }
|
||||
|
||||
@@ -58,7 +58,10 @@
|
||||
</StackPanel.Styles>
|
||||
<TextBlock Margin="0,0,6,0" FontSize="11" Text="DL
Limit" VerticalAlignment="Center" />
|
||||
<NumericUpDown
|
||||
Classes="SmallNumericUpDown"
|
||||
MinWidth="100"
|
||||
FontSize="12"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
TextConverter="{StaticResource myConverter}"
|
||||
Height="{Binding #cancelAllBtn.DesiredSize.Height}"
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<DataGrid.Styles>
|
||||
<Style Selector="DataGridColumnHeader">
|
||||
<Setter Property="ContextMenu">
|
||||
<ContextMenu Name="GridHeaderContextMenu" Opening="ContextMenu_ContextMenuOpening" Closed="ContextMenu_MenuClosed">
|
||||
<ContextMenu Name="GridHeaderContextMenu" Opening="GridHeaderContextMenu_Opening" Closed="GridHeaderContextMenu_Closed">
|
||||
<ContextMenu.Styles>
|
||||
<Style Selector="MenuItem">
|
||||
<Setter Property="Padding" Value="10,0,-10,0" />
|
||||
@@ -51,6 +51,11 @@
|
||||
</ContextMenu>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style Selector="DataGridCell">
|
||||
<Setter Property="ContextMenu">
|
||||
<ContextMenu Opening="GridCellContextMenu_Opening" Opened="GridCellContextMenu_Opened"/>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style Selector="DataGridCell > Panel">
|
||||
<Setter Property="VerticalAlignment" Value="Stretch"/>
|
||||
</Style>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using ApplicationServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input.Platform;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Styling;
|
||||
using DataLayer;
|
||||
@@ -30,13 +28,31 @@ namespace LibationAvalonia.Views
|
||||
public event EventHandler<LibraryBook[]>? ConvertToMp3Clicked;
|
||||
public event EventHandler<LibraryBook>? TagsButtonClicked;
|
||||
|
||||
|
||||
public static readonly StyledProperty<bool> DisableContextMenuProperty =
|
||||
AvaloniaProperty.Register<GroupBox, bool>(nameof(DisableContextMenu));
|
||||
|
||||
public static readonly StyledProperty<bool> DisableColumnCustomizationProperty =
|
||||
AvaloniaProperty.Register<GroupBox, bool>(nameof(DisableColumnCustomization));
|
||||
|
||||
public bool DisableContextMenu
|
||||
{
|
||||
get { return GetValue(DisableContextMenuProperty); }
|
||||
set { SetValue(DisableContextMenuProperty, value); }
|
||||
}
|
||||
|
||||
public bool DisableColumnCustomization
|
||||
{
|
||||
get { return GetValue(DisableColumnCustomizationProperty); }
|
||||
set { SetValue(DisableColumnCustomizationProperty, value); }
|
||||
}
|
||||
|
||||
private ProductsDisplayViewModel? _viewModel => DataContext as ProductsDisplayViewModel;
|
||||
ImageDisplayDialog? imageDisplayDialog;
|
||||
|
||||
public ProductsDisplay()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataGridContextMenus.CellContextMenuStripNeeded += ProductsGrid_CellContextMenuStripNeeded;
|
||||
|
||||
var cellSelector = Selectors.Is<DataGridCell>(null);
|
||||
rowHeightStyle = new Style(_ => cellSelector);
|
||||
@@ -91,6 +107,18 @@ namespace LibationAvalonia.Views
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(Avalonia.Controls.Primitives.TemplateAppliedEventArgs e)
|
||||
{
|
||||
ApplyDisableColumnCustimaziton();
|
||||
base.OnApplyTemplate(e);
|
||||
}
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
if (change.Property == DisableColumnCustomizationProperty)
|
||||
ApplyDisableColumnCustimaziton();
|
||||
base.OnPropertyChanged(change);
|
||||
}
|
||||
|
||||
private void ProductsDisplay_LoadingRow(object sender, DataGridRowEventArgs e)
|
||||
{
|
||||
if (e.Row.DataContext is LibraryBookEntry entry && entry.Liberate?.IsEpisode is true)
|
||||
@@ -180,10 +208,19 @@ namespace LibationAvalonia.Views
|
||||
#endregion
|
||||
|
||||
#region Cell Context Menu
|
||||
|
||||
public void ProductsGrid_CellContextMenuStripNeeded(object? sender, DataGridCellContextMenuStripNeededEventArgs args)
|
||||
public void GridCellContextMenu_Opening(object? sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
var entries = args.GridEntries;
|
||||
e.Cancel = DisableContextMenu;
|
||||
}
|
||||
|
||||
//Use Opened instead of opening because the parent is not set yet in Opening
|
||||
public void GridCellContextMenu_Opened(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not ContextMenu contextMenu ||
|
||||
DataGridCellContextMenu<GridEntry>.Create(contextMenu) is not { } args)
|
||||
return;
|
||||
|
||||
var entries = args.RowItems;
|
||||
var ctx = new GridContextMenu(entries, '_');
|
||||
|
||||
if (App.MainWindow?.Clipboard is IClipboard clipboard)
|
||||
@@ -206,8 +243,8 @@ namespace LibationAvalonia.Views
|
||||
});
|
||||
|
||||
args.ContextMenuItems.Add(new Separator());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
#region Liberate all Episodes (Single series only)
|
||||
|
||||
@@ -351,6 +388,20 @@ namespace LibationAvalonia.Views
|
||||
})
|
||||
});
|
||||
}
|
||||
#endregion
|
||||
#region Remove Audible Plus Books from Audible Library
|
||||
|
||||
if (entries.Length != 1 || ctx.RemoveFromAudibleEnabled)
|
||||
{
|
||||
args.ContextMenuItems.Add(new Separator());
|
||||
args.ContextMenuItems.Add(new MenuItem
|
||||
{
|
||||
Header = ctx.RemoveFromAudibleText,
|
||||
IsEnabled = ctx.RemoveFromAudibleEnabled,
|
||||
Command = ReactiveCommand.CreateFromTask(ctx.RemoveFromAudibleAsync)
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
if (entries.Length > 1)
|
||||
@@ -440,13 +491,13 @@ namespace LibationAvalonia.Views
|
||||
var itemName = column.SortMemberPath;
|
||||
if (itemName == nameof(GridEntry.Remove))
|
||||
continue;
|
||||
|
||||
|
||||
GridHeaderContextMenu.Items.Add(new MenuItem
|
||||
{
|
||||
Header = new CheckBox { Content = new TextBlock { Text = ((string)column.Header).Replace('\n', ' ') } },
|
||||
Tag = column,
|
||||
});
|
||||
|
||||
|
||||
column.IsVisible = Configuration.Instance.GetColumnVisibility(itemName);
|
||||
}
|
||||
|
||||
@@ -464,10 +515,19 @@ namespace LibationAvalonia.Views
|
||||
}
|
||||
}
|
||||
|
||||
public void ContextMenu_ContextMenuOpening(object? sender, System.ComponentModel.CancelEventArgs e)
|
||||
private void ApplyDisableColumnCustimaziton()
|
||||
{
|
||||
if (sender is not ContextMenu contextMenu)
|
||||
_viewModel?.DisablePersistColumnWidths = DisableColumnCustomization;
|
||||
productsGrid.CanUserReorderColumns = !DisableColumnCustomization;
|
||||
}
|
||||
|
||||
public void GridHeaderContextMenu_Opening(object? sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
if (DisableContextMenu || sender is not ContextMenu contextMenu)
|
||||
{
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
foreach (var mi in contextMenu.Items.OfType<MenuItem>())
|
||||
{
|
||||
if (mi.Tag is DataGridColumn column && mi.Header is CheckBox cbox)
|
||||
@@ -477,7 +537,7 @@ namespace LibationAvalonia.Views
|
||||
}
|
||||
}
|
||||
|
||||
public void ContextMenu_MenuClosed(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
public void GridHeaderContextMenu_Closed(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not ContextMenu contextMenu)
|
||||
return;
|
||||
@@ -504,6 +564,7 @@ namespace LibationAvalonia.Views
|
||||
|
||||
private void ProductsGrid_ColumnDisplayIndexChanged(object? sender, DataGridColumnEventArgs e)
|
||||
{
|
||||
if (DisableColumnCustomization) return;
|
||||
var config = Configuration.Instance;
|
||||
|
||||
var dictionary = config.GridColumnsDisplayIndices;
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
using ApplicationServices;
|
||||
using CommandLine;
|
||||
using DataLayer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationCli
|
||||
{
|
||||
[Verb("export", HelpText = "Must include path and flag for export file type: --xlsx , --csv , --json")]
|
||||
public class ExportOptions : OptionsBase
|
||||
{
|
||||
[Option(shortName: 'p', longName: "path", Required = true, HelpText = "Path to save file to.")]
|
||||
public string FilePath { get; set; }
|
||||
public string? FilePath { get; set; }
|
||||
|
||||
#region explanation of mutually exclusive options
|
||||
/*
|
||||
@@ -36,9 +40,12 @@ namespace LibationCli
|
||||
[Option(shortName: 'j', longName: "json", HelpText = "JavaScript Object Notation", SetName = "json")]
|
||||
public bool json { get; set; }
|
||||
|
||||
[Value(0, MetaName = "[asins]", HelpText = "Optional product IDs of books to process.")]
|
||||
public IEnumerable<string>? Asins { get; set; }
|
||||
|
||||
protected override Task ProcessAsync()
|
||||
{
|
||||
Action<string> exporter
|
||||
Action<string, IEnumerable<LibraryBook>?>? exporter
|
||||
= csv ? LibraryExporter.ToCsv
|
||||
: json ? LibraryExporter.ToJson
|
||||
: xlsx ? LibraryExporter.ToXlsx
|
||||
@@ -54,9 +61,18 @@ namespace LibationCli
|
||||
{
|
||||
PrintVerbUsage($"Undefined export format for file type \"{Path.GetExtension(FilePath)}\"");
|
||||
}
|
||||
else if (FilePath is null)
|
||||
{
|
||||
PrintVerbUsage($"Undefined export file name");
|
||||
}
|
||||
else
|
||||
{
|
||||
exporter(FilePath);
|
||||
IEnumerable<LibraryBook>? booksToScan = null;
|
||||
if (Asins?.Any() is true)
|
||||
{
|
||||
booksToScan = DbContexts.GetLibrary_Flat_NoTracking().IntersectBy(Asins, l => l.Book.AudibleProductId);
|
||||
}
|
||||
exporter(FilePath, booksToScan);
|
||||
Console.WriteLine($"Library exported to: {FilePath}");
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
|
||||
@@ -87,7 +87,7 @@ namespace LibationCli
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Failed to read license file: {@LicenseFile}", licFile);
|
||||
Serilog.Log.Error(ex, "Failed to read license file: {LicenseFile}", licFile);
|
||||
Console.Error.WriteLine("Error: Failed to read license file. Please ensure the file is a valid license file in JSON format.");
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using CommandLine;
|
||||
using CsvHelper.TypeConversion;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace LibationFileManager
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Error creating subdirectory in {@InProgress}", Configuration.Instance.InProgress);
|
||||
Serilog.Log.Error(ex, "Error creating subdirectory in {InProgress}", Configuration.Instance.InProgress);
|
||||
lastInProgressFail = DateTime.UtcNow;
|
||||
return null;
|
||||
}
|
||||
@@ -86,7 +86,7 @@ namespace LibationFileManager
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Error creating Books directory: {@BooksDirectory}", Configuration.Instance.Books);
|
||||
Serilog.Log.Error(ex, "Error creating Books directory: {BooksDirectory}", Configuration.Instance.Books);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -272,7 +272,7 @@ namespace LibationFileManager
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Error checking for asin in {@file}", path);
|
||||
Serilog.Log.Error(ex, "Error checking for asin in {file}", path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -82,6 +82,13 @@ namespace LibationFileManager
|
||||
from the decrypted audiobook. This does not require
|
||||
re-encoding.
|
||||
""" },
|
||||
{nameof(MinimumFileDuration), """
|
||||
The minimum duration (in minutes) for an chapter to
|
||||
be split into its own file. Chapters shorter than
|
||||
this duration will be merged with the following
|
||||
chapter. Merged chapter titles will be joined with
|
||||
a space between them.
|
||||
""" },
|
||||
{nameof(SpatialAudioCodec), """
|
||||
The Dolby Digital Plus (E-AC-3) codec is more widely
|
||||
supported than the AC-4 codec, but E-AC-3 files are
|
||||
|
||||
@@ -169,6 +169,9 @@ namespace LibationFileManager
|
||||
[Description("Split my books into multiple files by chapter")]
|
||||
public bool SplitFilesByChapter { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
[Description("Minimum file duration (seconds)")]
|
||||
public int MinimumFileDuration { get => Math.Max(0, GetNonString(defaultValue: 3)); set => SetNonString(value); }
|
||||
|
||||
[Description("Merge Opening/End Credits into the following/preceding chapters")]
|
||||
public bool MergeOpeningAndEndCredits { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace LibationFileManager
|
||||
catch (Exception e)
|
||||
{
|
||||
//None of the interop functions are strictly necessary for Libation to run.
|
||||
Serilog.Log.Logger.Error(e, "Unable to load types from assembly {@configApp}", configApp);
|
||||
Serilog.Log.Logger.Error(e, "Unable to load types from assembly {configApp}", configApp);
|
||||
}
|
||||
}
|
||||
private static string? getOSConfigApp()
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
|
||||
<PackageReference Include="NameParserSharp" Version="1.5.0" />
|
||||
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -114,24 +114,24 @@ public class LibationFiles
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, "Failed to load settings file: {@SettingsFile}", settingsFile);
|
||||
Log.Logger.Error(ex, "Failed to load settings file: {SettingsFile}", settingsFile);
|
||||
try
|
||||
{
|
||||
Log.Logger.Information("Deleting invalid settings file: {@SettingsFile}", settingsFile);
|
||||
Log.Logger.Information("Deleting invalid settings file: {SettingsFile}", settingsFile);
|
||||
FileUtility.SaferDelete(settingsFile);
|
||||
Log.Logger.Information("Creating a new, empty setting file: {@SettingsFile}", settingsFile);
|
||||
Log.Logger.Information("Creating a new, empty setting file: {SettingsFile}", settingsFile);
|
||||
try
|
||||
{
|
||||
File.WriteAllText(settingsFile, "{}");
|
||||
}
|
||||
catch (Exception createEx)
|
||||
{
|
||||
Log.Logger.Error(createEx, "Failed to create new settings file: {@SettingsFile}", settingsFile);
|
||||
Log.Logger.Error(createEx, "Failed to create new settings file: {SettingsFile}", settingsFile);
|
||||
}
|
||||
}
|
||||
catch (Exception deleteEx)
|
||||
{
|
||||
Log.Logger.Error(deleteEx, "Failed to delete the invalid settings file: {@SettingsFile}", settingsFile);
|
||||
Log.Logger.Error(deleteEx, "Failed to delete the invalid settings file: {SettingsFile}", settingsFile);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -59,6 +59,7 @@ namespace LibationSearchEngine
|
||||
{ FieldType.Bool, lb => lb.AbsentFromLastScan.ToString(), "AbsentFromLastScan", "Absent" },
|
||||
{ FieldType.Bool, lb => (!string.IsNullOrWhiteSpace(lb.Book.SeriesNames())).ToString(), "IsInSeries", "InSeries" },
|
||||
{ FieldType.Bool, lb => lb.Book.UserDefinedItem.IsFinished.ToString(), nameof(UserDefinedItem.IsFinished), "Finished", "IsFinished" },
|
||||
{ FieldType.Bool, lb => lb.IsAudiblePlus.ToString(), nameof(LibraryBook.IsAudiblePlus), "AudiblePlus", "Plus" },
|
||||
// all numbers are padded to 8 char.s
|
||||
// This will allow a single method to auto-pad numbers. The method will match these as well as date: yyyymmdd
|
||||
{ FieldType.Number, lb => lb.Book.LengthInMinutes.ToLuceneString(), nameof(Book.LengthInMinutes), "Length", "Minutes" },
|
||||
@@ -92,6 +93,12 @@ namespace LibationSearchEngine
|
||||
}
|
||||
}
|
||||
|
||||
public SearchEngine(string directory = null)
|
||||
{
|
||||
SearchEngineDirectory = directory
|
||||
?? new System.IO.DirectoryInfo(Configuration.Instance.LibationFiles.Location).CreateSubdirectoryEx("SearchEngine").FullName;
|
||||
}
|
||||
|
||||
/// <summary>Long running. Use await Task.Run(() => UpdateBook(productId))</summary>
|
||||
public void UpdateBook(LibationContext context, string productId)
|
||||
{
|
||||
@@ -130,7 +137,7 @@ namespace LibationSearchEngine
|
||||
public void UpdateTags(string productId, string tags) => updateAnalyzedField(productId, TAGS, tags);
|
||||
|
||||
// all fields are case-specific
|
||||
private static void updateAnalyzedField(string productId, string fieldName, string newValue)
|
||||
private void updateAnalyzedField(string productId, string fieldName, string newValue)
|
||||
=> updateDocument(
|
||||
productId,
|
||||
d =>
|
||||
@@ -169,7 +176,7 @@ namespace LibationSearchEngine
|
||||
d.AddIndexRule(rating, book);
|
||||
});
|
||||
|
||||
private static void updateDocument(string productId, Action<Document> action)
|
||||
private void updateDocument(string productId, Action<Document> action)
|
||||
{
|
||||
var productTerm = new Term(_ID_, productId);
|
||||
|
||||
@@ -276,10 +283,10 @@ namespace LibationSearchEngine
|
||||
}
|
||||
#endregion
|
||||
|
||||
private static Directory getIndex() => FSDirectory.Open(SearchEngineDirectory);
|
||||
private Directory getIndex() => FSDirectory.Open(SearchEngineDirectory);
|
||||
|
||||
// not customizable. don't move to config
|
||||
private static string SearchEngineDirectory { get; }
|
||||
= new System.IO.DirectoryInfo(Configuration.Instance.LibationFiles.Location).CreateSubdirectoryEx("SearchEngine").FullName;
|
||||
//Defaults to "LibationFiles/SearchEngine, but can be overridden
|
||||
//in constructor for use in TrashBinDialog search
|
||||
private string SearchEngineDirectory { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ public class FindBetterQualityBooksViewModel : ReactiveObject
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error checking for better quality for {@Asin}", b.Asin);
|
||||
Serilog.Log.Logger.Error(ex, "Error checking for better quality for {@Asin}", new { b.Asin });
|
||||
b.FoundFile = $"Error: {ex.Message}";
|
||||
b.ScanStatus = ProcessBookStatus.Failed;
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ namespace LibationUiBase.GridView
|
||||
public bool IsBook => !IsSeries && !IsEpisode;
|
||||
public bool IsUnavailable
|
||||
=> !IsSeries
|
||||
& isAbsent
|
||||
& (
|
||||
&& isAbsent
|
||||
&& (
|
||||
BookStatus is not LiberatedStatus.Liberated
|
||||
|| PdfStatus is not null and not LiberatedStatus.Liberated
|
||||
);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using LibationFileManager.Templates;
|
||||
using LibationUiBase.Forms;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -17,6 +20,7 @@ public class GridContextMenu
|
||||
public string SetDownloadedText => $"Set Download status to '{Accelerator}Downloaded'";
|
||||
public string SetNotDownloadedText => $"Set Download status to '{Accelerator}Not Downloaded'";
|
||||
public string RemoveText => $"{Accelerator}Remove from library";
|
||||
public string RemoveFromAudibleText => $"Remove Plus {(GridEntries.Count(e => e.LibraryBook.IsAudiblePlus) == 1 ? "Book" : "Books")} from Audible Library";
|
||||
public string LocateFileText => $"{Accelerator}Locate file...";
|
||||
public string LocateFileDialogTitle => $"Locate the audio file for '{GridEntries[0].Book?.TitleWithSubtitle ?? "[null]"}'";
|
||||
public string LocateFileErrorMessage => "Error saving book's location";
|
||||
@@ -37,6 +41,7 @@ public class GridContextMenu
|
||||
public bool ConvertToMp3Enabled => LibraryBookEntries.Any(ge => ge.Book?.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
|
||||
public bool DownloadAsChaptersEnabled => LibraryBookEntries.Any(ge => ge.Book?.UserDefinedItem.BookStatus is not LiberatedStatus.Error);
|
||||
public bool ReDownloadEnabled => LibraryBookEntries.Any(ge => ge.Book?.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
|
||||
public bool RemoveFromAudibleEnabled => LibraryBookEntries.Any(ge => ge.LibraryBook.IsAudiblePlus);
|
||||
|
||||
private GridEntry[] GridEntries { get; }
|
||||
public LibraryBookEntry[] LibraryBookEntries { get; }
|
||||
@@ -84,6 +89,85 @@ public class GridContextMenu
|
||||
await LibraryBookEntries.Select(e => e.LibraryBook).RemoveBooksAsync();
|
||||
}
|
||||
|
||||
public async Task RemoveFromAudibleAsync()
|
||||
{
|
||||
LibraryBook[] toRemove = LibraryBookEntries.Select(l => l.LibraryBook).Where(lb => lb.IsAudiblePlus).ToArray();
|
||||
if (toRemove.Length == 0)
|
||||
return;
|
||||
|
||||
string bookStr = "book".PluralizeWithCount(toRemove.Length), itsThem = toRemove.Length == 1 ? "it" : "them";
|
||||
string confirmMessage = $"""
|
||||
Libation is about to remove {bookStr} from your Audible account. The only way to get {itsThem} back
|
||||
is to re-add {itsThem} to your Audible Library through the Audible website or app.
|
||||
|
||||
Are you sure you want to remove the following {bookStr}?
|
||||
|
||||
{toRemove.AggregateTitles()}
|
||||
""";
|
||||
DialogResult result = await MessageBoxBase.Show(confirmMessage, "Confirm Remove from Audible Library", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1);
|
||||
if (result != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
List<LibraryBook> removedFromAudible = [];
|
||||
List<LibraryBook> failedToRemove = [];
|
||||
|
||||
//Getting the API loads AccountsSettings every time and es expensive
|
||||
//cache Api to improve perfomanc on large batches of deletions
|
||||
Dictionary<string, AudibleApi.Api> apis = [];
|
||||
|
||||
foreach (var entry in toRemove)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!apis.TryGetValue(entry.Account, out var api))
|
||||
{
|
||||
apis[entry.Account] = api = await entry.GetApiAsync();
|
||||
}
|
||||
|
||||
bool success = await api.RemoveItemFromLibraryAsync(entry.Book.AudibleProductId);
|
||||
if (success)
|
||||
{
|
||||
removedFromAudible.Add(entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
failedToRemove.Add(entry);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Failed to remove book from audible account. {@Book}", entry.LogFriendly());
|
||||
failedToRemove.Add(entry);
|
||||
}
|
||||
}
|
||||
if (failedToRemove.Count > 0)
|
||||
{
|
||||
string booksStr = "book".PluralizeWithCount(failedToRemove.Count);
|
||||
string message = $"""
|
||||
Failed to remove {booksStr} from Audible.
|
||||
|
||||
{failedToRemove.AggregateTitles()}
|
||||
""";
|
||||
await MessageBoxBase.Show(message, $"Failed to Remove {booksStr} from Audible");
|
||||
}
|
||||
try
|
||||
{
|
||||
await removedFromAudible.PermanentlyDeleteBooksAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Failed to delete locally removed from Audible books.");
|
||||
|
||||
string booksStr = "book".PluralizeWithCount(removedFromAudible.Count);
|
||||
string message = $"""
|
||||
Failed to delete {booksStr} from Libation.
|
||||
|
||||
{removedFromAudible.AggregateTitles()}
|
||||
""";
|
||||
await MessageBoxBase.Show(message, $"Failed to Delete {booksStr} from Libation");
|
||||
}
|
||||
}
|
||||
|
||||
public ITemplateEditor CreateTemplateEditor<T>(LibraryBook libraryBook, string existingTemplate)
|
||||
where T : Templates, ITemplate, new()
|
||||
{
|
||||
|
||||
@@ -146,7 +146,10 @@ namespace LibationUiBase.GridView
|
||||
{
|
||||
//If UserDefinedItem was changed on a different Book instance (such as when batch liberating via menus),
|
||||
//Liberate.Book and LibraryBook.Book instances will not have the current DB state.
|
||||
Invoke(() => UpdateLibraryBook(new LibraryBook(udi.Book, LibraryBook.DateAdded, LibraryBook.Account)));
|
||||
var newLB = new LibraryBook(udi.Book, LibraryBook.DateAdded, LibraryBook.Account);
|
||||
newLB.SetIncludedUntil(LibraryBook.IncludedUntil);
|
||||
newLB.SetIsAudiblePlus(LibraryBook.IsAudiblePlus);
|
||||
Invoke(() => UpdateLibraryBook(newLB));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,16 +45,13 @@ namespace LibationUiBase.GridView
|
||||
=> searchSet is null != otherSet is null ||
|
||||
(searchSet is not null &&
|
||||
otherSet is not null &&
|
||||
searchSet.Intersect(otherSet).Count() != searchSet.Count);
|
||||
searchSet.Intersect(otherSet).Count() != searchSet.Count);
|
||||
|
||||
[return: NotNullIfNotNull(nameof(searchString))]
|
||||
public static HashSet<GridEntry>? FilterEntries(this IEnumerable<GridEntry> entries, string? searchString)
|
||||
[return: NotNullIfNotNull(nameof(searchResultSet))]
|
||||
public static HashSet<GridEntry>? FilterEntries(this IEnumerable<GridEntry> entries, LibationSearchEngine.SearchResultSet? searchResultSet)
|
||||
{
|
||||
if (string.IsNullOrEmpty(searchString))
|
||||
if (searchResultSet is null)
|
||||
return null;
|
||||
|
||||
var searchResultSet = SearchEngineCommands.Search(searchString);
|
||||
|
||||
var booksFilteredIn = entries.IntersectBy(searchResultSet.Docs.Select(d => d.ProductId), l => l.AudibleProductId);
|
||||
|
||||
//Find all series containing children that match the search criteria
|
||||
|
||||
@@ -47,6 +47,7 @@ public class LibationContributor
|
||||
GitHubUser("twsouthwick"),
|
||||
GitHubUser("radiorambo"),
|
||||
GitHubUser("Youssef1313"),
|
||||
GitHubUser("niontrix"),
|
||||
]);
|
||||
|
||||
private LibationContributor(string name, LibationContributorType type,Uri link)
|
||||
|
||||
71
Source/LibationUiBase/LocatedAudiobooksViewModel.cs
Normal file
71
Source/LibationUiBase/LocatedAudiobooksViewModel.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationUiBase;
|
||||
|
||||
public class FoundAudiobook
|
||||
{
|
||||
public string ID => Entry.Id;
|
||||
public string FileName { get; }
|
||||
public FilePathCache.CacheEntry Entry { get; }
|
||||
public FoundAudiobook(FilePathCache.CacheEntry entry)
|
||||
{
|
||||
Entry = entry;
|
||||
FileName = Path.GetFileName(entry.Path);
|
||||
}
|
||||
}
|
||||
public class LocatedAudiobooksViewModel : ReactiveObject
|
||||
{
|
||||
public IList<FoundAudiobook> FoundFiles { get; }
|
||||
public int FoundAsinCount { get => field; private set => RaiseAndSetIfChanged(ref field, value); }
|
||||
private readonly HashSet<string> foundAsinsSet = [];
|
||||
public LocatedAudiobooksViewModel(IList<FoundAudiobook> fileList)
|
||||
{
|
||||
FoundFiles = fileList;
|
||||
}
|
||||
|
||||
public void AddFoundFile(FilePathCache.CacheEntry entry)
|
||||
{
|
||||
FoundAudiobook foundFile = new(entry);
|
||||
Invoke(() => FoundFiles?.Add(foundFile));
|
||||
if (!foundAsinsSet.Contains(entry.Id))
|
||||
{
|
||||
foundAsinsSet.Add(entry.Id);
|
||||
FoundAsinCount = foundAsinsSet.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task FindAndAddBooksAsync(string searchdir, CancellationToken cancellation)
|
||||
{
|
||||
await Task.Run(() => FindAndAddBooksInternal(searchdir, cancellation), cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task FindAndAddBooksInternal(string searchdir, CancellationToken cancellation)
|
||||
{
|
||||
await foreach (var book in AudioFileStorage.FindAudiobooksAsync(searchdir, cancellation))
|
||||
{
|
||||
try
|
||||
{
|
||||
FilePathCache.Insert(book);
|
||||
|
||||
var lb = DbContexts.GetLibraryBook_Flat_NoTracking(book.Id);
|
||||
if (lb is not null && lb.Book?.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated)
|
||||
await lb.UpdateBookStatusAsync(LiberatedStatus.Liberated);
|
||||
|
||||
cancellation.ThrowIfCancellationRequested();
|
||||
AddFoundFile(book);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Error adding found audiobook file to Libation. {@audioFile}", book);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,7 +168,7 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
|
||||
private bool IsBooksDirectoryValid(Configuration config)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(config.Books))
|
||||
if (string.IsNullOrWhiteSpace(config.Books?.Path))
|
||||
{
|
||||
Serilog.Log.Logger.Error("Books location is not set in configuration.");
|
||||
MessageBoxBase.Show(
|
||||
@@ -180,7 +180,7 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
}
|
||||
else if (AudibleFileStorage.BooksDirectory is null)
|
||||
{
|
||||
Serilog.Log.Logger.Error("Failed to create books directory: {@booksDir}", config.Books);
|
||||
Serilog.Log.Logger.Error("Failed to create books directory: {booksDir}", config.Books?.Path);
|
||||
MessageBoxBase.Show(
|
||||
$"Libation was unable to create the \"Books location\" folder at:\n{config.Books}\n\nPlease change the Books location in the settings menu.",
|
||||
"Failed to Create Books Directory",
|
||||
@@ -190,7 +190,7 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
}
|
||||
else if (AudibleFileStorage.DownloadsInProgressDirectory is null)
|
||||
{
|
||||
Serilog.Log.Logger.Error("Failed to create DownloadsInProgressDirectory in {@InProgress}", config.InProgress);
|
||||
Serilog.Log.Logger.Error("Failed to create DownloadsInProgressDirectory in {InProgress}", config.InProgress);
|
||||
MessageBoxBase.Show(
|
||||
$"Libation was unable to create the \"Downloads In Progress\" folder in:\n{config.InProgress}\n\nPlease change the In Progress location in the settings menu.",
|
||||
"Failed to Create Downloads In Progress Directory",
|
||||
@@ -200,7 +200,7 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
}
|
||||
else if (AudibleFileStorage.DecryptInProgressDirectory is null)
|
||||
{
|
||||
Serilog.Log.Logger.Error("Failed to create DecryptInProgressDirectory in {@InProgress}", config.InProgress);
|
||||
Serilog.Log.Logger.Error("Failed to create DecryptInProgressDirectory in {InProgress}", config.InProgress);
|
||||
MessageBoxBase.Show(
|
||||
$"Libation was unable to create the \"Decrypt In Progress\" folder in:\n{config.InProgress}\n\nPlease change the In Progress location in the settings menu.",
|
||||
"Failed to Create Decrypt In Progress Directory",
|
||||
|
||||
@@ -28,80 +28,111 @@
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
this.foundAudiobooksLV = new System.Windows.Forms.ListView();
|
||||
this.columnHeader1 = new System.Windows.Forms.ColumnHeader();
|
||||
this.columnHeader2 = new System.Windows.Forms.ColumnHeader();
|
||||
this.booksFoundLbl = new System.Windows.Forms.Label();
|
||||
this.SuspendLayout();
|
||||
components = new System.ComponentModel.Container();
|
||||
label1 = new System.Windows.Forms.Label();
|
||||
booksFoundLbl = new System.Windows.Forms.Label();
|
||||
dataGridView1 = new System.Windows.Forms.DataGridView();
|
||||
foundAudiobookBindingSource = new System.Windows.Forms.BindingSource(components);
|
||||
iDDataGridViewTextBoxColumn = new AccessibleDataGridViewColumn();
|
||||
fileNameDataGridViewTextBoxColumn = new AccessibleDataGridViewColumn();
|
||||
((System.ComponentModel.ISupportInitialize)dataGridView1).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)foundAudiobookBindingSource).BeginInit();
|
||||
SuspendLayout();
|
||||
//
|
||||
// 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(108, 15);
|
||||
this.label1.TabIndex = 1;
|
||||
this.label1.Text = "Found Audiobooks";
|
||||
//
|
||||
// foundAudiobooksLV
|
||||
//
|
||||
this.foundAudiobooksLV.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.foundAudiobooksLV.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
|
||||
this.columnHeader1,
|
||||
this.columnHeader2});
|
||||
this.foundAudiobooksLV.FullRowSelect = true;
|
||||
this.foundAudiobooksLV.Location = new System.Drawing.Point(12, 33);
|
||||
this.foundAudiobooksLV.Name = "foundAudiobooksLV";
|
||||
this.foundAudiobooksLV.Size = new System.Drawing.Size(321, 261);
|
||||
this.foundAudiobooksLV.TabIndex = 2;
|
||||
this.foundAudiobooksLV.UseCompatibleStateImageBehavior = false;
|
||||
this.foundAudiobooksLV.View = System.Windows.Forms.View.Details;
|
||||
//
|
||||
// columnHeader1
|
||||
//
|
||||
this.columnHeader1.Text = "Book ID";
|
||||
this.columnHeader1.Width = 85;
|
||||
//
|
||||
// columnHeader2
|
||||
//
|
||||
this.columnHeader2.Text = "Title";
|
||||
label1.AutoSize = true;
|
||||
label1.Location = new System.Drawing.Point(12, 9);
|
||||
label1.Name = "label1";
|
||||
label1.Size = new System.Drawing.Size(108, 15);
|
||||
label1.TabIndex = 1;
|
||||
label1.Text = "Found Audiobooks";
|
||||
//
|
||||
// booksFoundLbl
|
||||
//
|
||||
this.booksFoundLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.booksFoundLbl.AutoSize = true;
|
||||
this.booksFoundLbl.Location = new System.Drawing.Point(253, 9);
|
||||
this.booksFoundLbl.Name = "booksFoundLbl";
|
||||
this.booksFoundLbl.Size = new System.Drawing.Size(80, 15);
|
||||
this.booksFoundLbl.TabIndex = 3;
|
||||
this.booksFoundLbl.Text = "IDs Found: {0}";
|
||||
this.booksFoundLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
|
||||
booksFoundLbl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
booksFoundLbl.AutoSize = true;
|
||||
booksFoundLbl.Location = new System.Drawing.Point(253, 9);
|
||||
booksFoundLbl.Name = "booksFoundLbl";
|
||||
booksFoundLbl.Size = new System.Drawing.Size(72, 15);
|
||||
booksFoundLbl.TabIndex = 3;
|
||||
booksFoundLbl.Text = "IDs Found: 0";
|
||||
booksFoundLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
|
||||
//
|
||||
// dataGridView1
|
||||
//
|
||||
dataGridView1.AllowUserToAddRows = false;
|
||||
dataGridView1.AllowUserToDeleteRows = false;
|
||||
dataGridView1.AllowUserToResizeRows = false;
|
||||
dataGridView1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
dataGridView1.AutoGenerateColumns = false;
|
||||
dataGridView1.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.AllCells;
|
||||
dataGridView1.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||
dataGridView1.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { iDDataGridViewTextBoxColumn, fileNameDataGridViewTextBoxColumn });
|
||||
dataGridView1.DataSource = foundAudiobookBindingSource;
|
||||
dataGridView1.Location = new System.Drawing.Point(12, 27);
|
||||
dataGridView1.Name = "dataGridView1";
|
||||
dataGridView1.RowHeadersVisible = false;
|
||||
dataGridView1.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.CellSelect;
|
||||
dataGridView1.Size = new System.Drawing.Size(321, 267);
|
||||
dataGridView1.TabIndex = 4;
|
||||
dataGridView1.CellDoubleClick += dataGridView1_CellDoubleClick;
|
||||
//
|
||||
// foundAudiobookBindingSource
|
||||
//
|
||||
foundAudiobookBindingSource.DataSource = typeof(LibationUiBase.FoundAudiobook);
|
||||
//
|
||||
// iDDataGridViewTextBoxColumn
|
||||
//
|
||||
iDDataGridViewTextBoxColumn.AccessibilityDescription = "Audiobook's Audible product ID forund by the scan.";
|
||||
iDDataGridViewTextBoxColumn.AccessibilityName = "Found ASIN";
|
||||
iDDataGridViewTextBoxColumn.DataPropertyName = "ID";
|
||||
iDDataGridViewTextBoxColumn.HeaderText = "Found ID";
|
||||
iDDataGridViewTextBoxColumn.Name = "iDDataGridViewTextBoxColumn";
|
||||
iDDataGridViewTextBoxColumn.ReadOnly = true;
|
||||
iDDataGridViewTextBoxColumn.Resizable = System.Windows.Forms.DataGridViewTriState.True;
|
||||
iDDataGridViewTextBoxColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
iDDataGridViewTextBoxColumn.Width = 80;
|
||||
//
|
||||
// fileNameDataGridViewTextBoxColumn
|
||||
//
|
||||
fileNameDataGridViewTextBoxColumn.AccessibilityDescription = "Audiobook file found. Double-click to open containing folder.";
|
||||
fileNameDataGridViewTextBoxColumn.AccessibilityName = "Found File";
|
||||
fileNameDataGridViewTextBoxColumn.DataPropertyName = "FileName";
|
||||
fileNameDataGridViewTextBoxColumn.HeaderText = "Found File";
|
||||
fileNameDataGridViewTextBoxColumn.Name = "fileNameDataGridViewTextBoxColumn";
|
||||
fileNameDataGridViewTextBoxColumn.ReadOnly = true;
|
||||
fileNameDataGridViewTextBoxColumn.Resizable = System.Windows.Forms.DataGridViewTriState.True;
|
||||
fileNameDataGridViewTextBoxColumn.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.Automatic;
|
||||
fileNameDataGridViewTextBoxColumn.Width = 87;
|
||||
//
|
||||
// LocateAudiobooksDialog
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
this.ClientSize = new System.Drawing.Size(345, 306);
|
||||
this.Controls.Add(this.booksFoundLbl);
|
||||
this.Controls.Add(this.foundAudiobooksLV);
|
||||
this.Controls.Add(this.label1);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow;
|
||||
this.Name = "LocateAudiobooksDialog";
|
||||
this.Text = "Locate Audiobooks";
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
ClientSize = new System.Drawing.Size(345, 306);
|
||||
Controls.Add(dataGridView1);
|
||||
Controls.Add(booksFoundLbl);
|
||||
Controls.Add(label1);
|
||||
FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow;
|
||||
Name = "LocateAudiobooksDialog";
|
||||
Text = "Locate Audiobooks";
|
||||
FormClosing += LocateAudiobooks_FormClosing;
|
||||
Shown += LocateAudiobooks_Shown;
|
||||
((System.ComponentModel.ISupportInitialize)dataGridView1).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)foundAudiobookBindingSource).EndInit();
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.ListView foundAudiobooksLV;
|
||||
private System.Windows.Forms.ColumnHeader columnHeader1;
|
||||
private System.Windows.Forms.ColumnHeader columnHeader2;
|
||||
private System.Windows.Forms.Label booksFoundLbl;
|
||||
private System.Windows.Forms.DataGridView dataGridView1;
|
||||
private System.Windows.Forms.BindingSource foundAudiobookBindingSource;
|
||||
private AccessibleDataGridViewColumn iDDataGridViewTextBoxColumn;
|
||||
private AccessibleDataGridViewColumn fileNameDataGridViewTextBoxColumn;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +1,35 @@
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
public partial class LocateAudiobooksDialog : Form
|
||||
{
|
||||
private event EventHandler<FilePathCache.CacheEntry> FileFound;
|
||||
private readonly CancellationTokenSource tokenSource = new();
|
||||
private readonly List<string> foundAsins = new();
|
||||
private readonly string labelFormatText;
|
||||
private readonly LocatedAudiobooksViewModel _viewModel;
|
||||
public LocateAudiobooksDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
labelFormatText = booksFoundLbl.Text;
|
||||
setFoundBookCount(0);
|
||||
|
||||
this.SetLibationIcon();
|
||||
this.RestoreSizeAndLocation(Configuration.Instance);
|
||||
|
||||
Shown += LocateAudiobooks_Shown;
|
||||
FileFound += LocateAudiobooks_FileFound;
|
||||
FormClosing += LocateAudiobooks_FormClosing;
|
||||
_viewModel = new LocatedAudiobooksViewModel(new SortBindingList<FoundAudiobook>());
|
||||
dataGridView1.EnableHeadersVisualStyles = !Application.IsDarkModeEnabled;
|
||||
dataGridView1.RowsAdded += DataGridView1_RowsAdded;
|
||||
foundAudiobookBindingSource.DataSource = _viewModel.FoundFiles;
|
||||
booksFoundLbl.DataBindings.Add(new Binding(nameof(booksFoundLbl.Text), _viewModel, nameof(_viewModel.FoundAsinCount), true, DataSourceUpdateMode.OnPropertyChanged, 0, booksFoundLbl.Text));
|
||||
}
|
||||
|
||||
private void setFoundBookCount(int count)
|
||||
=> booksFoundLbl.Text = string.Format(labelFormatText, count);
|
||||
|
||||
private void LocateAudiobooks_FileFound(object sender, FilePathCache.CacheEntry e)
|
||||
private void DataGridView1_RowsAdded(object sender, DataGridViewRowsAddedEventArgs e)
|
||||
{
|
||||
foundAudiobooksLV.Items
|
||||
.Add(new ListViewItem(new string[] { $"[{e.Id}]", Path.GetFileName(e.Path) }))
|
||||
.EnsureVisible();
|
||||
|
||||
foundAudiobooksLV.AutoResizeColumn(1, ColumnHeaderAutoResizeStyle.ColumnContent);
|
||||
|
||||
if (!foundAsins.Any(asin => asin == e.Id))
|
||||
{
|
||||
foundAsins.Add(e.Id);
|
||||
setFoundBookCount(foundAsins.Count);
|
||||
}
|
||||
dataGridView1.FirstDisplayedScrollingRowIndex = e.RowIndex;
|
||||
}
|
||||
|
||||
private void LocateAudiobooks_FormClosing(object sender, FormClosingEventArgs e)
|
||||
@@ -65,34 +47,22 @@ namespace LibationWinForms.Dialogs
|
||||
InitialDirectory = Configuration.Instance.Books
|
||||
};
|
||||
|
||||
if (fbd.ShowDialog() != DialogResult.OK || !Directory.Exists(fbd.SelectedPath))
|
||||
var result = fbd.ShowDialog(this);
|
||||
if (result != DialogResult.OK || !Directory.Exists(fbd.SelectedPath))
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
DialogResult = result;
|
||||
}
|
||||
|
||||
await foreach (var book in AudioFileStorage.FindAudiobooksAsync(fbd.SelectedPath, tokenSource.Token))
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
FilePathCache.Insert(book);
|
||||
|
||||
var lb = DbContexts.GetLibraryBook_Flat_NoTracking(book.Id);
|
||||
if (lb.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Liberated)
|
||||
await lb.UpdateBookStatusAsync(LiberatedStatus.Liberated);
|
||||
|
||||
tokenSource.Token.ThrowIfCancellationRequested();
|
||||
this.Invoke(FileFound, this, book);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Error adding found audiobook file to Libation. {@audioFile}", book);
|
||||
}
|
||||
await _viewModel.FindAndAddBooksAsync(fbd.SelectedPath, tokenSource.Token);
|
||||
MessageBox.Show(this, $"Libation has found {_viewModel.FoundAsinCount} unique audiobooks and added them to its database. ", $"Found {_viewModel.FoundAsinCount} Audiobooks");
|
||||
}
|
||||
}
|
||||
|
||||
MessageBox.Show(this, $"Libation has found {foundAsins.Count} unique audiobooks and added them to its database. ", $"Found {foundAsins.Count} Audiobooks");
|
||||
Close();
|
||||
private void dataGridView1_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
|
||||
{
|
||||
if (e.RowIndex >= 0 && e.RowIndex < _viewModel.FoundFiles.Count)
|
||||
Go.To.File(_viewModel.FoundFiles[e.RowIndex].Entry.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace LibationWinForms.Dialogs
|
||||
this.retainAaxFileCbox.Text = desc(nameof(config.RetainAaxFile));
|
||||
this.combineNestedChapterTitlesCbox.Text = desc(nameof(config.CombineNestedChapterTitles));
|
||||
this.splitFilesByChapterCbox.Text = desc(nameof(config.SplitFilesByChapter));
|
||||
this.minFileDurationLbl.Text = desc(nameof(config.MinimumFileDuration));
|
||||
this.mergeOpeningEndCreditsCbox.Text = desc(nameof(config.MergeOpeningAndEndCredits));
|
||||
this.stripAudibleBrandingCbox.Text = desc(nameof(config.StripAudibleBrandAudio));
|
||||
this.stripUnabridgedCbox.Text = desc(nameof(config.StripUnabridged));
|
||||
@@ -40,6 +41,8 @@ namespace LibationWinForms.Dialogs
|
||||
toolTip.SetToolTip(requestSpatialCbox, Configuration.GetHelpText(nameof(config.RequestSpatial)));
|
||||
toolTip.SetToolTip(request_xHE_AAC_Cbox, Configuration.GetHelpText(nameof(config.Request_xHE_AAC)));
|
||||
toolTip.SetToolTip(spatialAudioCodecCb, Configuration.GetHelpText(nameof(config.SpatialAudioCodec)));
|
||||
toolTip.SetToolTip(minFileDurationLbl, Configuration.GetHelpText(nameof(config.SpatialAudioCodec)));
|
||||
toolTip.SetToolTip(minFileDurationNud, Configuration.GetHelpText(nameof(config.SpatialAudioCodec)));
|
||||
|
||||
fileDownloadQualityCb.Items.AddRange(
|
||||
[
|
||||
@@ -87,6 +90,7 @@ namespace LibationWinForms.Dialogs
|
||||
retainAaxFileCbox.Checked = config.RetainAaxFile;
|
||||
combineNestedChapterTitlesCbox.Checked = config.CombineNestedChapterTitles;
|
||||
splitFilesByChapterCbox.Checked = config.SplitFilesByChapter;
|
||||
minFileDurationNud.Value = config.MinimumFileDuration;
|
||||
mergeOpeningEndCreditsCbox.Checked = config.MergeOpeningAndEndCredits;
|
||||
stripUnabridgedCbox.Checked = config.StripUnabridged;
|
||||
stripAudibleBrandingCbox.Checked = config.StripAudibleBrandAudio;
|
||||
@@ -132,6 +136,7 @@ namespace LibationWinForms.Dialogs
|
||||
config.RetainAaxFile = retainAaxFileCbox.Checked;
|
||||
config.CombineNestedChapterTitles = combineNestedChapterTitlesCbox.Checked;
|
||||
config.SplitFilesByChapter = splitFilesByChapterCbox.Checked;
|
||||
config.MinimumFileDuration = (int)minFileDurationNud.Value;
|
||||
config.MergeOpeningAndEndCredits = mergeOpeningEndCreditsCbox.Checked;
|
||||
config.StripUnabridged = stripUnabridgedCbox.Checked;
|
||||
config.StripAudibleBrandAudio = stripAudibleBrandingCbox.Checked;
|
||||
@@ -167,7 +172,7 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
private void splitFilesByChapterCbox_CheckedChanged(object sender, EventArgs e)
|
||||
{
|
||||
chapterTitleTemplateGb.Enabled = splitFilesByChapterCbox.Checked;
|
||||
chapterTitleTemplateGb.Enabled = minFileDurationNud.Enabled = minFileDurationLbl.Enabled = splitFilesByChapterCbox.Checked;
|
||||
}
|
||||
|
||||
private void chapterTitleTemplateBtn_Click(object sender, EventArgs e)
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
clipsBookmarksFormatCb = new System.Windows.Forms.ComboBox();
|
||||
downloadClipsBookmarksCbox = new System.Windows.Forms.CheckBox();
|
||||
audiobookFixupsGb = new System.Windows.Forms.GroupBox();
|
||||
minFileDurationLbl = new System.Windows.Forms.Label();
|
||||
minFileDurationNud = new System.Windows.Forms.NumericUpDown();
|
||||
stripUnabridgedCbox = new System.Windows.Forms.CheckBox();
|
||||
chapterTitleTemplateGb = new System.Windows.Forms.GroupBox();
|
||||
chapterTitleTemplateBtn = new System.Windows.Forms.Button();
|
||||
@@ -154,6 +156,7 @@
|
||||
customFileNamingGb.SuspendLayout();
|
||||
tab4AudioFileOptions.SuspendLayout();
|
||||
audiobookFixupsGb.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)minFileDurationNud).BeginInit();
|
||||
chapterTitleTemplateGb.SuspendLayout();
|
||||
lameOptionsGb.SuspendLayout();
|
||||
lameBitrateGb.SuspendLayout();
|
||||
@@ -289,7 +292,7 @@
|
||||
// stripAudibleBrandingCbox
|
||||
//
|
||||
stripAudibleBrandingCbox.AutoSize = true;
|
||||
stripAudibleBrandingCbox.Location = new System.Drawing.Point(13, 70);
|
||||
stripAudibleBrandingCbox.Location = new System.Drawing.Point(13, 88);
|
||||
stripAudibleBrandingCbox.Name = "stripAudibleBrandingCbox";
|
||||
stripAudibleBrandingCbox.Size = new System.Drawing.Size(143, 34);
|
||||
stripAudibleBrandingCbox.TabIndex = 16;
|
||||
@@ -960,20 +963,41 @@
|
||||
//
|
||||
// audiobookFixupsGb
|
||||
//
|
||||
audiobookFixupsGb.Controls.Add(minFileDurationLbl);
|
||||
audiobookFixupsGb.Controls.Add(minFileDurationNud);
|
||||
audiobookFixupsGb.Controls.Add(splitFilesByChapterCbox);
|
||||
audiobookFixupsGb.Controls.Add(stripUnabridgedCbox);
|
||||
audiobookFixupsGb.Controls.Add(stripAudibleBrandingCbox);
|
||||
audiobookFixupsGb.Location = new System.Drawing.Point(6, 254);
|
||||
audiobookFixupsGb.Name = "audiobookFixupsGb";
|
||||
audiobookFixupsGb.Size = new System.Drawing.Size(416, 114);
|
||||
audiobookFixupsGb.Size = new System.Drawing.Size(416, 128);
|
||||
audiobookFixupsGb.TabIndex = 14;
|
||||
audiobookFixupsGb.TabStop = false;
|
||||
audiobookFixupsGb.Text = "Audiobook Fix-ups";
|
||||
//
|
||||
// minFileDurationLbl
|
||||
//
|
||||
minFileDurationLbl.AutoSize = true;
|
||||
minFileDurationLbl.Location = new System.Drawing.Point(34, 44);
|
||||
minFileDurationLbl.Margin = new System.Windows.Forms.Padding(0, 0, 2, 0);
|
||||
minFileDurationLbl.Name = "minFileDurationLbl";
|
||||
minFileDurationLbl.Size = new System.Drawing.Size(159, 15);
|
||||
minFileDurationLbl.TabIndex = 29;
|
||||
minFileDurationLbl.Text = "[MinimumFileDuration desc]";
|
||||
//
|
||||
// minFileDurationNud
|
||||
//
|
||||
minFileDurationNud.Location = new System.Drawing.Point(243, 41);
|
||||
minFileDurationNud.Maximum = new decimal(new int[] { 120, 0, 0, 0 });
|
||||
minFileDurationNud.Name = "minFileDurationNud";
|
||||
minFileDurationNud.Size = new System.Drawing.Size(51, 23);
|
||||
minFileDurationNud.TabIndex = 17;
|
||||
minFileDurationNud.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
|
||||
//
|
||||
// stripUnabridgedCbox
|
||||
//
|
||||
stripUnabridgedCbox.AutoSize = true;
|
||||
stripUnabridgedCbox.Location = new System.Drawing.Point(13, 46);
|
||||
stripUnabridgedCbox.Location = new System.Drawing.Point(13, 63);
|
||||
stripUnabridgedCbox.Name = "stripUnabridgedCbox";
|
||||
stripUnabridgedCbox.Size = new System.Drawing.Size(147, 19);
|
||||
stripUnabridgedCbox.TabIndex = 15;
|
||||
@@ -1498,6 +1522,7 @@
|
||||
tab4AudioFileOptions.PerformLayout();
|
||||
audiobookFixupsGb.ResumeLayout(false);
|
||||
audiobookFixupsGb.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)minFileDurationNud).EndInit();
|
||||
chapterTitleTemplateGb.ResumeLayout(false);
|
||||
chapterTitleTemplateGb.PerformLayout();
|
||||
lameOptionsGb.ResumeLayout(false);
|
||||
@@ -1627,5 +1652,7 @@
|
||||
private System.Windows.Forms.ComboBox themeCb;
|
||||
private System.Windows.Forms.Label themeLbl;
|
||||
private System.Windows.Forms.CheckBox importPlusTitlesCb;
|
||||
private System.Windows.Forms.Label minFileDurationLbl;
|
||||
private System.Windows.Forms.NumericUpDown minFileDurationNud;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
using Dinah.Core;
|
||||
using DocumentFormat.OpenXml.Drawing;
|
||||
using DocumentFormat.OpenXml.Office2013.Theme;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
|
||||
@@ -28,40 +28,35 @@
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
deletedCbl = new System.Windows.Forms.CheckedListBox();
|
||||
label1 = new System.Windows.Forms.Label();
|
||||
restoreBtn = new System.Windows.Forms.Button();
|
||||
permanentlyDeleteBtn = new System.Windows.Forms.Button();
|
||||
everythingCb = new System.Windows.Forms.CheckBox();
|
||||
deletedCheckedLbl = new System.Windows.Forms.Label();
|
||||
productsGrid1 = new LibationWinForms.GridView.ProductsGrid();
|
||||
label2 = new System.Windows.Forms.Label();
|
||||
textBox1 = new System.Windows.Forms.TextBox();
|
||||
button1 = new System.Windows.Forms.Button();
|
||||
audiblePlusCb = new System.Windows.Forms.CheckBox();
|
||||
plusBookcSheckedLbl = new System.Windows.Forms.Label();
|
||||
SuspendLayout();
|
||||
//
|
||||
// deletedCbl
|
||||
//
|
||||
deletedCbl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
deletedCbl.FormattingEnabled = true;
|
||||
deletedCbl.Location = new System.Drawing.Point(12, 27);
|
||||
deletedCbl.Name = "deletedCbl";
|
||||
deletedCbl.Size = new System.Drawing.Size(776, 364);
|
||||
deletedCbl.TabIndex = 3;
|
||||
deletedCbl.ItemCheck += deletedCbl_ItemCheck;
|
||||
//
|
||||
// label1
|
||||
//
|
||||
label1.AutoSize = true;
|
||||
label1.Location = new System.Drawing.Point(12, 9);
|
||||
label1.Name = "label1";
|
||||
label1.Size = new System.Drawing.Size(388, 15);
|
||||
label1.TabIndex = 4;
|
||||
label1.TabIndex = 0;
|
||||
label1.Text = "Check books you want to permanently delete from or restore to Libation";
|
||||
//
|
||||
// restoreBtn
|
||||
//
|
||||
restoreBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
restoreBtn.Location = new System.Drawing.Point(572, 398);
|
||||
restoreBtn.Location = new System.Drawing.Point(572, 450);
|
||||
restoreBtn.Name = "restoreBtn";
|
||||
restoreBtn.Size = new System.Drawing.Size(75, 40);
|
||||
restoreBtn.TabIndex = 5;
|
||||
restoreBtn.TabIndex = 6;
|
||||
restoreBtn.Text = "Restore";
|
||||
restoreBtn.UseVisualStyleBackColor = true;
|
||||
restoreBtn.Click += restoreBtn_Click;
|
||||
@@ -69,10 +64,10 @@
|
||||
// permanentlyDeleteBtn
|
||||
//
|
||||
permanentlyDeleteBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
permanentlyDeleteBtn.Location = new System.Drawing.Point(653, 398);
|
||||
permanentlyDeleteBtn.Location = new System.Drawing.Point(653, 450);
|
||||
permanentlyDeleteBtn.Name = "permanentlyDeleteBtn";
|
||||
permanentlyDeleteBtn.Size = new System.Drawing.Size(135, 40);
|
||||
permanentlyDeleteBtn.TabIndex = 5;
|
||||
permanentlyDeleteBtn.TabIndex = 7;
|
||||
permanentlyDeleteBtn.Text = "Permanently Remove\r\nfrom Libation";
|
||||
permanentlyDeleteBtn.UseVisualStyleBackColor = true;
|
||||
permanentlyDeleteBtn.Click += permanentlyDeleteBtn_Click;
|
||||
@@ -81,10 +76,11 @@
|
||||
//
|
||||
everythingCb.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;
|
||||
everythingCb.AutoSize = true;
|
||||
everythingCb.Location = new System.Drawing.Point(12, 410);
|
||||
everythingCb.Location = new System.Drawing.Point(12, 462);
|
||||
everythingCb.Margin = new System.Windows.Forms.Padding(10, 3, 3, 3);
|
||||
everythingCb.Name = "everythingCb";
|
||||
everythingCb.Size = new System.Drawing.Size(82, 19);
|
||||
everythingCb.TabIndex = 6;
|
||||
everythingCb.TabIndex = 4;
|
||||
everythingCb.Text = "Everything";
|
||||
everythingCb.ThreeState = true;
|
||||
everythingCb.UseVisualStyleBackColor = true;
|
||||
@@ -94,23 +90,93 @@
|
||||
//
|
||||
deletedCheckedLbl.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;
|
||||
deletedCheckedLbl.AutoSize = true;
|
||||
deletedCheckedLbl.Location = new System.Drawing.Point(126, 411);
|
||||
deletedCheckedLbl.Location = new System.Drawing.Point(100, 463);
|
||||
deletedCheckedLbl.Name = "deletedCheckedLbl";
|
||||
deletedCheckedLbl.Size = new System.Drawing.Size(104, 15);
|
||||
deletedCheckedLbl.TabIndex = 7;
|
||||
deletedCheckedLbl.TabIndex = 0;
|
||||
deletedCheckedLbl.Text = "Checked: {0} of {1}";
|
||||
//
|
||||
// productsGrid1
|
||||
//
|
||||
productsGrid1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
productsGrid1.AutoScroll = true;
|
||||
productsGrid1.DisableColumnCustomization = true;
|
||||
productsGrid1.DisableContextMenu = true;
|
||||
productsGrid1.Location = new System.Drawing.Point(12, 62);
|
||||
productsGrid1.Name = "productsGrid1";
|
||||
productsGrid1.SearchEngine = null;
|
||||
productsGrid1.Size = new System.Drawing.Size(776, 382);
|
||||
productsGrid1.TabIndex = 3;
|
||||
//
|
||||
// label2
|
||||
//
|
||||
label2.AutoSize = true;
|
||||
label2.Location = new System.Drawing.Point(12, 36);
|
||||
label2.Name = "label2";
|
||||
label2.Size = new System.Drawing.Size(123, 15);
|
||||
label2.TabIndex = 0;
|
||||
label2.Text = "Search Deleted Books:";
|
||||
//
|
||||
// textBox1
|
||||
//
|
||||
textBox1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
textBox1.Location = new System.Drawing.Point(141, 33);
|
||||
textBox1.Name = "textBox1";
|
||||
textBox1.Size = new System.Drawing.Size(574, 23);
|
||||
textBox1.TabIndex = 1;
|
||||
textBox1.KeyDown += textBox1_KeyDown;
|
||||
//
|
||||
// button1
|
||||
//
|
||||
button1.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
button1.Location = new System.Drawing.Point(721, 33);
|
||||
button1.Name = "button1";
|
||||
button1.Size = new System.Drawing.Size(67, 23);
|
||||
button1.TabIndex = 2;
|
||||
button1.Text = "Filter";
|
||||
button1.UseVisualStyleBackColor = true;
|
||||
button1.Click += searchBtn_Click;
|
||||
//
|
||||
// audiblePlusCb
|
||||
//
|
||||
audiblePlusCb.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;
|
||||
audiblePlusCb.AutoSize = true;
|
||||
audiblePlusCb.Location = new System.Drawing.Point(247, 462);
|
||||
audiblePlusCb.Margin = new System.Windows.Forms.Padding(10, 3, 3, 3);
|
||||
audiblePlusCb.Name = "audiblePlusCb";
|
||||
audiblePlusCb.Size = new System.Drawing.Size(127, 19);
|
||||
audiblePlusCb.TabIndex = 5;
|
||||
audiblePlusCb.Text = "Audible Plus Books";
|
||||
audiblePlusCb.ThreeState = true;
|
||||
audiblePlusCb.UseVisualStyleBackColor = true;
|
||||
audiblePlusCb.CheckStateChanged += audiblePlusCb_CheckStateChanged;
|
||||
//
|
||||
// plusBookcSheckedLbl
|
||||
//
|
||||
plusBookcSheckedLbl.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;
|
||||
plusBookcSheckedLbl.AutoSize = true;
|
||||
plusBookcSheckedLbl.Location = new System.Drawing.Point(380, 463);
|
||||
plusBookcSheckedLbl.Name = "plusBookcSheckedLbl";
|
||||
plusBookcSheckedLbl.Size = new System.Drawing.Size(104, 15);
|
||||
plusBookcSheckedLbl.TabIndex = 0;
|
||||
plusBookcSheckedLbl.Text = "Checked: {0} of {1}";
|
||||
//
|
||||
// TrashBinDialog
|
||||
//
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
|
||||
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
||||
ClientSize = new System.Drawing.Size(800, 450);
|
||||
ClientSize = new System.Drawing.Size(800, 502);
|
||||
Controls.Add(plusBookcSheckedLbl);
|
||||
Controls.Add(button1);
|
||||
Controls.Add(textBox1);
|
||||
Controls.Add(label2);
|
||||
Controls.Add(productsGrid1);
|
||||
Controls.Add(deletedCheckedLbl);
|
||||
Controls.Add(audiblePlusCb);
|
||||
Controls.Add(everythingCb);
|
||||
Controls.Add(permanentlyDeleteBtn);
|
||||
Controls.Add(restoreBtn);
|
||||
Controls.Add(label1);
|
||||
Controls.Add(deletedCbl);
|
||||
Name = "TrashBinDialog";
|
||||
Text = "Trash Bin";
|
||||
ResumeLayout(false);
|
||||
@@ -118,12 +184,16 @@
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.CheckedListBox deletedCbl;
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.Button restoreBtn;
|
||||
private System.Windows.Forms.Button permanentlyDeleteBtn;
|
||||
private System.Windows.Forms.CheckBox everythingCb;
|
||||
private System.Windows.Forms.Label deletedCheckedLbl;
|
||||
private GridView.ProductsGrid productsGrid1;
|
||||
private System.Windows.Forms.Label label2;
|
||||
private System.Windows.Forms.TextBox textBox1;
|
||||
private System.Windows.Forms.Button button1;
|
||||
private System.Windows.Forms.CheckBox audiblePlusCb;
|
||||
private System.Windows.Forms.Label plusBookcSheckedLbl;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using System.Collections;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
public partial class TrashBinDialog : Form
|
||||
{
|
||||
private readonly string deletedCheckedTemplate;
|
||||
private string lastGoodFilter = "";
|
||||
private TempSearchEngine SearchEngine { get; } = new TempSearchEngine();
|
||||
public TrashBinDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -21,29 +24,67 @@ namespace LibationWinForms.Dialogs
|
||||
this.RestoreSizeAndLocation(Configuration.Instance);
|
||||
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
|
||||
|
||||
deletedCheckedTemplate = deletedCheckedLbl.Text;
|
||||
|
||||
var deletedBooks = DbContexts.GetDeletedLibraryBooks();
|
||||
foreach (var lb in deletedBooks)
|
||||
deletedCbl.Items.Add(lb);
|
||||
|
||||
setLabel();
|
||||
deletedCheckedLbl.Text = "";
|
||||
plusBookcSheckedLbl.Text = "";
|
||||
productsGrid1.SearchEngine = SearchEngine;
|
||||
productsGrid1.RemovableCountChanged += (_, _) => UpdateCounts();
|
||||
productsGrid1.VisibleCountChanged += (_, _) => UpdateCounts();
|
||||
Load += TrashBinDialog_Load;
|
||||
}
|
||||
|
||||
private void deletedCbl_ItemCheck(object sender, ItemCheckEventArgs e)
|
||||
private IEnumerable<LibraryBook> GetCheckedBooks() => productsGrid1.GetVisibleGridEntries().Where(i => i.Remove is true).Select(i => i.LibraryBook);
|
||||
|
||||
private async void TrashBinDialog_Load(object? sender, EventArgs e)
|
||||
{
|
||||
// CheckedItems.Count is not updated until after the event fires
|
||||
setLabel(e.NewValue);
|
||||
productsGrid1.RemoveColumnVisible = true;
|
||||
await InitAsync();
|
||||
}
|
||||
|
||||
private void UpdateCounts()
|
||||
{
|
||||
var visible = productsGrid1.GetVisibleGridEntries().ToArray();
|
||||
var plusVisibleCount = visible.Count(e => e.LibraryBook.IsAudiblePlus);
|
||||
|
||||
var checkedCount = visible.Count(e => e.Remove is true);
|
||||
var plusCheckedCount = visible.Count(e => e.LibraryBook.IsAudiblePlus && e.Remove is true);
|
||||
|
||||
deletedCheckedLbl.Text = $"Checked: {checkedCount} of {visible.Length}";
|
||||
plusBookcSheckedLbl.Text = $"Checked: {plusCheckedCount} of {plusVisibleCount}";
|
||||
|
||||
everythingCb.CheckStateChanged -= everythingCb_CheckStateChanged;
|
||||
everythingCb.CheckState = checkedCount == 0 || visible.Length == 0 ? CheckState.Unchecked
|
||||
: checkedCount == visible.Length ? CheckState.Checked
|
||||
: CheckState.Indeterminate;
|
||||
everythingCb.CheckStateChanged += everythingCb_CheckStateChanged;
|
||||
|
||||
audiblePlusCb.CheckStateChanged -= audiblePlusCb_CheckStateChanged;
|
||||
audiblePlusCb.CheckState = plusCheckedCount == 0 || plusVisibleCount == 0 ? CheckState.Unchecked
|
||||
: plusCheckedCount == plusVisibleCount ? CheckState.Checked
|
||||
: CheckState.Indeterminate;
|
||||
audiblePlusCb.CheckStateChanged += audiblePlusCb_CheckStateChanged;
|
||||
}
|
||||
|
||||
private async Task InitAsync()
|
||||
{
|
||||
var deletedBooks = DbContexts.GetDeletedLibraryBooks();
|
||||
SearchEngine.ReindexSearchEngine(deletedBooks);
|
||||
await productsGrid1.BindToGridAsync(deletedBooks);
|
||||
}
|
||||
|
||||
private void Reload()
|
||||
{
|
||||
var deletedBooks = DbContexts.GetDeletedLibraryBooks();
|
||||
SearchEngine.ReindexSearchEngine(deletedBooks);
|
||||
productsGrid1.UpdateGrid(deletedBooks);
|
||||
}
|
||||
|
||||
private async void permanentlyDeleteBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
setControlsEnabled(false);
|
||||
|
||||
var removed = deletedCbl.CheckedItems.Cast<LibraryBook>().ToList();
|
||||
|
||||
removeFromCheckList(removed);
|
||||
await removed.PermanentlyDeleteBooksAsync();
|
||||
var qtyChanges = await GetCheckedBooks().PermanentlyDeleteBooksAsync();
|
||||
if (qtyChanges > 0)
|
||||
Reload();
|
||||
|
||||
setControlsEnabled(true);
|
||||
}
|
||||
@@ -52,65 +93,70 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
setControlsEnabled(false);
|
||||
|
||||
var removed = deletedCbl.CheckedItems.Cast<LibraryBook>().ToList();
|
||||
|
||||
removeFromCheckList(removed);
|
||||
await removed.RestoreBooksAsync();
|
||||
var qtyChanges = await GetCheckedBooks().RestoreBooksAsync();
|
||||
if (qtyChanges > 0)
|
||||
Reload();
|
||||
|
||||
setControlsEnabled(true);
|
||||
}
|
||||
|
||||
private void removeFromCheckList(IEnumerable objects)
|
||||
{
|
||||
foreach (var o in objects)
|
||||
deletedCbl.Items.Remove(o);
|
||||
|
||||
deletedCbl.Refresh();
|
||||
setLabel();
|
||||
}
|
||||
|
||||
private void setControlsEnabled(bool enabled)
|
||||
=> restoreBtn.Enabled = permanentlyDeleteBtn.Enabled = deletedCbl.Enabled = everythingCb.Enabled = enabled;
|
||||
=> Invoke(() => productsGrid1.Enabled = restoreBtn.Enabled = permanentlyDeleteBtn.Enabled = everythingCb.Enabled = enabled);
|
||||
|
||||
private void everythingCb_CheckStateChanged(object sender, EventArgs e)
|
||||
private void textBox1_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (everythingCb.CheckState is CheckState.Indeterminate)
|
||||
{
|
||||
everythingCb.CheckState = CheckState.Unchecked;
|
||||
return;
|
||||
}
|
||||
|
||||
deletedCbl.ItemCheck -= deletedCbl_ItemCheck;
|
||||
|
||||
for (var i = 0; i < deletedCbl.Items.Count; i++)
|
||||
deletedCbl.SetItemChecked(i, everythingCb.CheckState is CheckState.Checked);
|
||||
|
||||
setLabel();
|
||||
|
||||
deletedCbl.ItemCheck += deletedCbl_ItemCheck;
|
||||
if (e.KeyCode == Keys.Enter)
|
||||
searchBtn_Click(sender, e);
|
||||
}
|
||||
|
||||
|
||||
private void setLabel(CheckState? checkedState = null)
|
||||
private void searchBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
var pre = deletedCbl.CheckedItems.Count;
|
||||
int count = checkedState switch
|
||||
try
|
||||
{
|
||||
CheckState.Checked => pre + 1,
|
||||
CheckState.Unchecked => pre - 1,
|
||||
_ => pre,
|
||||
};
|
||||
productsGrid1.Filter(textBox1.Text);
|
||||
lastGoodFilter = textBox1.Text;
|
||||
}
|
||||
catch
|
||||
{
|
||||
productsGrid1.Filter(lastGoodFilter);
|
||||
}
|
||||
}
|
||||
|
||||
everythingCb.CheckStateChanged -= everythingCb_CheckStateChanged;
|
||||
private void audiblePlusCb_CheckStateChanged(object? sender, EventArgs e)
|
||||
{
|
||||
switch (audiblePlusCb.CheckState)
|
||||
{
|
||||
case CheckState.Checked:
|
||||
SetVisibleChecked(e => e.IsAudiblePlus, isChecked: true);
|
||||
break;
|
||||
case CheckState.Unchecked:
|
||||
SetVisibleChecked(e => e.IsAudiblePlus, isChecked: false);
|
||||
break;
|
||||
default:
|
||||
audiblePlusCb.CheckState = CheckState.Unchecked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
private void everythingCb_CheckStateChanged(object? sender, EventArgs e)
|
||||
{
|
||||
switch (everythingCb.CheckState)
|
||||
{
|
||||
case CheckState.Checked:
|
||||
SetVisibleChecked(_ => true, isChecked: true);
|
||||
break;
|
||||
case CheckState.Unchecked:
|
||||
SetVisibleChecked(_ => true, isChecked: false);
|
||||
break;
|
||||
default:
|
||||
everythingCb.CheckState = CheckState.Unchecked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
everythingCb.CheckState
|
||||
= count > 0 && count == deletedCbl.Items.Count ? CheckState.Checked
|
||||
: count == 0 ? CheckState.Unchecked
|
||||
: CheckState.Indeterminate;
|
||||
|
||||
everythingCb.CheckStateChanged += everythingCb_CheckStateChanged;
|
||||
|
||||
deletedCheckedLbl.Text = string.Format(deletedCheckedTemplate, count, deletedCbl.Items.Count);
|
||||
public void SetVisibleChecked(Func<LibraryBook, bool> predicate, bool isChecked)
|
||||
{
|
||||
productsGrid1.GetVisibleGridEntries().Where(e => predicate(e.LibraryBook)).ForEach(i => i.Remove = isChecked);
|
||||
UpdateCounts();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,64 @@
|
||||
<root>
|
||||
<?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">
|
||||
|
||||
@@ -24,21 +24,6 @@ namespace LibationWinForms.Dialogs
|
||||
releaseNotesTbox.Text = upgradeProperties.Notes;
|
||||
|
||||
Shown += (_, _) => yesBtn.Focus();
|
||||
Load += UpgradeNotificationDialog_Load;
|
||||
}
|
||||
|
||||
private void UpgradeNotificationDialog_Load(object sender, EventArgs e)
|
||||
{
|
||||
//This dialog starts before Form1, soposition it at the center of where Form1 will be.
|
||||
var savedState = Configuration.Instance.GetNonString<FormSizeAndPosition>(defaultValue: null, nameof(Form1));
|
||||
|
||||
if (savedState is null) return;
|
||||
|
||||
int x = savedState.X + (savedState.Width - Width) / 2;
|
||||
int y = savedState.Y + (savedState.Height - Height) / 2;
|
||||
|
||||
Location = new(x, y);
|
||||
TopMost = true;
|
||||
}
|
||||
|
||||
private void PackageDlLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
|
||||
|
||||
@@ -11,14 +11,12 @@ namespace LibationWinForms
|
||||
{
|
||||
setProgressVisible(false);
|
||||
#pragma warning disable CS8321 // Local function is declared but never used
|
||||
Task upgradeAvailable(UpgradeEventArgs e)
|
||||
async Task upgradeAvailable(UpgradeEventArgs e)
|
||||
{
|
||||
var notificationResult = new UpgradeNotificationDialog(e.UpgradeProperties).ShowDialog(this);
|
||||
var notificationResult = await new UpgradeNotificationDialog(e.UpgradeProperties).ShowDialogAsync(this);
|
||||
|
||||
e.Ignore = notificationResult == DialogResult.Ignore;
|
||||
e.InstallUpgrade = notificationResult == DialogResult.Yes;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
#pragma warning restore CS8321 // Local function is declared but never used
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
/*
|
||||
@@ -43,8 +44,10 @@ namespace LibationWinForms.GridView
|
||||
.OfType<LibraryBookEntry>()
|
||||
.Union(Items.OfType<LibraryBookEntry>());
|
||||
|
||||
public ISearchEngine? SearchEngine { get; set; }
|
||||
|
||||
public bool SupportsFiltering => true;
|
||||
public string Filter
|
||||
public string? Filter
|
||||
{
|
||||
get => FilterString;
|
||||
set
|
||||
@@ -54,7 +57,8 @@ namespace LibationWinForms.GridView
|
||||
if (Items.Count + FilterRemoved.Count == 0)
|
||||
return;
|
||||
|
||||
FilteredInGridEntries = AllItems().FilterEntries(FilterString);
|
||||
var searchResultSet = SearchEngine?.GetSearchResultSet(FilterString);
|
||||
FilteredInGridEntries = AllItems().FilterEntries(searchResultSet);
|
||||
refreshEntries();
|
||||
}
|
||||
}
|
||||
@@ -63,16 +67,16 @@ namespace LibationWinForms.GridView
|
||||
protected override bool SupportsSortingCore => true;
|
||||
protected override bool SupportsSearchingCore => true;
|
||||
protected override bool IsSortedCore => isSorted;
|
||||
protected override PropertyDescriptor SortPropertyCore => propertyDescriptor;
|
||||
protected override PropertyDescriptor? SortPropertyCore => propertyDescriptor;
|
||||
protected override ListSortDirection SortDirectionCore => Comparer.SortOrder;
|
||||
|
||||
/// <summary> Items that were removed from the base list due to filtering </summary>
|
||||
private readonly List<GridEntry> FilterRemoved = new();
|
||||
private string FilterString;
|
||||
private string? FilterString;
|
||||
private bool isSorted;
|
||||
private PropertyDescriptor propertyDescriptor;
|
||||
private PropertyDescriptor? propertyDescriptor;
|
||||
/// <summary> All GridEntries present in the current filter set. If null, no filter is applied and all entries are filtered in.(This was a performance choice)</summary>
|
||||
private HashSet<GridEntry> FilteredInGridEntries;
|
||||
private HashSet<GridEntry>? FilteredInGridEntries;
|
||||
|
||||
#region Unused - Advanced Filtering
|
||||
public bool SupportsAdvancedSorting => false;
|
||||
@@ -128,7 +132,7 @@ namespace LibationWinForms.GridView
|
||||
//(except for episodes that are collapsed)
|
||||
foreach (var addBack in addBackEntries)
|
||||
{
|
||||
if (addBack is LibraryBookEntry lbe && lbe.Parent is SeriesEntry se && !se.Liberate.Expanded)
|
||||
if (addBack is LibraryBookEntry lbe && lbe.Parent is SeriesEntry se && se.Liberate?.Expanded is not true)
|
||||
continue;
|
||||
|
||||
FilterRemoved.Remove(addBack);
|
||||
@@ -137,9 +141,10 @@ namespace LibationWinForms.GridView
|
||||
}
|
||||
}
|
||||
|
||||
private void SearchEngineCommands_SearchEngineUpdated(object sender, EventArgs e)
|
||||
private void SearchEngineCommands_SearchEngineUpdated(object? sender, EventArgs e)
|
||||
{
|
||||
var filterResults = AllItems().FilterEntries(FilterString);
|
||||
var searchResultSet = SearchEngine?.GetSearchResultSet(FilterString);
|
||||
var filterResults = AllItems().FilterEntries(searchResultSet);
|
||||
|
||||
if (FilteredInGridEntries.SearchSetsDiffer(filterResults))
|
||||
{
|
||||
@@ -168,7 +173,7 @@ namespace LibationWinForms.GridView
|
||||
base.Remove(episode);
|
||||
}
|
||||
|
||||
sEntry.Liberate.Expanded = false;
|
||||
sEntry.Liberate?.Expanded = false;
|
||||
}
|
||||
|
||||
public void ExpandItem(SeriesEntry sEntry)
|
||||
@@ -183,7 +188,7 @@ namespace LibationWinForms.GridView
|
||||
InsertItem(++sindex, episode);
|
||||
}
|
||||
}
|
||||
sEntry.Liberate.Expanded = true;
|
||||
sEntry.Liberate?.Expanded = true;
|
||||
}
|
||||
|
||||
public void RemoveFilter()
|
||||
@@ -216,7 +221,7 @@ namespace LibationWinForms.GridView
|
||||
itemsList.AddRange(sortedItems);
|
||||
}
|
||||
|
||||
private void GridEntryBindingList_ListChanged(object sender, ListChangedEventArgs e)
|
||||
private void GridEntryBindingList_ListChanged(object? sender, ListChangedEventArgs e)
|
||||
{
|
||||
if (e.ListChangedType == ListChangedType.ItemChanged && IsSortedCore && e.PropertyDescriptor == SortPropertyCore)
|
||||
refreshEntries();
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace LibationWinForms.GridView
|
||||
public ProductsDisplay()
|
||||
{
|
||||
InitializeComponent();
|
||||
productsGrid.SearchEngine = MainSearchEngine.Instance;
|
||||
}
|
||||
|
||||
#region Button controls
|
||||
@@ -262,7 +263,17 @@ namespace LibationWinForms.GridView
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region Remove Audible Plus Books from Audible Library
|
||||
|
||||
if (entries.Length != 1 || ctx.RemoveFromAudibleEnabled)
|
||||
{
|
||||
ctxMenu.Items.Add(new ToolStripSeparator());
|
||||
var removeFromAudibleMenuItem = new ToolStripMenuItem() { Text = ctx.RemoveFromAudibleText, Enabled = ctx.RemoveFromAudibleEnabled };
|
||||
removeFromAudibleMenuItem.Click += async (_, _) => await ctx.RemoveFromAudibleAsync();
|
||||
ctxMenu.Items.Add(removeFromAudibleMenuItem);
|
||||
}
|
||||
|
||||
#endregion
|
||||
if (entries.Length > 1)
|
||||
return;
|
||||
|
||||
@@ -422,7 +433,7 @@ namespace LibationWinForms.GridView
|
||||
|
||||
#endregion
|
||||
|
||||
internal List<LibraryBook> GetVisible() => productsGrid.GetVisibleBooks().ToList();
|
||||
internal List<LibraryBook> GetVisible() => productsGrid.GetVisibleBookEntries().ToList();
|
||||
|
||||
private void productsGrid_VisibleCountChanged(object sender, int count)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using DataLayer;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using Dinah.Core.WindowsDesktop.Forms;
|
||||
@@ -6,6 +7,7 @@ using LibationFileManager;
|
||||
using LibationUiBase.GridView;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
@@ -22,6 +24,23 @@ namespace LibationWinForms.GridView
|
||||
|
||||
public partial class ProductsGrid : UserControl
|
||||
{
|
||||
[DefaultValue(false)]
|
||||
[Category("Behavior")]
|
||||
[Description("Disable the grid context menu")]
|
||||
public bool DisableContextMenu { get; set; }
|
||||
[DefaultValue(false)]
|
||||
[Category("Behavior")]
|
||||
[Description("Disable grid column reordering and don't persist width changes")]
|
||||
public bool DisableColumnCustomization
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
gridEntryDataGridView.AllowUserToOrderColumns = !value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Number of visible rows has changed</summary>
|
||||
public event EventHandler<int>? VisibleCountChanged;
|
||||
public event LibraryBookEntryClickedEventHandler? LiberateClicked;
|
||||
@@ -33,13 +52,17 @@ namespace LibationWinForms.GridView
|
||||
public event ProductsGridCellContextMenuStripNeededEventHandler? LiberateContextMenuStripNeeded;
|
||||
|
||||
private GridEntryBindingList? bindingList;
|
||||
internal IEnumerable<LibraryBook> GetVisibleBooks()
|
||||
=> bindingList
|
||||
?.GetFilteredInItems()
|
||||
.Select(lbe => lbe.LibraryBook) ?? Enumerable.Empty<LibraryBook>();
|
||||
internal IEnumerable<LibraryBook> GetVisibleBookEntries()
|
||||
=> GetVisibleGridEntries().Select(lbe => lbe.LibraryBook);
|
||||
|
||||
public IEnumerable<LibraryBookEntry> GetVisibleGridEntries()
|
||||
=> bindingList?.GetFilteredInItems().OfType<LibraryBookEntry>() ?? [];
|
||||
|
||||
internal IEnumerable<LibraryBookEntry> GetAllBookEntries()
|
||||
=> bindingList?.AllItems().BookEntries() ?? Enumerable.Empty<LibraryBookEntry>();
|
||||
|
||||
public ISearchEngine? SearchEngine { get => field; set { field = value; bindingList?.SearchEngine = value; } }
|
||||
|
||||
public ProductsGrid()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -47,19 +70,7 @@ namespace LibationWinForms.GridView
|
||||
gridEntryDataGridView.Scroll += (_, s) => Scroll?.Invoke(this, s);
|
||||
gridEntryDataGridView.CellContextMenuStripNeeded += GridEntryDataGridView_CellContextMenuStripNeeded;
|
||||
removeGVColumn.Frozen = false;
|
||||
|
||||
defaultFont = gridEntryDataGridView.DefaultCellStyle.Font ?? gridEntryDataGridView.Font;
|
||||
setGridFontScale(Configuration.Instance.GridFontScaleFactor);
|
||||
setGridScale(Configuration.Instance.GridScaleFactor);
|
||||
Configuration.Instance.PropertyChanged += Configuration_ScaleChanged;
|
||||
Configuration.Instance.PropertyChanged += Configuration_FontScaleChanged;
|
||||
gridEntryDataGridView.EnableHeadersVisualStyles = !Application.IsDarkModeEnabled;
|
||||
|
||||
gridEntryDataGridView.Disposed += (_, _) =>
|
||||
{
|
||||
Configuration.Instance.PropertyChanged -= Configuration_ScaleChanged;
|
||||
Configuration.Instance.PropertyChanged -= Configuration_FontScaleChanged;
|
||||
};
|
||||
defaultFont = gridEntryDataGridView.DefaultCellStyle.Font ?? gridEntryDataGridView.Font;
|
||||
}
|
||||
|
||||
#region Scaling
|
||||
@@ -120,7 +131,7 @@ namespace LibationWinForms.GridView
|
||||
private void GridEntryDataGridView_CellContextMenuStripNeeded(object? sender, DataGridViewCellContextMenuStripNeededEventArgs e)
|
||||
{
|
||||
// header
|
||||
if (e.RowIndex < 0 || sender is not DataGridView dgv)
|
||||
if (DisableContextMenu || e.RowIndex < 0 || sender is not DataGridView dgv)
|
||||
return;
|
||||
|
||||
e.ContextMenuStrip = new ContextMenuStrip();
|
||||
@@ -313,7 +324,7 @@ namespace LibationWinForms.GridView
|
||||
}
|
||||
System.Threading.SynchronizationContext.SetSynchronizationContext(null);
|
||||
|
||||
bindingList = new GridEntryBindingList(geList);
|
||||
bindingList = new GridEntryBindingList(geList) { SearchEngine = SearchEngine };
|
||||
bindingList.CollapseAll();
|
||||
|
||||
//The syncBindingSource ensures that the IGridEntry list is added on the UI thread
|
||||
@@ -381,7 +392,8 @@ namespace LibationWinForms.GridView
|
||||
|
||||
RemoveBooks(removedBooks);
|
||||
|
||||
gridEntryDataGridView.FirstDisplayedScrollingRowIndex = topRow;
|
||||
if (topRow >= 0 && topRow < gridEntryDataGridView.RowCount)
|
||||
gridEntryDataGridView.FirstDisplayedScrollingRowIndex = topRow;
|
||||
}
|
||||
|
||||
public void RemoveBooks(IEnumerable<LibraryBookEntry> removedBooks)
|
||||
@@ -505,8 +517,21 @@ namespace LibationWinForms.GridView
|
||||
|
||||
private void ProductsGrid_Load(object sender, EventArgs e)
|
||||
{
|
||||
//https://stackoverflow.com/a/4498512/3335599
|
||||
if (System.ComponentModel.LicenseManager.UsageMode == System.ComponentModel.LicenseUsageMode.Designtime) return;
|
||||
//DesignMode is not set in constructor
|
||||
if (DesignMode)
|
||||
return;
|
||||
|
||||
setGridFontScale(Configuration.Instance.GridFontScaleFactor);
|
||||
setGridScale(Configuration.Instance.GridScaleFactor);
|
||||
Configuration.Instance.PropertyChanged += Configuration_ScaleChanged;
|
||||
Configuration.Instance.PropertyChanged += Configuration_FontScaleChanged;
|
||||
gridEntryDataGridView.EnableHeadersVisualStyles = !Application.IsDarkModeEnabled;
|
||||
|
||||
gridEntryDataGridView.Disposed += (_, _) =>
|
||||
{
|
||||
Configuration.Instance.PropertyChanged -= Configuration_ScaleChanged;
|
||||
Configuration.Instance.PropertyChanged -= Configuration_FontScaleChanged;
|
||||
};
|
||||
|
||||
gridEntryDataGridView.ColumnWidthChanged += gridEntryDataGridView_ColumnWidthChanged;
|
||||
gridEntryDataGridView.ColumnDisplayIndexChanged += gridEntryDataGridView_ColumnDisplayIndexChanged;
|
||||
@@ -523,6 +548,8 @@ namespace LibationWinForms.GridView
|
||||
|
||||
foreach (DataGridViewColumn column in gridEntryDataGridView.Columns)
|
||||
{
|
||||
if (column == removeGVColumn)
|
||||
continue;
|
||||
var itemName = column.DataPropertyName;
|
||||
var visible = config.GetColumnVisibility(itemName);
|
||||
|
||||
@@ -596,6 +623,7 @@ namespace LibationWinForms.GridView
|
||||
|
||||
private void gridEntryDataGridView_ColumnDisplayIndexChanged(object? sender, DataGridViewColumnEventArgs e)
|
||||
{
|
||||
if (DisableColumnCustomization) return;
|
||||
var config = Configuration.Instance;
|
||||
|
||||
var dictionary = config.GridColumnsDisplayIndices;
|
||||
@@ -613,6 +641,7 @@ namespace LibationWinForms.GridView
|
||||
|
||||
private void gridEntryDataGridView_ColumnWidthChanged(object? sender, DataGridViewColumnEventArgs e)
|
||||
{
|
||||
if (DisableColumnCustomization) return;
|
||||
var config = Configuration.Instance;
|
||||
|
||||
var dictionary = config.GridColumnsWidths;
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="10.0.0.1" />
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3595.46" />
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3650.58" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -69,6 +69,16 @@ Thanks to [mhdi](https://aur.archlinux.org/account/mhdi) for taking care of AUR
|
||||
|
||||
Thanks to [TomaSajt](https://github.com/tomasajt) for taking care of Nix package maintenance.
|
||||
|
||||
### Pacstall
|
||||
|
||||
Pacstall is the AUR Ubuntu wishes it had. It takes the concept of the AUR and puts a spin on it, making it easier to install programs without scouring github repos and the likes. See the [Pacstall](https://pacstall.dev/) project for more information.
|
||||
|
||||
```bash
|
||||
pacstall -I libation-deb
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
If your desktop uses gtk, you should now see Libation among your applications.
|
||||
|
||||
Additionally, you may launch Libation, LibationCli, and Hangover (the Libation recovery app) via the command line using 'libation, libationcli', and 'hangover' aliases respectively.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "9.0.10",
|
||||
"version": "10.0.1",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
],
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -2027,9 +2027,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.28.0",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.0.tgz",
|
||||
"integrity": "sha512-rytDAoiXr3+t6OIP3WGlDd0ouCUG1iCWzkcY3++Nreuoi17y6T5i/zRhe6uYfoVcxq6YU+sBtJouuRDsq8vvqA==",
|
||||
"version": "10.28.2",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz",
|
||||
"integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
|
||||
Reference in New Issue
Block a user