Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86efe631fe | ||
|
|
f5f1dc483b | ||
|
|
8aa4328c6c | ||
|
|
4b2387b621 | ||
|
|
74d16d8ef9 | ||
|
|
b1ea8f9fa7 | ||
|
|
c666fdeaff | ||
|
|
7068782975 | ||
|
|
c4cebbebe7 | ||
|
|
53d43d9fa9 | ||
|
|
11d59beeed | ||
|
|
ef71e297f4 | ||
|
|
1e4d1d1973 | ||
|
|
893d99854b | ||
|
|
db93980cd5 | ||
|
|
34fac30b2b | ||
|
|
2fa0bcb765 | ||
|
|
78fd09aa91 | ||
|
|
a54516b4f5 | ||
|
|
f193d6f376 | ||
|
|
8a82c294a1 | ||
|
|
9392cf4bf0 | ||
|
|
ec4deb9099 | ||
|
|
cf0548aab9 | ||
|
|
064801380b | ||
|
|
9dc2a7424a | ||
|
|
4c8a56a5b9 | ||
|
|
9aad263996 | ||
|
|
ce1ab7c20d | ||
|
|
c9217990cd | ||
|
|
90cbf3b7a6 | ||
|
|
c4f1b22ddf | ||
|
|
fb612ea6ab | ||
|
|
bce44b6f6d | ||
|
|
7575736991 | ||
|
|
06f8d055fc | ||
|
|
d64e043fe8 | ||
|
|
99564d9c25 | ||
|
|
29bccd3e33 | ||
|
|
20f65f6534 | ||
|
|
8ca72b2e2d | ||
|
|
75429f288f | ||
|
|
d1bb921346 | ||
|
|
b979b6ddad | ||
|
|
4eba41ddbb | ||
|
|
418f5062ff | ||
|
|
f736f7f909 | ||
|
|
96ead28246 | ||
|
|
34bad7a53d | ||
|
|
7ac1fff3a0 | ||
|
|
a4c5c53df3 | ||
|
|
87db5cfd94 | ||
|
|
85e7bbf366 | ||
|
|
c55c5fac23 | ||
|
|
e25e2f7211 | ||
|
|
f310d583d8 | ||
|
|
f05465b29b | ||
|
|
959e31972e | ||
|
|
17181811f0 | ||
|
|
6d2624d52b | ||
|
|
9dd5940c8c | ||
|
|
1927d19961 | ||
|
|
09cc838bb4 | ||
|
|
8af4c71101 | ||
|
|
7ffdf45164 | ||
|
|
e0999dc9ae | ||
|
|
a0f3d44e97 | ||
|
|
1510a86579 | ||
|
|
b3581455d2 | ||
|
|
8ee1019fa5 | ||
|
|
285b10a95f | ||
|
|
0ca33f864b | ||
|
|
a0823fa26c | ||
|
|
aa9040da5d | ||
|
|
222031ecc5 | ||
|
|
dda8f5a974 | ||
|
|
e9b484df04 | ||
|
|
d505264e86 | ||
|
|
c0b1f1dc0a | ||
|
|
1524d558a4 | ||
|
|
aea8c11dc4 | ||
|
|
86c7f89788 | ||
|
|
3272541e81 | ||
|
|
3b3d40e4e6 | ||
|
|
a47866b6f7 | ||
|
|
0df4dfdef5 | ||
|
|
fe2de6ecf7 | ||
|
|
fc25e73b1a | ||
|
|
a3df85c87e |
44
.github/workflows/dotnet-build.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
# This workflow will build a .NET project
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
|
||||
|
||||
name: build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
DOTNET_CONFIGURATION: 'Release'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os: [Linux, MacOS, Windows]
|
||||
ui: [Avalonia]
|
||||
release_name: [chardonnay]
|
||||
include:
|
||||
- os: Windows
|
||||
ui: WinForms
|
||||
release_name: classic
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '7.x'
|
||||
env:
|
||||
NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build
|
||||
working-directory: ./Source
|
||||
run: |
|
||||
dotnet publish -c ${{ env.DOTNET_CONFIGURATION }} -o bin\Publish\${{ matrix.os }}-${{ matrix.release_name }} Libation${{ matrix.ui }}\Libation${{ matrix.ui }}.csproj -p:PublishProfile=Libation${{ matrix.ui }}\Properties\PublishProfiles\${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish -c ${{ env.DOTNET_CONFIGURATION }} -o bin\Publish\${{ matrix.os }}-${{ matrix.release_name }} LoadByOS\${{ matrix.os }}ConfigApp\${{ matrix.os }}ConfigApp.csproj -p:PublishProfile=LoadByOS\Properties\${{ matrix.os }}ConfigApp\PublishProfiles\${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish -c ${{ env.DOTNET_CONFIGURATION }} -o bin\Publish\${{ matrix.os }}-${{ matrix.release_name }} LibationCli\LibationCli.csproj -p:PublishProfile=LibationCli\Properties\PublishProfiles\${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish -c ${{ env.DOTNET_CONFIGURATION }} -o bin\Publish\${{ matrix.os }}-${{ matrix.release_name }} Hangover${{ matrix.ui }}\Hangover${{ matrix.ui }}.csproj -p:PublishProfile=Hangover${{ matrix.ui }}\Properties\PublishProfiles\${{ matrix.os }}Profile.pubxml
|
||||
- name: Publish artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}-${{ matrix.release_name }}
|
||||
path: ./Source/bin/Publish/${{ matrix.os }}-${{ matrix.release_name }}/*
|
||||
if-no-files-found: error
|
||||
66
.github/workflows/dotnet-release.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
# This workflow will build a .NET project
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
env:
|
||||
DOTNET_VERSION: '7' # The .NET SDK version to use
|
||||
DOTNET_SOURCE: './Source'
|
||||
DOTNET_CONFIGURATION: 'Release'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./.github/workflows/dotnet-build.yml
|
||||
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Calculate version
|
||||
id: version
|
||||
run: |
|
||||
export TAG=${{ github.ref_name }}
|
||||
echo "version=${TAG#v}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Rename artifacts
|
||||
id: rename
|
||||
working-directory: ./artifacts
|
||||
run: |
|
||||
for FILENAME in *; do mv ${FILENAME} Libation.${{ steps.version.outputs.version }}-${FILENAME,,}; done
|
||||
mv Libation.${{ steps.version.outputs.version }}-windows-classic Classic-Libation.${{ steps.version.outputs.version }}-windows-classic
|
||||
|
||||
- name: Zip assets
|
||||
working-directory: ./artifacts
|
||||
run: |
|
||||
for FILENAME in *; do zip -r ${FILENAME}.zip ${FILENAME}; done
|
||||
mkdir ./assets
|
||||
mv *.zip ./assets
|
||||
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Libation ${{ steps.version.outputs.version }}
|
||||
body: <Put a body here>
|
||||
draft: true
|
||||
prerelease: false
|
||||
|
||||
- name: Upload release assets
|
||||
uses: dwenegar/upload-release-assets@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
release_id: ${{ steps.create_release.outputs.id }}
|
||||
assets_path: ./artifacts/assets
|
||||
19
.github/workflows/dotnet-validate.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# This workflow will build a .NET project
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
|
||||
|
||||
name: validate
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
env:
|
||||
DOTNET_VERSION: '7' # The .NET SDK version to use
|
||||
DOTNET_SLN: './Source/Libation.sln'
|
||||
DOTNET_CONFIGURATION: 'Release'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./.github/workflows/dotnet-build.yml
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"WindowsClassic": "Libation\\.\\d+\\.\\d+\\.\\d+-win-classic\\.zip",
|
||||
"WindowsAvalonia":"Libation\\.\\d+\\.\\d+\\.\\d+-win-chardonnay\\.zip",
|
||||
"WindowsClassic": "Libation\\.\\d+\\.\\d+\\.\\d+-windows-classic\\.zip",
|
||||
"WindowsAvalonia":"Libation\\.\\d+\\.\\d+\\.\\d+-windows-chardonnay\\.zip",
|
||||
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay",
|
||||
"MacOSAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-macos-chardonnay"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
# Advanced: Table of Contents
|
||||
|
||||
- [Files and folders](#files-and-folders)
|
||||
- [Linux and Mac (unofficial)](#linux-and-mac)
|
||||
- [Settings](#settings)
|
||||
- [Custom File Naming](#custom-file-naming)
|
||||
- [Command Line Interface](#command-line-interface)
|
||||
@@ -25,10 +24,6 @@ To make upgrades and reinstalls easier, Libation separates all of its responsibi
|
||||
|
||||
* The last important folder is the "books location." This is where Libation looks for your downloaded and decrypted books. This is how it knows which books still need to be downloaded. The Audible id must be somewhere in the book's file or folder name for Libation to detect your downloaded book.
|
||||
|
||||
### Linux and Mac
|
||||
|
||||
Although Libation only currently officially supports Windows, some users have had success with WINE. ([Linux](https://github.com/rmcrackan/Libation/issues/28#issuecomment-890594158), [OSX Crossover and WINE](https://github.com/rmcrackan/Libation/issues/150#issuecomment-1004918592), [Linux and WINE](https://github.com/rmcrackan/Libation/issues/28#issuecomment-1161111014))
|
||||
|
||||
### Settings
|
||||
|
||||
* Allow Libation to fix up audiobook metadata. After decrypting a title, Libation attempts to fix details like chapters and cover art. Some power users and/or control freaks prefer to manage this themselves. By unchecking this setting, Libation will only decrypt the book and will leave metadata as-is, warts and all.
|
||||
@@ -76,4 +71,15 @@ export library to file
|
||||
libationcli export -p "C:\foo\bar\my.csv" -c
|
||||
libationcli export -p "C:\foo\bar\my.xlsx" --xlsx
|
||||
libationcli export -p "C:\foo\bar\my.xlsx" -x
|
||||
|
||||
Set download statuses throughout library based on whether each book's audio file can be found.
|
||||
Must include at least one flag: --downloaded , --not-downloaded.
|
||||
Downloaded: If the audio file can be found, set download status to 'Downloaded'.
|
||||
Not Downloaded: If the audio file cannot be found, set download status to 'Not Downloaded'
|
||||
UI: Visible Books \> Set 'Downloaded' status automatically. Visible books. Prompts before saving changes
|
||||
CLI: Full library. No prompt
|
||||
|
||||
libationcli set-status -d
|
||||
libationcli set-status -n
|
||||
libationcli set-status -d -n
|
||||
```
|
||||
|
||||
@@ -22,7 +22,12 @@
|
||||
|
||||
### Installation
|
||||
|
||||
To install Libation, extract the zip file to a folder, for example `C:\Libation`, and then run Libation.exe from that folder to begin the configuration process and configure your account(s).
|
||||
* Windows
|
||||
|
||||
Extract the zip file to a folder and then run `Libation.exe` from inside of that folder. Do not put it in Program Files. The inability to edit files from there causes problems with configuration and updating.
|
||||
|
||||
* [Ubuntu Linux (beta)](InstallOnLinux.md)
|
||||
* [MacOS (beta)](InstallOnMac.md)
|
||||
|
||||
### Create Accounts
|
||||
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
# Run Libation on Ubuntu
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
|
||||
# Run Libation on Ubuntu (Beta)
|
||||
This walkthrough should get you up and running with Libation on your Ubuntu machine.
|
||||
|
||||
Some limitations of the linux release are:
|
||||
40
Documentation/InstallOnMac.md
Normal file
@@ -0,0 +1,40 @@
|
||||
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PalPal.me](https://paypal.me/mcrackan?locale.x=en_us)
|
||||
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
|
||||
|
||||
|
||||
|
||||
# Run Libation on MacOS (Beta)
|
||||
This walkthrough should get you up and running with Libation on your Mac.
|
||||
|
||||
## Install Libation
|
||||
|
||||
- Download latest MacOS zip to downloads folder
|
||||
- Extract and rename folder to Libation
|
||||
- in terminal type cd and then drag your folder of libation to terminal so it looks like `cd/users/YourName/Downloads/Libation`
|
||||
- Type following commands
|
||||
|
||||
```console
|
||||
chmod +x ./Libation
|
||||
sudo spctl --add --label "Libation" ./Libation
|
||||
./Libation
|
||||
```
|
||||
|
||||
## Trouble with Gatekeeper?
|
||||
|
||||
If Gatekeeper is giving you trouble with Libation:
|
||||
|
||||
Disable the block
|
||||
|
||||
`sudo spctl --master-disable`
|
||||
|
||||
Launch Libation and login, etc. and allow the rules to update then re-enable the block.
|
||||
|
||||
`sudo spctl --master-enable`
|
||||
|
||||
Once Gatekeeper reenabled, you can open Libation again without it being blocked.
|
||||
|
||||
Thanks [joseph-holland](https://github.com/rmcrackan/Libation/issues/327#issuecomment-1268993349)!
|
||||
|
||||
Report bugs to https://github.com/rmcrackan/Libation/issues
|
||||
@@ -14,7 +14,6 @@
|
||||
- [Getting started](Documentation/GettingStarted.md)
|
||||
- [Download Libation](Documentation/GettingStarted.md#download-libation-1)
|
||||
- [Installation](Documentation/GettingStarted.md#installation)
|
||||
- [Installation on Ubuntu](Source/LibationAvalonia/README.md)
|
||||
- [Create Accounts](Documentation/GettingStarted.md#create-accounts)
|
||||
- [Import your library](Documentation/GettingStarted.md#import-your-library)
|
||||
- [Download your books -- DRM-free!](Documentation/GettingStarted.md#download-your-books----drm-free)
|
||||
@@ -28,7 +27,6 @@
|
||||
- [Filters](Documentation/SearchingAndFiltering.md#filters)
|
||||
- [Advanced](Documentation/Advanced.md)
|
||||
- [Files and folders](Documentation/Advanced.md#files-and-folders)
|
||||
- [Linux and Mac (unofficial)](Documentation/Advanced.md#linux-and-mac)
|
||||
- [Settings](Documentation/Advanced.md#settings)
|
||||
- [Custom File Naming](Documentation/Advanced.md#custom-file-naming)
|
||||
- [Command Line Interface](Documentation/Advanced.md#command-line-interface)
|
||||
@@ -55,7 +53,7 @@
|
||||
|
||||
### The bad
|
||||
|
||||
* Windows only
|
||||
* Only fully supported in Windows. (Mac and Linux are in beta)
|
||||
* Large file size
|
||||
* Made by a programmer, not a designer so the goals are function rather than beauty. And it shows
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
@@ -13,7 +13,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean.Codecs" Version="0.2.12" />
|
||||
<PackageReference Include="AAXClean.Codecs" Version="0.2.14" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Version>8.3.4.1</Version>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Version>8.6.4.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="1.0.0" />
|
||||
<PackageReference Include="Octokit" Version="4.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.Logging;
|
||||
using LibationFileManager;
|
||||
@@ -23,19 +24,16 @@ namespace AppScaffolding
|
||||
MacOSAvalonia
|
||||
}
|
||||
|
||||
// I know I'm taking the wine metaphor a bit far by naming this "Variety", but I don't know what else to call it
|
||||
public enum VarietyType { None, Classic, Chardonnay }
|
||||
|
||||
public static class LibationScaffolding
|
||||
{
|
||||
public static readonly bool IsWindows;
|
||||
public static readonly bool IsLinux;
|
||||
public static readonly bool IsMacOs;
|
||||
static LibationScaffolding()
|
||||
{
|
||||
IsWindows = OperatingSystem.IsWindows();
|
||||
IsLinux = OperatingSystem.IsLinux();
|
||||
IsMacOs = OperatingSystem.IsMacOS();
|
||||
}
|
||||
|
||||
public static ReleaseIdentifier ReleaseIdentifier { get; private set; }
|
||||
public static VarietyType Variety
|
||||
=> ReleaseIdentifier == ReleaseIdentifier.WindowsClassic ? VarietyType.Classic
|
||||
: ReleaseIdentifier.In(ReleaseIdentifier.WindowsAvalonia, ReleaseIdentifier.LinuxAvalonia, ReleaseIdentifier.MacOSAvalonia) ? VarietyType.Chardonnay
|
||||
: VarietyType.None;
|
||||
|
||||
public static void SetReleaseIdentifier(ReleaseIdentifier releaseID)
|
||||
=> ReleaseIdentifier = releaseID;
|
||||
@@ -57,8 +55,8 @@ namespace AppScaffolding
|
||||
??= new[] { ExecutingAssembly.GetName(), EntryAssembly.GetName() }
|
||||
.Max(a => a.Version);
|
||||
|
||||
/// <summary>Run migrations before loading Configuration for the first time. Then load and return Configuration</summary>
|
||||
public static Configuration RunPreConfigMigrations()
|
||||
/// <summary>Run migrations before loading Configuration for the first time. Then load and return Configuration</summary>
|
||||
public static Configuration RunPreConfigMigrations()
|
||||
{
|
||||
// must occur before access to Configuration instance
|
||||
// // outdated. kept here as an example of what belongs in this area
|
||||
@@ -90,10 +88,13 @@ namespace AppScaffolding
|
||||
{
|
||||
config.InProgress ??= Configuration.WinTemp;
|
||||
|
||||
if (!config.Exists(nameof(config.BetaOptIn)))
|
||||
config.BetaOptIn = false;
|
||||
if (!config.Exists(nameof(config.UseCoverAsFolderIcon)))
|
||||
config.UseCoverAsFolderIcon = false;
|
||||
|
||||
if (!config.Exists(nameof(config.AllowLibationFixup)))
|
||||
if (!config.Exists(nameof(config.BetaOptIn)))
|
||||
config.BetaOptIn = false;
|
||||
|
||||
if (!config.Exists(nameof(config.AllowLibationFixup)))
|
||||
config.AllowLibationFixup = true;
|
||||
|
||||
if (!config.Exists(nameof(config.CreateCueSheet)))
|
||||
@@ -185,6 +186,8 @@ namespace AppScaffolding
|
||||
configureLogging(config);
|
||||
logStartupState(config);
|
||||
|
||||
// all else should occur after logging
|
||||
|
||||
wireUpSystemEvents(config);
|
||||
}
|
||||
|
||||
@@ -242,8 +245,8 @@ namespace AppScaffolding
|
||||
// However, empirical testing so far has shown no issues.
|
||||
Console.SetOut(new MultiTextWriter(origOut, new SerilogTextWriter()));
|
||||
|
||||
#region Console => Serilog tests
|
||||
/*
|
||||
#region Console => Serilog tests
|
||||
/*
|
||||
// all below apply to "Console." and "Console.Out."
|
||||
|
||||
// captured
|
||||
@@ -282,12 +285,12 @@ namespace AppScaffolding
|
||||
Console.Write("{0}{1}{2}", "zero|", "one|", "two");
|
||||
Console.Write("{0}", new object[] { "arr" });
|
||||
*/
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
// .Here() captures debug info via System.Runtime.CompilerServices attributes. Warning: expensive
|
||||
//var withLineNumbers_outputTemplate = "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}in method {MemberName} at {FilePath}:{LineNumber}{NewLine}{Exception}{NewLine}";
|
||||
//Log.Logger.Here().Debug("Begin Libation. Debug with line numbers");
|
||||
}
|
||||
// .Here() captures debug info via System.Runtime.CompilerServices attributes. Warning: expensive
|
||||
//var withLineNumbers_outputTemplate = "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}in method {MemberName} at {FilePath}:{LineNumber}{NewLine}{Exception}{NewLine}";
|
||||
//Log.Logger.Here().Debug("Begin Libation. Debug with line numbers");
|
||||
}
|
||||
|
||||
private static void logStartupState(Configuration config)
|
||||
{
|
||||
@@ -299,21 +302,16 @@ namespace AppScaffolding
|
||||
if (System.Diagnostics.Debugger.IsAttached)
|
||||
mode += " (Debugger attached)";
|
||||
|
||||
string OS
|
||||
= IsLinux ? "Linux"
|
||||
: IsMacOs ? "MacOS"
|
||||
: IsWindows ? "Windows"
|
||||
: "UNKNOWN_OS";
|
||||
|
||||
// begin logging session with a form feed
|
||||
Log.Logger.Information("\r\n\f");
|
||||
Log.Logger.Information("Begin. {@DebugInfo}", new
|
||||
{
|
||||
AppName = EntryAssembly.GetName().Name,
|
||||
Version = BuildVersion.ToString(),
|
||||
ReleaseIdentifier = ReleaseIdentifier,
|
||||
OS = OS,
|
||||
Mode = mode,
|
||||
ReleaseIdentifier,
|
||||
Configuration.OS,
|
||||
InteropFactory.InteropFunctionsType,
|
||||
Mode = mode,
|
||||
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
|
||||
LogLevel_Debug_Enabled = Log.Logger.IsDebugEnabled(),
|
||||
LogLevel_Information_Enabled = Log.Logger.IsInformationEnabled(),
|
||||
@@ -321,8 +319,9 @@ namespace AppScaffolding
|
||||
LogLevel_Error_Enabled = Log.Logger.IsErrorEnabled(),
|
||||
LogLevel_Fatal_Enabled = Log.Logger.IsFatalEnabled(),
|
||||
|
||||
config.BetaOptIn,
|
||||
config.LibationFiles,
|
||||
config.BetaOptIn,
|
||||
config.UseCoverAsFolderIcon,
|
||||
config.LibationFiles,
|
||||
AudibleFileStorage.BooksDirectory,
|
||||
|
||||
config.InProgress,
|
||||
@@ -333,9 +332,12 @@ namespace AppScaffolding
|
||||
AudibleFileStorage.DecryptInProgressDirectory,
|
||||
DecryptInProgressFiles = FileManager.FileUtility.SaferEnumerateFiles(AudibleFileStorage.DecryptInProgressDirectory).Count(),
|
||||
});
|
||||
}
|
||||
|
||||
private static void wireUpSystemEvents(Configuration configuration)
|
||||
if (InteropFactory.InteropFunctionsType is null)
|
||||
Serilog.Log.Logger.Warning("WARNING: OSInteropProxy.InteropFunctionsType is null");
|
||||
}
|
||||
|
||||
private static void wireUpSystemEvents(Configuration configuration)
|
||||
{
|
||||
LibraryCommands.LibrarySizeChanged += (_, __) => SearchEngineCommands.FullReIndex();
|
||||
LibraryCommands.BookUserDefinedItemCommitted += (_, books) => SearchEngineCommands.UpdateBooks(books);
|
||||
|
||||
29
Source/AppScaffolding/OSConfigBase.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
|
||||
namespace AppScaffolding
|
||||
{
|
||||
public abstract class OSConfigBase
|
||||
{
|
||||
public abstract Type InteropFunctionsType { get; }
|
||||
public virtual Type[] ReferencedTypes { get; } = new Type[0];
|
||||
|
||||
public void Run()
|
||||
{
|
||||
//Each of these types belongs to a different windows-only assembly that's needed by
|
||||
//the WinInterop methods. By referencing these types in main we force the runtime to
|
||||
//load their assemblies before execution reaches inside main. This allows the calling
|
||||
//process to find these assemblies in its module list.
|
||||
_ = ReferencedTypes;
|
||||
_ = InteropFunctionsType;
|
||||
|
||||
//Wait for the calling process to be ready to read the WriteLine()
|
||||
Console.ReadLine();
|
||||
|
||||
// Signal the calling process that execution has reached inside main, and that all referenced assemblies have been loaded.
|
||||
Console.WriteLine();
|
||||
|
||||
// Wait for the calling process to finish reading the process module list, then exit.
|
||||
Console.ReadLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ namespace AppScaffolding
|
||||
: value;
|
||||
|
||||
#region appsettings.json
|
||||
private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName), "appsettings.json");
|
||||
private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), "appsettings.json");
|
||||
|
||||
public static bool APPSETTINGS_Json_Exists => File.Exists(APPSETTINGS_JSON);
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="28.0.1" />
|
||||
<PackageReference Include="NPOI" Version="2.5.6" />
|
||||
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
||||
<PackageReference Include="NPOI" Version="2.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
78
Source/ApplicationServices/BulkSetDownloadStatus.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
|
||||
namespace ApplicationServices
|
||||
{
|
||||
public class BulkSetDownloadStatus
|
||||
{
|
||||
private List<(string message, LiberatedStatus newStatus, IEnumerable<Book> Books)> actionSets { get; } = new();
|
||||
|
||||
public int Count => actionSets.Count;
|
||||
|
||||
public IEnumerable<string> Messages => actionSets.Select(a => a.message);
|
||||
public string AggregateMessage => $"Are you sure you want to set {Messages.Aggregate((a, b) => $"{a} and {b}")}?";
|
||||
|
||||
private List<LibraryBook> _libraryBooks;
|
||||
private bool _setDownloaded;
|
||||
private bool _setNotDownloaded;
|
||||
|
||||
public BulkSetDownloadStatus(List<LibraryBook> libraryBooks, bool setDownloaded, bool setNotDownloaded)
|
||||
{
|
||||
_libraryBooks = libraryBooks;
|
||||
_setDownloaded = setDownloaded;
|
||||
_setNotDownloaded = setNotDownloaded;
|
||||
}
|
||||
|
||||
public int Discover()
|
||||
{
|
||||
var bookExistsList = _libraryBooks
|
||||
.Select(libraryBook => new
|
||||
{
|
||||
libraryBook.Book,
|
||||
FileExists = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId) is not null
|
||||
})
|
||||
.ToList();
|
||||
|
||||
if (_setDownloaded)
|
||||
{
|
||||
var books2change = bookExistsList
|
||||
.Where(a => a.FileExists && a.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated)
|
||||
.Select(a => a.Book)
|
||||
.ToList();
|
||||
|
||||
if (books2change.Any())
|
||||
actionSets.Add((
|
||||
$"{"book".PluralizeWithCount(books2change.Count)} to 'Downloaded'",
|
||||
LiberatedStatus.Liberated,
|
||||
books2change));
|
||||
}
|
||||
|
||||
if (_setNotDownloaded)
|
||||
{
|
||||
var books2change = bookExistsList
|
||||
.Where(a => !a.FileExists && a.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated)
|
||||
.Select(a => a.Book)
|
||||
.ToList();
|
||||
|
||||
if (books2change.Any())
|
||||
actionSets.Add((
|
||||
$"{"book".PluralizeWithCount(books2change.Count)} to 'Not Downloaded'",
|
||||
LiberatedStatus.NotLiberated,
|
||||
books2change));
|
||||
}
|
||||
|
||||
return Count;
|
||||
}
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
foreach (var a in actionSets)
|
||||
a.Books.UpdateBookStatus(a.newStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ using Dinah.Core;
|
||||
using DtoImporterService;
|
||||
using LibationFileManager;
|
||||
using Serilog;
|
||||
using static System.Reflection.Metadata.BlobBuilder;
|
||||
using static DtoImporterService.PerfLogger;
|
||||
|
||||
namespace ApplicationServices
|
||||
@@ -366,74 +367,101 @@ namespace ApplicationServices
|
||||
public static event EventHandler<IEnumerable<Book>> BookUserDefinedItemCommitted;
|
||||
|
||||
#region Update book details
|
||||
public static int UpdateBookStatus(this Book book, LiberatedStatus bookStatus)
|
||||
{
|
||||
book.UserDefinedItem.BookStatus = bookStatus;
|
||||
return UpdateUserDefinedItem(book);
|
||||
}
|
||||
public static int UpdatePdfStatus(this Book book, LiberatedStatus pdfStatus)
|
||||
{
|
||||
book.UserDefinedItem.PdfStatus = pdfStatus;
|
||||
return UpdateUserDefinedItem(book);
|
||||
}
|
||||
public static int UpdateBook(
|
||||
public static int UpdateUserDefinedItem(
|
||||
this Book book,
|
||||
string tags = null,
|
||||
LiberatedStatus? bookStatus = null,
|
||||
LiberatedStatus? pdfStatus = null)
|
||||
=> UpdateBooks(tags, bookStatus, pdfStatus, book);
|
||||
public static int UpdateBooks(
|
||||
=> new[] { book }.UpdateUserDefinedItem(tags, bookStatus, pdfStatus);
|
||||
|
||||
public static int UpdateUserDefinedItem(
|
||||
this IEnumerable<Book> books,
|
||||
string tags = null,
|
||||
LiberatedStatus? bookStatus = null,
|
||||
LiberatedStatus? pdfStatus = null,
|
||||
params Book[] books)
|
||||
LiberatedStatus? pdfStatus = null)
|
||||
=> updateUserDefinedItem(
|
||||
books,
|
||||
udi => {
|
||||
// blank tags are expected. null tags are not
|
||||
if (tags is not null && udi.Tags != tags)
|
||||
udi.Tags = tags;
|
||||
|
||||
if (bookStatus is not null && udi.BookStatus != bookStatus.Value)
|
||||
udi.BookStatus = bookStatus.Value;
|
||||
|
||||
// even though PdfStatus is nullable, there's no case where we'd actually overwrite with null
|
||||
if (pdfStatus is not null && udi.PdfStatus != pdfStatus.Value)
|
||||
udi.PdfStatus = pdfStatus.Value;
|
||||
});
|
||||
|
||||
public static int UpdateBookStatus(this Book book, LiberatedStatus bookStatus)
|
||||
=> book.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
|
||||
public static int UpdateBookStatus(this IEnumerable<Book> books, LiberatedStatus bookStatus)
|
||||
=> books.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
|
||||
public static int UpdateBookStatus(this LibraryBook libraryBook, LiberatedStatus bookStatus)
|
||||
=> libraryBook.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
|
||||
public static int UpdateBookStatus(this IEnumerable<LibraryBook> libraryBooks, LiberatedStatus bookStatus)
|
||||
=> libraryBooks.UpdateUserDefinedItem(udi => udi.BookStatus = bookStatus);
|
||||
|
||||
public static int UpdatePdfStatus(this Book book, LiberatedStatus pdfStatus)
|
||||
=> book.UpdateUserDefinedItem(udi => udi.PdfStatus = pdfStatus);
|
||||
public static int UpdatePdfStatus(this IEnumerable<Book> books, LiberatedStatus pdfStatus)
|
||||
=> books.UpdateUserDefinedItem(udi => udi.PdfStatus = pdfStatus);
|
||||
public static int UpdatePdfStatus(this LibraryBook libraryBook, LiberatedStatus pdfStatus)
|
||||
=> libraryBook.UpdateUserDefinedItem(udi => udi.PdfStatus = pdfStatus);
|
||||
public static int UpdatePdfStatus(this IEnumerable<LibraryBook> libraryBooks, LiberatedStatus pdfStatus)
|
||||
=> libraryBooks.UpdateUserDefinedItem(udi => udi.PdfStatus = pdfStatus);
|
||||
|
||||
public static int UpdateTags(this Book book, string tags)
|
||||
=> book.UpdateUserDefinedItem(udi => udi.Tags = tags);
|
||||
public static int UpdateTags(this IEnumerable<Book> books, string tags)
|
||||
=> books.UpdateUserDefinedItem(udi => udi.Tags = tags);
|
||||
public static int UpdateTags(this LibraryBook libraryBook, string tags)
|
||||
=> libraryBook.UpdateUserDefinedItem(udi => udi.Tags = tags);
|
||||
public static int UpdateTags(this IEnumerable<LibraryBook> libraryBooks, string tags)
|
||||
=> libraryBooks.UpdateUserDefinedItem(udi => udi.Tags = tags);
|
||||
|
||||
public static int UpdateUserDefinedItem(this LibraryBook libraryBook, Action<UserDefinedItem> action)
|
||||
=> libraryBook.Book.updateUserDefinedItem(action);
|
||||
public static int UpdateUserDefinedItem(this IEnumerable<LibraryBook> libraryBooks, Action<UserDefinedItem> action)
|
||||
=> libraryBooks.Select(lb => lb.Book).updateUserDefinedItem(action);
|
||||
|
||||
public static int UpdateUserDefinedItem(this Book book, Action<UserDefinedItem> action) => book.updateUserDefinedItem(action);
|
||||
public static int UpdateUserDefinedItem(this IEnumerable<Book> books, Action<UserDefinedItem> action) => books.updateUserDefinedItem(action);
|
||||
|
||||
private static int updateUserDefinedItem(this Book book, Action<UserDefinedItem> action) => new[] { book }.updateUserDefinedItem(action);
|
||||
private static int updateUserDefinedItem(this IEnumerable<Book> books, Action<UserDefinedItem> action)
|
||||
{
|
||||
foreach (var book in books)
|
||||
{
|
||||
// blank tags are expected. null tags are not
|
||||
if (tags is not null && book.UserDefinedItem.Tags != tags)
|
||||
book.UserDefinedItem.Tags = tags;
|
||||
try
|
||||
{
|
||||
if (books is null || !books.Any())
|
||||
return 0;
|
||||
|
||||
if (bookStatus is not null && book.UserDefinedItem.BookStatus != bookStatus.Value)
|
||||
book.UserDefinedItem.BookStatus = bookStatus.Value;
|
||||
|
||||
// even though PdfStatus is nullable, there's no case where we'd actually overwrite with null
|
||||
if (pdfStatus is not null && book.UserDefinedItem.PdfStatus != pdfStatus.Value)
|
||||
book.UserDefinedItem.PdfStatus = pdfStatus.Value;
|
||||
}
|
||||
|
||||
return UpdateUserDefinedItem(books);
|
||||
}
|
||||
public static int UpdateUserDefinedItem(params Book[] books) => UpdateUserDefinedItem(books.ToList());
|
||||
public static int UpdateUserDefinedItem(IEnumerable<Book> books)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (books is null || !books.Any())
|
||||
return 0;
|
||||
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
// Attach() NoTracking entities before SaveChanges()
|
||||
foreach (var book in books)
|
||||
context.Attach(book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
action?.Invoke(book.UserDefinedItem);
|
||||
|
||||
var qtyChanges = context.SaveChanges();
|
||||
if (qtyChanges > 0)
|
||||
BookUserDefinedItemCommitted?.Invoke(null, books);
|
||||
using var context = DbContexts.GetContext();
|
||||
|
||||
return qtyChanges;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, $"Error updating {nameof(Book.UserDefinedItem)}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
// Attach() NoTracking entities before SaveChanges()
|
||||
foreach (var book in books)
|
||||
context.Attach(book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
|
||||
|
||||
// must be here instead of in db layer due to AaxcExists
|
||||
public static LiberatedStatus Liberated_Status(Book book)
|
||||
var qtyChanges = context.SaveChanges();
|
||||
if (qtyChanges > 0)
|
||||
BookUserDefinedItemCommitted?.Invoke(null, books);
|
||||
|
||||
return qtyChanges;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Error(ex, $"Error updating {nameof(Book.UserDefinedItem)}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
// must be here instead of in db layer due to AaxcExists
|
||||
public static LiberatedStatus Liberated_Status(Book book)
|
||||
=> book.Audio_Exists() ? book.UserDefinedItem.BookStatus
|
||||
: AudibleFileStorage.AaxcExists(book.AudibleProductId) ? LiberatedStatus.PartialDownload
|
||||
: LiberatedStatus.NotLiberated;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="4.6.0.1" />
|
||||
<PackageReference Include="AudibleApi" Version="7.0.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -10,14 +10,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="4.4.1.1" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="5.0.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.7">
|
||||
<PackageReference Include="Dinah.Core" Version="7.0.0.2" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="7.0.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.7">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -84,7 +84,8 @@ namespace DtoImporterService
|
||||
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
|
||||
var authors = item
|
||||
.Authors
|
||||
.Select(a => contributorImporter.Cache[a.Name])
|
||||
.DistinctBy(a => a.Name)
|
||||
.Select(a => contributorImporter.Cache[a.Name])
|
||||
.ToList();
|
||||
|
||||
var narrators
|
||||
@@ -94,7 +95,8 @@ namespace DtoImporterService
|
||||
// nested logic is required so order of names is retained. else, contributors may appear in the order they were inserted into the db
|
||||
: item
|
||||
.Narrators
|
||||
.Select(n => contributorImporter.Cache[n.Name])
|
||||
.DistinctBy(a => a.Name)
|
||||
.Select(n => contributorImporter.Cache[n.Name])
|
||||
.ToList();
|
||||
|
||||
// categories are laid out for a breadcrumb. category is 1st, subcategory is 2nd
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
||||
@@ -14,185 +14,188 @@ using LibationFileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public class DownloadDecryptBook : AudioDecodable
|
||||
{
|
||||
public override string Name => "Download & Decrypt";
|
||||
private AudiobookDownloadBase abDownloader;
|
||||
public class DownloadDecryptBook : AudioDecodable
|
||||
{
|
||||
public override string Name => "Download & Decrypt";
|
||||
private AudiobookDownloadBase abDownloader;
|
||||
|
||||
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists();
|
||||
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists();
|
||||
|
||||
public override Task CancelAsync() => abDownloader?.CancelAsync() ?? Task.CompletedTask;
|
||||
public override Task CancelAsync() => abDownloader?.CancelAsync() ?? Task.CompletedTask;
|
||||
|
||||
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var entries = new List<FilePathCache.CacheEntry>();
|
||||
// these only work so minimally b/c CacheEntry is a record.
|
||||
// in case of parallel decrypts, only capture the ones for this book id.
|
||||
// if user somehow starts multiple decrypts of the same book in parallel: on their own head be it
|
||||
void FilePathCache_Inserted(object sender, FilePathCache.CacheEntry e)
|
||||
{
|
||||
if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId))
|
||||
entries.Add(e);
|
||||
}
|
||||
void FilePathCache_Removed(object sender, FilePathCache.CacheEntry e)
|
||||
{
|
||||
if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId))
|
||||
entries.Remove(e);
|
||||
}
|
||||
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var entries = new List<FilePathCache.CacheEntry>();
|
||||
// these only work so minimally b/c CacheEntry is a record.
|
||||
// in case of parallel decrypts, only capture the ones for this book id.
|
||||
// if user somehow starts multiple decrypts of the same book in parallel: on their own head be it
|
||||
void FilePathCache_Inserted(object sender, FilePathCache.CacheEntry e)
|
||||
{
|
||||
if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId))
|
||||
entries.Add(e);
|
||||
}
|
||||
void FilePathCache_Removed(object sender, FilePathCache.CacheEntry e)
|
||||
{
|
||||
if (e.Id.EqualsInsensitive(libraryBook.Book.AudibleProductId))
|
||||
entries.Remove(e);
|
||||
}
|
||||
|
||||
OnBegin(libraryBook);
|
||||
OnBegin(libraryBook);
|
||||
|
||||
try
|
||||
{
|
||||
if (libraryBook.Book.Audio_Exists())
|
||||
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
||||
try
|
||||
{
|
||||
if (libraryBook.Book.Audio_Exists())
|
||||
return new StatusHandler { "Cannot find decrypt. Final audio file already exists" };
|
||||
|
||||
bool success = false;
|
||||
try
|
||||
{
|
||||
FilePathCache.Inserted += FilePathCache_Inserted;
|
||||
FilePathCache.Removed += FilePathCache_Removed;
|
||||
bool success = false;
|
||||
try
|
||||
{
|
||||
FilePathCache.Inserted += FilePathCache_Inserted;
|
||||
FilePathCache.Removed += FilePathCache_Removed;
|
||||
|
||||
success = await downloadAudiobookAsync(libraryBook);
|
||||
}
|
||||
finally
|
||||
{
|
||||
FilePathCache.Inserted -= FilePathCache_Inserted;
|
||||
FilePathCache.Removed -= FilePathCache_Removed;
|
||||
}
|
||||
success = await downloadAudiobookAsync(libraryBook);
|
||||
}
|
||||
finally
|
||||
{
|
||||
FilePathCache.Inserted -= FilePathCache_Inserted;
|
||||
FilePathCache.Removed -= FilePathCache_Removed;
|
||||
}
|
||||
|
||||
// decrypt failed
|
||||
if (!success)
|
||||
{
|
||||
foreach (var tmpFile in entries.Where(f => f.FileType != FileType.AAXC))
|
||||
FileUtility.SaferDelete(tmpFile.Path);
|
||||
// decrypt failed
|
||||
if (!success)
|
||||
{
|
||||
foreach (var tmpFile in entries.Where(f => f.FileType != FileType.AAXC))
|
||||
FileUtility.SaferDelete(tmpFile.Path);
|
||||
|
||||
return abDownloader?.IsCanceled == true ?
|
||||
new StatusHandler { "Cancelled" } :
|
||||
new StatusHandler { "Decrypt failed" };
|
||||
}
|
||||
return abDownloader?.IsCanceled == true ?
|
||||
new StatusHandler { "Cancelled" } :
|
||||
new StatusHandler { "Decrypt failed" };
|
||||
}
|
||||
|
||||
// moves new files from temp dir to final dest.
|
||||
// This could take a few seconds if moving hundreds of files.
|
||||
var movedAudioFile = await Task.Run(() => moveFilesToBooksDir(libraryBook, entries));
|
||||
// moves new files from temp dir to final dest.
|
||||
// This could take a few seconds if moving hundreds of files.
|
||||
var finalStorageDir = await Task.Run(() => moveFilesToBooksDir(libraryBook, entries));
|
||||
|
||||
// decrypt failed
|
||||
if (!movedAudioFile)
|
||||
return new StatusHandler { "Cannot find final audio file after decryption" };
|
||||
// decrypt failed
|
||||
if (finalStorageDir is null)
|
||||
return new StatusHandler { "Cannot find final audio file after decryption" };
|
||||
|
||||
if (Configuration.Instance.DownloadCoverArt)
|
||||
DownloadCoverArt(libraryBook);
|
||||
if (Configuration.Instance.DownloadCoverArt)
|
||||
downloadCoverArt(libraryBook);
|
||||
|
||||
libraryBook.Book.UpdateBookStatus(LiberatedStatus.Liberated);
|
||||
// contains logic to check for config setting and OS
|
||||
WindowsDirectory.SetCoverAsFolderIcon(pictureId: libraryBook.Book.PictureId, directory: finalStorageDir);
|
||||
|
||||
return new StatusHandler();
|
||||
}
|
||||
finally
|
||||
{
|
||||
OnCompleted(libraryBook);
|
||||
}
|
||||
}
|
||||
libraryBook.Book.UpdateBookStatus(LiberatedStatus.Liberated);
|
||||
|
||||
private async Task<bool> downloadAudiobookAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
return new StatusHandler();
|
||||
}
|
||||
finally
|
||||
{
|
||||
OnCompleted(libraryBook);
|
||||
}
|
||||
}
|
||||
|
||||
downloadValidation(libraryBook);
|
||||
private async Task<bool> downloadAudiobookAsync(LibraryBook libraryBook)
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
|
||||
var dlOptions = BuildDownloadOptions(libraryBook, config, contentLic);
|
||||
downloadValidation(libraryBook);
|
||||
|
||||
var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, dlOptions.OutputFormat.ToString().ToLower());
|
||||
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
var contentLic = await api.GetDownloadLicenseAsync(libraryBook.Book.AudibleProductId);
|
||||
var dlOptions = BuildDownloadOptions(libraryBook, config, contentLic);
|
||||
|
||||
if (contentLic.DrmType != AudibleApi.Common.DrmType.Adrm)
|
||||
abDownloader = new UnencryptedAudiobookDownloader(outFileName, cacheDir, dlOptions);
|
||||
else
|
||||
{
|
||||
AaxcDownloadConvertBase converter
|
||||
= config.SplitFilesByChapter ?
|
||||
new AaxcDownloadMultiConverter(outFileName, cacheDir, dlOptions) :
|
||||
new AaxcDownloadSingleConverter(outFileName, cacheDir, dlOptions);
|
||||
var outFileName = AudibleFileStorage.Audio.GetInProgressFilename(libraryBook, dlOptions.OutputFormat.ToString().ToLower());
|
||||
var cacheDir = AudibleFileStorage.DownloadsInProgressDirectory;
|
||||
|
||||
if (config.AllowLibationFixup)
|
||||
converter.RetrievedMetadata += (_, tags) => tags.Generes = string.Join(", ", libraryBook.Book.CategoriesNames());
|
||||
if (contentLic.DrmType != AudibleApi.Common.DrmType.Adrm)
|
||||
abDownloader = new UnencryptedAudiobookDownloader(outFileName, cacheDir, dlOptions);
|
||||
else
|
||||
{
|
||||
AaxcDownloadConvertBase converter
|
||||
= config.SplitFilesByChapter ?
|
||||
new AaxcDownloadMultiConverter(outFileName, cacheDir, dlOptions) :
|
||||
new AaxcDownloadSingleConverter(outFileName, cacheDir, dlOptions);
|
||||
|
||||
abDownloader = converter;
|
||||
}
|
||||
if (config.AllowLibationFixup)
|
||||
converter.RetrievedMetadata += (_, tags) => tags.Generes = string.Join(", ", libraryBook.Book.CategoriesNames());
|
||||
|
||||
abDownloader.DecryptProgressUpdate += OnStreamingProgressChanged;
|
||||
abDownloader.DecryptTimeRemaining += OnStreamingTimeRemaining;
|
||||
abDownloader.RetrievedTitle += OnTitleDiscovered;
|
||||
abDownloader.RetrievedAuthors += OnAuthorsDiscovered;
|
||||
abDownloader.RetrievedNarrators += OnNarratorsDiscovered;
|
||||
abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
|
||||
abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path);
|
||||
abDownloader = converter;
|
||||
}
|
||||
|
||||
// REAL WORK DONE HERE
|
||||
var success = await abDownloader.RunAsync();
|
||||
abDownloader.DecryptProgressUpdate += OnStreamingProgressChanged;
|
||||
abDownloader.DecryptTimeRemaining += OnStreamingTimeRemaining;
|
||||
abDownloader.RetrievedTitle += OnTitleDiscovered;
|
||||
abDownloader.RetrievedAuthors += OnAuthorsDiscovered;
|
||||
abDownloader.RetrievedNarrators += OnNarratorsDiscovered;
|
||||
abDownloader.RetrievedCoverArt += AaxcDownloader_RetrievedCoverArt;
|
||||
abDownloader.FileCreated += (_, path) => OnFileCreated(libraryBook, path);
|
||||
|
||||
return success;
|
||||
}
|
||||
// REAL WORK DONE HERE
|
||||
var success = await abDownloader.RunAsync();
|
||||
|
||||
private DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, AudibleApi.Common.ContentLicense contentLic)
|
||||
{
|
||||
//I assume if ContentFormat == "MPEG" that the delivered file is an unencrypted mp3.
|
||||
//I also assume that if DrmType != Adrm, the file will be an mp3.
|
||||
//These assumptions may be wrong, and only time and bug reports will tell.
|
||||
return success;
|
||||
}
|
||||
|
||||
bool encrypted = contentLic.DrmType == AudibleApi.Common.DrmType.Adrm;
|
||||
private DownloadOptions BuildDownloadOptions(LibraryBook libraryBook, Configuration config, AudibleApi.Common.ContentLicense contentLic)
|
||||
{
|
||||
//I assume if ContentFormat == "MPEG" that the delivered file is an unencrypted mp3.
|
||||
//I also assume that if DrmType != Adrm, the file will be an mp3.
|
||||
//These assumptions may be wrong, and only time and bug reports will tell.
|
||||
|
||||
var outputFormat = !encrypted || (config.AllowLibationFixup && config.DecryptToLossy) ?
|
||||
OutputFormat.Mp3 : OutputFormat.M4b;
|
||||
bool encrypted = contentLic.DrmType == AudibleApi.Common.DrmType.Adrm;
|
||||
|
||||
long chapterStartMs = config.StripAudibleBrandAudio ?
|
||||
contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs : 0;
|
||||
var outputFormat = !encrypted || (config.AllowLibationFixup && config.DecryptToLossy) ?
|
||||
OutputFormat.Mp3 : OutputFormat.M4b;
|
||||
|
||||
var dlOptions = new DownloadOptions
|
||||
(
|
||||
libraryBook,
|
||||
contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl,
|
||||
Resources.USER_AGENT
|
||||
)
|
||||
{
|
||||
AudibleKey = contentLic?.Voucher?.Key,
|
||||
AudibleIV = contentLic?.Voucher?.Iv,
|
||||
OutputFormat = outputFormat,
|
||||
TrimOutputToChapterLength = config.AllowLibationFixup && config.StripAudibleBrandAudio,
|
||||
RetainEncryptedFile = config.RetainAaxFile && encrypted,
|
||||
StripUnabridged = config.AllowLibationFixup && config.StripUnabridged,
|
||||
Downsample = config.AllowLibationFixup && config.LameDownsampleMono,
|
||||
MatchSourceBitrate = config.AllowLibationFixup && config.LameMatchSourceBR && config.LameTargetBitrate,
|
||||
CreateCueSheet = config.CreateCueSheet,
|
||||
LameConfig = GetLameOptions(config),
|
||||
ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(chapterStartMs)),
|
||||
FixupFile = config.AllowLibationFixup
|
||||
};
|
||||
long chapterStartMs = config.StripAudibleBrandAudio ?
|
||||
contentLic.ContentMetadata.ChapterInfo.BrandIntroDurationMs : 0;
|
||||
|
||||
var chapters = flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters).OrderBy(c => c.StartOffsetMs).ToList();
|
||||
var dlOptions = new DownloadOptions
|
||||
(
|
||||
libraryBook,
|
||||
contentLic?.ContentMetadata?.ContentUrl?.OfflineUrl,
|
||||
Resources.USER_AGENT
|
||||
)
|
||||
{
|
||||
AudibleKey = contentLic?.Voucher?.Key,
|
||||
AudibleIV = contentLic?.Voucher?.Iv,
|
||||
OutputFormat = outputFormat,
|
||||
TrimOutputToChapterLength = config.AllowLibationFixup && config.StripAudibleBrandAudio,
|
||||
RetainEncryptedFile = config.RetainAaxFile && encrypted,
|
||||
StripUnabridged = config.AllowLibationFixup && config.StripUnabridged,
|
||||
Downsample = config.AllowLibationFixup && config.LameDownsampleMono,
|
||||
MatchSourceBitrate = config.AllowLibationFixup && config.LameMatchSourceBR && config.LameTargetBitrate,
|
||||
CreateCueSheet = config.CreateCueSheet,
|
||||
LameConfig = GetLameOptions(config),
|
||||
ChapterInfo = new AAXClean.ChapterInfo(TimeSpan.FromMilliseconds(chapterStartMs)),
|
||||
FixupFile = config.AllowLibationFixup
|
||||
};
|
||||
|
||||
if (config.MergeOpeningAndEndCredits)
|
||||
combineCredits(chapters);
|
||||
var chapters = flattenChapters(contentLic.ContentMetadata.ChapterInfo.Chapters).OrderBy(c => c.StartOffsetMs).ToList();
|
||||
|
||||
for (int i = 0; i < chapters.Count; i++)
|
||||
{
|
||||
var chapter = chapters[i];
|
||||
long chapLenMs = chapter.LengthMs;
|
||||
if (config.MergeOpeningAndEndCredits)
|
||||
combineCredits(chapters);
|
||||
|
||||
if (i == 0)
|
||||
chapLenMs -= chapterStartMs;
|
||||
for (int i = 0; i < chapters.Count; i++)
|
||||
{
|
||||
var chapter = chapters[i];
|
||||
long chapLenMs = chapter.LengthMs;
|
||||
|
||||
if (config.StripAudibleBrandAudio && i == chapters.Count - 1)
|
||||
chapLenMs -= contentLic.ContentMetadata.ChapterInfo.BrandOutroDurationMs;
|
||||
if (i == 0)
|
||||
chapLenMs -= chapterStartMs;
|
||||
|
||||
dlOptions.ChapterInfo.AddChapter(chapter.Title, TimeSpan.FromMilliseconds(chapLenMs));
|
||||
}
|
||||
if (config.StripAudibleBrandAudio && i == chapters.Count - 1)
|
||||
chapLenMs -= contentLic.ContentMetadata.ChapterInfo.BrandOutroDurationMs;
|
||||
|
||||
return dlOptions;
|
||||
}
|
||||
dlOptions.ChapterInfo.AddChapter(chapter.Title, TimeSpan.FromMilliseconds(chapLenMs));
|
||||
}
|
||||
|
||||
/*
|
||||
return dlOptions;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Flatten Audible's new hierarchical chapters, combining children into parents.
|
||||
|
||||
@@ -267,143 +270,138 @@ namespace FileLiberator
|
||||
|
||||
*/
|
||||
|
||||
public static List<AudibleApi.Common.Chapter> flattenChapters(IList<AudibleApi.Common.Chapter> chapters, string titleConcat = ": ")
|
||||
{
|
||||
List<AudibleApi.Common.Chapter> chaps = new();
|
||||
public static List<AudibleApi.Common.Chapter> flattenChapters(IList<AudibleApi.Common.Chapter> chapters, string titleConcat = ": ")
|
||||
{
|
||||
List<AudibleApi.Common.Chapter> chaps = new();
|
||||
|
||||
foreach (var c in chapters)
|
||||
{
|
||||
if (c.Chapters is not null)
|
||||
{
|
||||
if (c.LengthMs < 10000)
|
||||
{
|
||||
c.Chapters[0].StartOffsetMs = c.StartOffsetMs;
|
||||
c.Chapters[0].StartOffsetSec = c.StartOffsetSec;
|
||||
c.Chapters[0].LengthMs += c.LengthMs;
|
||||
}
|
||||
else
|
||||
chaps.Add(c);
|
||||
foreach (var c in chapters)
|
||||
{
|
||||
if (c.Chapters is not null)
|
||||
{
|
||||
if (c.LengthMs < 10000)
|
||||
{
|
||||
c.Chapters[0].StartOffsetMs = c.StartOffsetMs;
|
||||
c.Chapters[0].StartOffsetSec = c.StartOffsetSec;
|
||||
c.Chapters[0].LengthMs += c.LengthMs;
|
||||
}
|
||||
else
|
||||
chaps.Add(c);
|
||||
|
||||
var children = flattenChapters(c.Chapters);
|
||||
var children = flattenChapters(c.Chapters);
|
||||
|
||||
foreach (var child in children)
|
||||
child.Title = $"{c.Title}{titleConcat}{child.Title}";
|
||||
foreach (var child in children)
|
||||
child.Title = $"{c.Title}{titleConcat}{child.Title}";
|
||||
|
||||
chaps.AddRange(children);
|
||||
c.Chapters = null;
|
||||
}
|
||||
else
|
||||
chaps.Add(c);
|
||||
}
|
||||
return chaps;
|
||||
}
|
||||
chaps.AddRange(children);
|
||||
c.Chapters = null;
|
||||
}
|
||||
else
|
||||
chaps.Add(c);
|
||||
}
|
||||
return chaps;
|
||||
}
|
||||
|
||||
public static void combineCredits(IList<AudibleApi.Common.Chapter> chapters)
|
||||
{
|
||||
if (chapters.Count > 1 && chapters[0].Title == "Opening Credits")
|
||||
{
|
||||
chapters[1].StartOffsetMs = chapters[0].StartOffsetMs;
|
||||
chapters[1].StartOffsetSec = chapters[0].StartOffsetSec;
|
||||
chapters[1].LengthMs += chapters[0].LengthMs;
|
||||
chapters.RemoveAt(0);
|
||||
}
|
||||
if (chapters.Count > 1 && chapters[^1].Title == "End Credits")
|
||||
{
|
||||
chapters[^2].LengthMs += chapters[^1].LengthMs;
|
||||
chapters.Remove(chapters[^1]);
|
||||
}
|
||||
}
|
||||
public static void combineCredits(IList<AudibleApi.Common.Chapter> chapters)
|
||||
{
|
||||
if (chapters.Count > 1 && chapters[0].Title == "Opening Credits")
|
||||
{
|
||||
chapters[1].StartOffsetMs = chapters[0].StartOffsetMs;
|
||||
chapters[1].StartOffsetSec = chapters[0].StartOffsetSec;
|
||||
chapters[1].LengthMs += chapters[0].LengthMs;
|
||||
chapters.RemoveAt(0);
|
||||
}
|
||||
if (chapters.Count > 1 && chapters[^1].Title == "End Credits")
|
||||
{
|
||||
chapters[^2].LengthMs += chapters[^1].LengthMs;
|
||||
chapters.Remove(chapters[^1]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void downloadValidation(LibraryBook libraryBook)
|
||||
{
|
||||
string errorString(string field)
|
||||
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
|
||||
private static void downloadValidation(LibraryBook libraryBook)
|
||||
{
|
||||
string errorString(string field)
|
||||
=> $"{errorTitle()}\r\nCannot download book. {field} is not known. Try re-importing the account which owns this book.";
|
||||
|
||||
string errorTitle()
|
||||
{
|
||||
var title
|
||||
= (libraryBook.Book.Title.Length > 53)
|
||||
? $"{libraryBook.Book.Title.Truncate(50)}..."
|
||||
: libraryBook.Book.Title;
|
||||
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
|
||||
return errorBookTitle;
|
||||
};
|
||||
string errorTitle()
|
||||
{
|
||||
var title
|
||||
= (libraryBook.Book.Title.Length > 53)
|
||||
? $"{libraryBook.Book.Title.Truncate(50)}..."
|
||||
: libraryBook.Book.Title;
|
||||
var errorBookTitle = $"{title} [{libraryBook.Book.AudibleProductId}]";
|
||||
return errorBookTitle;
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(libraryBook.Account))
|
||||
throw new Exception(errorString("Account"));
|
||||
if (string.IsNullOrWhiteSpace(libraryBook.Account))
|
||||
throw new Exception(errorString("Account"));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale))
|
||||
throw new Exception(errorString("Locale"));
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(libraryBook.Book.Locale))
|
||||
throw new Exception(errorString("Locale"));
|
||||
}
|
||||
|
||||
private void AaxcDownloader_RetrievedCoverArt(object _, byte[] e)
|
||||
{
|
||||
if (e is not null)
|
||||
OnCoverImageDiscovered(e);
|
||||
else if (Configuration.Instance.AllowLibationFixup)
|
||||
abDownloader.SetCoverArt(OnRequestCoverArt());
|
||||
}
|
||||
private void AaxcDownloader_RetrievedCoverArt(object _, byte[] e)
|
||||
{
|
||||
if (e is not null)
|
||||
OnCoverImageDiscovered(e);
|
||||
else if (Configuration.Instance.AllowLibationFixup)
|
||||
abDownloader.SetCoverArt(OnRequestCoverArt());
|
||||
}
|
||||
|
||||
/// <summary>Move new files to 'Books' directory</summary>
|
||||
/// <returns>True if audiobook file(s) were successfully created and can be located on disk. Else false.</returns>
|
||||
private static bool moveFilesToBooksDir(LibraryBook libraryBook, List<FilePathCache.CacheEntry> entries)
|
||||
{
|
||||
// create final directory. move each file into it
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
/// <summary>Move new files to 'Books' directory</summary>
|
||||
/// <returns>Return directory if audiobook file(s) were successfully created and can be located on disk. Else null.</returns>
|
||||
private static string moveFilesToBooksDir(LibraryBook libraryBook, List<FilePathCache.CacheEntry> entries)
|
||||
{
|
||||
// create final directory. move each file into it
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
|
||||
Directory.CreateDirectory(destinationDir);
|
||||
|
||||
FilePathCache.CacheEntry getFirstAudio() => entries.FirstOrDefault(f => f.FileType == FileType.Audio);
|
||||
FilePathCache.CacheEntry getFirstAudio() => entries.FirstOrDefault(f => f.FileType == FileType.Audio);
|
||||
|
||||
if (getFirstAudio() == default)
|
||||
return false;
|
||||
if (getFirstAudio() == default)
|
||||
return null;
|
||||
|
||||
for (var i = 0; i < entries.Count; i++)
|
||||
{
|
||||
var entry = entries[i];
|
||||
for (var i = 0; i < entries.Count; i++)
|
||||
{
|
||||
var entry = entries[i];
|
||||
|
||||
var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)), Configuration.Instance.ReplacementCharacters);
|
||||
FilePathCache.Insert(libraryBook.Book.AudibleProductId, realDest);
|
||||
var realDest = FileUtility.SaferMoveToValidPath(entry.Path, Path.Combine(destinationDir, Path.GetFileName(entry.Path)), Configuration.Instance.ReplacementCharacters);
|
||||
FilePathCache.Insert(libraryBook.Book.AudibleProductId, realDest);
|
||||
|
||||
// propagate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop)
|
||||
entries[i] = entry with { Path = realDest };
|
||||
}
|
||||
// propagate corrected path. Must update cache with corrected path. Also want updated path for cue file (after this for-loop)
|
||||
entries[i] = entry with { Path = realDest };
|
||||
}
|
||||
|
||||
var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue);
|
||||
if (cue != default)
|
||||
Cue.UpdateFileName(cue.Path, getFirstAudio().Path);
|
||||
var cue = entries.FirstOrDefault(f => f.FileType == FileType.Cue);
|
||||
if (cue != default)
|
||||
Cue.UpdateFileName(cue.Path, getFirstAudio().Path);
|
||||
|
||||
AudibleFileStorage.Audio.Refresh();
|
||||
AudibleFileStorage.Audio.Refresh();
|
||||
|
||||
return true;
|
||||
}
|
||||
return destinationDir;
|
||||
}
|
||||
|
||||
private void DownloadCoverArt(LibraryBook libraryBook)
|
||||
{
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
|
||||
var coverPath = AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, ".jpg");
|
||||
coverPath = Path.Combine(destinationDir, Path.GetFileName(coverPath));
|
||||
private static void downloadCoverArt(LibraryBook libraryBook)
|
||||
{
|
||||
var coverPath = "[null]";
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(coverPath))
|
||||
FileUtility.SaferDelete(coverPath);
|
||||
try
|
||||
{
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
|
||||
coverPath = AudibleFileStorage.Audio.GetBooksDirectoryFilename(libraryBook, ".jpg");
|
||||
coverPath = Path.Combine(destinationDir, Path.GetFileName(coverPath));
|
||||
|
||||
(string picId, PictureSize size) = libraryBook.Book.PictureLarge is null ?
|
||||
(libraryBook.Book.PictureId, PictureSize.Native) :
|
||||
(libraryBook.Book.PictureLarge, PictureSize.Native);
|
||||
if (File.Exists(coverPath))
|
||||
FileUtility.SaferDelete(coverPath);
|
||||
|
||||
var picBytes = PictureStorage.GetPictureSynchronously(new PictureDefinition(picId, size));
|
||||
|
||||
if (picBytes.Length > 0)
|
||||
File.WriteAllBytes(coverPath, picBytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Failure to download cover art should not be
|
||||
//considered a failure to download the book
|
||||
Serilog.Log.Logger.Error(ex, $"Error downloading cover art of {libraryBook.Book.AudibleProductId} to {coverPath} catalog product.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
var picBytes = PictureStorage.GetPictureSynchronously(new(libraryBook.Book.PictureLarge ?? libraryBook.Book.PictureId, PictureSize.Native));
|
||||
if (picBytes.Length > 0)
|
||||
File.WriteAllBytes(coverPath, picBytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Failure to download cover art should not be considered a failure to download the book
|
||||
Serilog.Log.Logger.Error(ex, $"Error downloading cover art of {libraryBook.Book.AudibleProductId} to {coverPath} catalog product.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ namespace FileLiberator
|
||||
{
|
||||
public class DownloadOptions : IDownloadOptions
|
||||
{
|
||||
public LibraryBook LibraryBook { get; }
|
||||
public LibraryBookDto LibraryBookDto { get; }
|
||||
public string DownloadUrl { get; }
|
||||
public string UserAgent { get; }
|
||||
@@ -35,12 +34,12 @@ namespace FileLiberator
|
||||
|
||||
public DownloadOptions(LibraryBook libraryBook, string downloadUrl, string userAgent)
|
||||
{
|
||||
LibraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));
|
||||
LibraryBookDto = ArgumentValidator
|
||||
.EnsureNotNull(libraryBook, nameof(libraryBook))
|
||||
.ToDto();
|
||||
DownloadUrl = ArgumentValidator.EnsureNotNullOrEmpty(downloadUrl, nameof(downloadUrl));
|
||||
UserAgent = ArgumentValidator.EnsureNotNullOrEmpty(userAgent, nameof(userAgent));
|
||||
|
||||
LibraryBookDto = LibraryBook.ToDto();
|
||||
|
||||
// no null/empty check for key/iv. unencrypted files do not have them
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace FileLiberator
|
||||
{
|
||||
OnBegin(libraryBook);
|
||||
|
||||
try
|
||||
try
|
||||
{
|
||||
var proposedDownloadFilePath = getProposedDownloadFilePath(libraryBook);
|
||||
var actualDownloadedFilePath = await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
|
||||
@@ -32,13 +32,22 @@ namespace FileLiberator
|
||||
|
||||
libraryBook.Book.UpdatePdfStatus(result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated);
|
||||
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error downloading PDF");
|
||||
|
||||
var result = new StatusHandler();
|
||||
result.AddError($"Error downloading PDF. See log for details. Error summary: {ex.Message}");
|
||||
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
OnCompleted(libraryBook);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string getProposedDownloadFilePath(LibraryBook libraryBook)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace FileLiberator
|
||||
AudibleProductId = libraryBook.Book.AudibleProductId,
|
||||
Title = libraryBook.Book.Title ?? "",
|
||||
Locale = libraryBook.Book.Locale,
|
||||
YearPublished = libraryBook.Book.DatePublished?.Year,
|
||||
|
||||
Authors = libraryBook.Book.Authors.Select(c => c.Name).ToList(),
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="4.4.1.1" />
|
||||
<PackageReference Include="Dinah.Core" Version="7.0.0.2" />
|
||||
<PackageReference Include="Polly" Version="7.2.3" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace FileManager
|
||||
return FileUtility.GetValidFilename(Path.Join(directory, replaceFileName(fileNamePart, paramReplacements, LongPath.MaxFilenameLength - 5)), replacements, returnFirstExisting);
|
||||
}
|
||||
|
||||
private string replaceFileName(string filename, Dictionary<string,string> paramReplacements, int maxFilenameLength)
|
||||
private static string replaceFileName(string filename, Dictionary<string,string> paramReplacements, int maxFilenameLength)
|
||||
{
|
||||
List<StringBuilder> filenameParts = new();
|
||||
//Build the filename in parts, replacing replacement parameters with
|
||||
@@ -98,7 +98,7 @@ namespace FileManager
|
||||
return string.Join("", filenameParts);
|
||||
}
|
||||
|
||||
private string formatValue(object value, ReplacementCharacters replacements)
|
||||
private static string formatValue(object value, ReplacementCharacters replacements)
|
||||
{
|
||||
if (value is null)
|
||||
return "";
|
||||
|
||||
@@ -212,25 +212,23 @@ namespace FileManager
|
||||
{
|
||||
var foundFiles = Enumerable.Empty<LongPath>();
|
||||
|
||||
if (searchOption == SearchOption.AllDirectories)
|
||||
try
|
||||
{
|
||||
try
|
||||
if (searchOption == SearchOption.AllDirectories)
|
||||
{
|
||||
IEnumerable <LongPath> subDirs = Directory.EnumerateDirectories(path).Select(p => (LongPath)p);
|
||||
IEnumerable<LongPath> subDirs = Directory.EnumerateDirectories(path).Select(p => (LongPath)p);
|
||||
// Add files in subdirectories recursively to the list
|
||||
foreach (string dir in subDirs)
|
||||
foundFiles = foundFiles.Concat(SaferEnumerateFiles(dir, searchPattern, searchOption));
|
||||
}
|
||||
catch (UnauthorizedAccessException) { }
|
||||
catch (PathTooLongException) { }
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Add files from the current directory
|
||||
foundFiles = foundFiles.Concat(Directory.EnumerateFiles(path, searchPattern).Select(f => (LongPath)f));
|
||||
}
|
||||
catch (UnauthorizedAccessException) { }
|
||||
catch (PathTooLongException) { }
|
||||
// Symbolic links will result in DirectoryNotFoundException. Ohter logical directories might also. Just skip them. Don't want to risk (or have to handle) infinite recursion
|
||||
catch (DirectoryNotFoundException) { }
|
||||
|
||||
return foundFiles;
|
||||
}
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
using ApplicationServices;
|
||||
using AppScaffolding;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Hangover
|
||||
{
|
||||
public partial class Form1
|
||||
{
|
||||
private string dbFile;
|
||||
|
||||
private void Load_databaseTab()
|
||||
{
|
||||
dbFile = UNSAFE_MigrationHelper.DatabaseFile;
|
||||
if (dbFile is null)
|
||||
{
|
||||
databaseFileLbl.Text = $"Database file not found";
|
||||
return;
|
||||
}
|
||||
|
||||
databaseFileLbl.Text = $"Database file: {UNSAFE_MigrationHelper.DatabaseFile ?? "not found"}";
|
||||
}
|
||||
|
||||
private void databaseTab_VisibleChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (!databaseTab.Visible)
|
||||
return;
|
||||
}
|
||||
|
||||
private void sqlExecuteBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
ensureBackup();
|
||||
|
||||
sqlResultsTb.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
var sql = sqlTb.Text.Trim();
|
||||
|
||||
#region // explanation
|
||||
// Routing statements to non-query is a convenience.
|
||||
// I went down the rabbit hole of full parsing and it's more trouble than it's worth. The parsing is easy due to available libraries. The edge cases of what to do next got too complex for slight gains.
|
||||
// It's also not useful to take the extra effort to separate non-queries which don't return a row count. Eg: alter table, drop table
|
||||
// My half-assed solution here won't even catch simple mistakes like this -- and that's ok
|
||||
// -- line 1 is a comment
|
||||
// delete from foo
|
||||
#endregion
|
||||
var lower = sql.ToLower();
|
||||
if (lower.StartsWith("update") || lower.StartsWith("insert") || lower.StartsWith("delete"))
|
||||
nonQuery(sql);
|
||||
else
|
||||
query(sql);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sqlResultsTb.Text = $"{ex.Message}\r\n{ex.StackTrace}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
deleteUnneededBackups();
|
||||
}
|
||||
}
|
||||
|
||||
private string dbBackup;
|
||||
private DateTime dbFileLastModified;
|
||||
private void ensureBackup()
|
||||
{
|
||||
if (dbBackup is not null)
|
||||
return;
|
||||
|
||||
dbFileLastModified = File.GetLastWriteTimeUtc(dbFile);
|
||||
|
||||
dbBackup
|
||||
= Path.ChangeExtension(dbFile, "").TrimEnd('.')
|
||||
+ $"_backup_{DateTime.UtcNow:O}".Replace(':', '-').Replace('.', '-')
|
||||
+ Path.GetExtension(dbFile);
|
||||
File.Copy(dbFile, dbBackup);
|
||||
}
|
||||
|
||||
private void deleteUnneededBackups()
|
||||
{
|
||||
var newLastModified = File.GetLastWriteTimeUtc(dbFile);
|
||||
if (dbFileLastModified == newLastModified)
|
||||
{
|
||||
File.Delete(dbBackup);
|
||||
dbBackup = null;
|
||||
}
|
||||
}
|
||||
|
||||
void query(string sql)
|
||||
{
|
||||
// ef doesn't support truly generic queries. have to drop down to ado.net
|
||||
using var context = DbContexts.GetContext();
|
||||
using var conn = context.Database.GetDbConnection();
|
||||
conn.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
|
||||
var reader = cmd.ExecuteReader();
|
||||
var results = 0;
|
||||
var builder = new System.Text.StringBuilder();
|
||||
var lines = 0;
|
||||
while (reader.Read())
|
||||
{
|
||||
results++;
|
||||
|
||||
for (var i = 0; i < reader.FieldCount; i++)
|
||||
builder.Append(reader.GetValue(i) + "\t");
|
||||
builder.AppendLine();
|
||||
|
||||
lines++;
|
||||
if (lines % 10 == 0)
|
||||
{
|
||||
sqlResultsTb.AppendText(builder.ToString());
|
||||
builder.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
sqlResultsTb.AppendText(builder.ToString());
|
||||
builder.Clear();
|
||||
|
||||
if (results == 0)
|
||||
sqlResultsTb.Text = "[no results]";
|
||||
else
|
||||
{
|
||||
sqlResultsTb.AppendText($"\r\n{results} result");
|
||||
if (results != 1) sqlResultsTb.AppendText("s");
|
||||
}
|
||||
}
|
||||
|
||||
void nonQuery(string sql)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
var results = context.Database.ExecuteSqlRaw(sql);
|
||||
|
||||
sqlResultsTb.AppendText($"{results} record");
|
||||
if (results != 1) sqlResultsTb.AppendText("s");
|
||||
sqlResultsTb.AppendText(" affected");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<!--Avalonia doesen't support TrimMode=link currently,but we are working on that https://github.com/AvaloniaUI/Avalonia/issues/6892 -->
|
||||
<TrimMode>copyused</TrimMode>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
@@ -20,6 +20,16 @@
|
||||
<ApplicationIcon>hangover.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
HACK FOR COMPILER BUG 2021-09-14. Hopefully will be fixed in future versions
|
||||
- Not using SatelliteResourceLanguages will load all language packs: works
|
||||
- Specifying 'en' semicolon 1 more should load 1 language pack: works
|
||||
- Specifying only 'en' should load no language packs: broken, still loads all
|
||||
-->
|
||||
<SatelliteResourceLanguages>en;es</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<OutputPath>..\bin\Avalonia\Debug</OutputPath>
|
||||
<DebugType>embedded</DebugType>
|
||||
@@ -53,17 +63,15 @@
|
||||
<TrimmableAssembly Include="Avalonia.Themes.Default" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="0.10.17" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.17" />
|
||||
<PackageReference Include="Avalonia" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.18" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="0.10.17" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.17" />
|
||||
<PackageReference Include="XamlNameReferenceGenerator" Version="1.3.4" />
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.18" />
|
||||
<PackageReference Include="XamlNameReferenceGenerator" Version="1.5.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
|
||||
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
|
||||
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Release\linux-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Publish\Linux-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -6,12 +6,12 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Release\macos-chardonnay</PublishDir>
|
||||
<PublishDir>..\bin\Publish\MacOS-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -6,9 +6,9 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Release\win-chardonnay</PublishDir>
|
||||
<PublishDir>..\bin\Publish\Windows-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
using ApplicationServices;
|
||||
using AppScaffolding;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using HangoverBase;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace HangoverAvalonia.ViewModels
|
||||
{
|
||||
public class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
private string dbFile;
|
||||
private DatabaseTab _tab;
|
||||
|
||||
private string _databaseFileText;
|
||||
private bool _databaseFound;
|
||||
private string _sqlResults;
|
||||
@@ -22,129 +18,20 @@ namespace HangoverAvalonia.ViewModels
|
||||
|
||||
public MainWindowViewModel()
|
||||
{
|
||||
dbFile = UNSAFE_MigrationHelper.DatabaseFile;
|
||||
if (dbFile is null)
|
||||
_tab = new(new(() => SqlQuery, s => SqlResults = s, s => SqlResults = s));
|
||||
|
||||
_tab.LoadDatabaseFile();
|
||||
if (_tab.DbFile is null)
|
||||
{
|
||||
DatabaseFileText = $"Database file not found";
|
||||
DatabaseFound = false;
|
||||
return;
|
||||
}
|
||||
|
||||
DatabaseFileText = $"Database file: {UNSAFE_MigrationHelper.DatabaseFile ?? "not found"}";
|
||||
|
||||
DatabaseFound = UNSAFE_MigrationHelper.DatabaseFile is not null;
|
||||
DatabaseFileText = $"Database file: {_tab.DbFile}";
|
||||
DatabaseFound = true;
|
||||
}
|
||||
|
||||
public void ExecuteQuery()
|
||||
{
|
||||
ensureBackup();
|
||||
|
||||
SqlResults = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var sql = SqlQuery.Trim();
|
||||
|
||||
#region // explanation
|
||||
// Routing statements to non-query is a convenience.
|
||||
// I went down the rabbit hole of full parsing and it's more trouble than it's worth. The parsing is easy due to available libraries. The edge cases of what to do next got too complex for slight gains.
|
||||
// It's also not useful to take the extra effort to separate non-queries which don't return a row count. Eg: alter table, drop table
|
||||
// My half-assed solution here won't even catch simple mistakes like this -- and that's ok
|
||||
// -- line 1 is a comment
|
||||
// delete from foo
|
||||
#endregion
|
||||
var lower = sql.ToLower();
|
||||
if (lower.StartsWith("update") || lower.StartsWith("insert") || lower.StartsWith("delete"))
|
||||
nonQuery(sql);
|
||||
else
|
||||
query(sql);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SqlResults = $"{ex.Message}\r\n{ex.StackTrace}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
deleteUnneededBackups();
|
||||
}
|
||||
}
|
||||
|
||||
private string dbBackup;
|
||||
private DateTime dbFileLastModified;
|
||||
|
||||
private void ensureBackup()
|
||||
{
|
||||
if (dbBackup is not null)
|
||||
return;
|
||||
|
||||
dbFileLastModified = File.GetLastWriteTimeUtc(dbFile);
|
||||
|
||||
dbBackup
|
||||
= Path.ChangeExtension(dbFile, "").TrimEnd('.')
|
||||
+ $"_backup_{DateTime.UtcNow:O}".Replace(':', '-').Replace('.', '-')
|
||||
+ Path.GetExtension(dbFile);
|
||||
File.Copy(dbFile, dbBackup);
|
||||
}
|
||||
|
||||
private void deleteUnneededBackups()
|
||||
{
|
||||
var newLastModified = File.GetLastWriteTimeUtc(dbFile);
|
||||
if (dbFileLastModified == newLastModified)
|
||||
{
|
||||
File.Delete(dbBackup);
|
||||
dbBackup = null;
|
||||
}
|
||||
}
|
||||
|
||||
void query(string sql)
|
||||
{
|
||||
// ef doesn't support truly generic queries. have to drop down to ado.net
|
||||
using var context = DbContexts.GetContext();
|
||||
using var conn = context.Database.GetDbConnection();
|
||||
conn.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
|
||||
var reader = cmd.ExecuteReader();
|
||||
var results = 0;
|
||||
var builder = new StringBuilder();
|
||||
var lines = 0;
|
||||
while (reader.Read())
|
||||
{
|
||||
results++;
|
||||
|
||||
for (var i = 0; i < reader.FieldCount; i++)
|
||||
builder.Append(reader.GetValue(i) + "\t");
|
||||
builder.AppendLine();
|
||||
|
||||
lines++;
|
||||
if (lines % 10 == 0)
|
||||
{
|
||||
SqlResults += builder.ToString();
|
||||
builder.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
SqlResults += builder.ToString();
|
||||
builder.Clear();
|
||||
|
||||
if (results == 0)
|
||||
SqlResults = "[no results]";
|
||||
else
|
||||
{
|
||||
SqlResults += $"\r\n{results} result";
|
||||
if (results != 1) SqlResults += "s";
|
||||
}
|
||||
}
|
||||
|
||||
void nonQuery(string sql)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
var results = context.Database.ExecuteSqlRaw(sql);
|
||||
|
||||
SqlResults += $"{results} record";
|
||||
if (results != 1) SqlResults += "s";
|
||||
SqlResults += " affected";
|
||||
}
|
||||
public void ExecuteQuery() => _tab.ExecuteQuery();
|
||||
}
|
||||
}
|
||||
|
||||
154
Source/HangoverBase/DatabaseTab.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using System.Text;
|
||||
using ApplicationServices;
|
||||
using AppScaffolding;
|
||||
using Dinah.Core;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HangoverBase
|
||||
{
|
||||
public class DatabaseTabCommands
|
||||
{
|
||||
public Func<string> SqlInput { get; }
|
||||
public Action<string> SqlOutputAppend { get; }
|
||||
public Action<string> SqlOutputOverwrite { get; }
|
||||
|
||||
public DatabaseTabCommands() { }
|
||||
public DatabaseTabCommands(
|
||||
Func<string> sqlInput,
|
||||
Action<string> sqlDisplayAppend,
|
||||
Action<string> sqlDisplayOverwrite)
|
||||
{
|
||||
SqlInput = ArgumentValidator.EnsureNotNull(sqlInput, nameof(sqlInput));
|
||||
SqlOutputAppend = ArgumentValidator.EnsureNotNull(sqlDisplayAppend, nameof(sqlDisplayAppend));
|
||||
SqlOutputOverwrite = ArgumentValidator.EnsureNotNull(sqlDisplayOverwrite, nameof(sqlDisplayOverwrite));
|
||||
}
|
||||
}
|
||||
|
||||
public class DatabaseTab
|
||||
{
|
||||
private DatabaseTabCommands _commands { get; }
|
||||
public string DbFile { get; private set; }
|
||||
|
||||
public DatabaseTab(DatabaseTabCommands commands)
|
||||
{
|
||||
_commands = ArgumentValidator.EnsureNotNull(commands, nameof(commands));
|
||||
|
||||
ArgumentValidator.EnsureNotNull(commands, nameof(_commands.SqlInput));
|
||||
ArgumentValidator.EnsureNotNull(commands, nameof(_commands.SqlOutputAppend));
|
||||
ArgumentValidator.EnsureNotNull(commands, nameof(_commands.SqlOutputOverwrite));
|
||||
}
|
||||
|
||||
public void LoadDatabaseFile() => DbFile = UNSAFE_MigrationHelper.DatabaseFile;
|
||||
|
||||
private string dbBackup;
|
||||
private DateTime dbFileLastModified;
|
||||
public void EnsureBackup()
|
||||
{
|
||||
if (dbBackup is not null)
|
||||
return;
|
||||
|
||||
dbFileLastModified = File.GetLastWriteTimeUtc(DbFile);
|
||||
|
||||
dbBackup
|
||||
= Path.ChangeExtension(DbFile, "").TrimEnd('.')
|
||||
+ $"_backup_{DateTime.UtcNow:O}".Replace(':', '-').Replace('.', '-')
|
||||
+ Path.GetExtension(DbFile);
|
||||
File.Copy(DbFile, dbBackup);
|
||||
}
|
||||
|
||||
public void ExecuteQuery()
|
||||
{
|
||||
EnsureBackup();
|
||||
|
||||
_commands.SqlOutputOverwrite("");
|
||||
|
||||
try
|
||||
{
|
||||
var sql = _commands.SqlInput().Trim();
|
||||
|
||||
#region // explanation
|
||||
// Routing statements to non-query is a convenience.
|
||||
// I went down the rabbit hole of full parsing and it's more trouble than it's worth. The parsing is easy due to available libraries. The edge cases of what to do next got too complex for slight gains.
|
||||
// It's also not useful to take the extra effort to separate non-queries which don't return a row count. Eg: alter table, drop table
|
||||
// My half-assed solution here won't even catch simple mistakes like this -- and that's ok
|
||||
// -- line 1 is a comment
|
||||
// delete from foo
|
||||
#endregion
|
||||
var lower = sql.ToLower();
|
||||
if (lower.StartsWith("update") || lower.StartsWith("insert") || lower.StartsWith("delete"))
|
||||
NonQuery(sql);
|
||||
else
|
||||
Query(sql);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_commands.SqlOutputOverwrite($"{ex.Message}\r\n{ex.StackTrace}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
DeleteUnneededBackups();
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteUnneededBackups()
|
||||
{
|
||||
var newLastModified = File.GetLastWriteTimeUtc(DbFile);
|
||||
if (dbFileLastModified == newLastModified)
|
||||
{
|
||||
File.Delete(dbBackup);
|
||||
dbBackup = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Query(string sql)
|
||||
{
|
||||
// ef doesn't support truly generic queries. have to drop down to ado.net
|
||||
using var context = DbContexts.GetContext();
|
||||
using var conn = context.Database.GetDbConnection();
|
||||
conn.Open();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
|
||||
var reader = cmd.ExecuteReader();
|
||||
var results = 0;
|
||||
var builder = new StringBuilder();
|
||||
var lines = 0;
|
||||
while (reader.Read())
|
||||
{
|
||||
results++;
|
||||
|
||||
for (var i = 0; i < reader.FieldCount; i++)
|
||||
builder.Append(reader.GetValue(i) + "\t");
|
||||
builder.AppendLine();
|
||||
|
||||
lines++;
|
||||
if (lines % 10 == 0)
|
||||
{
|
||||
_commands.SqlOutputAppend(builder.ToString());
|
||||
builder.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
_commands.SqlOutputAppend(builder.ToString());
|
||||
builder.Clear();
|
||||
|
||||
if (results == 0)
|
||||
_commands.SqlOutputOverwrite("[no results]");
|
||||
else
|
||||
{
|
||||
_commands.SqlOutputAppend($"\r\n{results} result");
|
||||
if (results != 1) _commands.SqlOutputAppend("s");
|
||||
}
|
||||
}
|
||||
|
||||
public void NonQuery(string sql)
|
||||
{
|
||||
using var context = DbContexts.GetContext();
|
||||
var results = context.Database.ExecuteSqlRaw(sql);
|
||||
|
||||
_commands.SqlOutputAppend($"{results} record");
|
||||
if (results != 1) _commands.SqlOutputAppend("s");
|
||||
_commands.SqlOutputAppend(" affected");
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Source/HangoverBase/HangoverBase.csproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
|
||||
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,6 +1,6 @@
|
||||
using AppScaffolding;
|
||||
|
||||
namespace Hangover
|
||||
namespace HangoverWinForms
|
||||
{
|
||||
public partial class Form1
|
||||
{
|
||||
31
Source/HangoverWinForms/Form1.Database.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using HangoverBase;
|
||||
|
||||
namespace HangoverWinForms
|
||||
{
|
||||
public partial class Form1
|
||||
{
|
||||
private DatabaseTab _tab;
|
||||
|
||||
private void Load_databaseTab()
|
||||
{
|
||||
_tab = new(new(() => sqlTb.Text, sqlResultsTb.AppendText, s => sqlResultsTb.Text = s));
|
||||
|
||||
_tab.LoadDatabaseFile();
|
||||
if (_tab.DbFile is null)
|
||||
{
|
||||
databaseFileLbl.Text = $"Database file not found";
|
||||
return;
|
||||
}
|
||||
|
||||
databaseFileLbl.Text = $"Database file: {_tab.DbFile}";
|
||||
}
|
||||
|
||||
private void databaseTab_VisibleChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (!databaseTab.Visible)
|
||||
return;
|
||||
}
|
||||
|
||||
private void sqlExecuteBtn_Click(object sender, EventArgs e) => _tab.ExecuteQuery();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Hangover
|
||||
namespace HangoverWinForms
|
||||
{
|
||||
partial class Form1
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Hangover
|
||||
namespace HangoverWinForms
|
||||
{
|
||||
public partial class Form1 : Form
|
||||
{
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon>hangover.ico</ApplicationIcon>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
@@ -44,9 +44,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
|
||||
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
|
||||
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -55,7 +53,7 @@
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="SpicNSpan" AfterTargets="Clean">
|
||||
<Target Name="SpicNSpan" AfterTargets="Clean">
|
||||
<!-- Remove obj folder -->
|
||||
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
|
||||
<!-- Remove bin folder -->
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Hangover
|
||||
namespace HangoverWinForms
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Publish\classic</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 498 B After Width: | Height: | Size: 498 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
@@ -7,9 +7,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solutio
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
REFERENCE.txt = REFERENCE.txt
|
||||
_ARCHITECTURE NOTES.txt = _ARCHITECTURE NOTES.txt
|
||||
_AvaloniaUI Primer.txt = _AvaloniaUI Primer.txt
|
||||
_DB_NOTES.txt = _DB_NOTES.txt
|
||||
__README - COLLABORATORS.txt = __README - COLLABORATORS.txt
|
||||
__TODO.txt = __TODO.txt
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "5 Domain Utilities (db aware)", "5 Domain Utilities (db aware)", "{41CDCC73-9B81-49DD-9570-C54406E852AF}"
|
||||
@@ -64,12 +64,35 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileManager.Tests", "_Tests
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationFileManager.Tests", "_Tests\LibationFileManager.Tests\LibationFileManager.Tests.csproj", "{EB781571-8548-477E-82AD-FB9FAB548D2F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hangover", "Hangover\Hangover.csproj", "{40C67036-C1A7-4FDF-AA83-8EC902E257F3}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HangoverWinForms", "HangoverWinForms\HangoverWinForms.csproj", "{40C67036-C1A7-4FDF-AA83-8EC902E257F3}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibationAvalonia", "LibationAvalonia\LibationAvalonia.csproj", "{F612D06F-3134-4B9B-95CD-EB3FC798AE60}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HangoverAvalonia", "HangoverAvalonia\HangoverAvalonia.csproj", "{8A7B01D3-9830-44FD-91A1-D8D010996BEB}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HangoverBase", "HangoverBase\HangoverBase.csproj", "{5C7005BA-7D83-4E99-8073-D970943A7D61}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Demos", "_Demos", "{185AC9FF-381E-4AA1-B649-9771F4917214}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LoadByOS", "LoadByOS", "{59DF46F3-ECD0-43CA-AD12-3FEE8FCF9E4F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CrossPlatformClientExe", "_Demos\LoadByOS\CrossPlatformClientExe\CrossPlatformClientExe.csproj", "{CC275937-DFE4-4383-B1BF-1D5D42B70C98}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinuxConfigApp", "_Demos\LoadByOS\LinuxConfigApp\LinuxConfigApp.csproj", "{47325742-5B38-48E7-95FB-CD94E6E07332}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsConfigApp", "_Demos\LoadByOS\WindowsConfigApp\WindowsConfigApp.csproj", "{0520760A-9CFB-48A8-BCE4-6E951AFD6BE9}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LoadByOS", "LoadByOS", "{9B906374-1142-4D69-86FF-B384806CA5FE}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
LoadByOS\README.txt = LoadByOS\README.txt
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinuxConfigApp", "LoadByOS\LinuxConfigApp\LinuxConfigApp.csproj", "{357DF797-4EC2-4DBD-A4BD-D045277F2666}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MacOSConfigApp", "LoadByOS\MacOSConfigApp\MacOSConfigApp.csproj", "{ECED4E13-B676-4277-8A8F-C8B2507B7D8C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsConfigApp", "LoadByOS\WindowsConfigApp\WindowsConfigApp.csproj", "{5F65A509-26E3-4B02-B403-EEB6F0EF391F}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -156,6 +179,34 @@ Global
|
||||
{8A7B01D3-9830-44FD-91A1-D8D010996BEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8A7B01D3-9830-44FD-91A1-D8D010996BEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8A7B01D3-9830-44FD-91A1-D8D010996BEB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5C7005BA-7D83-4E99-8073-D970943A7D61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5C7005BA-7D83-4E99-8073-D970943A7D61}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5C7005BA-7D83-4E99-8073-D970943A7D61}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5C7005BA-7D83-4E99-8073-D970943A7D61}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CC275937-DFE4-4383-B1BF-1D5D42B70C98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CC275937-DFE4-4383-B1BF-1D5D42B70C98}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CC275937-DFE4-4383-B1BF-1D5D42B70C98}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CC275937-DFE4-4383-B1BF-1D5D42B70C98}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{47325742-5B38-48E7-95FB-CD94E6E07332}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{47325742-5B38-48E7-95FB-CD94E6E07332}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{47325742-5B38-48E7-95FB-CD94E6E07332}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{47325742-5B38-48E7-95FB-CD94E6E07332}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0520760A-9CFB-48A8-BCE4-6E951AFD6BE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0520760A-9CFB-48A8-BCE4-6E951AFD6BE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0520760A-9CFB-48A8-BCE4-6E951AFD6BE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0520760A-9CFB-48A8-BCE4-6E951AFD6BE9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{357DF797-4EC2-4DBD-A4BD-D045277F2666}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{357DF797-4EC2-4DBD-A4BD-D045277F2666}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{357DF797-4EC2-4DBD-A4BD-D045277F2666}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{357DF797-4EC2-4DBD-A4BD-D045277F2666}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{ECED4E13-B676-4277-8A8F-C8B2507B7D8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{ECED4E13-B676-4277-8A8F-C8B2507B7D8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{ECED4E13-B676-4277-8A8F-C8B2507B7D8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{ECED4E13-B676-4277-8A8F-C8B2507B7D8C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5F65A509-26E3-4B02-B403-EEB6F0EF391F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5F65A509-26E3-4B02-B403-EEB6F0EF391F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5F65A509-26E3-4B02-B403-EEB6F0EF391F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5F65A509-26E3-4B02-B403-EEB6F0EF391F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -181,6 +232,15 @@ Global
|
||||
{40C67036-C1A7-4FDF-AA83-8EC902E257F3} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{F612D06F-3134-4B9B-95CD-EB3FC798AE60} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{8A7B01D3-9830-44FD-91A1-D8D010996BEB} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{5C7005BA-7D83-4E99-8073-D970943A7D61} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{59DF46F3-ECD0-43CA-AD12-3FEE8FCF9E4F} = {185AC9FF-381E-4AA1-B649-9771F4917214}
|
||||
{CC275937-DFE4-4383-B1BF-1D5D42B70C98} = {59DF46F3-ECD0-43CA-AD12-3FEE8FCF9E4F}
|
||||
{47325742-5B38-48E7-95FB-CD94E6E07332} = {59DF46F3-ECD0-43CA-AD12-3FEE8FCF9E4F}
|
||||
{0520760A-9CFB-48A8-BCE4-6E951AFD6BE9} = {59DF46F3-ECD0-43CA-AD12-3FEE8FCF9E4F}
|
||||
{9B906374-1142-4D69-86FF-B384806CA5FE} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
|
||||
{357DF797-4EC2-4DBD-A4BD-D045277F2666} = {9B906374-1142-4D69-86FF-B384806CA5FE}
|
||||
{ECED4E13-B676-4277-8A8F-C8B2507B7D8C} = {9B906374-1142-4D69-86FF-B384806CA5FE}
|
||||
{5F65A509-26E3-4B02-B403-EEB6F0EF391F} = {9B906374-1142-4D69-86FF-B384806CA5FE}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}
|
||||
|
||||
@@ -29,33 +29,6 @@ namespace LibationAvalonia
|
||||
public static Stream OpenAsset(string assetRelativePath)
|
||||
=> AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath));
|
||||
|
||||
|
||||
public static bool GoToFile(string path)
|
||||
=> AppScaffolding.LibationScaffolding.IsWindows ? Go.To.File(path)
|
||||
: GoToFolder(path is null ? string.Empty : Path.GetDirectoryName(path));
|
||||
|
||||
public static bool GoToFolder(string path)
|
||||
{
|
||||
if (AppScaffolding.LibationScaffolding.IsWindows)
|
||||
return Go.To.Folder(path);
|
||||
else if (AppScaffolding.LibationScaffolding.IsLinux)
|
||||
{
|
||||
var startInfo = new System.Diagnostics.ProcessStartInfo()
|
||||
{
|
||||
FileName = "/bin/xdg-open",
|
||||
Arguments = path is null ? string.Empty : $"\"{path}\"",
|
||||
UseShellExecute = false, //Import in Linux environments
|
||||
CreateNoWindow = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
System.Diagnostics.Process.Start(startInfo);
|
||||
return true;
|
||||
}
|
||||
//Don't know how to do this for mac yet
|
||||
else return true;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
|
||||
@@ -9,4 +9,8 @@
|
||||
<SolidColorBrush x:Key="ProcessQueueBookDefaultBrush" Color="{StaticResource SystemAltHighColor}" />
|
||||
<SolidColorBrush x:Key="ProcessQueueBookBorderBrush" Color="Gray" />
|
||||
</Styles.Resources>
|
||||
<Style Selector="TextBox[IsReadOnly=true]">
|
||||
<Setter Property="Background" Value="LightGray" />
|
||||
<Setter Property="CaretBrush" Value="#00000000" />
|
||||
</Style>
|
||||
</Styles>
|
||||
@@ -66,7 +66,7 @@
|
||||
<DataGridTextColumn
|
||||
Width="2*"
|
||||
Binding="{Binding AccountId, Mode=TwoWay}"
|
||||
Header="Autible
email/login"/>
|
||||
Header="Audible
email/login"/>
|
||||
|
||||
<DataGridTemplateColumn Width="Auto" Header="Locale">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
|
||||
@@ -49,8 +49,8 @@ namespace LibationAvalonia.Dialogs
|
||||
}
|
||||
|
||||
protected override void SaveAndClose()
|
||||
{
|
||||
SaveButton_Clicked(null, null);
|
||||
{
|
||||
LibraryBook.Book.UpdateUserDefinedItem(NewTags, bookStatus: BookLiberatedStatus, pdfStatus: PdfLiberatedStatus);
|
||||
base.SaveAndClose();
|
||||
}
|
||||
|
||||
@@ -62,11 +62,9 @@ namespace LibationAvalonia.Dialogs
|
||||
}
|
||||
|
||||
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
LibraryBook.Book.UpdateBook(NewTags, bookStatus: BookLiberatedStatus, pdfStatus: PdfLiberatedStatus);
|
||||
}
|
||||
=> SaveAndClose();
|
||||
|
||||
private void InitializeComponent()
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
@@ -122,7 +120,7 @@ Narrator(s): {libraryBook.Book.NarratorNames()}
|
||||
Length: {(libraryBook.Book.LengthInMinutes == 0 ? "" : $"{libraryBook.Book.LengthInMinutes / 60} hr {libraryBook.Book.LengthInMinutes % 60} min")}
|
||||
Audio Bitrate: {libraryBook.Book.AudioFormat}
|
||||
Category: {string.Join(" > ", libraryBook.Book.CategoriesNames())}
|
||||
Purchase Date: {libraryBook.DateAdded.ToString("d")}
|
||||
Purchase Date: {libraryBook.DateAdded:d}
|
||||
Audible ID: {libraryBook.Book.AudibleProductId}
|
||||
".Trim();
|
||||
|
||||
|
||||
@@ -138,6 +138,7 @@ namespace LibationAvalonia.Dialogs
|
||||
AudibleProductId = "123456789",
|
||||
Title = "A Study in Scarlet: A Sherlock Holmes Novel",
|
||||
Locale = "us",
|
||||
YearPublished = 2017,
|
||||
Authors = new List<string> { "Arthur Conan Doyle", "Stephen Fry - introductions" },
|
||||
Narrators = new List<string> { "Stephen Fry" },
|
||||
SeriesName = "Sherlock Holmes",
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace LibationAvalonia.Dialogs
|
||||
saveFileDialog.Filters.Add(new FileDialogFilter { Name = "Jpeg", Extensions = new System.Collections.Generic.List<string>() { "jpg" } });
|
||||
saveFileDialog.InitialFileName = PictureFileName;
|
||||
saveFileDialog.Directory
|
||||
= !AppScaffolding.LibationScaffolding.IsWindows ? null
|
||||
= !LibationFileManager.Configuration.IsWindows ? null
|
||||
: Directory.Exists(BookSaveDirectory) ? BookSaveDirectory
|
||||
: Path.GetDirectoryName(BookSaveDirectory);
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="120"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
x:Class="LibationAvalonia.Dialogs.LiberatedStatusBatchAutoDialog"
|
||||
Title="Liberated status: Whether the book has been downloaded"
|
||||
MinHeight="100" MaxHeight="165"
|
||||
MinWidth="600" MaxWidth="800"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Icon="/Assets/libation.ico">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto">
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<CheckBox
|
||||
Margin="0,0,0,10"
|
||||
IsChecked="{Binding SetDownloaded, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="If the audio file can be found, set download status to 'Downloaded'" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<CheckBox
|
||||
Margin="0,0,0,10"
|
||||
IsChecked="{Binding SetNotDownloaded, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="If the audio file cannot be found, set download status to 'Not Downloaded'" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
|
||||
<Button
|
||||
Grid.Row="2"
|
||||
Padding="30,0,30,0"
|
||||
Margin="10,0,10,10"
|
||||
HorizontalAlignment="Right"
|
||||
Height="25"
|
||||
Content="Save"
|
||||
Click="SaveButton_Clicked"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,23 @@
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class LiberatedStatusBatchAutoDialog : DialogWindow
|
||||
{
|
||||
public bool SetDownloaded { get; set; }
|
||||
public bool SetNotDownloaded { get; set; }
|
||||
|
||||
public LiberatedStatusBatchAutoDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = this;
|
||||
}
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> SaveAndClose();
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="120"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
x:Class="LibationAvalonia.Dialogs.LiberatedStatusBatchDialog"
|
||||
x:Class="LibationAvalonia.Dialogs.LiberatedStatusBatchManualDialog"
|
||||
Title="Liberated status: Whether the book has been downloaded"
|
||||
MinWidth="400" MinHeight="120"
|
||||
MaxWidth="400" MaxHeight="120"
|
||||
@@ -5,7 +5,7 @@ using System.Collections.Generic;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
public partial class LiberatedStatusBatchDialog : DialogWindow
|
||||
public partial class LiberatedStatusBatchManualDialog : DialogWindow
|
||||
{
|
||||
private class liberatedComboBoxItem
|
||||
{
|
||||
@@ -34,7 +34,7 @@ namespace LibationAvalonia.Dialogs
|
||||
new liberatedComboBoxItem { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" },
|
||||
};
|
||||
|
||||
public LiberatedStatusBatchDialog()
|
||||
public LiberatedStatusBatchManualDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
SelectedItem = BookStatuses[0] as liberatedComboBoxItem;
|
||||
@@ -52,7 +52,7 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
try
|
||||
{
|
||||
App.GoToFolder(dir.ShortPathName);
|
||||
Go.To.Folder(dir.ShortPathName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
|
||||
MinWidth="800" MinHeight="600"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="620"
|
||||
MinWidth="800" MinHeight="620"
|
||||
x:Class="LibationAvalonia.Dialogs.SettingsDialog"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
Title="Edit Settings"
|
||||
@@ -206,7 +206,7 @@
|
||||
BorderThickness="2"
|
||||
BorderBrush="{DynamicResource DataGridGridLinesBrush}">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*">
|
||||
<controls:GroupBox
|
||||
Grid.Row="0"
|
||||
Margin="5"
|
||||
@@ -367,6 +367,20 @@
|
||||
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<CheckBox
|
||||
Grid.Row="3"
|
||||
Margin="5"
|
||||
VerticalAlignment="Top"
|
||||
IsVisible="{Binding IsWindows}"
|
||||
IsChecked="{Binding DownloadDecryptSettings.UseCoverAsFolderIcon, Mode=TwoWay}">
|
||||
|
||||
<TextBlock
|
||||
TextWrapping="Wrap"
|
||||
Text="{Binding DownloadDecryptSettings.UseCoverAsFolderIconText}" />
|
||||
|
||||
</CheckBox>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</TabItem>
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
public void OpenLogFolderButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
App.GoToFolder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
|
||||
Go.To.Folder(((LongPath)Configuration.Instance.LibationFiles).ShortPathName);
|
||||
}
|
||||
|
||||
public async void EditFolderTemplateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
@@ -107,6 +107,7 @@ namespace LibationAvalonia.Dialogs
|
||||
LoadSettings(config);
|
||||
}
|
||||
|
||||
public bool IsWindows => AppScaffolding.LibationScaffolding.ReleaseIdentifier is AppScaffolding.ReleaseIdentifier.WindowsAvalonia;
|
||||
public ImportantSettings ImportantSettings { get; private set; }
|
||||
public ImportSettings ImportSettings { get; private set; }
|
||||
public DownloadDecryptSettings DownloadDecryptSettings { get; private set; }
|
||||
@@ -257,6 +258,7 @@ namespace LibationAvalonia.Dialogs
|
||||
InProgressDirectory
|
||||
= config.InProgress == Configuration.AppDir_Absolute ? Configuration.KnownDirectories.AppDir
|
||||
: Configuration.GetKnownDirectory(config.InProgress);
|
||||
UseCoverAsFolderIcon = config.UseCoverAsFolderIcon;
|
||||
}
|
||||
|
||||
public async Task<bool> SaveSettingsAsync(Configuration config)
|
||||
@@ -294,9 +296,12 @@ namespace LibationAvalonia.Dialogs
|
||||
= InProgressDirectory is Configuration.KnownDirectories.AppDir ? Configuration.AppDir_Absolute
|
||||
: Configuration.GetKnownDirectoryPath(InProgressDirectory);
|
||||
|
||||
config.UseCoverAsFolderIcon = UseCoverAsFolderIcon;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public string UseCoverAsFolderIconText { get; } = Configuration.GetDescription(nameof(Configuration.UseCoverAsFolderIcon));
|
||||
public string BadBookGroupboxText { get; } = Configuration.GetDescription(nameof(Configuration.BadBook));
|
||||
public string BadBookAskText { get; } = Configuration.BadBookAction.Ask.GetDescription();
|
||||
public string BadBookAbortText { get; } = Configuration.BadBookAction.Abort.GetDescription();
|
||||
@@ -311,6 +316,7 @@ namespace LibationAvalonia.Dialogs
|
||||
public string FolderTemplate { get => _folderTemplate; set { this.RaiseAndSetIfChanged(ref _folderTemplate, value); } }
|
||||
public string FileTemplate { get => _fileTemplate; set { this.RaiseAndSetIfChanged(ref _fileTemplate, value); } }
|
||||
public string ChapterFileTemplate { get => _chapterFileTemplate; set { this.RaiseAndSetIfChanged(ref _chapterFileTemplate, value); } }
|
||||
public bool UseCoverAsFolderIcon { get; set; }
|
||||
|
||||
public bool BadBookAsk
|
||||
{
|
||||
@@ -381,7 +387,7 @@ namespace LibationAvalonia.Dialogs
|
||||
private int _lameVBRQuality;
|
||||
private string _chapterTitleTemplate;
|
||||
|
||||
public bool IsMp3Supported => AppScaffolding.LibationScaffolding.IsLinux || AppScaffolding.LibationScaffolding.IsWindows;
|
||||
public bool IsMp3Supported => Configuration.IsLinux || Configuration.IsWindows;
|
||||
|
||||
public AudioSettings(Configuration config)
|
||||
{
|
||||
|
||||
@@ -113,7 +113,7 @@ namespace LibationAvalonia
|
||||
|
||||
public static void HideMinMaxBtns(this Window form)
|
||||
{
|
||||
if (Design.IsDesignMode || !AppScaffolding.LibationScaffolding.IsWindows)
|
||||
if (Design.IsDesignMode || !Configuration.IsWindows)
|
||||
return;
|
||||
var handle = form.PlatformImpl.Handle.Handle;
|
||||
var currentStyle = GetWindowLong(handle, GWL_STYLE);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<!--Avalonia doesen't support TrimMode=link currently,but we are working on that https://github.com/AvaloniaUI/Avalonia/issues/6892 -->
|
||||
<TrimMode>copyused</TrimMode>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
@@ -92,6 +92,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Dialogs\LiberatedStatusBatchAutoDialog.axaml.cs">
|
||||
<DependentUpon>LiberatedStatusBatchAutoDialog.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Dialogs\LiberatedStatusBatchManualDialog.axaml.cs">
|
||||
<DependentUpon>LiberatedStatusBatchManualDialog.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Views\ProcessBookControl.axaml.cs">
|
||||
<DependentUpon>ProcessBookControl.axaml</DependentUpon>
|
||||
</Compile>
|
||||
@@ -124,12 +130,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="0.10.17" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.17" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.17" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.17" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.17" />
|
||||
<PackageReference Include="XamlNameReferenceGenerator" Version="1.3.4" />
|
||||
<PackageReference Include="Avalonia" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.18" />
|
||||
<PackageReference Include="XamlNameReferenceGenerator" Version="1.5.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -145,7 +151,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<Target Name="SpicNSpan" AfterTargets="Clean">
|
||||
<Target Name="SpicNSpan" AfterTargets="Clean">
|
||||
<!-- Remove obj folder -->
|
||||
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
|
||||
<!-- Remove bin folder -->
|
||||
|
||||
@@ -102,7 +102,10 @@ Libation.
|
||||
".Trim(), "Verbose logging enabled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
|
||||
public static async Task<DialogResult> ShowConfirmationDialog(Window owner, IEnumerable<LibraryBook> libraryBooks, string format, string title, MessageBoxDefaultButton defaultButton = MessageBoxDefaultButton.Button1)
|
||||
/// <summary>
|
||||
/// Note: the format field should use {0} and NOT use the `$` string interpolation. Formatting is done inside this method.
|
||||
/// </summary>
|
||||
public static async Task<DialogResult> ShowConfirmationDialog(Window owner, IEnumerable<LibraryBook> libraryBooks, string format, string title, MessageBoxDefaultButton defaultButton = MessageBoxDefaultButton.Button1)
|
||||
{
|
||||
if (libraryBooks is null || !libraryBooks.Any())
|
||||
return DialogResult.Cancel;
|
||||
|
||||
@@ -30,11 +30,11 @@ namespace LibationAvalonia
|
||||
var classicLifetimeTask = Task.Run(() => new ClassicDesktopStyleApplicationLifetime());
|
||||
var appBuilderTask = Task.Run(BuildAvaloniaApp);
|
||||
|
||||
if (AppScaffolding.LibationScaffolding.IsWindows)
|
||||
if (Configuration.IsWindows)
|
||||
AppScaffolding.LibationScaffolding.SetReleaseIdentifier(AppScaffolding.ReleaseIdentifier.WindowsAvalonia);
|
||||
else if (AppScaffolding.LibationScaffolding.IsLinux)
|
||||
else if (Configuration.IsLinux)
|
||||
AppScaffolding.LibationScaffolding.SetReleaseIdentifier(AppScaffolding.ReleaseIdentifier.LinuxAvalonia);
|
||||
else if (AppScaffolding.LibationScaffolding.IsMacOs)
|
||||
else if (Configuration.IsMacOs)
|
||||
AppScaffolding.LibationScaffolding.SetReleaseIdentifier(AppScaffolding.ReleaseIdentifier.MacOSAvalonia);
|
||||
else return;
|
||||
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Release\linux-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Publish\Linux-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -3,15 +3,15 @@
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Release\macos-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Publish\MacOS-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -6,9 +6,9 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Release\win-chardonnay</PublishDir>
|
||||
<PublishDir>..\bin\Publish\Windows-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace LibationAvalonia.ViewModels
|
||||
private int _visibleCount = 1;
|
||||
private LibraryCommands.LibraryStats _libraryStats;
|
||||
private int _visibleNotLiberated = 1;
|
||||
public bool IsMp3Supported => AppScaffolding.LibationScaffolding.IsLinux || AppScaffolding.LibationScaffolding.IsWindows;
|
||||
public bool IsMp3Supported => Configuration.IsLinux || Configuration.IsWindows;
|
||||
|
||||
/// <summary> The Process Queue's viewmodel </summary>
|
||||
public ProcessQueueViewModel ProcessQueue { get; } = new ProcessQueueViewModel();
|
||||
|
||||
@@ -7,6 +7,7 @@ using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using FileLiberator;
|
||||
using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
@@ -289,25 +290,36 @@ namespace LibationAvalonia.ViewModels
|
||||
Logger.Info($"{((Processable)sender).Name} Step, Completed: {libraryBook.Book}");
|
||||
UnlinkProcessable((Processable)sender);
|
||||
|
||||
if (Processes.Count > 0)
|
||||
{
|
||||
NextProcessable();
|
||||
LinkProcessable(CurrentProcessable);
|
||||
var result = await CurrentProcessable.ProcessSingleAsync(libraryBook, validate: true);
|
||||
if (Processes.Count == 0)
|
||||
{
|
||||
Completed?.Invoke(this, EventArgs.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.HasErrors)
|
||||
{
|
||||
foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed"))
|
||||
Logger.Error(errorMessage);
|
||||
NextProcessable();
|
||||
LinkProcessable(CurrentProcessable);
|
||||
|
||||
Completed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
else
|
||||
StatusHandler result;
|
||||
try
|
||||
{
|
||||
Completed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
result = await CurrentProcessable.ProcessSingleAsync(libraryBook, validate: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, $"{nameof(Processable_Completed)} error");
|
||||
|
||||
result = new StatusHandler();
|
||||
result.AddError($"{nameof(Processable_Completed)} error. See log for details. Error summary: {ex.Message}");
|
||||
}
|
||||
|
||||
if (result.HasErrors)
|
||||
{
|
||||
foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed"))
|
||||
Logger.Error(errorMessage);
|
||||
|
||||
Completed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -247,7 +247,8 @@ namespace LibationAvalonia.ViewModels
|
||||
var result = await MessageBox.ShowConfirmationDialog(
|
||||
null,
|
||||
libraryBooks,
|
||||
$"Are you sure you want to remove {selectedBooks.Count} books from Libation's library?",
|
||||
// do not use `$` string interpolation. See impl.
|
||||
"Are you sure you want to remove {0} from Libation's library?",
|
||||
"Remove books from Libation?");
|
||||
|
||||
if (result != DialogResult.Yes)
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace LibationAvalonia.Views
|
||||
// liberated: open explorer to file
|
||||
var filePath = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
|
||||
|
||||
if (!App.GoToFile(filePath?.ShortPathName))
|
||||
if (!Go.To.File(filePath?.ShortPathName))
|
||||
{
|
||||
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
|
||||
await MessageBox.Show($"File not found" + suffix);
|
||||
|
||||
@@ -15,6 +15,6 @@ namespace LibationAvalonia.Views
|
||||
=> await new Dialogs.SettingsDialog().ShowDialog(this);
|
||||
|
||||
public async void aboutToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> await MessageBox.Show($"Running Libation version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}");
|
||||
=> await MessageBox.Show($"Libation {AppScaffolding.LibationScaffolding.Variety}{Environment.NewLine}Version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using ApplicationServices;
|
||||
using Avalonia.Threading;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -50,20 +52,19 @@ namespace LibationAvalonia.Views
|
||||
var confirmationResult = await MessageBox.ShowConfirmationDialog(
|
||||
this,
|
||||
visibleLibraryBooks,
|
||||
"Are you sure you want to replace tags in {0}?",
|
||||
// do not use `$` string interpolation. See impl.
|
||||
"Are you sure you want to replace tags in {0}?",
|
||||
"Replace tags?");
|
||||
|
||||
if (confirmationResult != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
foreach (var libraryBook in visibleLibraryBooks)
|
||||
libraryBook.Book.UserDefinedItem.Tags = dialog.NewTags;
|
||||
LibraryCommands.UpdateUserDefinedItem(visibleLibraryBooks.Select(lb => lb.Book));
|
||||
}
|
||||
visibleLibraryBooks.UpdateTags(dialog.NewTags);
|
||||
}
|
||||
|
||||
public async void setDownloadedToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
|
||||
public async void setDownloadedManualToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
|
||||
{
|
||||
var dialog = new Dialogs.LiberatedStatusBatchDialog();
|
||||
var dialog = new Dialogs.LiberatedStatusBatchManualDialog();
|
||||
var result = await dialog.ShowDialog<DialogResult>(this);
|
||||
if (result != DialogResult.OK)
|
||||
return;
|
||||
@@ -73,25 +74,51 @@ namespace LibationAvalonia.Views
|
||||
var confirmationResult = await MessageBox.ShowConfirmationDialog(
|
||||
this,
|
||||
visibleLibraryBooks,
|
||||
"Are you sure you want to replace downloaded status in {0}?",
|
||||
// do not use `$` string interpolation. See impl.
|
||||
"Are you sure you want to replace downloaded status in {0}?",
|
||||
"Replace downloaded status?");
|
||||
|
||||
if (confirmationResult != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
foreach (var libraryBook in visibleLibraryBooks)
|
||||
libraryBook.Book.UserDefinedItem.BookStatus = dialog.BookLiberatedStatus;
|
||||
LibraryCommands.UpdateUserDefinedItem(visibleLibraryBooks.Select(lb => lb.Book));
|
||||
}
|
||||
visibleLibraryBooks.UpdateBookStatus(dialog.BookLiberatedStatus);
|
||||
}
|
||||
|
||||
public async void removeToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
|
||||
public async void setDownloadedAutoToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
|
||||
{
|
||||
var dialog = new Dialogs.LiberatedStatusBatchAutoDialog();
|
||||
var result = await dialog.ShowDialog<DialogResult>(this);
|
||||
if (result != DialogResult.OK)
|
||||
return;
|
||||
|
||||
var bulkSetStatus = new BulkSetDownloadStatus(_viewModel.ProductsDisplay.GetVisibleBookEntries(), dialog.SetDownloaded, dialog.SetNotDownloaded);
|
||||
var count = await Task.Run(() => bulkSetStatus.Discover());
|
||||
|
||||
if (count == 0)
|
||||
return;
|
||||
|
||||
var confirmationResult = await MessageBox.Show(
|
||||
bulkSetStatus.AggregateMessage,
|
||||
"Replace downloaded status?",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Question,
|
||||
MessageBoxDefaultButton.Button1);
|
||||
|
||||
if (confirmationResult != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
bulkSetStatus.Execute();
|
||||
}
|
||||
|
||||
public async void removeToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
|
||||
{
|
||||
var visibleLibraryBooks = _viewModel.ProductsDisplay.GetVisibleBookEntries();
|
||||
|
||||
var confirmationResult = await MessageBox.ShowConfirmationDialog(
|
||||
this,
|
||||
visibleLibraryBooks,
|
||||
"Are you sure you want to remove {0} from Libation's library?",
|
||||
// do not use `$` string interpolation. See impl.
|
||||
"Are you sure you want to remove {0} from Libation's library?",
|
||||
"Remove books from Libation?",
|
||||
MessageBoxDefaultButton.Button2);
|
||||
|
||||
|
||||
@@ -110,7 +110,8 @@
|
||||
</MenuItem.Styles>
|
||||
<MenuItem Click="liberateVisible" Header="{Binding LiberateVisibleToolStripText_2}" IsEnabled="{Binding AnyVisibleNotLiberated}" />
|
||||
<MenuItem Click="replaceTagsToolStripMenuItem_Click" Header="Replace _Tags..." />
|
||||
<MenuItem Click="setDownloadedToolStripMenuItem_Click" Header="Set '_Downloaded' status..." />
|
||||
<MenuItem Click="setDownloadedManualToolStripMenuItem_Click" Header="Set '_Downloaded' status manually..." />
|
||||
<MenuItem Click="setDownloadedAutoToolStripMenuItem_Click" Header="Set '_Downloaded' status automatically..." />
|
||||
<MenuItem Click="removeToolStripMenuItem_Click" Header="_Remove from library..." />
|
||||
</MenuItem>
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace LibationAvalonia.Views
|
||||
#if !DEBUG
|
||||
//This is temporaty until we have a solution for linux/mac so that
|
||||
//Libation doesn't download a zip every time it runs.
|
||||
if (!LibationScaffolding.IsWindows)
|
||||
if (!Configuration.IsWindows)
|
||||
return;
|
||||
|
||||
try
|
||||
@@ -85,15 +85,15 @@ namespace LibationAvalonia.Views
|
||||
if (result != DialogResult.Yes)
|
||||
return;
|
||||
|
||||
if (LibationScaffolding.IsWindows)
|
||||
{
|
||||
if (Configuration.IsWindows)
|
||||
{
|
||||
runWindowsUpgrader(zipFile);
|
||||
}
|
||||
else if (LibationScaffolding.IsLinux)
|
||||
else if (Configuration.IsLinux)
|
||||
{
|
||||
|
||||
}
|
||||
else if (LibationScaffolding.IsMacOs)
|
||||
else if (Configuration.IsMacOs)
|
||||
{
|
||||
|
||||
}
|
||||
@@ -149,8 +149,7 @@ namespace LibationAvalonia.Views
|
||||
|
||||
private void runWindowsUpgrader(string zipFile)
|
||||
{
|
||||
|
||||
var thisExe = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
|
||||
var thisExe = Environment.ProcessPath;
|
||||
var thisDir = System.IO.Path.GetDirectoryName(thisExe);
|
||||
|
||||
var args = $"--input {zipFile} --output {thisDir} --executable {thisExe}";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
@@ -44,7 +44,7 @@
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="SpicNSpan" AfterTargets="Clean">
|
||||
<Target Name="SpicNSpan" AfterTargets="Clean">
|
||||
<!-- Remove obj folder -->
|
||||
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
|
||||
<!-- Remove bin folder -->
|
||||
|
||||
@@ -8,7 +8,7 @@ using CommandLine;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
[Verb("export", HelpText = "Must include path and flag for export file type: --xlsx , --csv , --json]")]
|
||||
[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.")]
|
||||
|
||||
@@ -25,9 +25,10 @@ namespace LibationCli
|
||||
var downloadPdf = CreateProcessable<DownloadPdf>();
|
||||
|
||||
//Chain pdf download on DownloadDecryptBook.Completed
|
||||
async void onDownloadDecryptBookCompleted(object sender, LibraryBook e)
|
||||
void onDownloadDecryptBookCompleted(object sender, LibraryBook e)
|
||||
{
|
||||
await downloadPdf.TryProcessAsync(e);
|
||||
// this is fast anyway. run as sync for easy exception catching
|
||||
downloadPdf.TryProcessAsync(e).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
var downloadDecryptBook = CreateProcessable<DownloadDecryptBook>(onDownloadDecryptBookCompleted);
|
||||
|
||||
37
Source/LibationCli/Options/SetDownloadStatusOptions.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using CommandLine;
|
||||
|
||||
namespace LibationCli
|
||||
{
|
||||
[Verb("set-status", HelpText = """
|
||||
Set download statuses throughout library based on whether each book's audio file can be found.
|
||||
Must include at least one flag: --downloaded , --not-downloaded.
|
||||
Downloaded: If the audio file can be found, set download status to 'Downloaded'.
|
||||
Not Downloaded: If the audio file cannot be found, set download status to 'Not Downloaded'
|
||||
""")]
|
||||
public class SetDownloadStatusOptions : OptionsBase
|
||||
{
|
||||
[Option(shortName: 'd', longName: "downloaded", Required = true)]
|
||||
public bool SetDownloaded { get; set; }
|
||||
|
||||
[Option(shortName: 'n', longName: "not-downloaded", Required = true)]
|
||||
public bool SetNotDownloaded { get; set; }
|
||||
|
||||
protected override async Task ProcessAsync()
|
||||
{
|
||||
var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
|
||||
|
||||
var bulkSetStatus = new BulkSetDownloadStatus(libraryBooks, SetDownloaded, SetNotDownloaded);
|
||||
await Task.Run(() => bulkSetStatus.Discover());
|
||||
bulkSetStatus.Execute();
|
||||
|
||||
foreach (var msg in bulkSetStatus.Messages)
|
||||
Console.WriteLine(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,17 @@ namespace LibationCli
|
||||
strProc.Begin += (o, e) => Console.WriteLine($"{typeof(TProcessable).Name} Begin: {e}");
|
||||
strProc.Completed += (o, e) => Console.WriteLine($"{typeof(TProcessable).Name} Completed: {e}");
|
||||
|
||||
strProc.Completed += completedAction;
|
||||
strProc.Completed += (s, e) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
completedAction?.Invoke(s, e);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "CLI error");
|
||||
}
|
||||
};
|
||||
|
||||
return strProc;
|
||||
}
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Release\linux-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Publish\Linux-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -3,15 +3,15 @@
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Release\macos-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Publish\MacOS-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -3,15 +3,15 @@
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Release\win-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Publish\Windows-chardonnay</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -113,6 +113,5 @@ namespace LibationFileManager
|
||||
public void Refresh() => BookDirectoryFiles.RefreshFiles();
|
||||
|
||||
public LongPath GetPath(string productId) => GetFilePath(productId);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
21
Source/LibationFileManager/Configuration.Environment.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration
|
||||
{
|
||||
public static bool IsWindows { get; } = OperatingSystem.IsWindows();
|
||||
public static bool IsLinux { get; } = OperatingSystem.IsLinux();
|
||||
public static bool IsMacOs { get; } = OperatingSystem.IsMacOS();
|
||||
|
||||
public static string OS { get; }
|
||||
= IsLinux ? "Linux"
|
||||
: IsMacOs ? "MacOS"
|
||||
: IsWindows ? "Windows"
|
||||
: "UNKNOWN_OS";
|
||||
}
|
||||
}
|
||||
66
Source/LibationFileManager/Configuration.KnownDirectories.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration
|
||||
{
|
||||
public static string AppDir_Relative => $@".{Path.PathSeparator}{LIBATION_FILES_KEY}";
|
||||
public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Exe.FileLocationOnDisk), LIBATION_FILES_KEY));
|
||||
public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation"));
|
||||
public static string WinTemp => Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation"));
|
||||
public static string UserProfile => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Libation"));
|
||||
|
||||
public enum KnownDirectories
|
||||
{
|
||||
None = 0,
|
||||
|
||||
[Description("My Users folder")]
|
||||
UserProfile = 1,
|
||||
|
||||
[Description("The same folder that Libation is running from")]
|
||||
AppDir = 2,
|
||||
|
||||
[Description("Windows temporary folder")]
|
||||
WinTemp = 3,
|
||||
|
||||
[Description("My Documents")]
|
||||
MyDocs = 4,
|
||||
|
||||
[Description("Your settings folder (aka: Libation Files)")]
|
||||
LibationFiles = 5
|
||||
}
|
||||
// use func calls so we always get the latest value of LibationFiles
|
||||
private static List<(KnownDirectories directory, Func<string> getPathFunc)> directoryOptionsPaths { get; } = new()
|
||||
{
|
||||
(KnownDirectories.None, () => null),
|
||||
(KnownDirectories.UserProfile, () => UserProfile),
|
||||
(KnownDirectories.AppDir, () => AppDir_Relative),
|
||||
(KnownDirectories.WinTemp, () => WinTemp),
|
||||
(KnownDirectories.MyDocs, () => MyDocs),
|
||||
// this is important to not let very early calls try to accidentally load LibationFiles too early.
|
||||
// also, keep this at bottom of this list
|
||||
(KnownDirectories.LibationFiles, () => libationFilesPathCache)
|
||||
};
|
||||
public static string GetKnownDirectoryPath(KnownDirectories directory)
|
||||
{
|
||||
var dirFunc = directoryOptionsPaths.SingleOrDefault(dirFunc => dirFunc.directory == directory);
|
||||
return dirFunc == default ? null : dirFunc.getPathFunc();
|
||||
}
|
||||
public static KnownDirectories GetKnownDirectory(string directory)
|
||||
{
|
||||
// especially important so a very early call doesn't match null => LibationFiles
|
||||
if (string.IsNullOrWhiteSpace(directory))
|
||||
return KnownDirectories.None;
|
||||
|
||||
// 'First' instead of 'Single' because LibationFiles could match other directories. eg: default value of LibationFiles == UserProfile.
|
||||
// since it's a list, order matters and non-LibationFiles will be returned first
|
||||
var dirFunc = directoryOptionsPaths.FirstOrDefault(dirFunc => dirFunc.getPathFunc() == directory);
|
||||
return dirFunc == default ? KnownDirectories.None : dirFunc.directory;
|
||||
}
|
||||
}
|
||||
}
|
||||
113
Source/LibationFileManager/Configuration.LibationFiles.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using FileManager;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Serilog;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration
|
||||
{
|
||||
private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "appsettings.json");
|
||||
private const string LIBATION_FILES_KEY = "LibationFiles";
|
||||
|
||||
[Description("Location for storage of program-created files")]
|
||||
public string LibationFiles
|
||||
{
|
||||
get
|
||||
{
|
||||
if (libationFilesPathCache is not null)
|
||||
return libationFilesPathCache;
|
||||
|
||||
// FIRST: must write here before SettingsFilePath in next step reads cache
|
||||
libationFilesPathCache = getLibationFilesSettingFromJson();
|
||||
|
||||
// SECOND. before setting to json file with SetWithJsonPath, PersistentDictionary must exist
|
||||
persistentDictionary = new PersistentDictionary(SettingsFilePath);
|
||||
|
||||
// Config init in ensureSerilogConfig() only happens when serilog setting is first created (prob on 1st run).
|
||||
// This Set() enforces current LibationFiles every time we restart Libation or redirect LibationFiles
|
||||
var logPath = Path.Combine(LibationFiles, "Log.log");
|
||||
|
||||
// BAD: Serilog.WriteTo[1].Args
|
||||
// "[1]" assumes ordinal position
|
||||
// GOOD: Serilog.WriteTo[?(@.Name=='File')].Args
|
||||
var jsonpath = "Serilog.WriteTo[?(@.Name=='File')].Args";
|
||||
|
||||
SetWithJsonPath(jsonpath, "path", logPath, true);
|
||||
|
||||
return libationFilesPathCache;
|
||||
}
|
||||
}
|
||||
|
||||
private static string libationFilesPathCache { get; set; }
|
||||
|
||||
private string getLibationFilesSettingFromJson()
|
||||
{
|
||||
string startingContents = null;
|
||||
try
|
||||
{
|
||||
if (File.Exists(APPSETTINGS_JSON))
|
||||
{
|
||||
startingContents = File.ReadAllText(APPSETTINGS_JSON);
|
||||
var startingJObj = JObject.Parse(startingContents);
|
||||
|
||||
if (startingJObj.ContainsKey(LIBATION_FILES_KEY))
|
||||
{
|
||||
var startingValue = startingJObj[LIBATION_FILES_KEY].Value<string>();
|
||||
if (!string.IsNullOrWhiteSpace(startingValue))
|
||||
return startingValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// not found. write to file. read from file
|
||||
var endingContents = new JObject { { LIBATION_FILES_KEY, UserProfile.ToString() } }.ToString(Formatting.Indented);
|
||||
if (startingContents != endingContents)
|
||||
{
|
||||
File.WriteAllText(APPSETTINGS_JSON, endingContents);
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
|
||||
// do not check whether directory exists. special/meta directory (eg: AppDir) is valid
|
||||
// verify from live file. no try/catch. want failures to be visible
|
||||
var jObjFinal = JObject.Parse(File.ReadAllText(APPSETTINGS_JSON));
|
||||
var valueFinal = jObjFinal[LIBATION_FILES_KEY].Value<string>();
|
||||
return valueFinal;
|
||||
}
|
||||
|
||||
public void SetLibationFiles(string directory)
|
||||
{
|
||||
// ensure exists
|
||||
if (!File.Exists(APPSETTINGS_JSON))
|
||||
{
|
||||
// getter creates new file, loads PersistentDictionary
|
||||
var _ = LibationFiles;
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
|
||||
libationFilesPathCache = null;
|
||||
|
||||
var startingContents = File.ReadAllText(APPSETTINGS_JSON);
|
||||
var jObj = JObject.Parse(startingContents);
|
||||
|
||||
jObj[LIBATION_FILES_KEY] = directory;
|
||||
|
||||
var endingContents = JsonConvert.SerializeObject(jObj, Formatting.Indented);
|
||||
if (startingContents == endingContents)
|
||||
return;
|
||||
|
||||
// now it's set in the file again but no settings have moved yet
|
||||
File.WriteAllText(APPSETTINGS_JSON, endingContents);
|
||||
|
||||
try
|
||||
{
|
||||
Log.Logger.Information("Libation files changed {@DebugInfo}", new { APPSETTINGS_JSON, LIBATION_FILES_KEY, directory });
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
58
Source/LibationFileManager/Configuration.Logging.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Logging;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration
|
||||
{
|
||||
private IConfigurationRoot configuration;
|
||||
|
||||
public void ConfigureLogging()
|
||||
{
|
||||
configuration = new ConfigurationBuilder()
|
||||
.AddJsonFile(SettingsFilePath, optional: false, reloadOnChange: true)
|
||||
.Build();
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(configuration)
|
||||
.CreateLogger();
|
||||
}
|
||||
|
||||
[Description("The importance of a log event")]
|
||||
public LogEventLevel LogLevel
|
||||
{
|
||||
get
|
||||
{
|
||||
var logLevelStr = persistentDictionary.GetStringFromJsonPath("Serilog", "MinimumLevel");
|
||||
return Enum.TryParse<LogEventLevel>(logLevelStr, out var logLevelEnum) ? logLevelEnum : LogEventLevel.Information;
|
||||
}
|
||||
set
|
||||
{
|
||||
var valueWasChanged = persistentDictionary.SetWithJsonPath("Serilog", "MinimumLevel", value.ToString());
|
||||
if (!valueWasChanged)
|
||||
{
|
||||
Log.Logger.Debug("LogLevel.set attempt. No change");
|
||||
return;
|
||||
}
|
||||
|
||||
configuration.Reload();
|
||||
|
||||
Log.Logger.Information("Updated LogLevel MinimumLevel. {@DebugInfo}", new
|
||||
{
|
||||
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
|
||||
LogLevel_Debug_Enabled = Log.Logger.IsDebugEnabled(),
|
||||
LogLevel_Information_Enabled = Log.Logger.IsInformationEnabled(),
|
||||
LogLevel_Warning_Enabled = Log.Logger.IsWarningEnabled(),
|
||||
LogLevel_Error_Enabled = Log.Logger.IsErrorEnabled(),
|
||||
LogLevel_Fatal_Enabled = Log.Logger.IsFatalEnabled()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
323
Source/LibationFileManager/Configuration.PersistentSettings.cs
Normal file
@@ -0,0 +1,323 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using FileManager;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration
|
||||
{
|
||||
// note: any potential file manager static ctors can't compensate if storage dir is changed at run time via settings. this is partly bad architecture. but the side effect is desirable. if changing LibationFiles location: restart app
|
||||
|
||||
// default setting and directory creation occur in class responsible for files.
|
||||
// config class is only responsible for path. not responsible for setting defaults, dir validation, or dir creation
|
||||
// exceptions: appsettings.json, LibationFiles dir, Settings.json
|
||||
|
||||
private PersistentDictionary persistentDictionary;
|
||||
|
||||
public T GetNonString<T>(string propertyName) => persistentDictionary.GetNonString<T>(propertyName);
|
||||
public object GetObject(string propertyName) => persistentDictionary.GetObject(propertyName);
|
||||
public void SetObject(string propertyName, object newValue) => persistentDictionary.SetNonString(propertyName, newValue);
|
||||
|
||||
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
|
||||
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false)
|
||||
{
|
||||
var settingWasChanged = persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging);
|
||||
if (settingWasChanged)
|
||||
configuration?.Reload();
|
||||
}
|
||||
|
||||
public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json");
|
||||
|
||||
public static string GetDescription(string propertyName)
|
||||
{
|
||||
var attribute = typeof(Configuration)
|
||||
.GetProperty(propertyName)
|
||||
?.GetCustomAttributes(typeof(DescriptionAttribute), true)
|
||||
.SingleOrDefault()
|
||||
as DescriptionAttribute;
|
||||
|
||||
return attribute?.Description;
|
||||
}
|
||||
|
||||
public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName);
|
||||
|
||||
[Description("Set cover art as the folder's icon. (Windows only)")]
|
||||
public bool UseCoverAsFolderIcon
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(UseCoverAsFolderIcon));
|
||||
set => persistentDictionary.SetNonString(nameof(UseCoverAsFolderIcon), value);
|
||||
}
|
||||
|
||||
[Description("Use the beta version of Libation\r\nNew and experimental features, but probably buggy.\r\n(requires restart to take effect)")]
|
||||
public bool BetaOptIn
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(BetaOptIn));
|
||||
set => persistentDictionary.SetNonString(nameof(BetaOptIn), value);
|
||||
}
|
||||
|
||||
[Description("Location for book storage. Includes destination of newly liberated books")]
|
||||
public string Books
|
||||
{
|
||||
get => persistentDictionary.GetString(nameof(Books));
|
||||
set => persistentDictionary.SetString(nameof(Books), value);
|
||||
}
|
||||
|
||||
// temp/working dir(s) should be outside of dropbox
|
||||
[Description("Temporary location of files while they're in process of being downloaded and decrypted.\r\nWhen decryption is complete, the final file will be in Books location\r\nRecommend not using a folder which is backed up real time. Eg: Dropbox, iCloud, Google Drive")]
|
||||
public string InProgress
|
||||
{
|
||||
get => persistentDictionary.GetString(nameof(InProgress));
|
||||
set => persistentDictionary.SetString(nameof(InProgress), value);
|
||||
}
|
||||
|
||||
[Description("Allow Libation to fix up audiobook metadata")]
|
||||
public bool AllowLibationFixup
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(AllowLibationFixup));
|
||||
set => persistentDictionary.SetNonString(nameof(AllowLibationFixup), value);
|
||||
}
|
||||
|
||||
[Description("Create a cue sheet (.cue)")]
|
||||
public bool CreateCueSheet
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(CreateCueSheet));
|
||||
set => persistentDictionary.SetNonString(nameof(CreateCueSheet), value);
|
||||
}
|
||||
|
||||
[Description("Retain the Aax file after successfully decrypting")]
|
||||
public bool RetainAaxFile
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(RetainAaxFile));
|
||||
set => persistentDictionary.SetNonString(nameof(RetainAaxFile), value);
|
||||
}
|
||||
|
||||
[Description("Split my books into multiple files by chapter")]
|
||||
public bool SplitFilesByChapter
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(SplitFilesByChapter));
|
||||
set => persistentDictionary.SetNonString(nameof(SplitFilesByChapter), value);
|
||||
}
|
||||
|
||||
[Description("Merge Opening/End Credits into the following/preceding chapters")]
|
||||
public bool MergeOpeningAndEndCredits
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(MergeOpeningAndEndCredits));
|
||||
set => persistentDictionary.SetNonString(nameof(MergeOpeningAndEndCredits), value);
|
||||
}
|
||||
|
||||
[Description("Strip \"(Unabridged)\" from audiobook metadata tags")]
|
||||
public bool StripUnabridged
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(StripUnabridged));
|
||||
set => persistentDictionary.SetNonString(nameof(StripUnabridged), value);
|
||||
}
|
||||
|
||||
[Description("Strip audible branding from the start and end of audiobooks.\r\n(e.g. \"This is Audible\")")]
|
||||
public bool StripAudibleBrandAudio
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(StripAudibleBrandAudio));
|
||||
set => persistentDictionary.SetNonString(nameof(StripAudibleBrandAudio), value);
|
||||
}
|
||||
|
||||
[Description("Decrypt to lossy format?")]
|
||||
public bool DecryptToLossy
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(DecryptToLossy));
|
||||
set => persistentDictionary.SetNonString(nameof(DecryptToLossy), value);
|
||||
}
|
||||
|
||||
[Description("Lame encoder target. true = Bitrate, false = Quality")]
|
||||
public bool LameTargetBitrate
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(LameTargetBitrate));
|
||||
set => persistentDictionary.SetNonString(nameof(LameTargetBitrate), value);
|
||||
}
|
||||
|
||||
[Description("Lame encoder downsamples to mono")]
|
||||
public bool LameDownsampleMono
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(LameDownsampleMono));
|
||||
set => persistentDictionary.SetNonString(nameof(LameDownsampleMono), value);
|
||||
}
|
||||
|
||||
[Description("Lame target bitrate [16,320]")]
|
||||
public int LameBitrate
|
||||
{
|
||||
get => persistentDictionary.GetNonString<int>(nameof(LameBitrate));
|
||||
set => persistentDictionary.SetNonString(nameof(LameBitrate), value);
|
||||
}
|
||||
|
||||
[Description("Restrict encoder to constant bitrate?")]
|
||||
public bool LameConstantBitrate
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(LameConstantBitrate));
|
||||
set => persistentDictionary.SetNonString(nameof(LameConstantBitrate), value);
|
||||
}
|
||||
|
||||
[Description("Match the source bitrate?")]
|
||||
public bool LameMatchSourceBR
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(LameMatchSourceBR));
|
||||
set => persistentDictionary.SetNonString(nameof(LameMatchSourceBR), value);
|
||||
}
|
||||
|
||||
[Description("Lame target VBR quality [10,100]")]
|
||||
public int LameVBRQuality
|
||||
{
|
||||
get => persistentDictionary.GetNonString<int>(nameof(LameVBRQuality));
|
||||
set => persistentDictionary.SetNonString(nameof(LameVBRQuality), value);
|
||||
}
|
||||
|
||||
[Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")]
|
||||
public Dictionary<string, bool> GridColumnsVisibilities
|
||||
{
|
||||
get => persistentDictionary.GetNonString<Dictionary<string, bool>>(nameof(GridColumnsVisibilities));
|
||||
set => persistentDictionary.SetNonString(nameof(GridColumnsVisibilities), value);
|
||||
}
|
||||
|
||||
[Description("A Dictionary of GridView data property names and int indicating its column's display index in ProductsGrid")]
|
||||
public Dictionary<string, int> GridColumnsDisplayIndices
|
||||
{
|
||||
get => persistentDictionary.GetNonString<Dictionary<string, int>>(nameof(GridColumnsDisplayIndices));
|
||||
set => persistentDictionary.SetNonString(nameof(GridColumnsDisplayIndices), value);
|
||||
}
|
||||
|
||||
[Description("A Dictionary of GridView data property names and int indicating its column's width in ProductsGrid")]
|
||||
public Dictionary<string, int> GridColumnsWidths
|
||||
{
|
||||
get => persistentDictionary.GetNonString<Dictionary<string, int>>(nameof(GridColumnsWidths));
|
||||
set => persistentDictionary.SetNonString(nameof(GridColumnsWidths), value);
|
||||
}
|
||||
|
||||
[Description("Save cover image alongside audiobook?")]
|
||||
public bool DownloadCoverArt
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(DownloadCoverArt));
|
||||
set => persistentDictionary.SetNonString(nameof(DownloadCoverArt), value);
|
||||
}
|
||||
|
||||
public enum BadBookAction
|
||||
{
|
||||
[Description("Ask each time what action to take.")]
|
||||
Ask = 0,
|
||||
[Description("Stop processing books.")]
|
||||
Abort = 1,
|
||||
[Description("Retry book later. Skip for now. Continue processing books.")]
|
||||
Retry = 2,
|
||||
[Description("Permanently ignore book. Continue processing books. Do not try book again.")]
|
||||
Ignore = 3
|
||||
}
|
||||
|
||||
[Description("When liberating books and there is an error, Libation should:")]
|
||||
public BadBookAction BadBook
|
||||
{
|
||||
get
|
||||
{
|
||||
var badBookStr = persistentDictionary.GetString(nameof(BadBook));
|
||||
return Enum.TryParse<BadBookAction>(badBookStr, out var badBookEnum) ? badBookEnum : BadBookAction.Ask;
|
||||
}
|
||||
set => persistentDictionary.SetString(nameof(BadBook), value.ToString());
|
||||
}
|
||||
|
||||
[Description("Show number of newly imported titles? When unchecked, no pop-up will appear after library scan.")]
|
||||
public bool ShowImportedStats
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(ShowImportedStats));
|
||||
set => persistentDictionary.SetNonString(nameof(ShowImportedStats), value);
|
||||
}
|
||||
|
||||
[Description("Import episodes? (eg: podcasts) When unchecked, episodes will not be imported into Libation.")]
|
||||
public bool ImportEpisodes
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(ImportEpisodes));
|
||||
set => persistentDictionary.SetNonString(nameof(ImportEpisodes), value);
|
||||
}
|
||||
|
||||
[Description("Download episodes? (eg: podcasts). When unchecked, episodes already in Libation will not be downloaded.")]
|
||||
public bool DownloadEpisodes
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(DownloadEpisodes));
|
||||
set => persistentDictionary.SetNonString(nameof(DownloadEpisodes), value);
|
||||
}
|
||||
|
||||
public event EventHandler AutoScanChanged;
|
||||
|
||||
[Description("Automatically run periodic scans in the background?")]
|
||||
public bool AutoScan
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(AutoScan));
|
||||
set
|
||||
{
|
||||
if (AutoScan != value)
|
||||
{
|
||||
persistentDictionary.SetNonString(nameof(AutoScan), value);
|
||||
AutoScanChanged?.Invoke(null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Description("Auto download books? After scan, download new books in 'checked' accounts.")]
|
||||
// poorly named setting. Should just be 'AutoDownload'. It is NOT episode specific
|
||||
public bool AutoDownloadEpisodes
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(AutoDownloadEpisodes));
|
||||
set => persistentDictionary.SetNonString(nameof(AutoDownloadEpisodes), value);
|
||||
}
|
||||
|
||||
[Description("Save all podcast episodes in a series to the series parent folder?")]
|
||||
public bool SavePodcastsToParentFolder
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(SavePodcastsToParentFolder));
|
||||
set => persistentDictionary.SetNonString(nameof(SavePodcastsToParentFolder), value);
|
||||
}
|
||||
|
||||
#region templates: custom file naming
|
||||
|
||||
[Description("Edit how illegal filename characters are replaced")]
|
||||
public ReplacementCharacters ReplacementCharacters
|
||||
{
|
||||
get => persistentDictionary.GetNonString<ReplacementCharacters>(nameof(ReplacementCharacters));
|
||||
set => persistentDictionary.SetNonString(nameof(ReplacementCharacters), value);
|
||||
}
|
||||
|
||||
[Description("How to format the folders in which files will be saved")]
|
||||
public string FolderTemplate
|
||||
{
|
||||
get => getTemplate(nameof(FolderTemplate), Templates.Folder);
|
||||
set => setTemplate(nameof(FolderTemplate), Templates.Folder, value);
|
||||
}
|
||||
|
||||
[Description("How to format the saved pdf and audio files")]
|
||||
public string FileTemplate
|
||||
{
|
||||
get => getTemplate(nameof(FileTemplate), Templates.File);
|
||||
set => setTemplate(nameof(FileTemplate), Templates.File, value);
|
||||
}
|
||||
|
||||
[Description("How to format the saved audio files when split by chapters")]
|
||||
public string ChapterFileTemplate
|
||||
{
|
||||
get => getTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile);
|
||||
set => setTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile, value);
|
||||
}
|
||||
|
||||
[Description("How to format the file's Tile stored in metadata")]
|
||||
public string ChapterTitleTemplate
|
||||
{
|
||||
get => getTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle);
|
||||
set => setTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle, value);
|
||||
}
|
||||
|
||||
private string getTemplate(string settingName, Templates templ) => templ.GetValid(persistentDictionary.GetString(settingName));
|
||||
private void setTemplate(string settingName, Templates templ, string newValue)
|
||||
{
|
||||
var template = newValue?.Trim();
|
||||
if (templ.IsValid(template))
|
||||
persistentDictionary.SetString(settingName, template);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.Logging;
|
||||
using FileManager;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public class Configuration
|
||||
public partial class Configuration
|
||||
{
|
||||
public bool LibationSettingsAreValid
|
||||
=> File.Exists(APPSETTINGS_JSON)
|
||||
@@ -37,519 +31,9 @@ namespace LibationFileManager
|
||||
return true;
|
||||
}
|
||||
|
||||
#region persistent configuration settings/values
|
||||
|
||||
// note: any potential file manager static ctors can't compensate if storage dir is changed at run time via settings. this is partly bad architecture. but the side effect is desirable. if changing LibationFiles location: restart app
|
||||
|
||||
// default setting and directory creation occur in class responsible for files.
|
||||
// config class is only responsible for path. not responsible for setting defaults, dir validation, or dir creation
|
||||
// exceptions: appsettings.json, LibationFiles dir, Settings.json
|
||||
|
||||
private PersistentDictionary persistentDictionary;
|
||||
|
||||
public T GetNonString<T>(string propertyName) => persistentDictionary.GetNonString<T>(propertyName);
|
||||
public object GetObject(string propertyName) => persistentDictionary.GetObject(propertyName);
|
||||
public void SetObject(string propertyName, object newValue) => persistentDictionary.SetNonString(propertyName, newValue);
|
||||
|
||||
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
|
||||
public void SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false)
|
||||
{
|
||||
var settingWasChanged = persistentDictionary.SetWithJsonPath(jsonPath, propertyName, newValue, suppressLogging);
|
||||
if (settingWasChanged)
|
||||
configuration?.Reload();
|
||||
}
|
||||
|
||||
public string SettingsFilePath => Path.Combine(LibationFiles, "Settings.json");
|
||||
|
||||
public static string GetDescription(string propertyName)
|
||||
{
|
||||
var attribute = typeof(Configuration)
|
||||
.GetProperty(propertyName)
|
||||
?.GetCustomAttributes(typeof(DescriptionAttribute), true)
|
||||
.SingleOrDefault()
|
||||
as DescriptionAttribute;
|
||||
|
||||
return attribute?.Description;
|
||||
}
|
||||
|
||||
public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName);
|
||||
|
||||
[Description("Use the beta version of Libation\r\nNew and experimental features, but probably buggy.\r\n(requires restart to take effect)")]
|
||||
public bool BetaOptIn
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(BetaOptIn));
|
||||
set => persistentDictionary.SetNonString(nameof(BetaOptIn), value);
|
||||
}
|
||||
|
||||
[Description("Location for book storage. Includes destination of newly liberated books")]
|
||||
public string Books
|
||||
{
|
||||
get => persistentDictionary.GetString(nameof(Books));
|
||||
set => persistentDictionary.SetString(nameof(Books), value);
|
||||
}
|
||||
|
||||
// temp/working dir(s) should be outside of dropbox
|
||||
[Description("Temporary location of files while they're in process of being downloaded and decrypted.\r\nWhen decryption is complete, the final file will be in Books location\r\nRecommend not using a folder which is backed up real time. Eg: Dropbox, iCloud, Google Drive")]
|
||||
public string InProgress
|
||||
{
|
||||
get => persistentDictionary.GetString(nameof(InProgress));
|
||||
set => persistentDictionary.SetString(nameof(InProgress), value);
|
||||
}
|
||||
|
||||
[Description("Allow Libation to fix up audiobook metadata")]
|
||||
public bool AllowLibationFixup
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(AllowLibationFixup));
|
||||
set => persistentDictionary.SetNonString(nameof(AllowLibationFixup), value);
|
||||
}
|
||||
|
||||
[Description("Create a cue sheet (.cue)")]
|
||||
public bool CreateCueSheet
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(CreateCueSheet));
|
||||
set => persistentDictionary.SetNonString(nameof(CreateCueSheet), value);
|
||||
}
|
||||
|
||||
[Description("Retain the Aax file after successfully decrypting")]
|
||||
public bool RetainAaxFile
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(RetainAaxFile));
|
||||
set => persistentDictionary.SetNonString(nameof(RetainAaxFile), value);
|
||||
}
|
||||
|
||||
[Description("Split my books into multiple files by chapter")]
|
||||
public bool SplitFilesByChapter
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(SplitFilesByChapter));
|
||||
set => persistentDictionary.SetNonString(nameof(SplitFilesByChapter), value);
|
||||
}
|
||||
|
||||
[Description("Merge Opening/End Credits into the following/preceding chapters")]
|
||||
public bool MergeOpeningAndEndCredits
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(MergeOpeningAndEndCredits));
|
||||
set => persistentDictionary.SetNonString(nameof(MergeOpeningAndEndCredits), value);
|
||||
}
|
||||
|
||||
[Description("Strip \"(Unabridged)\" from audiobook metadata tags")]
|
||||
public bool StripUnabridged
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(StripUnabridged));
|
||||
set => persistentDictionary.SetNonString(nameof(StripUnabridged), value);
|
||||
}
|
||||
|
||||
[Description("Strip audible branding from the start and end of audiobooks.\r\n(e.g. \"This is Audible\")")]
|
||||
public bool StripAudibleBrandAudio
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(StripAudibleBrandAudio));
|
||||
set => persistentDictionary.SetNonString(nameof(StripAudibleBrandAudio), value);
|
||||
}
|
||||
|
||||
[Description("Decrypt to lossy format?")]
|
||||
public bool DecryptToLossy
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(DecryptToLossy));
|
||||
set => persistentDictionary.SetNonString(nameof(DecryptToLossy), value);
|
||||
}
|
||||
|
||||
[Description("Lame encoder target. true = Bitrate, false = Quality")]
|
||||
public bool LameTargetBitrate
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(LameTargetBitrate));
|
||||
set => persistentDictionary.SetNonString(nameof(LameTargetBitrate), value);
|
||||
}
|
||||
|
||||
[Description("Lame encoder downsamples to mono")]
|
||||
public bool LameDownsampleMono
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(LameDownsampleMono));
|
||||
set => persistentDictionary.SetNonString(nameof(LameDownsampleMono), value);
|
||||
}
|
||||
|
||||
[Description("Lame target bitrate [16,320]")]
|
||||
public int LameBitrate
|
||||
{
|
||||
get => persistentDictionary.GetNonString<int>(nameof(LameBitrate));
|
||||
set => persistentDictionary.SetNonString(nameof(LameBitrate), value);
|
||||
}
|
||||
|
||||
[Description("Restrict encoder to constant bitrate?")]
|
||||
public bool LameConstantBitrate
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(LameConstantBitrate));
|
||||
set => persistentDictionary.SetNonString(nameof(LameConstantBitrate), value);
|
||||
}
|
||||
|
||||
[Description("Match the source bitrate?")]
|
||||
public bool LameMatchSourceBR
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(LameMatchSourceBR));
|
||||
set => persistentDictionary.SetNonString(nameof(LameMatchSourceBR), value);
|
||||
}
|
||||
|
||||
[Description("Lame target VBR quality [10,100]")]
|
||||
public int LameVBRQuality
|
||||
{
|
||||
get => persistentDictionary.GetNonString<int>(nameof(LameVBRQuality));
|
||||
set => persistentDictionary.SetNonString(nameof(LameVBRQuality), value);
|
||||
}
|
||||
|
||||
[Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")]
|
||||
public Dictionary<string, bool> GridColumnsVisibilities
|
||||
{
|
||||
get => persistentDictionary.GetNonString<Dictionary<string, bool>>(nameof(GridColumnsVisibilities));
|
||||
set => persistentDictionary.SetNonString(nameof(GridColumnsVisibilities), value);
|
||||
}
|
||||
|
||||
[Description("A Dictionary of GridView data property names and int indicating its column's display index in ProductsGrid")]
|
||||
public Dictionary<string, int> GridColumnsDisplayIndices
|
||||
{
|
||||
get => persistentDictionary.GetNonString<Dictionary<string,int>>(nameof(GridColumnsDisplayIndices));
|
||||
set => persistentDictionary.SetNonString(nameof(GridColumnsDisplayIndices), value);
|
||||
}
|
||||
|
||||
[Description("A Dictionary of GridView data property names and int indicating its column's width in ProductsGrid")]
|
||||
public Dictionary<string, int> GridColumnsWidths
|
||||
{
|
||||
get => persistentDictionary.GetNonString<Dictionary<string,int>>(nameof(GridColumnsWidths));
|
||||
set => persistentDictionary.SetNonString(nameof(GridColumnsWidths), value);
|
||||
}
|
||||
|
||||
[Description("Save cover image alongside audiobook?")]
|
||||
public bool DownloadCoverArt
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(DownloadCoverArt));
|
||||
set => persistentDictionary.SetNonString(nameof(DownloadCoverArt), value);
|
||||
}
|
||||
|
||||
public enum BadBookAction
|
||||
{
|
||||
[Description("Ask each time what action to take.")]
|
||||
Ask = 0,
|
||||
[Description("Stop processing books.")]
|
||||
Abort = 1,
|
||||
[Description("Retry book later. Skip for now. Continue processing books.")]
|
||||
Retry = 2,
|
||||
[Description("Permanently ignore book. Continue processing books. Do not try book again.")]
|
||||
Ignore = 3
|
||||
}
|
||||
|
||||
[Description("When liberating books and there is an error, Libation should:")]
|
||||
public BadBookAction BadBook
|
||||
{
|
||||
get
|
||||
{
|
||||
var badBookStr = persistentDictionary.GetString(nameof(BadBook));
|
||||
return Enum.TryParse<BadBookAction>(badBookStr, out var badBookEnum) ? badBookEnum : BadBookAction.Ask;
|
||||
}
|
||||
set => persistentDictionary.SetString(nameof(BadBook), value.ToString());
|
||||
}
|
||||
|
||||
[Description("Show number of newly imported titles? When unchecked, no pop-up will appear after library scan.")]
|
||||
public bool ShowImportedStats
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(ShowImportedStats));
|
||||
set => persistentDictionary.SetNonString(nameof(ShowImportedStats), value);
|
||||
}
|
||||
|
||||
[Description("Import episodes? (eg: podcasts) When unchecked, episodes will not be imported into Libation.")]
|
||||
public bool ImportEpisodes
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(ImportEpisodes));
|
||||
set => persistentDictionary.SetNonString(nameof(ImportEpisodes), value);
|
||||
}
|
||||
|
||||
[Description("Download episodes? (eg: podcasts). When unchecked, episodes already in Libation will not be downloaded.")]
|
||||
public bool DownloadEpisodes
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(DownloadEpisodes));
|
||||
set => persistentDictionary.SetNonString(nameof(DownloadEpisodes), value);
|
||||
}
|
||||
|
||||
public event EventHandler AutoScanChanged;
|
||||
|
||||
[Description("Automatically run periodic scans in the background?")]
|
||||
public bool AutoScan
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(AutoScan));
|
||||
set
|
||||
{
|
||||
if (AutoScan != value)
|
||||
{
|
||||
persistentDictionary.SetNonString(nameof(AutoScan), value);
|
||||
AutoScanChanged?.Invoke(null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Description("Auto download episodes? After scan, download new books in 'checked' accounts.")]
|
||||
public bool AutoDownloadEpisodes
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(AutoDownloadEpisodes));
|
||||
set => persistentDictionary.SetNonString(nameof(AutoDownloadEpisodes), value);
|
||||
}
|
||||
|
||||
[Description("Save all podcast episodes in a series to the series parent folder?")]
|
||||
public bool SavePodcastsToParentFolder
|
||||
{
|
||||
get => persistentDictionary.GetNonString<bool>(nameof(SavePodcastsToParentFolder));
|
||||
set => persistentDictionary.SetNonString(nameof(SavePodcastsToParentFolder), value);
|
||||
}
|
||||
|
||||
#region templates: custom file naming
|
||||
|
||||
[Description("Edit how illegal filename characters are replaced")]
|
||||
public ReplacementCharacters ReplacementCharacters
|
||||
{
|
||||
get => persistentDictionary.GetNonString<ReplacementCharacters>(nameof(ReplacementCharacters));
|
||||
set => persistentDictionary.SetNonString(nameof(ReplacementCharacters), value);
|
||||
}
|
||||
|
||||
[Description("How to format the folders in which files will be saved")]
|
||||
public string FolderTemplate
|
||||
{
|
||||
get => getTemplate(nameof(FolderTemplate), Templates.Folder);
|
||||
set => setTemplate(nameof(FolderTemplate), Templates.Folder, value);
|
||||
}
|
||||
|
||||
[Description("How to format the saved pdf and audio files")]
|
||||
public string FileTemplate
|
||||
{
|
||||
get => getTemplate(nameof(FileTemplate), Templates.File);
|
||||
set => setTemplate(nameof(FileTemplate), Templates.File, value);
|
||||
}
|
||||
|
||||
[Description("How to format the saved audio files when split by chapters")]
|
||||
public string ChapterFileTemplate
|
||||
{
|
||||
get => getTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile);
|
||||
set => setTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile, value);
|
||||
}
|
||||
|
||||
[Description("How to format the file's Tile stored in metadata")]
|
||||
public string ChapterTitleTemplate
|
||||
{
|
||||
get => getTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle);
|
||||
set => setTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle, value);
|
||||
}
|
||||
|
||||
private string getTemplate(string settingName, Templates templ) => templ.GetValid(persistentDictionary.GetString(settingName));
|
||||
private void setTemplate(string settingName, Templates templ, string newValue)
|
||||
{
|
||||
var template = newValue?.Trim();
|
||||
if (templ.IsValid(template))
|
||||
persistentDictionary.SetString(settingName, template);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
#region known directories
|
||||
public static string AppDir_Relative => $@".{Path.PathSeparator}{LIBATION_FILES_KEY}";
|
||||
public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Exe.FileLocationOnDisk), LIBATION_FILES_KEY));
|
||||
public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation"));
|
||||
public static string WinTemp => Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation"));
|
||||
public static string UserProfile => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Libation"));
|
||||
|
||||
public enum KnownDirectories
|
||||
{
|
||||
None = 0,
|
||||
|
||||
[Description("My Users folder")]
|
||||
UserProfile = 1,
|
||||
|
||||
[Description("The same folder that Libation is running from")]
|
||||
AppDir = 2,
|
||||
|
||||
[Description("Windows temporary folder")]
|
||||
WinTemp = 3,
|
||||
|
||||
[Description("My Documents")]
|
||||
MyDocs = 4,
|
||||
|
||||
[Description("Your settings folder (aka: Libation Files)")]
|
||||
LibationFiles = 5
|
||||
}
|
||||
// use func calls so we always get the latest value of LibationFiles
|
||||
private static List<(KnownDirectories directory, Func<string> getPathFunc)> directoryOptionsPaths { get; } = new()
|
||||
{
|
||||
(KnownDirectories.None, () => null),
|
||||
(KnownDirectories.UserProfile, () => UserProfile),
|
||||
(KnownDirectories.AppDir, () => AppDir_Relative),
|
||||
(KnownDirectories.WinTemp, () => WinTemp),
|
||||
(KnownDirectories.MyDocs, () => MyDocs),
|
||||
// this is important to not let very early calls try to accidentally load LibationFiles too early.
|
||||
// also, keep this at bottom of this list
|
||||
(KnownDirectories.LibationFiles, () => libationFilesPathCache)
|
||||
};
|
||||
public static string GetKnownDirectoryPath(KnownDirectories directory)
|
||||
{
|
||||
var dirFunc = directoryOptionsPaths.SingleOrDefault(dirFunc => dirFunc.directory == directory);
|
||||
return dirFunc == default ? null : dirFunc.getPathFunc();
|
||||
}
|
||||
public static KnownDirectories GetKnownDirectory(string directory)
|
||||
{
|
||||
// especially important so a very early call doesn't match null => LibationFiles
|
||||
if (string.IsNullOrWhiteSpace(directory))
|
||||
return KnownDirectories.None;
|
||||
|
||||
// 'First' instead of 'Single' because LibationFiles could match other directories. eg: default value of LibationFiles == UserProfile.
|
||||
// since it's a list, order matters and non-LibationFiles will be returned first
|
||||
var dirFunc = directoryOptionsPaths.FirstOrDefault(dirFunc => dirFunc.getPathFunc() == directory);
|
||||
return dirFunc == default ? KnownDirectories.None : dirFunc.directory;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region logging
|
||||
private IConfigurationRoot configuration;
|
||||
|
||||
public void ConfigureLogging()
|
||||
{
|
||||
configuration = new ConfigurationBuilder()
|
||||
.AddJsonFile(SettingsFilePath, optional: false, reloadOnChange: true)
|
||||
.Build();
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(configuration)
|
||||
.CreateLogger();
|
||||
}
|
||||
|
||||
[Description("The importance of a log event")]
|
||||
public LogEventLevel LogLevel
|
||||
{
|
||||
get
|
||||
{
|
||||
var logLevelStr = persistentDictionary.GetStringFromJsonPath("Serilog", "MinimumLevel");
|
||||
return Enum.TryParse<LogEventLevel>(logLevelStr, out var logLevelEnum) ? logLevelEnum : LogEventLevel.Information;
|
||||
}
|
||||
set
|
||||
{
|
||||
var valueWasChanged = persistentDictionary.SetWithJsonPath("Serilog", "MinimumLevel", value.ToString());
|
||||
if (!valueWasChanged)
|
||||
{
|
||||
Log.Logger.Debug("LogLevel.set attempt. No change");
|
||||
return;
|
||||
}
|
||||
|
||||
configuration.Reload();
|
||||
|
||||
Log.Logger.Information("Updated LogLevel MinimumLevel. {@DebugInfo}", new
|
||||
{
|
||||
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
|
||||
LogLevel_Debug_Enabled = Log.Logger.IsDebugEnabled(),
|
||||
LogLevel_Information_Enabled = Log.Logger.IsInformationEnabled(),
|
||||
LogLevel_Warning_Enabled = Log.Logger.IsWarningEnabled(),
|
||||
LogLevel_Error_Enabled = Log.Logger.IsErrorEnabled(),
|
||||
LogLevel_Fatal_Enabled = Log.Logger.IsFatalEnabled()
|
||||
});
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region singleton stuff
|
||||
public static Configuration Instance { get; } = new Configuration();
|
||||
private Configuration() { }
|
||||
#endregion
|
||||
|
||||
#region LibationFiles
|
||||
private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "appsettings.json");
|
||||
private const string LIBATION_FILES_KEY = "LibationFiles";
|
||||
|
||||
[Description("Location for storage of program-created files")]
|
||||
public string LibationFiles
|
||||
{
|
||||
get
|
||||
{
|
||||
if (libationFilesPathCache is not null)
|
||||
return libationFilesPathCache;
|
||||
|
||||
// FIRST: must write here before SettingsFilePath in next step reads cache
|
||||
libationFilesPathCache = getLibationFilesSettingFromJson();
|
||||
|
||||
// SECOND. before setting to json file with SetWithJsonPath, PersistentDictionary must exist
|
||||
persistentDictionary = new PersistentDictionary(SettingsFilePath);
|
||||
|
||||
// Config init in ensureSerilogConfig() only happens when serilog setting is first created (prob on 1st run).
|
||||
// This Set() enforces current LibationFiles every time we restart Libation or redirect LibationFiles
|
||||
var logPath = Path.Combine(LibationFiles, "Log.log");
|
||||
|
||||
// BAD: Serilog.WriteTo[1].Args
|
||||
// "[1]" assumes ordinal position
|
||||
// GOOD: Serilog.WriteTo[?(@.Name=='File')].Args
|
||||
var jsonpath = "Serilog.WriteTo[?(@.Name=='File')].Args";
|
||||
|
||||
SetWithJsonPath(jsonpath, "path", logPath, true);
|
||||
|
||||
return libationFilesPathCache;
|
||||
}
|
||||
}
|
||||
|
||||
private static string libationFilesPathCache { get; set; }
|
||||
|
||||
private string getLibationFilesSettingFromJson()
|
||||
{
|
||||
string startingContents = null;
|
||||
try
|
||||
{
|
||||
if (File.Exists(APPSETTINGS_JSON))
|
||||
{
|
||||
startingContents = File.ReadAllText(APPSETTINGS_JSON);
|
||||
var startingJObj = JObject.Parse(startingContents);
|
||||
|
||||
if (startingJObj.ContainsKey(LIBATION_FILES_KEY))
|
||||
{
|
||||
var startingValue = startingJObj[LIBATION_FILES_KEY].Value<string>();
|
||||
if (!string.IsNullOrWhiteSpace(startingValue))
|
||||
return startingValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// not found. write to file. read from file
|
||||
var endingContents = new JObject { { LIBATION_FILES_KEY, UserProfile.ToString() } }.ToString(Formatting.Indented);
|
||||
if (startingContents != endingContents)
|
||||
{
|
||||
File.WriteAllText(APPSETTINGS_JSON, endingContents);
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
|
||||
// do not check whether directory exists. special/meta directory (eg: AppDir) is valid
|
||||
// verify from live file. no try/catch. want failures to be visible
|
||||
var jObjFinal = JObject.Parse(File.ReadAllText(APPSETTINGS_JSON));
|
||||
var valueFinal = jObjFinal[LIBATION_FILES_KEY].Value<string>();
|
||||
return valueFinal;
|
||||
}
|
||||
|
||||
public void SetLibationFiles(string directory)
|
||||
{
|
||||
// ensure exists
|
||||
if (!File.Exists(APPSETTINGS_JSON))
|
||||
{
|
||||
// getter creates new file, loads PersistentDictionary
|
||||
var _ = LibationFiles;
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
|
||||
libationFilesPathCache = null;
|
||||
|
||||
var startingContents = File.ReadAllText(APPSETTINGS_JSON);
|
||||
var jObj = JObject.Parse(startingContents);
|
||||
|
||||
jObj[LIBATION_FILES_KEY] = directory;
|
||||
|
||||
var endingContents = JsonConvert.SerializeObject(jObj, Formatting.Indented);
|
||||
if (startingContents == endingContents)
|
||||
return;
|
||||
|
||||
// now it's set in the file again but no settings have moved yet
|
||||
File.WriteAllText(APPSETTINGS_JSON, endingContents);
|
||||
|
||||
try
|
||||
{
|
||||
Log.Logger.Information("Libation files changed {@DebugInfo}", new { APPSETTINGS_JSON, LIBATION_FILES_KEY, directory });
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,23 +24,24 @@ namespace LibationFileManager
|
||||
static FilePathCache()
|
||||
{
|
||||
// load json into memory. if file doesn't exist, nothing to do. save() will create if needed
|
||||
if (File.Exists(jsonFile))
|
||||
{
|
||||
var list = JsonConvert.DeserializeObject<List<CacheEntry>>(File.ReadAllText(jsonFile));
|
||||
if (!File.Exists(jsonFile))
|
||||
return;
|
||||
|
||||
// file exists but deser is null. this will never happen when file is healthy
|
||||
if (list is null)
|
||||
{
|
||||
lock (locker)
|
||||
{
|
||||
Serilog.Log.Logger.Error("Error deserializing file. Wrong format. Possibly corrupt. Deleting file. {@DebugInfo}", new { jsonFile });
|
||||
File.Delete(jsonFile);
|
||||
return;
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
var list = JsonConvert.DeserializeObject<List<CacheEntry>>(File.ReadAllText(jsonFile));
|
||||
if (list is null)
|
||||
throw new NullReferenceException("File exists but deserialize is null. This will never happen when file is healthy.");
|
||||
|
||||
cache = new Cache<CacheEntry>(list);
|
||||
}
|
||||
cache = new Cache<CacheEntry>(list);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Error deserializing file. Wrong format. Possibly corrupt. Deleting file. {@DebugInfo}", new { jsonFile });
|
||||
lock (locker)
|
||||
File.Delete(jsonFile);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool Exists(string id, FileType type) => GetFirstPath(id, type) is not null;
|
||||
|
||||
10
Source/LibationFileManager/IInteropFunctions.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public interface IInteropFunctions
|
||||
{
|
||||
void SetFolderIcon(string image, string directory);
|
||||
void DeleteFolderIcon(string directory);
|
||||
}
|
||||
}
|
||||