Compare commits
192 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 | ||
|
|
553a936e7e | ||
|
|
635764625e | ||
|
|
f5599f7c57 | ||
|
|
dc6aaf2dd6 | ||
|
|
f1ba2b4ae8 | ||
|
|
742310b8d6 | ||
|
|
073787173d | ||
|
|
66679ace2f | ||
|
|
3982537d46 | ||
|
|
7cf4c63d79 | ||
|
|
7c4575cf66 | ||
|
|
f4749d703f | ||
|
|
f2f562619b | ||
|
|
16c019a9c6 | ||
|
|
644dcbdd4d | ||
|
|
6b112f5248 | ||
|
|
0bfa609058 | ||
|
|
8020ded642 | ||
|
|
c4cd6b16fc | ||
|
|
310012fd17 | ||
|
|
06163db6ff | ||
|
|
7689eed711 | ||
|
|
d396d697d7 | ||
|
|
27ed11d904 | ||
|
|
9e7670b918 | ||
|
|
31e97defd1 | ||
|
|
1a447627c7 | ||
|
|
962b386d07 | ||
|
|
d69ff24c2d | ||
|
|
070ed1d373 | ||
|
|
47729bf7b0 | ||
|
|
ed0ce2976b | ||
|
|
2224f46ed5 | ||
|
|
433974323c | ||
|
|
7525d318c0 | ||
|
|
92327dcc0d | ||
|
|
aeaf234edd | ||
|
|
a99b644917 | ||
|
|
d79a55e5c9 | ||
|
|
16b0feeb82 | ||
|
|
7b3a25e45a | ||
|
|
8effdcb92d | ||
|
|
b12bef81bd | ||
|
|
f04a5e0168 | ||
|
|
e093729707 | ||
|
|
369151ada2 | ||
|
|
1f685ae8a0 | ||
|
|
bbe91099cb | ||
|
|
92015ba4c2 | ||
|
|
3bcacabadc | ||
|
|
f5736d9151 | ||
|
|
59015f438e | ||
|
|
3af47ab395 | ||
|
|
308619b01a | ||
|
|
4efce57488 | ||
|
|
c8ee950f7d | ||
|
|
0bba0f9256 | ||
|
|
05bdff5123 | ||
|
|
e58e6cfb9f | ||
|
|
b052871004 | ||
|
|
d738f4f35f | ||
|
|
7286aee9dd | ||
|
|
ca455978a5 | ||
|
|
9c38bea5b7 | ||
|
|
fbec1bc569 | ||
|
|
6dd885f0b2 | ||
|
|
ab38eb5571 | ||
|
|
0e4b9ab396 | ||
|
|
7dfedbc73b | ||
|
|
625ae1d63c | ||
|
|
71098ef02f | ||
|
|
d63a6de543 | ||
|
|
2a71a85306 | ||
|
|
6de3a8a2bf | ||
|
|
3fc1da66de | ||
|
|
683c221ca8 | ||
|
|
fe6cfc899b | ||
|
|
ffd947eb2e | ||
|
|
8dd59cb08a | ||
|
|
1e4c489983 | ||
|
|
17b0da358f | ||
|
|
6aa0a1f8b9 | ||
|
|
ab731a63af | ||
|
|
07d2c656fc | ||
|
|
9ecb32c3d2 | ||
|
|
503e1e143e | ||
|
|
e34ce67a2c | ||
|
|
a0fd0a3de6 | ||
|
|
7f3cbc454f | ||
|
|
30eb117fa1 | ||
|
|
63877160aa | ||
|
|
77e61479cf | ||
|
|
ca71283108 | ||
|
|
285563af5e | ||
|
|
62cbad0d8f | ||
|
|
2cb2479d63 | ||
|
|
e7c5b1d8dc | ||
|
|
7f086aeaac | ||
|
|
78186d4973 | ||
|
|
4d84174ba6 | ||
|
|
a4ff739684 | ||
|
|
9e06c70319 | ||
|
|
0c98ce000b |
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve Libation
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Platform**
|
||||
|
||||
[e.g. Windows 10, Windows 11, Mac, Linux (State distribution)]
|
||||
|
||||
**Log Files**
|
||||
Attach your Libation log file here.
|
||||
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
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
.gitignore
vendored
@@ -370,3 +370,4 @@ FodyWeavers.xsd
|
||||
|
||||
/__TODO.txt
|
||||
/DataLayer/LibationContext.db
|
||||
*/bin-Avalonia
|
||||
6
.releaseindex.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"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
|
||||
|
||||
|
||||
100
Documentation/InstallOnLinux.md
Normal file
@@ -0,0 +1,100 @@
|
||||
## [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:
|
||||
- Cannot customize how illegial filename characters are replaced.
|
||||
- The Auto-update function is unavailable
|
||||
|
||||
## Dependencies
|
||||
|
||||
### FFMpeg (Optional)
|
||||
If you want to convert your audiobooks to mp3, install FFMpeg using the following command:
|
||||
|
||||
```console
|
||||
sudo apt-get install -y ffmpeg
|
||||
```
|
||||
|
||||
## Install Libation
|
||||
|
||||
Download the most recent linux-64 binaries zip file and save it as `libation-linux-bin.zip`. Save the 'install-libation.sh' bash script to a file. From the terminal make the script file executable:
|
||||
|
||||
<details>
|
||||
<summary>install-libation.sh</summary>
|
||||
|
||||
```BASH
|
||||
#!/bin/bash
|
||||
|
||||
FILE=$1
|
||||
|
||||
if [ -z "$FILE" ]
|
||||
then echo "This script must be called with a the Libation Linux bin zip file as an argument."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ "$EUID" -ne 0 ]]
|
||||
then echo "Please run as root"
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ ! -f "$FILE" ]
|
||||
then echo "The file \"$FILE\" does not exist."
|
||||
exit
|
||||
fi
|
||||
|
||||
echo "Extracting $FILE"
|
||||
|
||||
FOLDER="$(dirname "$FILE")/libation_src"
|
||||
echo "$FOLDER"
|
||||
|
||||
sudo -u $SUDO_USER unzip -q -o ${FILE} -d ${FOLDER}
|
||||
|
||||
if [ $? -ne 0 ]
|
||||
then echo "Error unzipping ${FILE}"
|
||||
exit
|
||||
fi
|
||||
|
||||
sudo -u $SUDO_USER chmod +700 ${FOLDER}/Libation
|
||||
sudo -u $SUDO_USER chmod +700 ${FOLDER}/Hangover
|
||||
sudo -u $SUDO_USER chmod +700 ${FOLDER}/LibationCli
|
||||
|
||||
#Remove previous installation program files and sym link
|
||||
rm /usr/bin/Libation
|
||||
rm /usr/bin/Hangover
|
||||
rm /usr/bin/LibationCli
|
||||
rm /usr/bin/libationcli
|
||||
rm /usr/lib/libation -r
|
||||
|
||||
#Copy install files, icon and desktop file
|
||||
cp ${FOLDER}/glass-with-glow_256.svg /usr/share/icons/hicolor/scalable/apps/libation.svg
|
||||
cp ${FOLDER}/Libation.desktop /usr/share/applications/Libation.desktop
|
||||
mv ${FOLDER}/ /usr/lib/libation
|
||||
|
||||
chmod +666 /usr/share/icons/hicolor/scalable/apps/libation.svg
|
||||
gtk-update-icon-cache -f /usr/share/icons/hicolor/
|
||||
ln -s /usr/lib/libation/Libation /usr/bin/Libation
|
||||
ln -s /usr/lib/libation/Hangover /usr/bin/Hangover
|
||||
ln -s /usr/lib/libation/LibationCli /usr/bin/LibationCli
|
||||
ln -s /usr/lib/libation/LibationCli /usr/bin/libationcli
|
||||
|
||||
echo "Done!"
|
||||
```
|
||||
</details>
|
||||
|
||||
```console
|
||||
chmod +700 install-libation.sh
|
||||
```
|
||||
Then run the script with the libation binaries zipfile as an argument.
|
||||
```console
|
||||
sudo ./install-libation.sh libation-linux-bin.zip
|
||||
```
|
||||
|
||||
You should now see Libation among your applications.
|
||||
|
||||
Report bugs to https://github.com/rmcrackan/Libation/issues
|
||||
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
|
||||
@@ -27,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)
|
||||
@@ -54,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,13 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean.Codecs" Version="0.2.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
@@ -16,6 +12,9 @@
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean.Codecs" Version="0.2.14" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FileManager\FileManager.csproj" />
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<Version>8.2.2.1</Version>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Version>8.6.4.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="0.51.0" />
|
||||
<PackageReference Include="Octokit" Version="4.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
|
||||
<ProjectReference Include="..\AudibleUtilities\AudibleUtilities.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -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;
|
||||
@@ -14,8 +15,29 @@ using Serilog;
|
||||
|
||||
namespace AppScaffolding
|
||||
{
|
||||
public enum ReleaseIdentifier
|
||||
{
|
||||
None,
|
||||
WindowsClassic,
|
||||
WindowsAvalonia,
|
||||
LinuxAvalonia,
|
||||
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 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;
|
||||
|
||||
// AppScaffolding
|
||||
private static Assembly _executingAssembly;
|
||||
private static Assembly ExecutingAssembly
|
||||
@@ -33,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
|
||||
@@ -66,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)))
|
||||
@@ -91,20 +116,20 @@ namespace AppScaffolding
|
||||
config.DecryptToLossy = false;
|
||||
|
||||
if (!config.Exists(nameof(config.LameTargetBitrate)))
|
||||
config.LameTargetBitrate = false;
|
||||
|
||||
config.LameTargetBitrate = false;
|
||||
|
||||
if (!config.Exists(nameof(config.LameDownsampleMono)))
|
||||
config.LameDownsampleMono = true;
|
||||
|
||||
|
||||
if (!config.Exists(nameof(config.LameBitrate)))
|
||||
config.LameBitrate = 64;
|
||||
|
||||
|
||||
if (!config.Exists(nameof(config.LameConstantBitrate)))
|
||||
config.LameConstantBitrate = false;
|
||||
|
||||
|
||||
if (!config.Exists(nameof(config.LameMatchSourceBR)))
|
||||
config.LameMatchSourceBR = true;
|
||||
|
||||
|
||||
if (!config.Exists(nameof(config.LameVBRQuality)))
|
||||
config.LameVBRQuality = 2;
|
||||
|
||||
@@ -161,6 +186,8 @@ namespace AppScaffolding
|
||||
configureLogging(config);
|
||||
logStartupState(config);
|
||||
|
||||
// all else should occur after logging
|
||||
|
||||
wireUpSystemEvents(config);
|
||||
}
|
||||
|
||||
@@ -218,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
|
||||
@@ -258,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)
|
||||
{
|
||||
@@ -281,7 +308,10 @@ namespace AppScaffolding
|
||||
{
|
||||
AppName = EntryAssembly.GetName().Name,
|
||||
Version = BuildVersion.ToString(),
|
||||
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(),
|
||||
@@ -289,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,
|
||||
@@ -301,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);
|
||||
@@ -312,8 +346,9 @@ namespace AppScaffolding
|
||||
public static UpgradeProperties GetLatestRelease()
|
||||
{
|
||||
// timed out
|
||||
var latest = getLatestRelease(TimeSpan.FromSeconds(10));
|
||||
if (latest is null)
|
||||
(var latest, var zip) = getLatestRelease(TimeSpan.FromSeconds(10));
|
||||
|
||||
if (latest is null || zip is null)
|
||||
return null;
|
||||
|
||||
var latestVersionString = latest.TagName.Trim('v');
|
||||
@@ -325,7 +360,7 @@ namespace AppScaffolding
|
||||
return null;
|
||||
|
||||
// we have an update
|
||||
var zip = latest.Assets.FirstOrDefault(a => a.BrowserDownloadUrl.EndsWith(".zip"));
|
||||
|
||||
var zipUrl = zip?.BrowserDownloadUrl;
|
||||
|
||||
Log.Logger.Information("Update available: {@DebugInfo}", new
|
||||
@@ -337,11 +372,11 @@ namespace AppScaffolding
|
||||
|
||||
return new(zipUrl, latest.HtmlUrl, zip.Name, latestRelease);
|
||||
}
|
||||
private static Octokit.Release getLatestRelease(TimeSpan timeout)
|
||||
private static (Octokit.Release, Octokit.ReleaseAsset) getLatestRelease(TimeSpan timeout)
|
||||
{
|
||||
try
|
||||
{
|
||||
var task = System.Threading.Tasks.Task.Run(() => getLatestRelease());
|
||||
var task = getLatestRelease();
|
||||
if (task.Wait(timeout))
|
||||
return task.Result;
|
||||
|
||||
@@ -351,16 +386,26 @@ namespace AppScaffolding
|
||||
{
|
||||
Log.Logger.Error(aggEx, "Checking for new version too often");
|
||||
}
|
||||
return null;
|
||||
return (null, null);
|
||||
}
|
||||
private static Octokit.Release getLatestRelease()
|
||||
private static async System.Threading.Tasks.Task<(Octokit.Release, Octokit.ReleaseAsset)> getLatestRelease()
|
||||
{
|
||||
var gitHubClient = new Octokit.GitHubClient(new Octokit.ProductHeaderValue("Libation"));
|
||||
var ownerAccount = "rmcrackan";
|
||||
var repoName = "Libation";
|
||||
|
||||
var gitHubClient = new Octokit.GitHubClient(new Octokit.ProductHeaderValue(repoName));
|
||||
|
||||
//Download the release index
|
||||
var bts = await gitHubClient.Repository.Content.GetRawContent(ownerAccount, repoName, ".releaseindex.json");
|
||||
var releaseIndex = JObject.Parse(System.Text.Encoding.ASCII.GetString(bts));
|
||||
var regexPattern = releaseIndex.Value<string>(ReleaseIdentifier.ToString());
|
||||
|
||||
// https://octokitnet.readthedocs.io/en/latest/releases/
|
||||
var releases = gitHubClient.Repository.Release.GetAll("rmcrackan", "Libation").GetAwaiter().GetResult();
|
||||
var latest = releases.First(r => !r.Draft && !r.Prerelease);
|
||||
return latest;
|
||||
var releases = await gitHubClient.Repository.Release.GetAll(ownerAccount, repoName);
|
||||
|
||||
var regex = new System.Text.RegularExpressions.Regex(regexPattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||
var latestRelease = releases.FirstOrDefault(r => !r.Draft && !r.Prerelease && r.Assets.Any(a => regex.IsMatch(a.Name)));
|
||||
return (latestRelease, latestRelease?.Assets?.FirstOrDefault(a => regex.IsMatch(a.Name)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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-windows</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="27.2.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;
|
||||
|
||||
@@ -100,6 +100,9 @@ namespace ApplicationServices
|
||||
|
||||
[Name("Content Type")]
|
||||
public string ContentType { get; set; }
|
||||
|
||||
[Name("Audio Format")]
|
||||
public string AudioFormat { get; set; }
|
||||
}
|
||||
public static class LibToDtos
|
||||
{
|
||||
@@ -132,7 +135,8 @@ namespace ApplicationServices
|
||||
MyLibationTags = a.Book.UserDefinedItem.Tags,
|
||||
BookStatus = a.Book.UserDefinedItem.BookStatus.ToString(),
|
||||
PdfStatus = a.Book.UserDefinedItem.PdfStatus.ToString(),
|
||||
ContentType = a.Book.ContentType.ToString()
|
||||
ContentType = a.Book.ContentType.ToString(),
|
||||
AudioFormat = a.Book.AudioFormat.ToString()
|
||||
}).ToList();
|
||||
}
|
||||
public static class LibraryExporter
|
||||
@@ -176,33 +180,34 @@ namespace ApplicationServices
|
||||
var row = sheet.CreateRow(rowIndex);
|
||||
|
||||
var columns = new[] {
|
||||
nameof (ExportDto.Account),
|
||||
nameof (ExportDto.DateAdded),
|
||||
nameof (ExportDto.AudibleProductId),
|
||||
nameof (ExportDto.Locale),
|
||||
nameof (ExportDto.Title),
|
||||
nameof (ExportDto.AuthorNames),
|
||||
nameof (ExportDto.NarratorNames),
|
||||
nameof (ExportDto.LengthInMinutes),
|
||||
nameof (ExportDto.Description),
|
||||
nameof (ExportDto.Publisher),
|
||||
nameof (ExportDto.HasPdf),
|
||||
nameof (ExportDto.SeriesNames),
|
||||
nameof (ExportDto.SeriesOrder),
|
||||
nameof (ExportDto.CommunityRatingOverall),
|
||||
nameof (ExportDto.CommunityRatingPerformance),
|
||||
nameof (ExportDto.CommunityRatingStory),
|
||||
nameof (ExportDto.PictureId),
|
||||
nameof (ExportDto.IsAbridged),
|
||||
nameof (ExportDto.DatePublished),
|
||||
nameof (ExportDto.CategoriesNames),
|
||||
nameof (ExportDto.MyRatingOverall),
|
||||
nameof (ExportDto.MyRatingPerformance),
|
||||
nameof (ExportDto.MyRatingStory),
|
||||
nameof (ExportDto.MyLibationTags),
|
||||
nameof (ExportDto.BookStatus),
|
||||
nameof (ExportDto.PdfStatus),
|
||||
nameof (ExportDto.ContentType)
|
||||
nameof(ExportDto.Account),
|
||||
nameof(ExportDto.DateAdded),
|
||||
nameof(ExportDto.AudibleProductId),
|
||||
nameof(ExportDto.Locale),
|
||||
nameof(ExportDto.Title),
|
||||
nameof(ExportDto.AuthorNames),
|
||||
nameof(ExportDto.NarratorNames),
|
||||
nameof(ExportDto.LengthInMinutes),
|
||||
nameof(ExportDto.Description),
|
||||
nameof(ExportDto.Publisher),
|
||||
nameof(ExportDto.HasPdf),
|
||||
nameof(ExportDto.SeriesNames),
|
||||
nameof(ExportDto.SeriesOrder),
|
||||
nameof(ExportDto.CommunityRatingOverall),
|
||||
nameof(ExportDto.CommunityRatingPerformance),
|
||||
nameof(ExportDto.CommunityRatingStory),
|
||||
nameof(ExportDto.PictureId),
|
||||
nameof(ExportDto.IsAbridged),
|
||||
nameof(ExportDto.DatePublished),
|
||||
nameof(ExportDto.CategoriesNames),
|
||||
nameof(ExportDto.MyRatingOverall),
|
||||
nameof(ExportDto.MyRatingPerformance),
|
||||
nameof(ExportDto.MyRatingStory),
|
||||
nameof(ExportDto.MyLibationTags),
|
||||
nameof(ExportDto.BookStatus),
|
||||
nameof(ExportDto.PdfStatus),
|
||||
nameof(ExportDto.ContentType),
|
||||
nameof(ExportDto.AudioFormat)
|
||||
};
|
||||
var col = 0;
|
||||
foreach (var c in columns)
|
||||
@@ -268,6 +273,7 @@ namespace ApplicationServices
|
||||
row.CreateCell(col++).SetCellValue(dto.BookStatus);
|
||||
row.CreateCell(col++).SetCellValue(dto.PdfStatus);
|
||||
row.CreateCell(col++).SetCellValue(dto.ContentType);
|
||||
row.CreateCell(col++).SetCellValue(dto.AudioFormat);
|
||||
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
@@ -152,6 +152,11 @@ namespace AudibleUtilities
|
||||
Serilog.Log.Logger.Debug("Completed library scan.");
|
||||
|
||||
#if DEBUG
|
||||
//// this will not work for multi accounts
|
||||
//var library_json = "library.json";
|
||||
//library_json = System.IO.Path.GetFullPath(library_json);
|
||||
//if (System.IO.File.Exists(library_json))
|
||||
// items = AudibleApi.Common.Converter.FromJson<List<Item>>(System.IO.File.ReadAllText(library_json));
|
||||
//System.IO.File.WriteAllText(library_json, AudibleApi.Common.Converter.ToJson(items));
|
||||
#endif
|
||||
var validators = new List<IValidator>();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="4.3.1.1" />
|
||||
<PackageReference Include="AudibleApi" Version="7.0.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -10,13 +10,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="4.1.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.6">
|
||||
<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.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.6">
|
||||
<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>
|
||||
@@ -29,10 +30,6 @@
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LibationFileManager\LibationFileManager.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="migrate.json">
|
||||
|
||||
@@ -17,7 +17,6 @@ namespace DataLayer
|
||||
|
||||
public class AudioFormat : IComparable<AudioFormat>, IComparable
|
||||
{
|
||||
|
||||
internal int AudioFormatID { get; private set; }
|
||||
public int Bitrate { get; private init; }
|
||||
public int SampleRate { get; private init; }
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DataLayer
|
||||
{
|
||||
|
||||
@@ -30,10 +30,6 @@ namespace DataLayer
|
||||
{
|
||||
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||
Book = book;
|
||||
|
||||
// import previously saved tags
|
||||
ArgumentValidator.EnsureNotNullOrWhiteSpace(book.AudibleProductId, nameof(book.AudibleProductId));
|
||||
Tags = LibationFileManager.TagsPersistence.GetTags(book.AudibleProductId);
|
||||
}
|
||||
|
||||
#region Tags
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
using DataLayer.Configurations;
|
||||
using Dinah.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DataLayer
|
||||
{
|
||||
public class LibationContext : InterceptableDbContext
|
||||
public class LibationContext : DbContext
|
||||
{
|
||||
// IMPORTANT: USING DbSet<>
|
||||
// ========================
|
||||
@@ -35,14 +34,6 @@ namespace DataLayer
|
||||
// see DesignTimeDbContextFactoryBase for info about ctors and connection strings/OnConfiguring()
|
||||
internal LibationContext(DbContextOptions options) : base(options) { }
|
||||
|
||||
// called on each instantiation
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
AddInterceptor(new TagPersistenceInterceptor());
|
||||
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
}
|
||||
|
||||
// typically only called once per execution; NOT once per instantiation
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dinah.Core.Collections.Generic;
|
||||
using Dinah.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DataLayer
|
||||
{
|
||||
internal class TagPersistenceInterceptor : IDbInterceptor
|
||||
{
|
||||
public void Executed(DbContext context) { }
|
||||
|
||||
public void Executing(DbContext context)
|
||||
{
|
||||
var tagsCollection
|
||||
= context
|
||||
.ChangeTracker
|
||||
.Entries()
|
||||
.Where(e => e.State.In(EntityState.Modified, EntityState.Added))
|
||||
.Select(e => e.Entity as UserDefinedItem)
|
||||
.Where(udi => udi is not null)
|
||||
// do NOT filter out entires with blank tags. blank is the valid way to show the absence of tags
|
||||
.Select(t => (t.Book.AudibleProductId, t.Tags))
|
||||
.ToList();
|
||||
|
||||
LibationFileManager.TagsPersistence.Save(tagsCollection);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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-windows</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-windows</TargetFramework>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -31,13 +31,18 @@ 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(),
|
||||
|
||||
Narrators = libraryBook.Book.Narrators.Select(c => c.Name).ToList(),
|
||||
|
||||
SeriesName = libraryBook.Book.SeriesLink.FirstOrDefault()?.Series.Name,
|
||||
SeriesNumber = libraryBook.Book.SeriesLink.FirstOrDefault()?.Order
|
||||
SeriesNumber = libraryBook.Book.SeriesLink.FirstOrDefault()?.Order,
|
||||
|
||||
BitRate = libraryBook.Book.AudioFormat.Bitrate,
|
||||
SampleRate = libraryBook.Book.AudioFormat.SampleRate,
|
||||
Channels = libraryBook.Book.AudioFormat.Channels,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -20,8 +20,13 @@ namespace FileManager
|
||||
public string Path { get; init; }
|
||||
public override string ToString() => Path;
|
||||
|
||||
private static readonly PlatformID PlatformID = Environment.OSVersion.Platform;
|
||||
|
||||
|
||||
public static implicit operator LongPath(string path)
|
||||
{
|
||||
if (PlatformID is PlatformID.Unix) return new LongPath { Path = path };
|
||||
|
||||
if (path is null) return null;
|
||||
|
||||
//File I/O functions in the Windows API convert "/" to "\" as part of converting
|
||||
@@ -58,6 +63,8 @@ namespace FileManager
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PlatformID is PlatformID.Unix) return Path;
|
||||
|
||||
//Short Path names are useful for navigating to the file in windows explorer,
|
||||
//which will not recognize paths longer than MAX_PATH. Short path names are not
|
||||
//always enabled on every volume. So to check if a volume enables short path
|
||||
@@ -96,6 +103,7 @@ namespace FileManager
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PlatformID is PlatformID.Unix) return Path;
|
||||
if (Path is null) return null;
|
||||
|
||||
StringBuilder longPathBuffer = new(MaxPathLength);
|
||||
@@ -106,15 +114,21 @@ namespace FileManager
|
||||
|
||||
[JsonIgnore]
|
||||
public string PathWithoutPrefix
|
||||
=> Path?.StartsWith(LONG_PATH_PREFIX) == true ?
|
||||
Path.Remove(0, LONG_PATH_PREFIX.Length) :
|
||||
Path;
|
||||
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PlatformID is PlatformID.Unix) return Path;
|
||||
return
|
||||
Path?.StartsWith(LONG_PATH_PREFIX) == true ? Path.Remove(0, LONG_PATH_PREFIX.Length)
|
||||
:Path;
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int GetShortPathName([MarshalAs(UnmanagedType.LPWStr)] string path, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder shortPath, int shortPathLength);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int GetLongPathName([MarshalAs(UnmanagedType.LPWStr)] string lpszShortPath, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpszLongPath, int cchBuffer);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace FileManager
|
||||
internal const char QUOTE_MARK = '"';
|
||||
[JsonIgnore] public bool Mandatory { get; internal set; }
|
||||
[JsonProperty] public char CharacterToReplace { get; private set; }
|
||||
[JsonProperty] public string ReplacementString { get; private set; }
|
||||
[JsonProperty] public string ReplacementString { get; set; }
|
||||
[JsonProperty] public string Description { get; private set; }
|
||||
public override string ToString() => $"{CharacterToReplace} → {ReplacementString} ({Description})";
|
||||
|
||||
@@ -168,8 +168,10 @@ namespace FileManager
|
||||
}
|
||||
|
||||
|
||||
public static bool ContainsInvalid(string path)
|
||||
public static bool ContainsInvalidPathChar(string path)
|
||||
=> path.Any(c => invalidChars.Contains(c));
|
||||
public static bool ContainsInvalidFilenameChar(string path)
|
||||
=> path.Any(c => invalidChars.Concat(new char[] { '\\', '/' }).Contains(c));
|
||||
|
||||
public string ReplaceInvalidFilenameChars(string fileName)
|
||||
{
|
||||
@@ -246,7 +248,7 @@ namespace FileManager
|
||||
dict[3].CharacterToReplace != default3.CharacterToReplace || dict[3].Description != default3.Description ||
|
||||
dict[4].CharacterToReplace != default4.CharacterToReplace || dict[4].Description != default4.Description ||
|
||||
dict[5].CharacterToReplace != default5.CharacterToReplace || dict[5].Description != default5.Description ||
|
||||
dict.Any(r => ReplacementCharacters.ContainsInvalid(r.ReplacementString))
|
||||
dict.Any(r => ReplacementCharacters.ContainsInvalidPathChar(r.ReplacementString))
|
||||
)
|
||||
{
|
||||
dict = ReplacementCharacters.Default.Replacements;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Source/HangoverAvalonia/App.axaml
Normal file
@@ -0,0 +1,12 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:HangoverAvalonia"
|
||||
x:Class="HangoverAvalonia.App">
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator/>
|
||||
</Application.DataTemplates>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme Mode="Light"/>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
29
Source/HangoverAvalonia/App.axaml.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using HangoverAvalonia.ViewModels;
|
||||
using HangoverAvalonia.Views;
|
||||
|
||||
namespace HangoverAvalonia
|
||||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow
|
||||
{
|
||||
DataContext = new MainWindowViewModel(),
|
||||
};
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
85
Source/HangoverAvalonia/HangoverAvalonia.csproj
Normal file
@@ -0,0 +1,85 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<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>
|
||||
<IsPublishable>true</IsPublishable>
|
||||
<AssemblyName>Hangover</AssemblyName>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<StartupObject />
|
||||
<IsPublishable>true</IsPublishable>
|
||||
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
|
||||
<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>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>..\bin\Avalonia\Release</OutputPath>
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
<AvaloniaXaml Remove="Models\**" />
|
||||
<Compile Remove="Models\**" />
|
||||
<EmbeddedResource Remove="Models\**" />
|
||||
<None Remove="Models\**" />
|
||||
<None Remove=".gitignore" />
|
||||
<None Remove="Assets\hangover.ico" />
|
||||
<None Remove="hangover.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="hangover.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<!--This helps with theme dll-s trimming.
|
||||
If you will publish your application in self-contained mode with p:PublishTrimmed=true and it will use Fluent theme Default theme will be trimmed from the output and vice versa.
|
||||
https://github.com/AvaloniaUI/Avalonia/issues/5593 -->
|
||||
<TrimmableAssembly Include="Avalonia.Themes.Fluent" />
|
||||
<TrimmableAssembly Include="Avalonia.Themes.Default" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<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.18" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.18" />
|
||||
<PackageReference Include="XamlNameReferenceGenerator" Version="1.5.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<Target Name="SpicNSpan" AfterTargets="Clean">
|
||||
<!-- Remove obj folder -->
|
||||
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
|
||||
<!-- Remove bin folder -->
|
||||
<RemoveDir Directories="$(BaseOutputPath)" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
24
Source/HangoverAvalonia/Program.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.ReactiveUI;
|
||||
using System;
|
||||
|
||||
namespace HangoverAvalonia
|
||||
{
|
||||
internal class Program
|
||||
{
|
||||
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||
// yet and stuff might break.
|
||||
[STAThread]
|
||||
public static void Main(string[] args) => BuildAvaloniaApp()
|
||||
.StartWithClassicDesktopLifetime(args);
|
||||
|
||||
// Avalonia configuration, don't remove; also used by visual designer.
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.LogToTrace()
|
||||
.UseReactiveUI();
|
||||
}
|
||||
}
|
||||
@@ -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\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>
|
||||
@@ -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\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>
|
||||
@@ -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\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>
|
||||
@@ -1,9 +1,9 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
using LibationWinForms.AvaloniaUI.ViewModels;
|
||||
using HangoverAvalonia.ViewModels;
|
||||
using System;
|
||||
|
||||
namespace LibationWinForms.AvaloniaUI
|
||||
namespace HangoverAvalonia
|
||||
{
|
||||
public class ViewLocator : IDataTemplate
|
||||
{
|
||||
37
Source/HangoverAvalonia/ViewModels/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using HangoverBase;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace HangoverAvalonia.ViewModels
|
||||
{
|
||||
public class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
private DatabaseTab _tab;
|
||||
|
||||
private string _databaseFileText;
|
||||
private bool _databaseFound;
|
||||
private string _sqlResults;
|
||||
public string DatabaseFileText { get => _databaseFileText; set => this.RaiseAndSetIfChanged(ref _databaseFileText, value); }
|
||||
public string SqlQuery { get; set; }
|
||||
public bool DatabaseFound { get => _databaseFound; set => this.RaiseAndSetIfChanged(ref _databaseFound, value); }
|
||||
public string SqlResults { get => _sqlResults; set => this.RaiseAndSetIfChanged(ref _sqlResults, value); }
|
||||
|
||||
public MainWindowViewModel()
|
||||
{
|
||||
_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: {_tab.DbFile}";
|
||||
DatabaseFound = true;
|
||||
}
|
||||
|
||||
public void ExecuteQuery() => _tab.ExecuteQuery();
|
||||
}
|
||||
}
|
||||
11
Source/HangoverAvalonia/ViewModels/ViewModelBase.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace HangoverAvalonia.ViewModels
|
||||
{
|
||||
public class ViewModelBase : ReactiveObject
|
||||
{
|
||||
}
|
||||
}
|
||||
75
Source/HangoverAvalonia/Views/MainWindow.axaml
Normal file
@@ -0,0 +1,75 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:HangoverAvalonia.ViewModels"
|
||||
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="500"
|
||||
Width="800" Height="500"
|
||||
x:Class="HangoverAvalonia.Views.MainWindow"
|
||||
Icon="/Assets/hangover.ico "
|
||||
Title="Hangover: Libation debug and recovery tool">
|
||||
|
||||
<Design.DataContext>
|
||||
<vm:MainWindowViewModel/>
|
||||
</Design.DataContext>
|
||||
|
||||
<TabControl Grid.Row="0">
|
||||
<TabControl.Styles>
|
||||
<Style Selector="ItemsPresenter#PART_ItemsPresenter">
|
||||
<Setter Property="Height" Value="18"/>
|
||||
</Style>
|
||||
<Style Selector="TabItem">
|
||||
<Setter Property="MinHeight" Value="30"/>
|
||||
<Setter Property="Height" Value="30"/>
|
||||
<Setter Property="Padding" Value="8,2,8,0"/>
|
||||
</Style>
|
||||
<Style Selector="TabItem#Header TextBlock">
|
||||
<Setter Property="MinHeight" Value="5"/>
|
||||
</Style>
|
||||
</TabControl.Styles>
|
||||
|
||||
<!-- Database Tab -->
|
||||
<TabItem>
|
||||
<TabItem.Header>
|
||||
<TextBlock FontSize="14" VerticalAlignment="Center">Database</TextBlock>
|
||||
</TabItem.Header>
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto,2*">
|
||||
|
||||
<TextBlock
|
||||
Margin="0,10,0,5"
|
||||
Grid.Row="0"
|
||||
Text="{Binding DatabaseFileText}" />
|
||||
|
||||
<TextBlock
|
||||
Margin="0,5,0,5"
|
||||
Grid.Row="1"
|
||||
Text="SQL (database command)" />
|
||||
|
||||
<TextBox
|
||||
Margin="0,5,0,5"
|
||||
Grid.Row="2" Text="{Binding SqlQuery, Mode=OneWayToSource}" />
|
||||
|
||||
<Button
|
||||
Grid.Row="3"
|
||||
Padding="20,5,20,5"
|
||||
Content="Execute"
|
||||
IsEnabled="{Binding DatabaseFound}"
|
||||
Click="Execute_Click" />
|
||||
|
||||
<TextBox
|
||||
Margin="0,5,0,10"
|
||||
IsReadOnly="True"
|
||||
Grid.Row="4"
|
||||
Text="{Binding SqlResults}" />
|
||||
</Grid>
|
||||
|
||||
</TabItem>
|
||||
<!-- Command Line Interface Tab -->
|
||||
<TabItem>
|
||||
<TabItem.Header>
|
||||
<TextBlock FontSize="14" VerticalAlignment="Center">Command Line Interface</TextBlock>
|
||||
</TabItem.Header>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Window>
|
||||
19
Source/HangoverAvalonia/Views/MainWindow.axaml.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Avalonia.Controls;
|
||||
using HangoverAvalonia.ViewModels;
|
||||
|
||||
namespace HangoverAvalonia.Views
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
MainWindowViewModel _viewModel => DataContext as MainWindowViewModel;
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public void Execute_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
_viewModel.ExecuteQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
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,10 +2,11 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon>hangover.ico</ApplicationIcon>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPublishable>true</IsPublishable>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
@@ -43,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>
|
||||
@@ -54,4 +53,11 @@
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="SpicNSpan" AfterTargets="Clean">
|
||||
<!-- Remove obj folder -->
|
||||
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
|
||||
<!-- Remove bin folder -->
|
||||
<RemoveDir Directories="$(BaseOutputPath)" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Hangover
|
||||
namespace HangoverWinForms
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
@@ -5,12 +5,13 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
<PublishDir>..\bin\publish\</PublishDir>
|
||||
<Platform>Any CPU</Platform>
|
||||
<PublishDir>..\bin\Publish\classic</PublishDir>
|
||||
<PublishProtocol>FileSystem</PublishProtocol>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
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 |
BIN
Source/HangoverWinForms/hangover.ico
Normal file
|
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,7 +64,34 @@ 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
|
||||
@@ -144,6 +171,42 @@ Global
|
||||
{40C67036-C1A7-4FDF-AA83-8EC902E257F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{40C67036-C1A7-4FDF-AA83-8EC902E257F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{40C67036-C1A7-4FDF-AA83-8EC902E257F3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F612D06F-3134-4B9B-95CD-EB3FC798AE60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F612D06F-3134-4B9B-95CD-EB3FC798AE60}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F612D06F-3134-4B9B-95CD-EB3FC798AE60}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F612D06F-3134-4B9B-95CD-EB3FC798AE60}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8A7B01D3-9830-44FD-91A1-D8D010996BEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{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
|
||||
@@ -167,6 +230,17 @@ Global
|
||||
{F2E04270-4551-41C4-99FF-E7125BED708C} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{EB781571-8548-477E-82AD-FB9FAB548D2F} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
|
||||
{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}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:LibationWinForms.AvaloniaUI"
|
||||
x:Class="LibationWinForms.AvaloniaUI.App">
|
||||
xmlns:local="using:LibationAvalonia"
|
||||
x:Class="LibationAvalonia.App">
|
||||
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator/>
|
||||
@@ -11,7 +11,7 @@
|
||||
<FluentTheme Mode="Light"/>
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
|
||||
<StyleInclude Source="/AvaloniaUI/Assets/DataGridTheme.xaml"/>
|
||||
<StyleInclude Source="/AvaloniaUI/Assets/LibationStyles.xaml"/>
|
||||
<StyleInclude Source="/Assets/DataGridTheme.xaml"/>
|
||||
<StyleInclude Source="/Assets/LibationStyles.xaml"/>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
238
Source/LibationAvalonia/App.axaml.cs
Normal file
@@ -0,0 +1,238 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using LibationFileManager;
|
||||
using LibationAvalonia.Views;
|
||||
using System;
|
||||
using Avalonia.Platform;
|
||||
using LibationAvalonia.Dialogs;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using ApplicationServices;
|
||||
using Dinah.Core;
|
||||
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
public class App : Application
|
||||
{
|
||||
public static IBrush ProcessQueueBookFailedBrush { get; private set; }
|
||||
public static IBrush ProcessQueueBookCompletedBrush { get; private set; }
|
||||
public static IBrush ProcessQueueBookCancelledBrush { get; private set; }
|
||||
public static IBrush ProcessQueueBookDefaultBrush { get; private set; }
|
||||
public static IBrush SeriesEntryGridBackgroundBrush { get; private set; }
|
||||
|
||||
public static IAssetLoader AssetLoader { get; private set; }
|
||||
|
||||
public static readonly Uri AssetUriBase = new("avares://Libation/Assets/");
|
||||
public static Stream OpenAsset(string assetRelativePath)
|
||||
=> AssetLoader.Open(new Uri(AssetUriBase, assetRelativePath));
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
AssetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();
|
||||
}
|
||||
|
||||
public static Task<List<DataLayer.LibraryBook>> LibraryTask;
|
||||
public static bool SetupRequired;
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
LoadStyles();
|
||||
|
||||
var SEGOEUI = new Typeface(new FontFamily(new Uri("avares://Libation/Assets/WINGDING.TTF"), "SEGOEUI_Local"));
|
||||
var gtf = FontManager.Current.GetOrAddGlyphTypeface(SEGOEUI);
|
||||
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
if (SetupRequired)
|
||||
{
|
||||
var config = Configuration.Instance;
|
||||
|
||||
var defaultLibationFilesDir = Configuration.UserProfile;
|
||||
|
||||
// check for existing settings in default location
|
||||
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
|
||||
if (Configuration.SettingsFileIsValid(defaultSettingsFile))
|
||||
config.SetLibationFiles(defaultLibationFilesDir);
|
||||
|
||||
if (config.LibationSettingsAreValid)
|
||||
{
|
||||
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
ShowMainWindow(desktop);
|
||||
}
|
||||
else
|
||||
{
|
||||
var setupDialog = new SetupDialog { Config = config };
|
||||
setupDialog.Closing += Setup_Closing;
|
||||
desktop.MainWindow = setupDialog;
|
||||
}
|
||||
}
|
||||
else
|
||||
ShowMainWindow(desktop);
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
|
||||
private async void Setup_Closing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
var setupDialog = sender as SetupDialog;
|
||||
var desktop = ApplicationLifetime as IClassicDesktopStyleApplicationLifetime;
|
||||
|
||||
try
|
||||
{
|
||||
// all returns should be preceded by either:
|
||||
// - if config.LibationSettingsAreValid
|
||||
// - error message, Exit()
|
||||
if (setupDialog.IsNewUser)
|
||||
{
|
||||
setupDialog.Config.SetLibationFiles(Configuration.UserProfile);
|
||||
ShowSettingsWindow(desktop, setupDialog.Config, OnSettingsCompleted);
|
||||
}
|
||||
else if (setupDialog.IsReturningUser)
|
||||
{
|
||||
ShowLibationFilesDialog(desktop, setupDialog.Config, OnLibationFilesCompleted);
|
||||
}
|
||||
else
|
||||
{
|
||||
await CancelInstallation();
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var title = "Fatal error, pre-logging";
|
||||
var body = "An unrecoverable error occurred. Since this error happened before logging could be initialized, this error can not be written to the log file.";
|
||||
try
|
||||
{
|
||||
await MessageBox.ShowAdminAlert(null, body, title, ex);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await MessageBox.Show($"{body}\r\n\r\n{ex.Message}\r\n\r\n{ex.StackTrace}", title, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunMigrationsAsync(Configuration config)
|
||||
{
|
||||
// most migrations go in here
|
||||
AppScaffolding.LibationScaffolding.RunPostConfigMigrations(config);
|
||||
|
||||
await MessageBox.VerboseLoggingWarning_ShowIfTrue();
|
||||
|
||||
// logging is init'd here
|
||||
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(config);
|
||||
}
|
||||
|
||||
private void ShowSettingsWindow(IClassicDesktopStyleApplicationLifetime desktop, Configuration config, Action<IClassicDesktopStyleApplicationLifetime, SettingsDialog, Configuration> OnClose)
|
||||
{
|
||||
config.Books ??= Path.Combine(Configuration.UserProfile, "Books");
|
||||
|
||||
AppScaffolding.LibationScaffolding.PopulateMissingConfigValues(config);
|
||||
|
||||
var settingsDialog = new SettingsDialog();
|
||||
desktop.MainWindow = settingsDialog;
|
||||
settingsDialog.RestoreSizeAndLocation(Configuration.Instance);
|
||||
settingsDialog.Show();
|
||||
|
||||
void WindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
settingsDialog.Closing -= WindowClosing;
|
||||
e.Cancel = true;
|
||||
OnClose?.Invoke(desktop, settingsDialog, config);
|
||||
}
|
||||
settingsDialog.Closing += WindowClosing;
|
||||
}
|
||||
|
||||
private async void OnSettingsCompleted(IClassicDesktopStyleApplicationLifetime desktop, SettingsDialog settingsDialog, Configuration config)
|
||||
{
|
||||
if (config.LibationSettingsAreValid)
|
||||
{
|
||||
await RunMigrationsAsync(config);
|
||||
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
ShowMainWindow(desktop);
|
||||
}
|
||||
else
|
||||
await CancelInstallation();
|
||||
|
||||
settingsDialog.Close();
|
||||
}
|
||||
|
||||
|
||||
private void ShowLibationFilesDialog(IClassicDesktopStyleApplicationLifetime desktop, Configuration config, Action<IClassicDesktopStyleApplicationLifetime, LibationFilesDialog, Configuration> OnClose)
|
||||
{
|
||||
var libationFilesDialog = new LibationFilesDialog();
|
||||
desktop.MainWindow = libationFilesDialog;
|
||||
libationFilesDialog.Show();
|
||||
|
||||
void WindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
libationFilesDialog.Closing -= WindowClosing;
|
||||
e.Cancel = true;
|
||||
OnClose?.Invoke(desktop, libationFilesDialog, config);
|
||||
}
|
||||
libationFilesDialog.Closing += WindowClosing;
|
||||
}
|
||||
|
||||
private async void OnLibationFilesCompleted(IClassicDesktopStyleApplicationLifetime desktop, LibationFilesDialog libationFilesDialog, Configuration config)
|
||||
{
|
||||
config.SetLibationFiles(libationFilesDialog.SelectedDirectory);
|
||||
if (config.LibationSettingsAreValid)
|
||||
{
|
||||
await RunMigrationsAsync(config);
|
||||
|
||||
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
|
||||
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
ShowMainWindow(desktop);
|
||||
}
|
||||
else
|
||||
{
|
||||
// path did not result in valid settings
|
||||
var continueResult = await MessageBox.Show(
|
||||
$"No valid settings were found at this location.\r\nWould you like to create a new install settings in this folder?\r\n\r\n{libationFilesDialog.SelectedDirectory}",
|
||||
"New install?",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Question);
|
||||
|
||||
if (continueResult == DialogResult.Yes)
|
||||
ShowSettingsWindow(desktop, config, OnSettingsCompleted);
|
||||
else
|
||||
await CancelInstallation();
|
||||
|
||||
}
|
||||
libationFilesDialog.Close();
|
||||
}
|
||||
|
||||
static async Task CancelInstallation()
|
||||
{
|
||||
await MessageBox.Show("Initial set up cancelled.", "Cancelled", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
var mainWindow = new MainWindow();
|
||||
desktop.MainWindow = mainWindow;
|
||||
mainWindow.RestoreSizeAndLocation(Configuration.Instance);
|
||||
mainWindow.OnLoad();
|
||||
mainWindow.OnLibraryLoaded(LibraryTask.GetAwaiter().GetResult());
|
||||
mainWindow.Show();
|
||||
}
|
||||
|
||||
private static void LoadStyles()
|
||||
{
|
||||
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookFailedBrush");
|
||||
ProcessQueueBookCompletedBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookCompletedBrush");
|
||||
ProcessQueueBookCancelledBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookCancelledBrush");
|
||||
ProcessQueueBookDefaultBrush = AvaloniaUtils.GetBrushFromResources("ProcessQueueBookDefaultBrush");
|
||||
SeriesEntryGridBackgroundBrush = AvaloniaUtils.GetBrushFromResources("SeriesEntryGridBackgroundBrush");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 95 B After Width: | Height: | Size: 95 B |
@@ -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>
|
||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
BIN
Source/LibationAvalonia/Assets/SEGOEUI.TTF
Normal file
BIN
Source/LibationAvalonia/Assets/WINGDING.TTF
Normal file
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
Source/LibationAvalonia/Assets/download-arrow.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
Source/LibationAvalonia/Assets/edit-tags-25x25.png
Normal file
|
After Width: | Height: | Size: 314 B |
BIN
Source/LibationAvalonia/Assets/edit-tags-50x50.png
Normal file
|
After Width: | Height: | Size: 573 B |
|
Before Width: | Height: | Size: 747 B After Width: | Height: | Size: 747 B |
BIN
Source/LibationAvalonia/Assets/edit_64x64.png
Normal file
|
After Width: | Height: | Size: 813 B |
BIN
Source/LibationAvalonia/Assets/error.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 482 B After Width: | Height: | Size: 482 B |
|
After Width: | Height: | Size: 6.0 KiB |