mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-01-02 19:08:39 -05:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2f68760b2 | ||
|
|
faf86711a5 | ||
|
|
4a78b9d28f | ||
|
|
1b0a7f5062 | ||
|
|
49982043e0 | ||
|
|
378cf7057e | ||
|
|
abdc0f018e | ||
|
|
c65f61b92e | ||
|
|
c12805c8ce | ||
|
|
67f9a6db78 | ||
|
|
bb6336ce2a | ||
|
|
af7a4a6acf | ||
|
|
21d18aa565 | ||
|
|
c96875ba5d | ||
|
|
6ebbfb8e59 | ||
|
|
1e6e28cd57 | ||
|
|
defed72862 | ||
|
|
71503b34b5 | ||
|
|
a00849fb6f | ||
|
|
14b63c0883 | ||
|
|
59d556733e | ||
|
|
a99a175683 | ||
|
|
26fedcfb60 | ||
|
|
dde8024506 | ||
|
|
25f7c29380 | ||
|
|
2f347e83e8 | ||
|
|
080a74884d | ||
|
|
2dbeb64c38 | ||
|
|
bb508c0718 | ||
|
|
9a450b0d63 | ||
|
|
c1de0e60d2 | ||
|
|
dc7c03661d | ||
|
|
952eee6d32 | ||
|
|
472a0f30b9 | ||
|
|
73533c58a8 | ||
|
|
65ef018719 | ||
|
|
f0ca349539 | ||
|
|
500b287721 | ||
|
|
21f3ae45d3 | ||
|
|
d496564f0d | ||
|
|
6fdd6293ce | ||
|
|
3bca495521 | ||
|
|
0fb580f1a5 | ||
|
|
a7cd47e0b1 |
65
.github/workflows/build-linux.yml
vendored
65
.github/workflows/build-linux.yml
vendored
@@ -1,5 +1,5 @@
|
||||
# build-linux.yml
|
||||
# Reusable workflow that builds the Linux and MacOS versions of Libation.
|
||||
# Reusable workflow that builds the Linux and MacOS (x64 and arm64) versions of Libation.
|
||||
---
|
||||
name: build
|
||||
|
||||
@@ -19,6 +19,7 @@ on:
|
||||
env:
|
||||
DOTNET_CONFIGURATION: 'Release'
|
||||
DOTNET_VERSION: '7.0.x'
|
||||
RELEASE_NAME: 'chardonnay'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -26,8 +27,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [Linux, MacOS]
|
||||
ui: [Avalonia]
|
||||
release_name: [chardonnay]
|
||||
arch: [x64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup .NET
|
||||
@@ -57,25 +57,50 @@ jobs:
|
||||
- name: Publish
|
||||
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: Zip artifact
|
||||
id: zip
|
||||
working-directory: ./Source/bin/Publish/${{ matrix.os }}-${{ matrix.release_name }}
|
||||
os=${{ matrix.os }}
|
||||
RUNTIME_IDENTIFIER="$(echo ${os,} | sed 's/macOS/osx/')-${{ matrix.arch }}"
|
||||
echo "$RUNTIME_IDENTIFIER"
|
||||
dotnet publish \
|
||||
LibationAvalonia/LibationAvalonia.csproj \
|
||||
--runtime "$RUNTIME_IDENTIFIER" \
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
||||
--output bin/Publish/${{ matrix.os }}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
||||
-p:PublishProfile=LibationAvalonia/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish \
|
||||
LoadByOS/${{ matrix.os }}ConfigApp/${{ matrix.os }}ConfigApp.csproj \
|
||||
--runtime "$RUNTIME_IDENTIFIER" \
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
||||
--output bin/Publish/${{ matrix.os }}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
||||
-p:PublishProfile=LoadByOS/Properties/${{ matrix.os }}ConfigApp/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish \
|
||||
LibationCli/LibationCli.csproj \
|
||||
--runtime "$RUNTIME_IDENTIFIER" \
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
||||
--output bin/Publish/${{ matrix.os }}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
||||
-p:PublishProfile=LibationCli/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish \
|
||||
HangoverAvalonia/HangoverAvalonia.csproj \
|
||||
--runtime "$RUNTIME_IDENTIFIER" \
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
||||
--output bin/Publish/${{ matrix.os }}-${{ matrix.arch }}-${{ env.RELEASE_NAME }} \
|
||||
-p:PublishProfile=HangoverAvalonia/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
|
||||
- name: Build bundle
|
||||
id: bundle
|
||||
working-directory: ./Source/bin/Publish/${{ matrix.os }}-${{ matrix.arch }}-${{ env.RELEASE_NAME }}
|
||||
run: |
|
||||
delfiles=("libmp3lame.x86.dll" "libmp3lame.x64.dll" "ffmpegaac.x86.dll" "ffmpegaac.x64.dll" "ZipExtractor.exe")
|
||||
for n in "${delfiles[@]}"; do rm "$n"; done
|
||||
osbuild="$(echo '${{ matrix.os }}' | tr '[:upper:]' '[:lower:]')"
|
||||
artifact="Libation.${{ steps.get_version.outputs.version }}-${osbuild}-${{ matrix.release_name }}"
|
||||
BUNDLE_DIR=$(pwd)
|
||||
echo "Bundle dir: ${BUNDLE_DIR}"
|
||||
cd ..
|
||||
SCRIPT=../../../Scripts/Bundle_${{ matrix.os }}.sh
|
||||
chmod +rx ${SCRIPT}
|
||||
${SCRIPT} "${BUNDLE_DIR}" "${{ steps.get_version.outputs.version }}" "${{ matrix.arch }}"
|
||||
artifact=$(ls ./bundle)
|
||||
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
|
||||
tar -zcvf "../${artifact}.tar.gz" .
|
||||
|
||||
- name: Publish artifact
|
||||
|
||||
- name: Publish bundle
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ steps.zip.outputs.artifact }}.tar.gz
|
||||
path: ./Source/bin/Publish/${{ steps.zip.outputs.artifact }}.tar.gz
|
||||
name: ${{ steps.bundle.outputs.artifact }}
|
||||
path: ./Source/bin/Publish/bundle/${{ steps.bundle.outputs.artifact }}
|
||||
if-no-files-found: error
|
||||
|
||||
44
.github/workflows/build-windows.yml
vendored
44
.github/workflows/build-windows.yml
vendored
@@ -60,21 +60,49 @@ jobs:
|
||||
- name: Publish
|
||||
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
|
||||
dotnet publish `
|
||||
Libation${{ matrix.ui }}/Libation${{ matrix.ui }}.csproj `
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} `
|
||||
--output bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} `
|
||||
-p:PublishProfile=Libation${{ matrix.ui }}/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish `
|
||||
LoadByOS/${{ matrix.os }}ConfigApp/${{ matrix.os }}ConfigApp.csproj `
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} `
|
||||
--output bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} `
|
||||
-p:PublishProfile=LoadByOS/Properties/${{ matrix.os }}ConfigApp/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish `
|
||||
LibationCli/LibationCli.csproj `
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} `
|
||||
--output bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} `
|
||||
-p:PublishProfile=LibationCli/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
dotnet publish `
|
||||
Hangover${{ matrix.ui }}/Hangover${{ matrix.ui }}.csproj `
|
||||
--configuration ${{ env.DOTNET_CONFIGURATION }} `
|
||||
--output bin/Publish/${{ matrix.os }}-${{ matrix.release_name }} `
|
||||
-p:PublishProfile=Hangover${{ matrix.ui }}/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
||||
|
||||
- name: Zip artifact
|
||||
id: zip
|
||||
working-directory: ./Source/bin/Publish
|
||||
run: |
|
||||
$dir = "${{ matrix.os }}-${{ matrix.release_name }}\"
|
||||
$delfiles = @("libmp3lame.so", "ffmpegaac.so", "glass-with-glow_256.svg", "Libation.desktop")
|
||||
foreach ($file in $delfiles){ if (test-path $dir$file){ Remove-Item $dir$file } }
|
||||
$bin_dir = "${{ matrix.os }}-${{ matrix.release_name }}\"
|
||||
$delfiles = @(
|
||||
"libmp3lame.x64.so",
|
||||
"libmp3lame.arm64.so",
|
||||
"libmp3lame.x64.dylib",
|
||||
"libmp3lame.arm64.dylib",
|
||||
"ffmpegaac.x64.so",
|
||||
"ffmpegaac.arm64.so",
|
||||
"ffmpegaac.x64.dylib",
|
||||
"ffmpegaac.arm64.dylib",
|
||||
"WindowsConfigApp.exe",
|
||||
"WindowsConfigApp.runtimeconfig.json",
|
||||
"WindowsConfigApp.deps.json"
|
||||
)
|
||||
foreach ($file in $delfiles){ if (test-path $bin_dir$file){ Remove-Item $bin_dir$file } }
|
||||
$artifact="${{ matrix.prefix }}Libation.${{ steps.get_version.outputs.version }}-" + "${{ matrix.os }}".ToLower() + "-${{ matrix.release_name }}"
|
||||
"artifact=$artifact" >> $env:GITHUB_OUTPUT
|
||||
Compress-Archive -Path "${dir}*" -DestinationPath "$artifact.zip"
|
||||
Compress-Archive -Path "${bin_dir}*" -DestinationPath "$artifact.zip"
|
||||
|
||||
- name: Publish artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
|
||||
38
.github/workflows/deb.yml
vendored
38
.github/workflows/deb.yml
vendored
@@ -1,38 +0,0 @@
|
||||
# deb.yml
|
||||
# Reusable workflow that builds the Linux Debian package.
|
||||
---
|
||||
name: deb
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
type: string
|
||||
description: 'Version number'
|
||||
required: true
|
||||
|
||||
env:
|
||||
FILE_NAME: "Libation.${{ inputs.version }}-linux-chardonnay"
|
||||
|
||||
jobs:
|
||||
build_deb:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: "${{ env.FILE_NAME }}.tar.gz"
|
||||
|
||||
- name: Build .deb
|
||||
id: deb
|
||||
run: |
|
||||
./Scripts/targz2deb.sh "${{ env.FILE_NAME }}.tar.gz" ${{ inputs.version }}
|
||||
|
||||
- name: Publish .deb
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ env.FILE_NAME }}.deb
|
||||
path: ${{ env.FILE_NAME }}.deb
|
||||
if-no-files-found: error
|
||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -33,15 +33,9 @@ jobs:
|
||||
with:
|
||||
version_override: ${{ needs.prerelease.outputs.version }}
|
||||
run_unit_tests: false
|
||||
|
||||
deb:
|
||||
needs: [prerelease,build]
|
||||
uses: ./.github/workflows/deb.yml
|
||||
with:
|
||||
version: ${{ needs.prerelease.outputs.version }}
|
||||
|
||||
release:
|
||||
needs: [prerelease,build,deb]
|
||||
needs: [prerelease,build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
@@ -53,7 +47,7 @@ jobs:
|
||||
id: release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: Libation ${{ needs.prerelease.outputs.version }}
|
||||
name: Libation v${{ needs.prerelease.outputs.version }}
|
||||
body: <Put a body here>
|
||||
draft: true
|
||||
prerelease: false
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"WindowsClassic": "Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-classic\\.zip",
|
||||
"WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-chardonnay\\.zip",
|
||||
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay",
|
||||
"MacOSAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-macos-chardonnay"
|
||||
"WindowsClassic": "Classic-Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-classic\\.zip",
|
||||
"WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-chardonnay\\.zip",
|
||||
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-amd64\\.deb",
|
||||
"MacOSAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-macOS-chardonnay-x64\\.tgz",
|
||||
"LinuxAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay-arm64\\.deb",
|
||||
"MacOSAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+-macOS-chardonnay-arm64\\.tgz"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## [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)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.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**.
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## [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)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.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**.
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## [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)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.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**.
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## [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)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.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**.
|
||||
|
||||
|
||||
|
||||
@@ -1,44 +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)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.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
|
||||
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
|
||||
- Download the `Libation.app-macOS-x64-x.x.x.tgz` file from the latest release and extract it.
|
||||
- Move the extracted Libation app bundle to your applications folder.
|
||||
- Open a terminal (Go > Utilities > Terminal)
|
||||
- Copy/paste/run the following command (you'll be prompted to enter your password)
|
||||
```Console
|
||||
sudo spctl --master-disable && sudo spctl --add --label "Libation" /Applications/Libation.app && open /Applications/Libation.app && sudo spctl --master-enable
|
||||
```
|
||||
- Close the terminal and use Libation!
|
||||
|
||||
```console
|
||||
chmod +x ./Libation
|
||||
sudo spctl --add --label "Libation" ./Libation
|
||||
./Libation
|
||||
## Running Hangover
|
||||
|
||||
Libation comes with a recovery app called Hangover. You can start it by running this command:
|
||||
```Console
|
||||
open /Applications/Libation.app --args hangover
|
||||
```
|
||||
|
||||
## Trouble with Gatekeeper?
|
||||
## Runnign LibationCli
|
||||
|
||||
If Gatekeeper is giving you trouble with Libation:
|
||||
Libation comes with a command-line interface. Unfortunately, due to the way apps are sandboxed on mac, its use is somewhat limited. To open a new sandboxed terminal in LibationCli's directory, run the following command:
|
||||
```Console
|
||||
open /Applications/Libation.app --args cli
|
||||
```
|
||||
To use LibationCli from an unsandboxed terminal, you must disable gatekeeper again and run the program directly at `/Applications/Libation.app/Contents/MacOS/LibationCli`
|
||||
|
||||
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
|
||||
Then use `./LibationCli` to execute a command.
|
||||
|
||||
## Get Libation running on Mac
|
||||
|
||||
[Run Libation on MacOS](https://user-images.githubusercontent.com/37587114/213933357-983d8ede-2738-4b32-9c6e-40de21ff09c2.mp4)
|
||||
[Run Libation on MacOS](https://user-images.githubusercontent.com/37587114/219271379-a922e4e1-48a0-48e4-bd81-48aa1226a4f5.mp4)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## [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)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.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**.
|
||||
|
||||
|
||||
|
||||
32
Images/libation_cheers.svg
Normal file
32
Images/libation_cheers.svg
Normal file
@@ -0,0 +1,32 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" width="512px" enable-background="new 0 0 512 512">
|
||||
<path id="slosh" transform=
|
||||
"translate(-50 23)
|
||||
scale(0.7, 0.7)
|
||||
rotate(12 256,256)"
|
||||
d=
|
||||
"M139,2
|
||||
A 192,200 0 0 0 103,84
|
||||
A 222,334 41 0 0 241,320
|
||||
V478
|
||||
H160
|
||||
A 16,16 0 0 0 160,510
|
||||
H352
|
||||
A16 16 0 0 0 352,478
|
||||
H271
|
||||
V320
|
||||
A 222,334 -41 0 0 409,84
|
||||
A 192,200 0 0 0 373,2
|
||||
M355,32
|
||||
A 192,200 0 0 1 381,127
|
||||
A 187.5,334 -35 0 1 256,286
|
||||
A 187.5,334 35 0 1 131,127
|
||||
A 192,200 0 0 1 157,32
|
||||
H355
|
||||
M146,147
|
||||
A 168,300 35 0 0 256,270
|
||||
A 168,300 -35 0 0 366,128
|
||||
S 360,50 280,110
|
||||
S 192,128 147,147
|
||||
z" />
|
||||
<use href="#slosh" transform="translate(512 0) scale(-1 1)" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 736 B |
28
Images/libation_glass.svg
Normal file
28
Images/libation_glass.svg
Normal file
@@ -0,0 +1,28 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" enable-background="new 0 0 512 512">
|
||||
<path id="glass" d=
|
||||
"M139,2
|
||||
A 192,200 0 0 0 103,84
|
||||
A 222,334 41 0 0 241,320
|
||||
V478
|
||||
H160
|
||||
A 16,16 0 0 0 160,510
|
||||
H352
|
||||
A16 16 0 0 0 352,478
|
||||
H271
|
||||
V320
|
||||
A 222,334 -41 0 0 409,84
|
||||
A 192,200 0 0 0 373,2
|
||||
M355,32
|
||||
A 192,200 0 0 1 381,127
|
||||
A 187.5,334 -35 0 1 256,286
|
||||
A 187.5,334 35 0 1 131,127
|
||||
A 192,200 0 0 1 157,32
|
||||
H355
|
||||
z" />
|
||||
|
||||
<path id="wine-level" d=
|
||||
"M146,128
|
||||
A 168,300 35 0 0 256,270
|
||||
A 168,300 -35 0 0 366,128
|
||||
z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 585 B |
30
Images/libation_hangover.svg
Normal file
30
Images/libation_hangover.svg
Normal file
@@ -0,0 +1,30 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" enable-background="new 0 0 512 512">
|
||||
|
||||
<g transform="translate(0 80) rotate(90 256,256)">
|
||||
<path id="glass" d=
|
||||
"M139,2
|
||||
A 192,200 0 0 0 103,84
|
||||
A 222,334 41 0 0 241,320
|
||||
V478
|
||||
H160
|
||||
A 16,16 0 0 0 160,510
|
||||
H352
|
||||
A16 16 0 0 0 352,478
|
||||
H271
|
||||
V320
|
||||
A 222,334 -41 0 0 409,84
|
||||
A 192,200 0 0 0 373,2
|
||||
M355,32
|
||||
A 192,200 0 0 1 381,127
|
||||
A 187.5,334 -35 0 1 256,286
|
||||
A 187.5,334 35 0 1 131,127
|
||||
A 192,200 0 0 1 157,32
|
||||
H355
|
||||
z" />
|
||||
<path id="wine-level" d=
|
||||
"M345,44
|
||||
A 192,184 0 0 1 366,126
|
||||
A 320,180 55 0 1 345,226
|
||||
z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 638 B |
33
Images/libation_slosh.svg
Normal file
33
Images/libation_slosh.svg
Normal file
@@ -0,0 +1,33 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" enable-background="new 0 0 512 512">
|
||||
<path
|
||||
transform=
|
||||
"rotate(15 256,256)
|
||||
translate(0 25)
|
||||
scale(0.93, 0.93)"
|
||||
d=
|
||||
"M139,2
|
||||
A 192,200 0 0 0 103,84
|
||||
A 222,334 41 0 0 241,320
|
||||
V478
|
||||
H160
|
||||
A 16,16 0 0 0 160,510
|
||||
H352
|
||||
A16 16 0 0 0 352,478
|
||||
H271
|
||||
V320
|
||||
A 222,334 -41 0 0 409,84
|
||||
A 192,200 0 0 0 373,2
|
||||
M355,32
|
||||
A 192,200 0 0 1 381,127
|
||||
A 187.5,334 -35 0 1 256,286
|
||||
A 187.5,334 35 0 1 131,127
|
||||
A 192,200 0 0 1 157,32
|
||||
H355
|
||||
M146,147
|
||||
A 168,300 35 0 0 256,270
|
||||
A 168,300 -35 0 0 366,128
|
||||
S 360,50 280,110
|
||||
S 192,128 147,147
|
||||
z" />
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 649 B |
@@ -2,7 +2,7 @@
|
||||
|
||||
## [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)
|
||||
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.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**.
|
||||
|
||||
# Table of Contents
|
||||
|
||||
152
Scripts/Bundle_Linux.sh
Normal file
152
Scripts/Bundle_Linux.sh
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/bin/bash
|
||||
|
||||
BIN_DIR=$1; shift
|
||||
VERSION=$1; shift
|
||||
ARCH=$1; shift
|
||||
|
||||
if [ -z "$BIN_DIR" ]
|
||||
then
|
||||
echo "This script must be called with a the Libation Linux bins directory as an argument."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ ! -d "$BIN_DIR" ]
|
||||
then
|
||||
echo "The directory \"$BIN_DIR\" does not exist."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ -z "$VERSION" ]
|
||||
then
|
||||
echo "This script must be called with the Libation version number as an argument."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ -z "$ARCH" ]
|
||||
then
|
||||
echo "This script must be called with the Libation cpu architecture as an argument."
|
||||
exit
|
||||
fi
|
||||
|
||||
contains() { case "$1" in *"$2"*) true ;; *) false ;; esac }
|
||||
|
||||
if ! contains "$BIN_DIR" "$ARCH"
|
||||
then
|
||||
echo "This script must be called with a Libation binaries for ${ARCH}."
|
||||
exit
|
||||
fi
|
||||
|
||||
ARCH=$(echo $ARCH | sed 's/x64/amd64/')
|
||||
|
||||
DEB_DIR=./deb
|
||||
|
||||
FOLDER_EXEC=$DEB_DIR/usr/lib/libation
|
||||
echo "Exec dir: $FOLDER_EXEC"
|
||||
mkdir -p $FOLDER_EXEC
|
||||
|
||||
echo "Moving bins from $BIN_DIR to $FOLDER_EXEC"
|
||||
mv "${BIN_DIR}/"* $FOLDER_EXEC
|
||||
|
||||
if [ $? -ne 0 ]
|
||||
then echo "Error moving ${BIN_DIR} files"
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
delfiles=('libmp3lame.arm64.dylib' 'libmp3lame.x64.dylib' 'libmp3lame.x64.dll' 'libmp3lame.x86.dll' 'ffmpegaac.arm64.dylib' 'ffmpegaac.x64.dylib' 'ffmpegaac.x64.dll' 'ffmpegaac.x86.dll' 'LinuxConfigApp' 'LinuxConfigApp.deps.json' 'LinuxConfigApp.runtimeconfig.json')
|
||||
if [[ "$ARCH" == "arm64" ]]
|
||||
then
|
||||
delfiles+=('libmp3lame.x64.so' 'ffmpegaac.x64.so')
|
||||
else
|
||||
delfiles+=('libmp3lame.arm64.so' 'ffmpegaac.arm64.so')
|
||||
fi
|
||||
|
||||
for n in "${delfiles[@]}"
|
||||
do
|
||||
echo "Deleting $n"
|
||||
rm $FOLDER_EXEC/$n
|
||||
done
|
||||
|
||||
FOLDER_ICON=$DEB_DIR/usr/share/icons/hicolor/scalable/apps/
|
||||
echo "Icon dir: $FOLDER_ICON"
|
||||
|
||||
FOLDER_DESKTOP=$DEB_DIR/usr/share/applications
|
||||
echo "Desktop dir: $FOLDER_DESKTOP"
|
||||
|
||||
FOLDER_DEBIAN=$DEB_DIR/DEBIAN
|
||||
echo "Debian dir: $FOLDER_DEBIAN"
|
||||
|
||||
mkdir -p $FOLDER_ICON
|
||||
mkdir -p $FOLDER_DESKTOP
|
||||
mkdir -p $FOLDER_DEBIAN
|
||||
|
||||
echo "Copying icon..."
|
||||
cp $FOLDER_EXEC/libation_glass.svg $FOLDER_ICON/libation.svg
|
||||
|
||||
echo "Copying desktop file..."
|
||||
cp $FOLDER_EXEC/Libation.desktop $FOLDER_DESKTOP/Libation.desktop
|
||||
|
||||
echo "Creating pre-install file..."
|
||||
echo "#!/bin/bash
|
||||
|
||||
# Pre-install script, removes previous installation program files and sym links
|
||||
|
||||
echo \"Removing previously created symlinks...\"
|
||||
|
||||
rm /usr/bin/libation
|
||||
rm /usr/bin/hangover
|
||||
rm /usr/bin/libationcli
|
||||
|
||||
echo \"Removing previously installed Libation files...\"
|
||||
|
||||
rm -r /usr/lib/libation
|
||||
|
||||
# making sure it won't stop installation
|
||||
exit 0
|
||||
" >> $FOLDER_DEBIAN/preinst
|
||||
|
||||
echo "Creating post-install file..."
|
||||
echo "#!/bin/bash
|
||||
|
||||
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
|
||||
|
||||
# Increase the maximum number of inotify instances
|
||||
|
||||
if ! grep -q 'fs.inotify.max_user_instances=524288' /etc/sysctl.conf; then
|
||||
echo fs.inotify.max_user_instances=524288 | tee -a /etc/sysctl.conf && sysctl -p
|
||||
fi
|
||||
|
||||
# workaround until this file is moved to the user's home directory
|
||||
touch /usr/lib/libation/appsettings.json
|
||||
chmod 666 /usr/lib/libation/appsettings.json
|
||||
" >> $FOLDER_DEBIAN/postinst
|
||||
|
||||
echo "Creating control file..."
|
||||
echo "Package: Libation
|
||||
Version: $VERSION
|
||||
Architecture: $ARCH
|
||||
Essential: no
|
||||
Priority: optional
|
||||
Maintainer: github.com/rmcrackan
|
||||
Description: liberate your audiobooks
|
||||
" >> $FOLDER_DEBIAN/control
|
||||
|
||||
echo "Changing permissions for pre- and post-install files..."
|
||||
chmod +x "$FOLDER_DEBIAN/preinst"
|
||||
chmod +x "$FOLDER_DEBIAN/postinst"
|
||||
|
||||
DEB_FILE=Libation.${VERSION}-linux-chardonnay-${ARCH}.deb
|
||||
echo "Creating $DEB_FILE"
|
||||
dpkg-deb -Zxz --build $DEB_DIR ./$DEB_FILE
|
||||
|
||||
echo "moving to ./bundle/$DEB_FILE"
|
||||
mkdir bundle
|
||||
mv $DEB_FILE ./bundle/$DEB_FILE
|
||||
|
||||
rm -r "$BIN_DIR"
|
||||
|
||||
echo "Done!"
|
||||
108
Scripts/Bundle_MacOS.sh
Normal file
108
Scripts/Bundle_MacOS.sh
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/bin/bash
|
||||
|
||||
BIN_DIR=$1; shift
|
||||
VERSION=$1; shift
|
||||
ARCH=$1; shift
|
||||
|
||||
if [ -z "$BIN_DIR" ]
|
||||
then
|
||||
echo "This script must be called with a the Libation macos bins directory as an argument."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ ! -d "$BIN_DIR" ]
|
||||
then
|
||||
echo "The directory \"$BIN_DIR\" does not exist."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ -z $VERSION ]
|
||||
then
|
||||
echo "This script must be called with the Libation version number as an argument."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ -z $ARCH ]
|
||||
then
|
||||
echo "This script must be called with the Libation cpu architecture as an argument."
|
||||
exit
|
||||
fi
|
||||
|
||||
contains() { case "$1" in *"$2"*) true ;; *) false ;; esac }
|
||||
|
||||
if ! contains "$BIN_DIR" $ARCH
|
||||
then
|
||||
echo "This script must be called with a Libation binaries for ${ARCH}."
|
||||
exit
|
||||
fi
|
||||
|
||||
BUNDLE=./Libation.app
|
||||
echo "Bundle dir: $BUNDLE"
|
||||
|
||||
if [[ -d $BUNDLE ]]
|
||||
then
|
||||
echo "$BUNDLE directory already exists, aborting."
|
||||
exit
|
||||
fi
|
||||
|
||||
BUNDLE_CONTENTS=$BUNDLE/Contents
|
||||
echo "Bundle Contents dir: $BUNDLE_CONTENTS"
|
||||
|
||||
BUNDLE_RESOURCES=$BUNDLE_CONTENTS/Resources
|
||||
echo "Resources dir: $BUNDLE_RESOURCES"
|
||||
|
||||
BUNDLE_MACOS=$BUNDLE_CONTENTS/MacOS
|
||||
echo "MacOS dir: $BUNDLE_MACOS"
|
||||
|
||||
mkdir -p $BUNDLE_CONTENTS
|
||||
mkdir -p $BUNDLE_RESOURCES
|
||||
mkdir -p $BUNDLE_MACOS
|
||||
|
||||
mv "${BIN_DIR}/"* $BUNDLE_MACOS
|
||||
|
||||
if [ $? -ne 0 ]
|
||||
then echo "Error moving ${BIN_DIR} files"
|
||||
exit
|
||||
fi
|
||||
|
||||
echo "Moving icon..."
|
||||
mv $BUNDLE_MACOS/libation.icns $BUNDLE_RESOURCES/libation.icns
|
||||
|
||||
echo "Moving Info.plist file..."
|
||||
mv $BUNDLE_MACOS/Info.plist $BUNDLE_CONTENTS/Info.plist
|
||||
|
||||
PLIST_ARCH=$(echo $ARCH | sed 's/x64/x86_64/')
|
||||
echo "Set LSArchitecturePriority to $PLIST_ARCH"
|
||||
sed -i -e "s/ARCHITECTURE_STRING/$PLIST_ARCH/" $BUNDLE_CONTENTS/Info.plist
|
||||
|
||||
echo "Set CFBundleVersion to $VERSION"
|
||||
sed -i -e "s/VERSION_STRING/$VERSION/" $BUNDLE_CONTENTS/Info.plist
|
||||
|
||||
|
||||
delfiles=( 'libmp3lame.arm64.so' 'libmp3lame.x64.so' 'libmp3lame.x64.dll' 'libmp3lame.x86.dll' 'ffmpegaac.arm64.so' 'ffmpegaac.x64.so' 'ffmpegaac.x64.dll' 'ffmpegaac.x86.dll' 'MacOSConfigApp' 'MacOSConfigApp.deps.json' 'MacOSConfigApp.runtimeconfig.json')
|
||||
if [[ "$ARCH" == "arm64" ]]
|
||||
then
|
||||
delfiles+=('libmp3lame.x64.dylib' 'ffmpegaac.x64.dylib')
|
||||
else
|
||||
delfiles+=('libmp3lame.arm64.dylib' 'ffmpegaac.arm64.dylib')
|
||||
fi
|
||||
|
||||
|
||||
for n in "${delfiles[@]}"
|
||||
do
|
||||
echo "Deleting $n"
|
||||
rm $BUNDLE_MACOS/$n
|
||||
done
|
||||
|
||||
APP_FILE=Libation.${VERSION}-macOS-chardonnay-${ARCH}.tgz
|
||||
|
||||
echo "Creating app bundle: $APP_FILE"
|
||||
tar -czvf $APP_FILE $BUNDLE
|
||||
|
||||
mkdir bundle
|
||||
echo "moving to ./bundle/$APP_FILE"
|
||||
mv $APP_FILE ./bundle/$APP_FILE
|
||||
|
||||
rm -r $BUNDLE
|
||||
|
||||
echo "Done!"
|
||||
@@ -1,136 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
FILE=$1; shift
|
||||
VERSION=$1; shift
|
||||
|
||||
if [ -z "$FILE" ]
|
||||
then
|
||||
echo "This script must be called with a the Libation Linux bin zip file as an argument."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ ! -f "$FILE" ]
|
||||
then
|
||||
echo "The file \"$FILE\" does not exist."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [ -z "$VERSION" ]
|
||||
then
|
||||
echo "This script must be called with the Libation version number as an argument."
|
||||
exit
|
||||
fi
|
||||
|
||||
contains() { case "$1" in *"$2"*) true ;; *) false ;; esac }
|
||||
|
||||
if ! contains "$FILE" "$VERSION"
|
||||
then
|
||||
echo "This script must be called with a Libation version number that is present in the filename passed."
|
||||
exit
|
||||
fi
|
||||
|
||||
# remove trailing ".tar.gz"
|
||||
FOLDER_MAIN=${FILE::-7}
|
||||
echo "Working dir: $FOLDER_MAIN"
|
||||
|
||||
if [[ -d "$FOLDER_MAIN" ]]
|
||||
then
|
||||
echo "$FOLDER_MAIN directory already exists, aborting."
|
||||
exit
|
||||
fi
|
||||
|
||||
FOLDER_EXEC="$FOLDER_MAIN/usr/lib/libation"
|
||||
echo "Exec dir: $FOLDER_EXEC"
|
||||
|
||||
FOLDER_ICON="$FOLDER_MAIN/usr/share/icons/hicolor/scalable/apps/"
|
||||
echo "Icon dir: $FOLDER_ICON"
|
||||
|
||||
FOLDER_DESKTOP="$FOLDER_MAIN/usr/share/applications"
|
||||
echo "Desktop dir: $FOLDER_DESKTOP"
|
||||
|
||||
FOLDER_DEBIAN="$FOLDER_MAIN/DEBIAN"
|
||||
echo "Debian dir: $FOLDER_DEBIAN"
|
||||
|
||||
mkdir -p "$FOLDER_EXEC"
|
||||
mkdir -p "$FOLDER_ICON"
|
||||
mkdir -p "$FOLDER_DESKTOP"
|
||||
mkdir -p "$FOLDER_DEBIAN"
|
||||
|
||||
echo "Extracting $FILE to $FOLDER_EXEC..."
|
||||
tar -xzf ${FILE} -C ${FOLDER_EXEC}
|
||||
|
||||
if [ $? -ne 0 ]
|
||||
then echo "Error extracting ${FILE}"
|
||||
exit
|
||||
fi
|
||||
|
||||
echo "Copying icon..."
|
||||
cp "$FOLDER_EXEC/glass-with-glow_256.svg" "$FOLDER_ICON/libation.svg"
|
||||
|
||||
echo "Copying desktop file..."
|
||||
cp "$FOLDER_EXEC/Libation.desktop" "$FOLDER_DESKTOP/Libation.desktop"
|
||||
|
||||
echo "Workaround for desktop file..."
|
||||
sed -i '/^Exec=Libation/c\Exec=/usr/bin/libation' "$FOLDER_DESKTOP/Libation.desktop"
|
||||
|
||||
echo "Creating pre-install file..."
|
||||
echo "#!/bin/bash
|
||||
|
||||
# Pre-install script, removes previous installation program files and sym links
|
||||
|
||||
echo \"Removing previously created symlinks...\"
|
||||
|
||||
rm /usr/bin/libation
|
||||
rm /usr/bin/Libation
|
||||
rm /usr/bin/hangover
|
||||
rm /usr/bin/Hangover
|
||||
rm /usr/bin/libationcli
|
||||
rm /usr/bin/LibationCli
|
||||
|
||||
echo \"Removing previously installed Libation files...\"
|
||||
|
||||
rm -r /usr/lib/libation
|
||||
rm -r /usr/lib/Libation
|
||||
|
||||
# making sure it won't stop installation
|
||||
exit 0
|
||||
" >> "$FOLDER_DEBIAN/preinst"
|
||||
|
||||
echo "Creating post-install file..."
|
||||
echo "#!/bin/bash
|
||||
|
||||
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
|
||||
|
||||
# Increase the maximum number of inotify instances
|
||||
echo fs.inotify.max_user_instances=524288 | tee -a /etc/sysctl.conf && sysctl -p
|
||||
|
||||
# workaround until this file is moved to the user's home directory
|
||||
touch /usr/lib/libation/appsettings.json
|
||||
chmod 666 /usr/lib/libation/appsettings.json
|
||||
" >> "$FOLDER_DEBIAN/postinst"
|
||||
|
||||
echo "Creating control file..."
|
||||
echo "Package: Libation
|
||||
Version: $VERSION
|
||||
Architecture: all
|
||||
Essential: no
|
||||
Priority: optional
|
||||
Maintainer: github.com/rmcrackan
|
||||
Description: liberate your audiobooks
|
||||
" >> "$FOLDER_DEBIAN/control"
|
||||
|
||||
echo "Changing permissions for pre- and post-install files..."
|
||||
chmod +x "$FOLDER_DEBIAN/preinst"
|
||||
chmod +x "$FOLDER_DEBIAN/postinst"
|
||||
|
||||
echo "Creating .deb file..."
|
||||
dpkg-deb -Zxz --build $FOLDER_MAIN
|
||||
|
||||
rm -r "$FOLDER_MAIN"
|
||||
|
||||
echo "Done!"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean.Codecs" Version="0.5.12" />
|
||||
<PackageReference Include="AAXClean.Codecs" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -40,8 +40,14 @@ namespace AaxDecrypter
|
||||
AaxFile.AppleTags.Album = AaxFile.AppleTags.Album?.Replace(" (Unabridged)", "");
|
||||
}
|
||||
|
||||
if (DownloadOptions.FixupFile && !string.IsNullOrWhiteSpace(AaxFile.AppleTags.Narrator))
|
||||
AaxFile.AppleTags.AppleListBox.EditOrAddTag("TCOM", AaxFile.AppleTags.Narrator);
|
||||
if (DownloadOptions.FixupFile)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(AaxFile.AppleTags.Narrator))
|
||||
AaxFile.AppleTags.AppleListBox.EditOrAddTag("TCOM", AaxFile.AppleTags.Narrator);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(AaxFile.AppleTags.Copyright))
|
||||
AaxFile.AppleTags.Copyright = AaxFile.AppleTags.Copyright.Replace("(P)", "℗").Replace("©", "©");
|
||||
}
|
||||
|
||||
//Finishing configuring lame encoder.
|
||||
if (DownloadOptions.OutputFormat == OutputFormat.Mp3)
|
||||
|
||||
@@ -93,15 +93,13 @@ That naming may not be desirable for everyone, but it's an easy change to instea
|
||||
? AaxFile.ConvertToMultiMp4aAsync
|
||||
(
|
||||
splitChapters,
|
||||
newSplitCallback => newSplit(++chapterCount, splitChapters, newSplitCallback),
|
||||
DownloadOptions.TrimOutputToChapterLength
|
||||
newSplitCallback => newSplit(++chapterCount, splitChapters, newSplitCallback)
|
||||
)
|
||||
: AaxFile.ConvertToMultiMp3Async
|
||||
(
|
||||
splitChapters,
|
||||
newSplitCallback => newSplit(++chapterCount, splitChapters, newSplitCallback),
|
||||
DownloadOptions.LameConfig,
|
||||
DownloadOptions.TrimOutputToChapterLength
|
||||
DownloadOptions.LameConfig
|
||||
);
|
||||
|
||||
void newSplit(int currentChapter, ChapterInfo splitChapters, NewSplitCallback newSplitCallback)
|
||||
|
||||
@@ -3,6 +3,7 @@ using AAXClean.Codecs;
|
||||
using Dinah.Core.Net.Http;
|
||||
using FileManager;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -14,12 +15,15 @@ namespace AaxDecrypter
|
||||
public AaxcDownloadSingleConverter(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
|
||||
: base(outFileName, cacheDirectory, dlOptions)
|
||||
{
|
||||
var step = 1;
|
||||
|
||||
AsyncSteps.Name = $"Download and Convert Aaxc To {DownloadOptions.OutputFormat}";
|
||||
AsyncSteps["Step 1: Get Aaxc Metadata"] = () => Task.Run(Step_GetMetadata);
|
||||
AsyncSteps["Step 2: Download Decrypted Audiobook"] = Step_DownloadAndDecryptAudiobookAsync;
|
||||
AsyncSteps["Step 3: Download Clips and Bookmarks"] = Step_DownloadClipsBookmarksAsync;
|
||||
AsyncSteps["Step 4: Create Cue"] = Step_CreateCueAsync;
|
||||
AsyncSteps[$"Step {step++}: Get Aaxc Metadata"] = () => Task.Run(Step_GetMetadata);
|
||||
AsyncSteps[$"Step {step++}: Download Decrypted Audiobook"] = Step_DownloadAndDecryptAudiobookAsync;
|
||||
if (DownloadOptions.MoveMoovToBeginning && DownloadOptions.OutputFormat is OutputFormat.M4b)
|
||||
AsyncSteps[$"Step {step++}: Move moov atom to beginning"] = Step_MoveMoov;
|
||||
AsyncSteps[$"Step {step++}: Download Clips and Bookmarks"] = Step_DownloadClipsBookmarksAsync;
|
||||
AsyncSteps[$"Step {step++}: Create Cue"] = Step_CreateCueAsync;
|
||||
}
|
||||
|
||||
protected async override Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
||||
@@ -31,18 +35,7 @@ namespace AaxDecrypter
|
||||
|
||||
try
|
||||
{
|
||||
await (AaxConversion = decryptAsync(outputFile));
|
||||
|
||||
if (AaxConversion.IsCompletedSuccessfully
|
||||
&& DownloadOptions.MoveMoovToBeginning
|
||||
&& DownloadOptions.OutputFormat is OutputFormat.M4b)
|
||||
{
|
||||
outputFile.Close();
|
||||
AaxConversion = Mp4File.RelocateMoovAsync(OutputFileName);
|
||||
AaxConversion.ConversionProgressUpdate += AaxConversion_MoovProgressUpdate;
|
||||
await AaxConversion;
|
||||
AaxConversion.ConversionProgressUpdate -= AaxConversion_MoovProgressUpdate;
|
||||
}
|
||||
await (AaxConversion = decryptAsync(outputFile));
|
||||
|
||||
return AaxConversion.IsCompletedSuccessfully;
|
||||
}
|
||||
@@ -52,23 +45,30 @@ namespace AaxDecrypter
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> Step_MoveMoov()
|
||||
{
|
||||
AaxConversion = Mp4File.RelocateMoovAsync(OutputFileName);
|
||||
AaxConversion.ConversionProgressUpdate += AaxConversion_MoovProgressUpdate;
|
||||
await AaxConversion;
|
||||
AaxConversion.ConversionProgressUpdate -= AaxConversion_MoovProgressUpdate;
|
||||
return AaxConversion.IsCompletedSuccessfully;
|
||||
}
|
||||
|
||||
private void AaxConversion_MoovProgressUpdate(object sender, ConversionProgressEventArgs e)
|
||||
{
|
||||
averageSpeed.AddPosition(e.ProcessPosition.TotalSeconds);
|
||||
|
||||
var remainingTimeToProcess = (e.TotalDuration - e.ProcessPosition).TotalSeconds;
|
||||
var remainingTimeToProcess = (e.EndTime - e.ProcessPosition).TotalSeconds;
|
||||
var estTimeRemaining = remainingTimeToProcess / averageSpeed.Average;
|
||||
|
||||
if (double.IsNormal(estTimeRemaining))
|
||||
OnDecryptTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
|
||||
|
||||
var progressPercent = 100d * (1 - remainingTimeToProcess / e.TotalDuration.TotalSeconds);
|
||||
|
||||
OnDecryptProgressUpdate(
|
||||
new DownloadProgress
|
||||
{
|
||||
ProgressPercentage = progressPercent,
|
||||
BytesReceived = (long)(InputFileStream.Length * progressPercent),
|
||||
ProgressPercentage = 100 * e.FractionCompleted,
|
||||
BytesReceived = (long)(InputFileStream.Length * e.FractionCompleted),
|
||||
TotalBytesToReceive = InputFileStream.Length
|
||||
});
|
||||
}
|
||||
@@ -79,15 +79,13 @@ namespace AaxDecrypter
|
||||
(
|
||||
outputFile,
|
||||
DownloadOptions.LameConfig,
|
||||
DownloadOptions.ChapterInfo,
|
||||
DownloadOptions.TrimOutputToChapterLength
|
||||
DownloadOptions.ChapterInfo
|
||||
)
|
||||
: DownloadOptions.FixupFile
|
||||
? AaxFile.ConvertToMp4aAsync
|
||||
(
|
||||
outputFile,
|
||||
DownloadOptions.ChapterInfo,
|
||||
DownloadOptions.TrimOutputToChapterLength
|
||||
DownloadOptions.ChapterInfo
|
||||
)
|
||||
: AaxFile.ConvertToMp4aAsync(outputFile);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Dinah.Core.Net.Http;
|
||||
using Dinah.Core.StepRunner;
|
||||
using FileManager;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -26,6 +27,7 @@ namespace AaxDecrypter
|
||||
protected IDownloadOptions DownloadOptions { get; }
|
||||
protected NetworkFileStream InputFileStream => nfsPersister.NetworkFileStream;
|
||||
protected virtual long InputFilePosition => InputFileStream.Position;
|
||||
private bool downloadFinished;
|
||||
|
||||
private readonly NetworkFileStreamPersister nfsPersister;
|
||||
private readonly DownloadProgress zeroProgress;
|
||||
@@ -58,7 +60,7 @@ namespace AaxDecrypter
|
||||
{
|
||||
BytesReceived = 0,
|
||||
ProgressPercentage = 0,
|
||||
TotalBytesToReceive = InputFileStream.Length
|
||||
TotalBytesToReceive = 0
|
||||
};
|
||||
|
||||
OnDecryptProgressUpdate(zeroProgress);
|
||||
@@ -66,6 +68,7 @@ namespace AaxDecrypter
|
||||
|
||||
public async Task<bool> RunAsync()
|
||||
{
|
||||
await InputFileStream.BeginDownloadingAsync();
|
||||
var progressTask = Task.Run(reportProgress);
|
||||
|
||||
AsyncSteps[$"Cleanup"] = CleanupAsync;
|
||||
@@ -82,7 +85,11 @@ namespace AaxDecrypter
|
||||
{
|
||||
AverageSpeed averageSpeed = new();
|
||||
|
||||
while (InputFileStream.CanRead && InputFileStream.Length > InputFilePosition && !InputFileStream.IsCancelled)
|
||||
while (
|
||||
InputFileStream.CanRead
|
||||
&& InputFileStream.Length > InputFilePosition
|
||||
&& !InputFileStream.IsCancelled
|
||||
&& !downloadFinished)
|
||||
{
|
||||
averageSpeed.AddPosition(InputFilePosition);
|
||||
|
||||
@@ -136,8 +143,7 @@ namespace AaxDecrypter
|
||||
protected virtual void FinalizeDownload()
|
||||
{
|
||||
nfsPersister?.Dispose();
|
||||
OnDecryptTimeRemaining(TimeSpan.Zero);
|
||||
OnDecryptProgressUpdate(zeroProgress);
|
||||
downloadFinished = true;
|
||||
}
|
||||
|
||||
protected async Task<bool> Step_DownloadClipsBookmarksAsync()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using AAXClean;
|
||||
using NAudio.Lame;
|
||||
using System;
|
||||
|
||||
namespace AaxDecrypter
|
||||
{
|
||||
@@ -9,17 +10,26 @@ namespace AaxDecrypter
|
||||
{
|
||||
double bitrateMultiple = 1;
|
||||
|
||||
if (mp4File.TimeScale < lameConfig.OutputSampleRate)
|
||||
{
|
||||
lameConfig.OutputSampleRate = mp4File.TimeScale;
|
||||
}
|
||||
else if (mp4File.TimeScale > lameConfig.OutputSampleRate)
|
||||
{
|
||||
bitrateMultiple *= (double)lameConfig.OutputSampleRate / mp4File.TimeScale;
|
||||
}
|
||||
|
||||
if (mp4File.AudioChannels == 2)
|
||||
{
|
||||
if (downsample)
|
||||
bitrateMultiple = 0.5;
|
||||
bitrateMultiple /= 2;
|
||||
else
|
||||
lameConfig.Mode = MPEGMode.Stereo;
|
||||
}
|
||||
|
||||
if (matchSourceBitrate)
|
||||
{
|
||||
int kbps = (int)(mp4File.AverageBitrate * bitrateMultiple / 1024);
|
||||
int kbps = (int)Math.Round(mp4File.AverageBitrate * bitrateMultiple / 1024);
|
||||
|
||||
if (lameConfig.VBR is null)
|
||||
lameConfig.BitRate = kbps;
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace AaxDecrypter
|
||||
public class NetworkFileStream : Stream, IUpdatable
|
||||
{
|
||||
public event EventHandler Updated;
|
||||
public event EventHandler DownloadCompleted;
|
||||
|
||||
#region Public Properties
|
||||
|
||||
@@ -136,10 +137,13 @@ namespace AaxDecrypter
|
||||
|
||||
/// <summary> Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread. </summary>
|
||||
/// <returns>The downloader <see cref="Task"/></returns>
|
||||
private Task BeginDownloading()
|
||||
public async Task BeginDownloadingAsync()
|
||||
{
|
||||
if (ContentLength != 0 && WritePosition == ContentLength)
|
||||
return Task.CompletedTask;
|
||||
{
|
||||
_backgroundDownloadTask = Task.CompletedTask;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ContentLength != 0 && WritePosition > ContentLength)
|
||||
throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10}).");
|
||||
@@ -149,7 +153,7 @@ namespace AaxDecrypter
|
||||
foreach (var header in RequestHeaders)
|
||||
request.Headers.Add(header.Key, header.Value);
|
||||
|
||||
var response = new HttpClient().Send(request, HttpCompletionOption.ResponseHeadersRead, _cancellationSource.Token);
|
||||
var response = await new HttpClient().SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _cancellationSource.Token);
|
||||
|
||||
if (response.StatusCode != HttpStatusCode.PartialContent)
|
||||
throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}.");
|
||||
@@ -159,11 +163,11 @@ namespace AaxDecrypter
|
||||
if (WritePosition == 0)
|
||||
ContentLength = response.Content.Headers.ContentLength.GetValueOrDefault();
|
||||
|
||||
var networkStream = response.Content.ReadAsStream(_cancellationSource.Token);
|
||||
var networkStream = await response.Content.ReadAsStreamAsync(_cancellationSource.Token);
|
||||
_downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
|
||||
|
||||
//Download the file in the background.
|
||||
return Task.Run(() => DownloadFile(networkStream), _cancellationSource.Token);
|
||||
_backgroundDownloadTask = Task.Run(() => DownloadFile(networkStream), _cancellationSource.Token);
|
||||
}
|
||||
|
||||
/// <summary> Download <see cref="Uri"/> to <see cref="SaveFilePath"/>.</summary>
|
||||
@@ -230,6 +234,7 @@ namespace AaxDecrypter
|
||||
_writeFile.Close();
|
||||
_downloadedPiece.Set();
|
||||
OnUpdate();
|
||||
DownloadCompleted?.Invoke(this, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +256,8 @@ namespace AaxDecrypter
|
||||
{
|
||||
get
|
||||
{
|
||||
_backgroundDownloadTask ??= BeginDownloading();
|
||||
if (_backgroundDownloadTask is null)
|
||||
throw new InvalidOperationException($"Background downloader must first be started by calling {nameof(BeginDownloadingAsync)}");
|
||||
return ContentLength;
|
||||
}
|
||||
}
|
||||
@@ -274,7 +280,8 @@ namespace AaxDecrypter
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
_backgroundDownloadTask ??= BeginDownloading();
|
||||
if (_backgroundDownloadTask is null)
|
||||
throw new InvalidOperationException($"Background downloader must first be started by calling {nameof(BeginDownloadingAsync)}");
|
||||
|
||||
var toRead = Math.Min(count, Length - Position);
|
||||
WaitToPosition(Position + toRead);
|
||||
|
||||
@@ -26,9 +26,11 @@ namespace AaxDecrypter
|
||||
|
||||
protected override async Task<bool> Step_DownloadAndDecryptAudiobookAsync()
|
||||
{
|
||||
// MUST put InputFileStream.Length first, because it starts background downloader.
|
||||
while (InputFileStream.Length > InputFilePosition && !InputFileStream.IsCancelled)
|
||||
await Task.Delay(200);
|
||||
TaskCompletionSource completionSource = new();
|
||||
|
||||
InputFileStream.DownloadCompleted += (_, _) => completionSource.SetResult();
|
||||
|
||||
await completionSource.Task;
|
||||
|
||||
if (IsCanceled)
|
||||
return false;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Version>9.3.0.1</Version>
|
||||
<Version>9.4.0.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="5.0.0" />
|
||||
|
||||
@@ -9,7 +9,7 @@ using Dinah.Core;
|
||||
using Dinah.Core.IO;
|
||||
using Dinah.Core.Logging;
|
||||
using LibationFileManager;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Runtime.InteropServices;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
|
||||
@@ -18,14 +18,22 @@ namespace AppScaffolding
|
||||
public enum ReleaseIdentifier
|
||||
{
|
||||
None,
|
||||
WindowsClassic,
|
||||
WindowsAvalonia,
|
||||
LinuxAvalonia,
|
||||
MacOSAvalonia
|
||||
WindowsClassic = OS.Windows | Variety.Classic | Architecture.X64,
|
||||
WindowsAvalonia = OS.Windows | Variety.Chardonnay | Architecture.X64,
|
||||
LinuxAvalonia = OS.Linux | Variety.Chardonnay | Architecture.X64,
|
||||
MacOSAvalonia = OS.MacOS | Variety.Chardonnay | Architecture.X64,
|
||||
LinuxAvalonia_Arm64 = OS.Linux | Variety.Chardonnay | Architecture.Arm64,
|
||||
MacOSAvalonia_Arm64 = OS.MacOS | Variety.Chardonnay | Architecture.Arm64
|
||||
}
|
||||
|
||||
// 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 }
|
||||
[Flags]
|
||||
public enum Variety
|
||||
{
|
||||
None,
|
||||
Classic = 0x10000,
|
||||
Chardonnay = 0x20000,
|
||||
}
|
||||
|
||||
public static class LibationScaffolding
|
||||
{
|
||||
@@ -33,13 +41,22 @@ namespace AppScaffolding
|
||||
public const string WebsiteUrl = "ht" + "tps://getlibation.com";
|
||||
public const string RepositoryLatestUrl = "ht" + "tps://github.com/rmcrackan/Libation/releases/latest";
|
||||
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 Variety Variety { get; private set; }
|
||||
|
||||
public static void SetReleaseIdentifier(ReleaseIdentifier releaseID)
|
||||
=> ReleaseIdentifier = releaseID;
|
||||
public static void SetReleaseIdentifier(Variety varietyType)
|
||||
{
|
||||
Variety = Enum.IsDefined(varietyType) ? varietyType : Variety.None;
|
||||
|
||||
var releaseID = (ReleaseIdentifier)((int)varietyType | (int)Configuration.OS | (int)RuntimeInformation.ProcessArchitecture);
|
||||
|
||||
if (Enum.IsDefined(releaseID))
|
||||
ReleaseIdentifier = releaseID;
|
||||
else
|
||||
{
|
||||
ReleaseIdentifier = ReleaseIdentifier.None;
|
||||
Serilog.Log.Logger.Warning("Unknown release identifier @{DebugInfo}", new { Variety = varietyType, Configuration.OS, RuntimeInformation.ProcessArchitecture });
|
||||
}
|
||||
}
|
||||
|
||||
// AppScaffolding
|
||||
private static Assembly _executingAssembly;
|
||||
@@ -296,8 +313,8 @@ namespace AppScaffolding
|
||||
}
|
||||
private static async System.Threading.Tasks.Task<(Octokit.Release, Octokit.ReleaseAsset)> getLatestRelease()
|
||||
{
|
||||
var ownerAccount = "rmcrackan";
|
||||
var repoName = "Libation";
|
||||
const string ownerAccount = "rmcrackan";
|
||||
const string repoName = "Libation";
|
||||
|
||||
var gitHubClient = new Octokit.GitHubClient(new Octokit.ProductHeaderValue(repoName));
|
||||
|
||||
@@ -305,12 +322,11 @@ namespace AppScaffolding
|
||||
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 = 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)));
|
||||
|
||||
//https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#get-the-latest-release
|
||||
var latestRelease = await gitHubClient.Repository.Release.GetLatest(ownerAccount, repoName);
|
||||
|
||||
return (latestRelease, latestRelease?.Assets?.FirstOrDefault(a => regex.IsMatch(a.Name)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
@@ -25,9 +26,6 @@ namespace AppScaffolding
|
||||
: value;
|
||||
|
||||
#region 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);
|
||||
|
||||
public static bool APPSETTINGS_TryGet(string key, out string value)
|
||||
{
|
||||
@@ -61,11 +59,7 @@ namespace AppScaffolding
|
||||
/// <param name="save">True: save if contents changed. False: no not attempt save</param>
|
||||
private static void process_APPSETTINGS_Json(Action<JObject> action, bool save = true)
|
||||
{
|
||||
// only insert if not exists
|
||||
if (!APPSETTINGS_Json_Exists)
|
||||
return;
|
||||
|
||||
var startingContents = File.ReadAllText(APPSETTINGS_JSON);
|
||||
var startingContents = File.ReadAllText(Configuration.AppsettingsJsonFile);
|
||||
|
||||
JObject jObj;
|
||||
try
|
||||
@@ -88,7 +82,7 @@ namespace AppScaffolding
|
||||
if (startingContents.EqualsInsensitive(endingContents_indented) || startingContents.EqualsInsensitive(endingContents_compact))
|
||||
return;
|
||||
|
||||
File.WriteAllText(APPSETTINGS_JSON, endingContents_indented);
|
||||
File.WriteAllText(Configuration.AppsettingsJsonFile, endingContents_indented);
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="7.3.2.1" />
|
||||
<PackageReference Include="AudibleApi" Version="8.0.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="7.2.1.1" />
|
||||
<PackageReference Include="Dinah.Core" Version="7.2.2.1" />
|
||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="7.1.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.2">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.2">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -17,10 +17,14 @@ namespace FileLiberator
|
||||
|
||||
protected LameConfig GetLameOptions(Configuration config)
|
||||
{
|
||||
LameConfig lameConfig = new();
|
||||
lameConfig.Mode = MPEGMode.Mono;
|
||||
LameConfig lameConfig = new()
|
||||
{
|
||||
Mode = MPEGMode.Mono,
|
||||
Quality = config.LameEncoderQuality,
|
||||
OutputSampleRate = (int)config.MaxSampleRate
|
||||
};
|
||||
|
||||
if (config.LameTargetBitrate)
|
||||
if (config.LameTargetBitrate)
|
||||
{
|
||||
if (config.LameConstantBitrate)
|
||||
lameConfig.BitRate = config.LameBitrate;
|
||||
|
||||
@@ -103,20 +103,20 @@ namespace FileLiberator
|
||||
{
|
||||
averageSpeed.AddPosition(e.ProcessPosition.TotalSeconds);
|
||||
|
||||
var remainingTimeToProcess = (e.TotalDuration - e.ProcessPosition).TotalSeconds;
|
||||
var remainingTimeToProcess = (e.EndTime - e.ProcessPosition).TotalSeconds;
|
||||
var estTimeRemaining = remainingTimeToProcess / averageSpeed.Average;
|
||||
|
||||
if (double.IsNormal(estTimeRemaining))
|
||||
OnStreamingTimeRemaining(TimeSpan.FromSeconds(estTimeRemaining));
|
||||
|
||||
double progressPercent = 100 * e.ProcessPosition.TotalSeconds / e.TotalDuration.TotalSeconds;
|
||||
double progressPercent = 100 * e.FractionCompleted;
|
||||
|
||||
OnStreamingProgressChanged(
|
||||
new DownloadProgress
|
||||
{
|
||||
ProgressPercentage = progressPercent,
|
||||
BytesReceived = (long)e.ProcessPosition.TotalSeconds,
|
||||
TotalBytesToReceive = (long)e.TotalDuration.TotalSeconds
|
||||
BytesReceived = (long)(e.ProcessPosition - e.StartTime).TotalSeconds,
|
||||
TotalBytesToReceive = (long)(e.EndTime - e.StartTime).TotalSeconds
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace FileLiberator
|
||||
public OutputFormat OutputFormat { get; init; }
|
||||
public ChapterInfo ChapterInfo { get; init; }
|
||||
public NAudio.Lame.LameConfig LameConfig { get; init; }
|
||||
public string UserAgent => AudibleApi.Resources.USER_AGENT;
|
||||
public string UserAgent => AudibleApi.Resources.Download_User_Agent;
|
||||
public bool TrimOutputToChapterLength => config.AllowLibationFixup && config.StripAudibleBrandAudio;
|
||||
public bool StripUnabridged => config.AllowLibationFixup && config.StripUnabridged;
|
||||
public bool CreateCueSheet => config.CreateCueSheet;
|
||||
|
||||
@@ -54,6 +54,8 @@ namespace FileLiberator
|
||||
= (await ProcessAsync(libraryBook))
|
||||
?? new StatusHandler { "Processable should never return a null status" };
|
||||
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="7.2.1.1" />
|
||||
<PackageReference Include="Dinah.Core" Version="7.2.2.1" />
|
||||
<PackageReference Include="Polly" Version="7.2.3" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace HangoverAvalonia.ViewModels
|
||||
|
||||
private void Load_databaseVM()
|
||||
{
|
||||
_tab = new(new(() => SqlQuery, s => SqlResults = s, s => SqlResults = s));
|
||||
_tab = new(new DatabaseTabCommands(() => SqlQuery, s => SqlResults += s, s => SqlResults = s));
|
||||
|
||||
_tab.LoadDatabaseFile();
|
||||
if (_tab.DbFile is null)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||
<AssemblyName>Hangover</AssemblyName>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationIcon>hangover.ico</ApplicationIcon>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
@@ -11,7 +11,6 @@ using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using ApplicationServices;
|
||||
using Dinah.Core;
|
||||
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
@@ -53,7 +52,7 @@ namespace LibationAvalonia
|
||||
// check for existing settings in default location
|
||||
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
|
||||
if (Configuration.SettingsFileIsValid(defaultSettingsFile))
|
||||
config.SetLibationFiles(defaultLibationFilesDir);
|
||||
Configuration.SetLibationFiles(defaultLibationFilesDir);
|
||||
|
||||
if (config.LibationSettingsAreValid)
|
||||
{
|
||||
@@ -86,7 +85,7 @@ namespace LibationAvalonia
|
||||
// - error message, Exit()
|
||||
if (setupDialog.IsNewUser)
|
||||
{
|
||||
setupDialog.Config.SetLibationFiles(Configuration.UserProfile);
|
||||
Configuration.SetLibationFiles(Configuration.UserProfile);
|
||||
ShowSettingsWindow(desktop, setupDialog.Config, OnSettingsCompleted);
|
||||
}
|
||||
else if (setupDialog.IsReturningUser)
|
||||
@@ -178,7 +177,7 @@ namespace LibationAvalonia
|
||||
|
||||
private async void OnLibationFilesCompleted(IClassicDesktopStyleApplicationLifetime desktop, LibationFilesDialog libationFilesDialog, Configuration config)
|
||||
{
|
||||
config.SetLibationFiles(libationFilesDialog.SelectedDirectory);
|
||||
Configuration.SetLibationFiles(libationFilesDialog.SelectedDirectory);
|
||||
if (config.LibationSettingsAreValid)
|
||||
{
|
||||
await RunMigrationsAsync(config);
|
||||
|
||||
@@ -133,7 +133,7 @@ namespace LibationAvalonia.Controls
|
||||
: Configuration.GetKnownDirectoryPath(directorySelectControl.SelectedDirectory);
|
||||
selectedDir ??= string.Empty;
|
||||
|
||||
Directory = customStates.CustomChecked ? selectedDir : System.IO.Path.Combine(selectedDir, SubDirectory);
|
||||
Directory = customStates.CustomChecked ? selectedDir : System.IO.Path.Combine(selectedDir, SubDirectory ?? "");
|
||||
}
|
||||
|
||||
private void DirectoryOrCustomSelectControl_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
|
||||
|
||||
@@ -117,10 +117,13 @@ namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
Title = $"Select the audible-cli [account].json file",
|
||||
AllowMultiple = false,
|
||||
SuggestedStartLocation = new BclStorageFolder(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)),
|
||||
FileTypeFilter = new FilePickerFileType[]
|
||||
{
|
||||
new("JSON files (*.json)") { Patterns = new[] { "*.json" } },
|
||||
new("JSON files (*.json)")
|
||||
{
|
||||
Patterns = new[] { "*.json" },
|
||||
AppleUniformTypeIdentifiers = new[] { "public.json" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -274,13 +277,16 @@ namespace LibationAvalonia.Dialogs
|
||||
var options = new FilePickerSaveOptions
|
||||
{
|
||||
Title = $"Save Sover Image",
|
||||
SuggestedStartLocation = new BclStorageFolder(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)),
|
||||
SuggestedFileName = $"{acc.AccountId}.json",
|
||||
DefaultExtension = "json",
|
||||
ShowOverwritePrompt = true,
|
||||
FileTypeChoices = new FilePickerFileType[]
|
||||
{
|
||||
new("JSON files (*.json)") { Patterns = new[] { "*.json" } },
|
||||
new("JSON files (*.json)")
|
||||
{
|
||||
Patterns = new[] { "*.json" },
|
||||
AppleUniformTypeIdentifiers = new[] { "public.json" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -153,10 +153,22 @@ namespace LibationAvalonia.Dialogs
|
||||
ShowOverwritePrompt = true,
|
||||
FileTypeChoices = new FilePickerFileType[]
|
||||
{
|
||||
new("Excel Workbook (*.xlsx)") { Patterns = new[] { "*.xlsx" } },
|
||||
new("CSV files (*.csv)") { Patterns = new[] { "*.csv" } },
|
||||
new("JSON files (*.json)") { Patterns = new[] { "*.json" } },
|
||||
new("All files (*.*)") { Patterns = new[] { "*" } }
|
||||
new("Excel Workbook (*.xlsx)")
|
||||
{
|
||||
Patterns = new[] { "*.xlsx" },
|
||||
AppleUniformTypeIdentifiers = new[] { "org.openxmlformats.spreadsheetml.sheet" }
|
||||
},
|
||||
new("CSV files (*.csv)")
|
||||
{
|
||||
Patterns = new[] { "*.csv" },
|
||||
AppleUniformTypeIdentifiers = new[] { "public.comma-separated-values-text" }
|
||||
},
|
||||
new("JSON files (*.json)")
|
||||
{
|
||||
Patterns = new[] { "*.json" },
|
||||
AppleUniformTypeIdentifiers = new[] { "public.json" }
|
||||
},
|
||||
new("All files (*.*)") { Patterns = new[] { "*" } },
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -56,7 +56,11 @@ namespace LibationAvalonia.Dialogs
|
||||
ShowOverwritePrompt = true,
|
||||
FileTypeChoices = new FilePickerFileType[]
|
||||
{
|
||||
new("Jpeg (*.jpg)") { Patterns = new[] { "jpg" } }
|
||||
new("Jpeg (*.jpg)")
|
||||
{
|
||||
Patterns = new[] { "jpg" },
|
||||
AppleUniformTypeIdentifiers = new[] { "public.jpeg" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="850" d:DesignHeight="620"
|
||||
MinWidth="800" MinHeight="620"
|
||||
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="700"
|
||||
MinWidth="900" MinHeight="700"
|
||||
x:Class="LibationAvalonia.Dialogs.SettingsDialog"
|
||||
xmlns:controls="clr-namespace:LibationAvalonia.Controls"
|
||||
Title="Edit Settings"
|
||||
@@ -365,7 +365,6 @@
|
||||
Text="{Binding DownloadDecryptSettings.InProgressDescriptionText}" />
|
||||
|
||||
<controls:DirectoryOrCustomSelectControl
|
||||
SubDirectory="Libation"
|
||||
Directory="{Binding DownloadDecryptSettings.InProgressDirectory, Mode=TwoWay}"
|
||||
KnownDirectories="{Binding DownloadDecryptSettings.KnownDirectories}" />
|
||||
|
||||
@@ -604,6 +603,25 @@
|
||||
</CheckBox>
|
||||
</Grid>
|
||||
|
||||
<Grid Margin="5,5,5,0" RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<TextBlock Margin="0,0,0,5" Text="Max audio sample rate:" />
|
||||
<controls:WheelComboBox
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Items="{Binding AudioSettings.SampleRates}"
|
||||
SelectedItem="{Binding AudioSettings.SelectedSampleRate, Mode=TwoWay}"/>
|
||||
|
||||
<TextBlock Margin="0,0,0,5" Grid.Column="2" Text="Encoder Quality:" />
|
||||
|
||||
<controls:WheelComboBox
|
||||
Grid.Column="2"
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Items="{Binding AudioSettings.EncoderQualities}"
|
||||
SelectedItem="{Binding AudioSettings.SelectedEncoderQuality, Mode=TwoWay}"/>
|
||||
</Grid>
|
||||
|
||||
<controls:GroupBox
|
||||
Margin="5,5,5,0"
|
||||
BorderWidth="1"
|
||||
|
||||
@@ -11,6 +11,7 @@ using System.Linq;
|
||||
using FileManager;
|
||||
using System.IO;
|
||||
using Avalonia.Collections;
|
||||
using LibationUiBase;
|
||||
|
||||
namespace LibationAvalonia.Dialogs
|
||||
{
|
||||
@@ -141,7 +142,7 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
public void LoadSettings(Configuration config)
|
||||
{
|
||||
BooksDirectory = config.Books;
|
||||
BooksDirectory = config.Books.PathWithoutPrefix;
|
||||
SavePodcastsToParentFolder = config.SavePodcastsToParentFolder;
|
||||
LoggingLevel = config.LogLevel;
|
||||
BetaOptIn = config.BetaOptIn;
|
||||
@@ -372,6 +373,31 @@ namespace LibationAvalonia.Dialogs
|
||||
private int _lameBitrate;
|
||||
private int _lameVBRQuality;
|
||||
private string _chapterTitleTemplate;
|
||||
public SampleRateSelection SelectedSampleRate { get; set; }
|
||||
public NAudio.Lame.EncoderQuality SelectedEncoderQuality { get; set; }
|
||||
|
||||
public AvaloniaList<SampleRateSelection> SampleRates { get; }
|
||||
= new(
|
||||
new []
|
||||
{
|
||||
AAXClean.SampleRate.Hz_44100,
|
||||
AAXClean.SampleRate.Hz_32000,
|
||||
AAXClean.SampleRate.Hz_24000,
|
||||
AAXClean.SampleRate.Hz_22050,
|
||||
AAXClean.SampleRate.Hz_16000,
|
||||
AAXClean.SampleRate.Hz_12000,
|
||||
}
|
||||
.Select(s => new SampleRateSelection(s)));
|
||||
|
||||
public AvaloniaList<NAudio.Lame.EncoderQuality> EncoderQualities { get; }
|
||||
= new(
|
||||
new[]
|
||||
{
|
||||
NAudio.Lame.EncoderQuality.High,
|
||||
NAudio.Lame.EncoderQuality.Standard,
|
||||
NAudio.Lame.EncoderQuality.Fast,
|
||||
});
|
||||
|
||||
|
||||
public AudioSettings(Configuration config)
|
||||
{
|
||||
@@ -398,6 +424,9 @@ namespace LibationAvalonia.Dialogs
|
||||
LameMatchSource = config.LameMatchSourceBR;
|
||||
LameBitrate = config.LameBitrate;
|
||||
LameVBRQuality = config.LameVBRQuality;
|
||||
|
||||
SelectedSampleRate = SampleRates.FirstOrDefault(s => s.SampleRate == config.MaxSampleRate);
|
||||
SelectedEncoderQuality = config.LameEncoderQuality;
|
||||
}
|
||||
|
||||
public Task<bool> SaveSettingsAsync(Configuration config)
|
||||
@@ -422,6 +451,9 @@ namespace LibationAvalonia.Dialogs
|
||||
config.LameBitrate = LameBitrate;
|
||||
config.LameVBRQuality = LameVBRQuality;
|
||||
|
||||
config.LameEncoderQuality = SelectedEncoderQuality;
|
||||
config.MaxSampleRate = SelectedSampleRate?.SampleRate ?? config.MaxSampleRate;
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -116,20 +116,6 @@
|
||||
<ProjectReference Include="..\LibationUiBase\LibationUiBase.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="glass-with-glow_256.svg">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Libation.desktop">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="ZipExtractor.exe">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<Target Name="SpicNSpan" AfterTargets="Clean">
|
||||
<!-- Remove obj folder -->
|
||||
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using AppScaffolding;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.ReactiveUI;
|
||||
@@ -13,15 +14,34 @@ namespace LibationAvalonia
|
||||
{
|
||||
static class Program
|
||||
{
|
||||
static void Main()
|
||||
static void Main(string[] args)
|
||||
{
|
||||
|
||||
if (Configuration.IsMacOs && args?.Length > 0 && args[0] == "hangover")
|
||||
{
|
||||
//Launch the Hangover app within the sandbox
|
||||
//We can do this because we're already executing inside the sandbox.
|
||||
//Any process created in the sandbox executes in the same sandbox.
|
||||
//Unfortunately, all sandbox files are read/execute, so no writing!
|
||||
Process.Start("Hangover");
|
||||
return;
|
||||
}
|
||||
if (Configuration.IsMacOs && args?.Length > 0 && args[0] == "cli")
|
||||
{
|
||||
//Open a new Terminal in the sandbox
|
||||
Process.Start(
|
||||
"/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal",
|
||||
$"\"{Configuration.ProcessDirectory}\"");
|
||||
return;
|
||||
}
|
||||
|
||||
//***********************************************//
|
||||
// //
|
||||
// do not use Configuration before this line //
|
||||
// //
|
||||
//***********************************************//
|
||||
// Migrations which must occur before configuration is loaded for the first time. Usually ones which alter the Configuration
|
||||
var config = AppScaffolding.LibationScaffolding.RunPreConfigMigrations();
|
||||
var config = LibationScaffolding.RunPreConfigMigrations();
|
||||
|
||||
App.SetupRequired = !config.LibationSettingsAreValid;
|
||||
|
||||
@@ -29,13 +49,10 @@ namespace LibationAvalonia
|
||||
var classicLifetimeTask = Task.Run(() => new ClassicDesktopStyleApplicationLifetime());
|
||||
var appBuilderTask = Task.Run(BuildAvaloniaApp);
|
||||
|
||||
if (Configuration.IsWindows)
|
||||
AppScaffolding.LibationScaffolding.SetReleaseIdentifier(AppScaffolding.ReleaseIdentifier.WindowsAvalonia);
|
||||
else if (Configuration.IsLinux)
|
||||
AppScaffolding.LibationScaffolding.SetReleaseIdentifier(AppScaffolding.ReleaseIdentifier.LinuxAvalonia);
|
||||
else if (Configuration.IsMacOs)
|
||||
AppScaffolding.LibationScaffolding.SetReleaseIdentifier(AppScaffolding.ReleaseIdentifier.MacOSAvalonia);
|
||||
else return;
|
||||
LibationScaffolding.SetReleaseIdentifier(Variety.Chardonnay);
|
||||
|
||||
if (LibationScaffolding.ReleaseIdentifier is ReleaseIdentifier.None)
|
||||
return;
|
||||
|
||||
|
||||
if (!App.SetupRequired)
|
||||
@@ -62,8 +79,8 @@ namespace LibationAvalonia
|
||||
try
|
||||
{
|
||||
// most migrations go in here
|
||||
AppScaffolding.LibationScaffolding.RunPostConfigMigrations(config);
|
||||
AppScaffolding.LibationScaffolding.RunPostMigrationScaffolding(config);
|
||||
LibationScaffolding.RunPostConfigMigrations(config);
|
||||
LibationScaffolding.RunPostMigrationScaffolding(config);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using AudibleApi;
|
||||
using AudibleApi.Common;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
@@ -60,12 +61,12 @@ namespace LibationAvalonia.ViewModels
|
||||
#region Properties exposed to the view
|
||||
public ProcessBookResult Result { get => _result; set { this.RaiseAndSetIfChanged(ref _result, value); this.RaisePropertyChanged(nameof(StatusText)); } }
|
||||
public ProcessBookStatus Status { get => _status; set { this.RaiseAndSetIfChanged(ref _status, value); this.RaisePropertyChanged(nameof(BackgroundColor)); this.RaisePropertyChanged(nameof(IsFinished)); this.RaisePropertyChanged(nameof(IsDownloading)); this.RaisePropertyChanged(nameof(Queued)); } }
|
||||
public string Narrator { get => _narrator; set { this.RaiseAndSetIfChanged(ref _narrator, value); } }
|
||||
public string Author { get => _author; set { this.RaiseAndSetIfChanged(ref _author, value); } }
|
||||
public string Title { get => _title; set { this.RaiseAndSetIfChanged(ref _title, value); } }
|
||||
public int Progress { get => _progress; private set { this.RaiseAndSetIfChanged(ref _progress, value); } }
|
||||
public string ETA { get => _eta; private set { this.RaiseAndSetIfChanged(ref _eta, value); } }
|
||||
public Bitmap Cover { get => _cover; private set { this.RaiseAndSetIfChanged(ref _cover, value); } }
|
||||
public string Narrator { get => _narrator; set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _narrator, value)); }
|
||||
public string Author { get => _author; set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _author, value)); }
|
||||
public string Title { get => _title; set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _title, value)); }
|
||||
public int Progress { get => _progress; private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _progress, value)); }
|
||||
public string ETA { get => _eta; private set => Dispatcher.UIThread.Post(() =>this.RaiseAndSetIfChanged(ref _eta, value)); }
|
||||
public Bitmap Cover { get => _cover; private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _cover, value)); }
|
||||
public bool IsFinished => Status is not ProcessBookStatus.Queued and not ProcessBookStatus.Working;
|
||||
public bool IsDownloading => Status is ProcessBookStatus.Working;
|
||||
public bool Queued => Status is ProcessBookStatus.Queued;
|
||||
@@ -131,6 +132,7 @@ namespace LibationAvalonia.ViewModels
|
||||
public async Task<ProcessBookResult> ProcessOneAsync()
|
||||
{
|
||||
string procName = CurrentProcessable.Name;
|
||||
ProcessBookResult result = ProcessBookResult.None;
|
||||
try
|
||||
{
|
||||
LinkProcessable(CurrentProcessable);
|
||||
@@ -138,32 +140,34 @@ namespace LibationAvalonia.ViewModels
|
||||
var statusHandler = await CurrentProcessable.ProcessSingleAsync(LibraryBook, validate: true);
|
||||
|
||||
if (statusHandler.IsSuccess)
|
||||
return Result = ProcessBookResult.Success;
|
||||
result = ProcessBookResult.Success;
|
||||
else if (statusHandler.Errors.Contains("Cancelled"))
|
||||
{
|
||||
Logger.Info($"{procName}: Process was cancelled - {LibraryBook.Book}");
|
||||
return Result = ProcessBookResult.Cancelled;
|
||||
result = ProcessBookResult.Cancelled;
|
||||
}
|
||||
else if (statusHandler.Errors.Contains("Validation failed"))
|
||||
{
|
||||
Logger.Info($"{procName}: Validation failed - {LibraryBook.Book}");
|
||||
return Result = ProcessBookResult.ValidationFail;
|
||||
result = ProcessBookResult.ValidationFail;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var errorMessage in statusHandler.Errors)
|
||||
Logger.Error($"{procName}: {errorMessage}");
|
||||
}
|
||||
|
||||
foreach (var errorMessage in statusHandler.Errors)
|
||||
Logger.Error($"{procName}: {errorMessage}");
|
||||
}
|
||||
catch (ContentLicenseDeniedException ldex)
|
||||
{
|
||||
if (ldex.AYCL?.RejectionReason is null or RejectionReason.GenericError)
|
||||
{
|
||||
Logger.Info($"{procName}: Content license was denied, but this error appears to be caused by a temporary interruption of service. - {LibraryBook.Book}");
|
||||
return Result = ProcessBookResult.LicenseDeniedPossibleOutage;
|
||||
result = ProcessBookResult.LicenseDeniedPossibleOutage;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Info($"{procName}: Content license denied. Check your Audible account to see if you have access to this title. - {LibraryBook.Book}");
|
||||
return Result = ProcessBookResult.LicenseDenied;
|
||||
result = ProcessBookResult.LicenseDenied;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -172,18 +176,21 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Result == ProcessBookResult.None)
|
||||
Result = await showRetry(LibraryBook);
|
||||
if (result == ProcessBookResult.None)
|
||||
result = await showRetry(LibraryBook);
|
||||
|
||||
Status = Result switch
|
||||
var status = result switch
|
||||
{
|
||||
ProcessBookResult.Success => ProcessBookStatus.Completed,
|
||||
ProcessBookResult.Cancelled => ProcessBookStatus.Cancelled,
|
||||
_ => ProcessBookStatus.Failed,
|
||||
};
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() => Status = status);
|
||||
}
|
||||
|
||||
return Result;
|
||||
await Dispatcher.UIThread.InvokeAsync(() => Result = result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task CancelAsync()
|
||||
@@ -294,9 +301,9 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
#region Processable event handlers
|
||||
|
||||
private void Processable_Begin(object sender, LibraryBook libraryBook)
|
||||
private async void Processable_Begin(object sender, LibraryBook libraryBook)
|
||||
{
|
||||
Status = ProcessBookStatus.Working;
|
||||
await Dispatcher.UIThread.InvokeAsync(() => Status = ProcessBookStatus.Working);
|
||||
|
||||
Logger.Info($"{Environment.NewLine}{((Processable)sender).Name} Step, Begin: {libraryBook.Book}");
|
||||
|
||||
|
||||
@@ -44,11 +44,11 @@ namespace LibationAvalonia.ViewModels
|
||||
private bool _progressBarVisible;
|
||||
private decimal _speedLimit;
|
||||
|
||||
public int CompletedCount { get => _completedCount; private set { this.RaiseAndSetIfChanged(ref _completedCount, value); this.RaisePropertyChanged(nameof(AnyCompleted)); } }
|
||||
public int QueuedCount { get => _queuedCount; private set { this.RaiseAndSetIfChanged(ref _queuedCount, value); this.RaisePropertyChanged(nameof(AnyQueued)); } }
|
||||
public int ErrorCount { get => _errorCount; private set { this.RaiseAndSetIfChanged(ref _errorCount, value); this.RaisePropertyChanged(nameof(AnyErrors)); } }
|
||||
public string RunningTime { get => _runningTime; set { this.RaiseAndSetIfChanged(ref _runningTime, value); } }
|
||||
public bool ProgressBarVisible { get => _progressBarVisible; set { this.RaiseAndSetIfChanged(ref _progressBarVisible, value); } }
|
||||
public int CompletedCount { get => _completedCount; private set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _completedCount, value); this.RaisePropertyChanged(nameof(AnyCompleted)); }); }
|
||||
public int QueuedCount { get => _queuedCount; private set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _queuedCount, value); this.RaisePropertyChanged(nameof(AnyQueued)); }); }
|
||||
public int ErrorCount { get => _errorCount; private set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _errorCount, value); this.RaisePropertyChanged(nameof(AnyErrors)); }); }
|
||||
public string RunningTime { get => _runningTime; set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _runningTime, value); }); }
|
||||
public bool ProgressBarVisible { get => _progressBarVisible; set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _progressBarVisible, value); }); }
|
||||
public bool AnyCompleted => CompletedCount > 0;
|
||||
public bool AnyQueued => QueuedCount > 0;
|
||||
public bool AnyErrors => ErrorCount > 0;
|
||||
@@ -78,8 +78,11 @@ namespace LibationAvalonia.ViewModels
|
||||
: _speedLimit > 1 ? 0.1m
|
||||
: 0.01m;
|
||||
|
||||
this.RaisePropertyChanged(nameof(SpeedLimitIncrement));
|
||||
this.RaisePropertyChanged();
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
this.RaisePropertyChanged(nameof(SpeedLimitIncrement));
|
||||
this.RaisePropertyChanged();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,12 +95,12 @@ namespace LibationAvalonia.ViewModels
|
||||
|
||||
ErrorCount = errCount;
|
||||
CompletedCount = completeCount;
|
||||
this.RaisePropertyChanged(nameof(Progress));
|
||||
Dispatcher.UIThread.Post(() => this.RaisePropertyChanged(nameof(Progress)));
|
||||
}
|
||||
private void Queue_QueuededCountChanged(object sender, int cueCount)
|
||||
{
|
||||
QueuedCount = cueCount;
|
||||
this.RaisePropertyChanged(nameof(Progress));
|
||||
Dispatcher.UIThread.Post(() => this.RaisePropertyChanged(nameof(Progress)));
|
||||
}
|
||||
|
||||
public void WriteLine(string text)
|
||||
|
||||
@@ -10,8 +10,6 @@ using ApplicationServices;
|
||||
using AudibleUtilities;
|
||||
using LibationAvalonia.Dialogs.Login;
|
||||
using Avalonia.Collections;
|
||||
using LibationSearchEngine;
|
||||
using Octokit.Internal;
|
||||
|
||||
namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
@@ -62,6 +60,7 @@ namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
var existingSeriesEntries = SOURCE.SeriesEntries().ToList();
|
||||
|
||||
FilteredInGridEntries?.Clear();
|
||||
SOURCE.Clear();
|
||||
SOURCE.AddRange(CreateGridEntries(dbBooks));
|
||||
|
||||
@@ -164,7 +163,7 @@ namespace LibationAvalonia.ViewModels
|
||||
return FilteredInGridEntries.Contains(item);
|
||||
}
|
||||
|
||||
private static List<GridEntry> QueryResults(List<GridEntry> entries, string searchString)
|
||||
private static List<GridEntry> QueryResults(IEnumerable<GridEntry> entries, string searchString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(searchString)) return null;
|
||||
|
||||
|
||||
@@ -26,10 +26,23 @@ namespace LibationAvalonia.Views
|
||||
ShowOverwritePrompt = true,
|
||||
FileTypeChoices = new FilePickerFileType[]
|
||||
{
|
||||
new("Excel Workbook (*.xlsx)") { Patterns = new[] { "*.xlsx" } },
|
||||
new("CSV files (*.csv)") { Patterns = new[] { "*.csv" } },
|
||||
new("JSON files (*.json)") { Patterns = new[] { "*.json" } },
|
||||
new("All files (*.*)") { Patterns = new[] { "*" } },
|
||||
new("Excel Workbook (*.xlsx)")
|
||||
{
|
||||
Patterns = new[] { "*.xlsx" },
|
||||
//https://gist.github.com/RhetTbull/7221ef3cfd9d746f34b2550d4419a8c2
|
||||
AppleUniformTypeIdentifiers = new[] { "org.openxmlformats.spreadsheetml.sheet" }
|
||||
},
|
||||
new("CSV files (*.csv)")
|
||||
{
|
||||
Patterns = new[] { "*.csv" },
|
||||
AppleUniformTypeIdentifiers = new[] { "public.comma-separated-values-text" }
|
||||
},
|
||||
new("JSON files (*.json)")
|
||||
{
|
||||
Patterns = new[] { "*.json" },
|
||||
AppleUniformTypeIdentifiers = new[] { "public.json" }
|
||||
},
|
||||
new("All files (*.*)") { Patterns = new[] { "*" } }
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -47,6 +47,23 @@ namespace LibationAvalonia.Views
|
||||
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBook);
|
||||
}
|
||||
}
|
||||
|
||||
public void ProductsDisplay_ConvertToMp3Clicked(object sender, LibraryBook libraryBook)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", libraryBook);
|
||||
SetQueueCollapseState(false);
|
||||
_viewModel.ProcessQueue.AddConvertMp3(libraryBook);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBook);
|
||||
}
|
||||
}
|
||||
private void SetQueueCollapseState(bool collapsed)
|
||||
{
|
||||
_viewModel.QueueOpen = !collapsed;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using LibationFileManager;
|
||||
using System;
|
||||
|
||||
namespace LibationAvalonia.Views
|
||||
{
|
||||
@@ -16,5 +16,17 @@ namespace LibationAvalonia.Views
|
||||
|
||||
public async void aboutToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> await MessageBox.Show($"Libation {AppScaffolding.LibationScaffolding.Variety}{Environment.NewLine}Version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}");
|
||||
|
||||
public void launchHangoverToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start("Hangover" + (Configuration.IsWindows ? ".exe" : ""));
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Failed to launch Hangover");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,19 +14,22 @@ namespace LibationAvalonia.Views
|
||||
Opened += async (_, _) => await checkForUpdates();
|
||||
}
|
||||
|
||||
private async Task checkForUpdates()
|
||||
private async Task checkForUpdates()
|
||||
{
|
||||
async Task<string> downloadUpdate(UpgradeProperties upgradeProperties)
|
||||
{
|
||||
if (upgradeProperties.ZipUrl is null)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Download link for new version not found");
|
||||
Serilog.Log.Logger.Warning("Download link for new version not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
//Silently download the update in the background, save it to a temp file.
|
||||
|
||||
var zipFile = Path.GetTempFileName();
|
||||
var zipFile = Path.Combine(Path.GetTempPath(), Path.GetFileName(upgradeProperties.ZipUrl));
|
||||
|
||||
Serilog.Log.Logger.Information($"Downloading {zipFile}");
|
||||
|
||||
try
|
||||
{
|
||||
System.Net.Http.HttpClient cli = new();
|
||||
@@ -42,36 +45,6 @@ namespace LibationAvalonia.Views
|
||||
return zipFile;
|
||||
}
|
||||
|
||||
void runWindowsUpgrader(string zipFile)
|
||||
{
|
||||
var thisExe = Environment.ProcessPath;
|
||||
var thisDir = Path.GetDirectoryName(thisExe);
|
||||
|
||||
var zipExtractor = Path.Combine(Path.GetTempPath(), "ZipExtractor.exe");
|
||||
|
||||
File.Copy("ZipExtractor.exe", zipExtractor, overwrite: true);
|
||||
|
||||
var psi = new System.Diagnostics.ProcessStartInfo()
|
||||
{
|
||||
FileName = zipExtractor,
|
||||
UseShellExecute = true,
|
||||
Verb = "runas",
|
||||
WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal,
|
||||
CreateNoWindow = true,
|
||||
ArgumentList =
|
||||
{
|
||||
"--input",
|
||||
zipFile,
|
||||
"--output",
|
||||
thisDir,
|
||||
"--executable",
|
||||
thisExe
|
||||
}
|
||||
};
|
||||
|
||||
System.Diagnostics.Process.Start(psi);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var upgradeProperties = await Task.Run(LibationScaffolding.GetLatestRelease);
|
||||
@@ -83,26 +56,27 @@ namespace LibationAvalonia.Views
|
||||
if (config.GetString(propertyName: ignoreUpdate) == upgradeProperties.LatestRelease.ToString())
|
||||
return;
|
||||
|
||||
var notificationResult = await new UpgradeNotificationDialog(upgradeProperties, Configuration.IsWindows).ShowDialog<DialogResult>(this);
|
||||
var interop = InteropFactory.Create();
|
||||
|
||||
if (!interop.CanUpdate)
|
||||
Serilog.Log.Logger.Information("Can't perform update automatically");
|
||||
|
||||
var notificationResult = await new UpgradeNotificationDialog(upgradeProperties, interop.CanUpdate).ShowDialog<DialogResult>(this);
|
||||
|
||||
if (notificationResult == DialogResult.Ignore)
|
||||
config.SetString(upgradeProperties.LatestRelease.ToString(), ignoreUpdate);
|
||||
|
||||
if (notificationResult != DialogResult.OK || !Configuration.IsWindows) return;
|
||||
if (notificationResult != DialogResult.OK) return;
|
||||
|
||||
//Download the update file in the background,
|
||||
//then wire up installaion on window close.
|
||||
string updateBundle = await downloadUpdate(upgradeProperties);
|
||||
|
||||
string zipFile = await downloadUpdate(upgradeProperties);
|
||||
if (string.IsNullOrEmpty(updateBundle) || !File.Exists(updateBundle)) return;
|
||||
|
||||
if (string.IsNullOrEmpty(zipFile) || !File.Exists(zipFile))
|
||||
return;
|
||||
|
||||
Closed += (_, _) =>
|
||||
{
|
||||
if (File.Exists(zipFile))
|
||||
runWindowsUpgrader(zipFile);
|
||||
};
|
||||
//Install the update
|
||||
Serilog.Log.Logger.Information($"Begin running auto-updater");
|
||||
interop.InstallUpdate(updateBundle);
|
||||
Serilog.Log.Logger.Information($"Completed running auto-updater");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -131,6 +131,8 @@
|
||||
<MenuItem Click="accountsToolStripMenuItem_Click" Header="_Accounts..." />
|
||||
<MenuItem Click="basicSettingsToolStripMenuItem_Click" Header="_Settings..." />
|
||||
<Separator />
|
||||
<MenuItem Click="launchHangoverToolStripMenuItem_Click" Header="Launch _Hangover" />
|
||||
<Separator />
|
||||
<MenuItem Click="aboutToolStripMenuItem_Click" Header="A_bout..." />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
@@ -186,7 +188,8 @@
|
||||
<views:ProductsDisplay
|
||||
Name="productsDisplay"
|
||||
DataContext="{Binding ProductsDisplay}"
|
||||
LiberateClicked="ProductsDisplay_LiberateClicked"/>
|
||||
LiberateClicked="ProductsDisplay_LiberateClicked"
|
||||
ConvertToMp3Clicked="ProductsDisplay_ConvertToMp3Clicked" />
|
||||
</SplitView>
|
||||
</Border>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ApplicationServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
@@ -18,6 +19,7 @@ namespace LibationAvalonia.Views
|
||||
public partial class ProductsDisplay : UserControl
|
||||
{
|
||||
public event EventHandler<LibraryBook> LiberateClicked;
|
||||
public event EventHandler<LibraryBook> ConvertToMp3Clicked;
|
||||
|
||||
private ProductsDisplayViewModel _viewModel => DataContext as ProductsDisplayViewModel;
|
||||
ImageDisplayDialog imageDisplayDialog;
|
||||
@@ -100,9 +102,9 @@ namespace LibationAvalonia.Views
|
||||
setNotDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.NotLiberated);
|
||||
|
||||
var removeMenuItem = new MenuItem() { Header = "_Remove from library" };
|
||||
removeMenuItem.Click += (_, __) => LibraryCommands.RemoveBook(entry.AudibleProductId);
|
||||
removeMenuItem.Click += async (_, __) => await Task.Run(() => LibraryCommands.RemoveBook(entry.AudibleProductId));
|
||||
|
||||
var locateFileMenuItem = new MenuItem() { Header = "_Locate file..." };
|
||||
var locateFileMenuItem = new MenuItem() { Header = "_Locate file..." };
|
||||
locateFileMenuItem.Click += async (_, __) =>
|
||||
{
|
||||
try
|
||||
@@ -130,6 +132,12 @@ namespace LibationAvalonia.Views
|
||||
await MessageBox.ShowAdminAlert(null, msg, msg, ex);
|
||||
}
|
||||
};
|
||||
var convertToMp3MenuItem = new MenuItem
|
||||
{
|
||||
Header = "_Convert to Mp3",
|
||||
IsEnabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated
|
||||
};
|
||||
convertToMp3MenuItem.Click += (_, _) => ConvertToMp3Clicked?.Invoke(this, entry.LibraryBook);
|
||||
|
||||
var bookRecordMenuItem = new MenuItem { Header = "View _Bookmarks/Clips" };
|
||||
bookRecordMenuItem.Click += async (_, _) => await new BookRecordsDialog(entry.LibraryBook).ShowDialog(VisualRoot as Window);
|
||||
@@ -140,6 +148,7 @@ namespace LibationAvalonia.Views
|
||||
setNotDownloadMenuItem,
|
||||
removeMenuItem,
|
||||
locateFileMenuItem,
|
||||
convertToMp3MenuItem,
|
||||
new Separator(),
|
||||
bookRecordMenuItem
|
||||
});
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="256.000000pt" height="256.000000pt" viewBox="0 0 256.000000 256.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,256.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M681 2513 c-57 -66 -116 -190 -148 -309 -25 -92 -27 -113 -27 -314
|
||||
-1 -237 10 -307 74 -468 94 -233 283 -387 542 -440 l78 -17 0 -402 0 -403
|
||||
-215 0 c-216 0 -216 0 -240 -25 -33 -32 -33 -78 0 -110 l24 -25 511 0 511 0
|
||||
24 25 c16 15 25 36 25 55 0 19 -9 40 -25 55 -24 25 -24 25 -240 25 l-215 0 0
|
||||
403 0 402 78 17 c259 53 448 207 542 440 64 161 75 231 74 468 0 201 -2 222
|
||||
-27 314 -32 119 -91 243 -148 309 l-41 47 -558 0 -558 0 -41 -47z m1115 -159
|
||||
c84 -143 124 -364 104 -575 -21 -226 -85 -385 -196 -489 -115 -107 -255 -160
|
||||
-424 -160 -237 0 -435 114 -529 303 -136 274 -127 696 20 934 l21 33 488 0
|
||||
489 0 27 -46z"/>
|
||||
<path d="M735 1828 c28 -375 165 -559 452 -606 83 -14 103 -14 186 0 289 47
|
||||
422 228 452 611 l7 87 -552 0 -552 0 7 -92z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -6,16 +6,25 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration
|
||||
[Flags]
|
||||
public enum OS
|
||||
{
|
||||
Unknown,
|
||||
Windows = 0x100000,
|
||||
Linux = 0x200000,
|
||||
MacOS = 0x400000,
|
||||
}
|
||||
|
||||
public partial class Configuration
|
||||
{
|
||||
public static bool IsWindows { get; } = OperatingSystem.IsWindows();
|
||||
public static bool IsWindows { get; } = OperatingSystem.IsWindows();
|
||||
public static bool IsLinux { get; } = OperatingSystem.IsLinux();
|
||||
public static bool IsMacOs { get; } = OperatingSystem.IsMacOS();
|
||||
|
||||
public static string OS { get; }
|
||||
= IsLinux ? "Linux"
|
||||
: IsMacOs ? "MacOS"
|
||||
: IsWindows ? "Windows"
|
||||
: "UNKNOWN_OS";
|
||||
public static OS OS { get; }
|
||||
= IsLinux ? OS.Linux
|
||||
: IsMacOs ? OS.MacOS
|
||||
: IsWindows ? OS.Windows
|
||||
: OS.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration
|
||||
{
|
||||
public static string AppDir_Relative => $@".{Path.PathSeparator}{LIBATION_FILES_KEY}";
|
||||
public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Exe.FileLocationOnDisk), LIBATION_FILES_KEY));
|
||||
public static string ProcessDirectory { get; } = Path.GetDirectoryName(Exe.FileLocationOnDisk);
|
||||
public static string AppDir_Relative => $@".{Path.PathSeparator}{LIBATION_FILES_KEY}";
|
||||
public static string AppDir_Absolute => Path.GetFullPath(Path.Combine(ProcessDirectory, LIBATION_FILES_KEY));
|
||||
public static string MyDocs => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Libation"));
|
||||
public static string WinTemp => Path.GetFullPath(Path.Combine(Path.GetTempPath(), "Libation"));
|
||||
public static string UserProfile => Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Libation"));
|
||||
|
||||
@@ -5,20 +5,22 @@ using FileManager;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Serilog;
|
||||
using Dinah.Core.Logging;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration
|
||||
{
|
||||
private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "appsettings.json");
|
||||
private const string LIBATION_FILES_KEY = "LibationFiles";
|
||||
public static string AppsettingsJsonFile { get; } = getOrCreateAppsettingsFile();
|
||||
|
||||
private const string LIBATION_FILES_KEY = "LibationFiles";
|
||||
|
||||
[Description("Location for storage of program-created files")]
|
||||
public string LibationFiles
|
||||
{
|
||||
get
|
||||
{
|
||||
if (libationFilesPathCache is not null)
|
||||
if (libationFilesPathCache is not null)
|
||||
return libationFilesPathCache;
|
||||
|
||||
// FIRST: must write here before SettingsFilePath in next step reads cache
|
||||
@@ -44,54 +46,93 @@ namespace LibationFileManager
|
||||
|
||||
private static string libationFilesPathCache { get; set; }
|
||||
|
||||
private string getLibationFilesSettingFromJson()
|
||||
/// <summary>
|
||||
/// Try to find appsettings.json in the following locations:
|
||||
/// <list type="number">
|
||||
/// <item>
|
||||
/// <description>[App Directory]</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>%LocalAppData%\Libation</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>%AppData%\Libation</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>%Temp%\Libation</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
///
|
||||
/// If not found, try to create it in each of the same locations in-order until successful.
|
||||
///
|
||||
/// <para>This method must complete successfully for Libation to continue.</para>
|
||||
/// </summary>
|
||||
/// <returns>appsettings.json file path</returns>
|
||||
/// <exception cref="ApplicationException">appsettings.json could not be found or created.</exception>
|
||||
private static string getOrCreateAppsettingsFile()
|
||||
{
|
||||
const string appsettings_filename = "appsettings.json";
|
||||
|
||||
//Possible appsettings.json locations, in order of preference.
|
||||
string[] possibleAppsettingsFiles = new[]
|
||||
{
|
||||
Path.Combine(ProcessDirectory, appsettings_filename),
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Libation", appsettings_filename),
|
||||
Path.Combine(UserProfile, appsettings_filename),
|
||||
Path.Combine(Path.GetTempPath(), "Libation", appsettings_filename)
|
||||
};
|
||||
|
||||
//Try to find and validate appsettings.json in each folder
|
||||
foreach (var appsettingsFile in possibleAppsettingsFiles)
|
||||
{
|
||||
if (File.Exists(appsettingsFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
var appSettings = JObject.Parse(File.ReadAllText(appsettingsFile));
|
||||
|
||||
if (appSettings.ContainsKey(LIBATION_FILES_KEY)
|
||||
&& appSettings[LIBATION_FILES_KEY] is JValue jval
|
||||
&& jval.Value is string settingsPath
|
||||
&& !string.IsNullOrWhiteSpace(settingsPath))
|
||||
return appsettingsFile;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
//Valid appsettings.json not found. Try to create it in each folder.
|
||||
var endingContents = new JObject { { LIBATION_FILES_KEY, UserProfile } }.ToString(Formatting.Indented);
|
||||
foreach (var appsettingsFile in possibleAppsettingsFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(appsettingsFile, endingContents);
|
||||
return appsettingsFile;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
Log.Logger.TryLogError(ex, $"Failed to create {appsettingsFile}");
|
||||
}
|
||||
}
|
||||
|
||||
throw new ApplicationException($"Could not locate or create {appsettings_filename}");
|
||||
}
|
||||
|
||||
private static string getLibationFilesSettingFromJson()
|
||||
{
|
||||
string startingContents = null;
|
||||
try
|
||||
{
|
||||
if (File.Exists(APPSETTINGS_JSON))
|
||||
{
|
||||
startingContents = File.ReadAllText(APPSETTINGS_JSON);
|
||||
var startingJObj = JObject.Parse(startingContents);
|
||||
|
||||
if (startingJObj.ContainsKey(LIBATION_FILES_KEY))
|
||||
{
|
||||
var startingValue = startingJObj[LIBATION_FILES_KEY].Value<string>();
|
||||
if (!string.IsNullOrWhiteSpace(startingValue))
|
||||
return startingValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// not found. write to file. read from file
|
||||
var endingContents = new JObject { { LIBATION_FILES_KEY, UserProfile.ToString() } }.ToString(Formatting.Indented);
|
||||
if (startingContents != endingContents)
|
||||
{
|
||||
File.WriteAllText(APPSETTINGS_JSON, endingContents);
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
|
||||
// do not check whether directory exists. special/meta directory (eg: AppDir) is valid
|
||||
// verify from live file. no try/catch. want failures to be visible
|
||||
var jObjFinal = JObject.Parse(File.ReadAllText(APPSETTINGS_JSON));
|
||||
var jObjFinal = JObject.Parse(File.ReadAllText(AppsettingsJsonFile));
|
||||
var valueFinal = jObjFinal[LIBATION_FILES_KEY].Value<string>();
|
||||
return valueFinal;
|
||||
}
|
||||
|
||||
public void SetLibationFiles(string directory)
|
||||
public static void SetLibationFiles(string directory)
|
||||
{
|
||||
// ensure exists
|
||||
if (!File.Exists(APPSETTINGS_JSON))
|
||||
{
|
||||
// getter creates new file, loads PersistentDictionary
|
||||
var _ = LibationFiles;
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
|
||||
libationFilesPathCache = null;
|
||||
|
||||
var startingContents = File.ReadAllText(APPSETTINGS_JSON);
|
||||
var startingContents = File.ReadAllText(AppsettingsJsonFile);
|
||||
var jObj = JObject.Parse(startingContents);
|
||||
|
||||
jObj[LIBATION_FILES_KEY] = directory;
|
||||
@@ -100,14 +141,17 @@ namespace LibationFileManager
|
||||
if (startingContents == endingContents)
|
||||
return;
|
||||
|
||||
// now it's set in the file again but no settings have moved yet
|
||||
File.WriteAllText(APPSETTINGS_JSON, endingContents);
|
||||
|
||||
try
|
||||
{
|
||||
Log.Logger.Information("Libation files changed {@DebugInfo}", new { APPSETTINGS_JSON, LIBATION_FILES_KEY, directory });
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
// now it's set in the file again but no settings have moved yet
|
||||
File.WriteAllText(AppsettingsJsonFile, endingContents);
|
||||
|
||||
Log.Logger.TryLogInformation("Libation files changed {@DebugInfo}", new { AppsettingsJsonFile, LIBATION_FILES_KEY, directory });
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Log.Logger.TryLogError(ex, "Failed to change Libation files location {@DebugInfo}", new { AppsettingsJsonFile, LIBATION_FILES_KEY, directory });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,12 @@ namespace LibationFileManager
|
||||
[Description("Lame encoder target. true = Bitrate, false = Quality")]
|
||||
public bool LameTargetBitrate { get => GetNonString(defaultValue: false); set => SetNonString(value); }
|
||||
|
||||
[Description("Maximum audio sample rate")]
|
||||
public AAXClean.SampleRate MaxSampleRate { get => GetNonString(defaultValue: AAXClean.SampleRate.Hz_44100); set => SetNonString(value); }
|
||||
|
||||
[Description("Lame encoder quality")]
|
||||
public NAudio.Lame.EncoderQuality LameEncoderQuality { get => GetNonString(defaultValue: NAudio.Lame.EncoderQuality.High); set => SetNonString(value); }
|
||||
|
||||
[Description("Lame encoder downsamples to mono")]
|
||||
public bool LameDownsampleMono { get => GetNonString(defaultValue: true); set => SetNonString(value); }
|
||||
|
||||
|
||||
@@ -9,9 +9,7 @@ namespace LibationFileManager
|
||||
{
|
||||
public partial class Configuration : PropertyChangeFilter
|
||||
{
|
||||
public bool LibationSettingsAreValid
|
||||
=> File.Exists(APPSETTINGS_JSON)
|
||||
&& SettingsFileIsValid(SettingsFilePath);
|
||||
public bool LibationSettingsAreValid => SettingsFileIsValid(SettingsFilePath);
|
||||
|
||||
public static bool SettingsFileIsValid(string settingsFile)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
@@ -6,6 +7,8 @@ namespace LibationFileManager
|
||||
{
|
||||
void SetFolderIcon(string image, string directory);
|
||||
void DeleteFolderIcon(string directory);
|
||||
void CopyTextToClipboard(string text);
|
||||
Process RunAsRoot(string exe, string args);
|
||||
void InstallUpdate(string updateBundle);
|
||||
bool CanUpdate { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Dinah.Core;
|
||||
|
||||
namespace LibationFileManager
|
||||
@@ -25,21 +23,29 @@ namespace LibationFileManager
|
||||
instance ??=
|
||||
InteropFunctionsType is null
|
||||
? new NullInteropFunctions()
|
||||
//: values is null || values.Length == 0 ? Activator.CreateInstance(InteropFunctionsType) as IInteropFunctions
|
||||
: Activator.CreateInstance(InteropFunctionsType, values) as IInteropFunctions;
|
||||
return instance;
|
||||
}
|
||||
|
||||
#region load types
|
||||
#region load types
|
||||
|
||||
public static Func<string, bool> MatchesOS { get; }
|
||||
private const string CONFIG_APP_ENDING = "ConfigApp.dll";
|
||||
|
||||
public static Func<string, bool> MatchesOS { get; }
|
||||
= Configuration.IsWindows ? a => Path.GetFileName(a).StartsWithInsensitive("win")
|
||||
: Configuration.IsLinux ? a => Path.GetFileName(a).StartsWithInsensitive("linux")
|
||||
: Configuration.IsMacOs ? a => Path.GetFileName(a).StartsWithInsensitive("mac") || Path.GetFileName(a).StartsWithInsensitive("osx")
|
||||
: _ => false;
|
||||
|
||||
private const string CONFIG_APP_ENDING = "ConfigApp.dll";
|
||||
private static List<ProcessModule> ModuleList { get; } = new();
|
||||
private static readonly EnumerationOptions enumerationOptions = new()
|
||||
{
|
||||
MatchType = MatchType.Simple,
|
||||
MatchCasing = MatchCasing.CaseInsensitive,
|
||||
IgnoreInaccessible = true,
|
||||
RecurseSubdirectories = false,
|
||||
ReturnSpecialDirectories = false
|
||||
};
|
||||
|
||||
static InteropFactory()
|
||||
{
|
||||
// searches file names for potential matches; doesn't run anything
|
||||
@@ -52,94 +58,36 @@ namespace LibationFileManager
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Commented code used to locate assemblies from the *ConfigApp.exe's module list.
|
||||
* Use this method to locate dependencies when they are not in Libation's program files directory.
|
||||
#if DEBUG
|
||||
|
||||
// runs the exe and gets the exe's loaded modules
|
||||
ModuleList = LoadModuleList(Path.GetFileNameWithoutExtension(configApp))
|
||||
.OrderBy(x => x.ModuleName)
|
||||
.ToList();
|
||||
#endif
|
||||
*/
|
||||
|
||||
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
|
||||
|
||||
var configAppAssembly = Assembly.LoadFrom(configApp);
|
||||
var type = typeof(IInteropFunctions);
|
||||
InteropFunctionsType = configAppAssembly
|
||||
.GetTypes()
|
||||
.FirstOrDefault(t => type.IsAssignableFrom(t));
|
||||
.FirstOrDefault(type.IsAssignableFrom);
|
||||
}
|
||||
private static string getOSConfigApp()
|
||||
{
|
||||
var here = Path.GetDirectoryName(Environment.ProcessPath);
|
||||
|
||||
// find '*ConfigApp.dll' files
|
||||
var appName =
|
||||
Directory.EnumerateFiles(here, $"*{CONFIG_APP_ENDING}", SearchOption.TopDirectoryOnly)
|
||||
// sanity check. shouldn't ever be true
|
||||
.Except(new[] { Environment.ProcessPath })
|
||||
Directory.EnumerateFiles(Configuration.ProcessDirectory, $"*{CONFIG_APP_ENDING}", enumerationOptions)
|
||||
.FirstOrDefault(exe => MatchesOS(exe));
|
||||
|
||||
return appName;
|
||||
}
|
||||
|
||||
/*
|
||||
* Use this method to locate dependencies when they are not in Libation's program files directory.
|
||||
*
|
||||
private static List<ProcessModule> LoadModuleList(string exeName)
|
||||
{
|
||||
var proc = new Process
|
||||
{
|
||||
StartInfo = new()
|
||||
{
|
||||
FileName = exeName,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
UseShellExecute = false
|
||||
}
|
||||
};
|
||||
|
||||
var waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
|
||||
|
||||
proc.OutputDataReceived += (_, _) => waitHandle.Set();
|
||||
proc.Start();
|
||||
proc.BeginOutputReadLine();
|
||||
|
||||
//Let the win process know we're ready to receive its standard output
|
||||
proc.StandardInput.WriteLine();
|
||||
|
||||
if (!waitHandle.WaitOne(2000))
|
||||
throw new Exception("Failed to start program");
|
||||
|
||||
//The win process has finished loading and is now waiting inside Main().
|
||||
//Copy it process module list.
|
||||
var modules = proc.Modules.Cast<ProcessModule>().ToList();
|
||||
|
||||
//Let the win process know we're done reading its module list
|
||||
proc.StandardInput.WriteLine();
|
||||
|
||||
return modules;
|
||||
}
|
||||
*/
|
||||
|
||||
private static Dictionary<string, Assembly> lowEffortCache { get; } = new();
|
||||
private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
|
||||
{
|
||||
// e.g. "System.Windows.Forms, Version=6.0.2.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
|
||||
var asmName = args.Name.Split(',')[0] + ".dll";
|
||||
var here = Path.GetDirectoryName(Environment.ProcessPath);
|
||||
var asmName = new AssemblyName(args.Name);
|
||||
var here = Configuration.ProcessDirectory;
|
||||
|
||||
var key = $"{asmName}|{here}";
|
||||
|
||||
if (lowEffortCache.TryGetValue(key, out var value))
|
||||
return value;
|
||||
|
||||
var assembly = CurrentDomain_AssemblyResolve_internal(asmName: asmName, here: here);
|
||||
var assembly = CurrentDomain_AssemblyResolve_internal(asmName, here: here);
|
||||
lowEffortCache[key] = assembly;
|
||||
|
||||
//Let the runtime handle any dll not found exceptions.
|
||||
@@ -149,27 +97,22 @@ namespace LibationFileManager
|
||||
return assembly;
|
||||
}
|
||||
|
||||
private static Assembly CurrentDomain_AssemblyResolve_internal(string asmName, string here)
|
||||
private static Assembly CurrentDomain_AssemblyResolve_internal(AssemblyName asmName, string here)
|
||||
{
|
||||
/*
|
||||
* Commented code used to locate assemblies from the *ConfigApp.exe's module list.
|
||||
* Use this method to locate dependencies when they are not in Libation's program files directory.
|
||||
#if DEBUG
|
||||
|
||||
var modulePath = ModuleList.SingleOrDefault(m => m.ModuleName.EqualsInsensitive(asmName))?.FileName;
|
||||
#else
|
||||
*/
|
||||
|
||||
// find the requested assembly in the program files directory
|
||||
* Find the requested assembly in the program files directory.
|
||||
* Assumes that all assemblies are in this application's directory.
|
||||
* If they're not (e.g. the app is not self-contained), you will need
|
||||
* to located them. The original way of doing this was to execute the
|
||||
* config app, wait for the runtime to load all dependencies, and
|
||||
* then seach the Process.Modules for the assembly name. Code for
|
||||
* this approach is still in the _Demos projects.
|
||||
*/
|
||||
var modulePath =
|
||||
Directory.EnumerateFiles(here, asmName, SearchOption.TopDirectoryOnly)
|
||||
Directory.EnumerateFiles(here, $"{asmName.Name}.dll", enumerationOptions)
|
||||
.SingleOrDefault();
|
||||
|
||||
//#endif
|
||||
if (modulePath is null)
|
||||
return null;
|
||||
|
||||
return Assembly.LoadFrom(modulePath);
|
||||
return modulePath is null ? null : Assembly.LoadFrom(modulePath);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
95
Source/LibationFileManager/NameListFormat.cs
Normal file
95
Source/LibationFileManager/NameListFormat.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using FileManager.NamingTemplate;
|
||||
using NameParser;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
internal partial class NameListFormat
|
||||
{
|
||||
public static string Formatter(ITemplateTag _, IEnumerable<string> names, string formatString)
|
||||
{
|
||||
var humanNames = names.Select(n => new HumanName(RemoveSuffix(n), Prefer.FirstOverPrefix));
|
||||
|
||||
var sortedNames = Sort(humanNames, formatString);
|
||||
var nameFormatString = Format(formatString, defaultValue: "{T} {F} {M} {L} {S}");
|
||||
var separatorString = Separator(formatString, defaultValue: ", ");
|
||||
var maxNames = Max(formatString, defaultValue: humanNames.Count());
|
||||
|
||||
var formattedNames = string.Join(separatorString, sortedNames.Take(maxNames).Select(n => FormatName(n, nameFormatString)));
|
||||
|
||||
while (formattedNames.Contains(" "))
|
||||
formattedNames = formattedNames.Replace(" ", " ");
|
||||
|
||||
return formattedNames;
|
||||
}
|
||||
|
||||
private static string RemoveSuffix(string namesString)
|
||||
{
|
||||
namesString = namesString.Replace('’', '\'').Replace(" - Ret.", ", Ret.");
|
||||
int dashIndex = namesString.IndexOf(" - ");
|
||||
return (dashIndex > 0 ? namesString[..dashIndex] : namesString).Trim();
|
||||
}
|
||||
|
||||
private static IEnumerable<HumanName> Sort(IEnumerable<HumanName> humanNames, string formatString)
|
||||
{
|
||||
var sortMatch = SortRegex().Match(formatString);
|
||||
return
|
||||
sortMatch.Success
|
||||
? sortMatch.Groups[1].Value == "F" ? humanNames.OrderBy(n => n.First)
|
||||
: sortMatch.Groups[1].Value == "M" ? humanNames.OrderBy(n => n.Middle)
|
||||
: sortMatch.Groups[1].Value == "L" ? humanNames.OrderBy(n => n.Last)
|
||||
: humanNames
|
||||
: humanNames;
|
||||
}
|
||||
|
||||
private static string Format(string formatString, string defaultValue)
|
||||
{
|
||||
var formatMatch = FormatRegex().Match(formatString);
|
||||
return formatMatch.Success ? formatMatch.Groups[1].Value : defaultValue;
|
||||
}
|
||||
|
||||
private static string Separator(string formatString, string defaultValue)
|
||||
{
|
||||
var separatorMatch = SeparatorRegex().Match(formatString);
|
||||
return separatorMatch.Success ? separatorMatch.Groups[1].Value : defaultValue;
|
||||
}
|
||||
|
||||
private static int Max(string formatString, int defaultValue)
|
||||
{
|
||||
var maxMatch = MaxRegex().Match(formatString);
|
||||
return maxMatch.Success && int.TryParse(maxMatch.Groups[1].Value, out var max) ? int.Max(1, max) : defaultValue;
|
||||
}
|
||||
|
||||
private static string FormatName(HumanName humanName, string nameFormatString)
|
||||
{
|
||||
//Single-word names parse as first names. Use it as last name.
|
||||
var lastName = string.IsNullOrWhiteSpace(humanName.Last) ? humanName.First : humanName.Last;
|
||||
|
||||
nameFormatString
|
||||
= nameFormatString
|
||||
.Replace("{T}", "{0}")
|
||||
.Replace("{F}", "{1}")
|
||||
.Replace("{M}", "{2}")
|
||||
.Replace("{L}", "{3}")
|
||||
.Replace("{S}", "{4}");
|
||||
|
||||
return string.Format(nameFormatString, humanName.Title, humanName.First, humanName.Middle, lastName, humanName.Suffix).Trim();
|
||||
}
|
||||
|
||||
/// <summary> Sort must have exactly one of the characters F, M, or L </summary>
|
||||
[GeneratedRegex(@"[Ss]ort\(\s*?([FML])\s*?\)")]
|
||||
private static partial Regex SortRegex();
|
||||
/// <summary> Format must have at least one of the string {T}, {F}, {M}, {L}, or {S} </summary>
|
||||
[GeneratedRegex(@"[Ff]ormat\((.*?(?:{[TFMLS]})+.*?)\)")]
|
||||
private static partial Regex FormatRegex();
|
||||
/// <summary> Separator can be anything </summary>
|
||||
[GeneratedRegex(@"[Ss]eparator\((.*?)\)")]
|
||||
private static partial Regex SeparatorRegex();
|
||||
/// <summary> Max must have a 1 or 2-digit number </summary>
|
||||
[GeneratedRegex(@"[Mm]ax\(\s*?(\d{1,2})\s*?\)")]
|
||||
private static partial Regex MaxRegex();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace LibationFileManager
|
||||
{
|
||||
public class NullInteropFunctions : IInteropFunctions
|
||||
{
|
||||
public NullInteropFunctions() { }
|
||||
|
||||
public NullInteropFunctions() { }
|
||||
public NullInteropFunctions(params object[] values) { }
|
||||
|
||||
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
|
||||
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
|
||||
public void CopyTextToClipboard(string text) => throw new PlatformNotSupportedException();
|
||||
}
|
||||
public bool CanUpdate => throw new PlatformNotSupportedException();
|
||||
public Process RunAsRoot(string exe, string args) => throw new PlatformNotSupportedException();
|
||||
public void InstallUpdate(string updateBundle) => throw new PlatformNotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using AaxDecrypter;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
@@ -19,7 +18,7 @@ namespace LibationFileManager
|
||||
static abstract IEnumerable<TagCollection> TagCollections { get; }
|
||||
}
|
||||
|
||||
public abstract partial class Templates
|
||||
public abstract class Templates
|
||||
{
|
||||
public const string ERROR_FULL_PATH_IS_INVALID = @"No colons or full paths allowed. Eg: should not start with C:\";
|
||||
public const string WARNING_NO_CHAPTER_NUMBER_TAG = "Should include chapter number tag in template used for naming files which are split by chapter. Ie: <ch#> or <ch# 0>";
|
||||
@@ -203,9 +202,9 @@ namespace LibationFileManager
|
||||
{ TemplateTags.Id, lb => lb.AudibleProductId, v => v },
|
||||
{ TemplateTags.Title, lb => lb.Title },
|
||||
{ TemplateTags.TitleShort, lb => getTitleShort(lb.Title) },
|
||||
{ TemplateTags.Author, lb => lb.Authors, NameListFormatter },
|
||||
{ TemplateTags.Author, lb => lb.Authors, NameListFormat.Formatter },
|
||||
{ TemplateTags.FirstAuthor, lb => lb.FirstAuthor },
|
||||
{ TemplateTags.Narrator, lb => lb.Narrators, NameListFormatter },
|
||||
{ TemplateTags.Narrator, lb => lb.Narrators, NameListFormat.Formatter },
|
||||
{ TemplateTags.FirstNarrator, lb => lb.FirstNarrator },
|
||||
{ TemplateTags.Series, lb => lb.SeriesName },
|
||||
{ TemplateTags.SeriesNumber, lb => lb.SeriesNumber },
|
||||
@@ -252,89 +251,6 @@ namespace LibationFileManager
|
||||
|
||||
#region Tag Formatters
|
||||
|
||||
/// <summary> Sort must have exactly one of the characters F, M, or L </summary>
|
||||
[GeneratedRegex(@"[Ss]ort\(\s*?([FML])\s*?\)")]
|
||||
private static partial Regex NamesSortRegex();
|
||||
/// <summary> Format must have at least one of the string {T}, {F}, {M}, {L}, or {S} </summary>
|
||||
[GeneratedRegex(@"[Ff]ormat\((.*?(?:{[TFMLS]})+.*?)\)")]
|
||||
private static partial Regex NamesFormatRegex();
|
||||
/// <summary> Separator can be anything </summary>
|
||||
[GeneratedRegex(@"[Ss]eparator\((.*?)\)")]
|
||||
private static partial Regex NamesSeparatorRegex();
|
||||
/// <summary> Max must have a 1 or 2-digit number </summary>
|
||||
[GeneratedRegex(@"[Mm]ax\(\s*?(\d{1,2})\s*?\)")]
|
||||
private static partial Regex NamesMaxRegex();
|
||||
|
||||
private static string NameListFormatter(ITemplateTag templateTag, IEnumerable<string> names, string formatString)
|
||||
{
|
||||
var humanNames = names.Select(n => new HumanName(removeSuffix(n), Prefer.FirstOverPrefix));
|
||||
|
||||
var sortedNames = sort(humanNames, formatString);
|
||||
var nameFormatString = format(formatString, defaultValue: "{T} {F} {M} {L} {S}");
|
||||
var separatorString = separator(formatString, defaultValue: ", ");
|
||||
var maxNames = max(formatString, defaultValue: humanNames.Count());
|
||||
|
||||
var formattedNames = string.Join(separatorString, sortedNames.Take(maxNames).Select(n => formatName(n, nameFormatString)));
|
||||
|
||||
while (formattedNames.Contains(" "))
|
||||
formattedNames = formattedNames.Replace(" ", " ");
|
||||
|
||||
return formattedNames;
|
||||
|
||||
static string removeSuffix(string namesString)
|
||||
{
|
||||
namesString = namesString.Replace('’', '\'').Replace(" - Ret.", ", Ret.");
|
||||
int dashIndex = namesString.IndexOf(" - ");
|
||||
return (dashIndex > 0 ? namesString[..dashIndex] : namesString).Trim();
|
||||
}
|
||||
|
||||
static IEnumerable<HumanName> sort(IEnumerable<HumanName> humanNames, string formatString)
|
||||
{
|
||||
var sortMatch = NamesSortRegex().Match(formatString);
|
||||
return
|
||||
sortMatch.Success
|
||||
? sortMatch.Groups[1].Value == "F" ? humanNames.OrderBy(n => n.First)
|
||||
: sortMatch.Groups[1].Value == "M" ? humanNames.OrderBy(n => n.Middle)
|
||||
: sortMatch.Groups[1].Value == "L" ? humanNames.OrderBy(n => n.Last)
|
||||
: humanNames
|
||||
: humanNames;
|
||||
}
|
||||
|
||||
static string format(string formatString, string defaultValue)
|
||||
{
|
||||
var formatMatch = NamesFormatRegex().Match(formatString);
|
||||
return formatMatch.Success ? formatMatch.Groups[1].Value : defaultValue;
|
||||
}
|
||||
|
||||
static string separator(string formatString, string defaultValue)
|
||||
{
|
||||
var separatorMatch = NamesSeparatorRegex().Match(formatString);
|
||||
return separatorMatch.Success ? separatorMatch.Groups[1].Value : defaultValue;
|
||||
}
|
||||
|
||||
static int max(string formatString, int defaultValue)
|
||||
{
|
||||
var maxMatch = NamesMaxRegex().Match(formatString);
|
||||
return maxMatch.Success && int.TryParse(maxMatch.Groups[1].Value, out var max) ? int.Max(1, max) : defaultValue;
|
||||
}
|
||||
|
||||
static string formatName(HumanName humanName, string nameFormatString)
|
||||
{
|
||||
//Single-word names parse as first names. Use it as last name.
|
||||
var lastName = string.IsNullOrWhiteSpace(humanName.Last) ? humanName.First : humanName.Last;
|
||||
|
||||
nameFormatString
|
||||
= nameFormatString
|
||||
.Replace("{T}", "{0}")
|
||||
.Replace("{F}", "{1}")
|
||||
.Replace("{M}", "{2}")
|
||||
.Replace("{L}", "{3}")
|
||||
.Replace("{S}", "{4}");
|
||||
|
||||
return string.Format(nameFormatString, humanName.Title, humanName.First, humanName.Middle, lastName, humanName.Suffix).Trim();
|
||||
}
|
||||
}
|
||||
|
||||
private static string getTitleShort(string title)
|
||||
=> title?.IndexOf(':') > 0 ? title.Substring(0, title.IndexOf(':')) : title;
|
||||
|
||||
|
||||
18
Source/LibationUiBase/SampleRateSelection.cs
Normal file
18
Source/LibationUiBase/SampleRateSelection.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationUiBase
|
||||
{
|
||||
public class SampleRateSelection
|
||||
{
|
||||
public AAXClean.SampleRate SampleRate { get; }
|
||||
public SampleRateSelection(AAXClean.SampleRate sampleRate)
|
||||
{
|
||||
SampleRate = sampleRate;
|
||||
}
|
||||
public override string ToString() => $"{(int)SampleRate} Hz";
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using LibationFileManager;
|
||||
using System.Linq;
|
||||
using LibationUiBase;
|
||||
|
||||
namespace LibationWinForms.Dialogs
|
||||
{
|
||||
@@ -26,6 +27,25 @@ namespace LibationWinForms.Dialogs
|
||||
Configuration.ClipBookmarkFormat.Json
|
||||
});
|
||||
|
||||
maxSampleRateCb.Items.AddRange(
|
||||
new object[]
|
||||
{
|
||||
new SampleRateSelection(AAXClean.SampleRate.Hz_44100),
|
||||
new SampleRateSelection(AAXClean.SampleRate.Hz_32000),
|
||||
new SampleRateSelection(AAXClean.SampleRate.Hz_24000),
|
||||
new SampleRateSelection(AAXClean.SampleRate.Hz_22050),
|
||||
new SampleRateSelection(AAXClean.SampleRate.Hz_16000),
|
||||
new SampleRateSelection(AAXClean.SampleRate.Hz_12000)
|
||||
});
|
||||
|
||||
encoderQualityCb.Items.AddRange(
|
||||
new object[]
|
||||
{
|
||||
NAudio.Lame.EncoderQuality.High,
|
||||
NAudio.Lame.EncoderQuality.Standard,
|
||||
NAudio.Lame.EncoderQuality.Fast,
|
||||
});
|
||||
|
||||
allowLibationFixupCbox.Checked = config.AllowLibationFixup;
|
||||
createCueSheetCbox.Checked = config.CreateCueSheet;
|
||||
downloadCoverArtCbox.Checked = config.DownloadCoverArt;
|
||||
@@ -42,6 +62,8 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
lameTargetBitrateRb.Checked = config.LameTargetBitrate;
|
||||
lameTargetQualityRb.Checked = !config.LameTargetBitrate;
|
||||
maxSampleRateCb.SelectedItem = maxSampleRateCb.Items.Cast<SampleRateSelection>().Single(s => s.SampleRate == config.MaxSampleRate);
|
||||
encoderQualityCb.SelectedItem = config.LameEncoderQuality;
|
||||
lameDownsampleMonoCbox.Checked = config.LameDownsampleMono;
|
||||
lameBitrateTb.Value = config.LameBitrate;
|
||||
lameConstantBitrateCbox.Checked = config.LameConstantBitrate;
|
||||
@@ -75,6 +97,9 @@ namespace LibationWinForms.Dialogs
|
||||
config.MoveMoovToBeginning = moveMoovAtomCbox.Checked;
|
||||
|
||||
config.LameTargetBitrate = lameTargetBitrateRb.Checked;
|
||||
config.MaxSampleRate = ((SampleRateSelection)maxSampleRateCb.SelectedItem).SampleRate;
|
||||
config.LameEncoderQuality = (NAudio.Lame.EncoderQuality)encoderQualityCb.SelectedItem;
|
||||
encoderQualityCb.SelectedItem = config.LameEncoderQuality;
|
||||
config.LameDownsampleMono = lameDownsampleMonoCbox.Checked;
|
||||
config.LameBitrate = lameBitrateTb.Value;
|
||||
config.LameConstantBitrate = lameConstantBitrateCbox.Checked;
|
||||
|
||||
@@ -115,6 +115,10 @@
|
||||
this.retainAaxFileCbox = new System.Windows.Forms.CheckBox();
|
||||
this.downloadCoverArtCbox = new System.Windows.Forms.CheckBox();
|
||||
this.createCueSheetCbox = new System.Windows.Forms.CheckBox();
|
||||
this.maxSampleRateCb = new System.Windows.Forms.ComboBox();
|
||||
this.encoderQualityCb = new System.Windows.Forms.ComboBox();
|
||||
this.label20 = new System.Windows.Forms.Label();
|
||||
this.label21 = new System.Windows.Forms.Label();
|
||||
this.badBookGb.SuspendLayout();
|
||||
this.tabControl.SuspendLayout();
|
||||
this.tab1ImportantSettings.SuspendLayout();
|
||||
@@ -743,6 +747,10 @@
|
||||
//
|
||||
// lameOptionsGb
|
||||
//
|
||||
this.lameOptionsGb.Controls.Add(this.label20);
|
||||
this.lameOptionsGb.Controls.Add(this.label21);
|
||||
this.lameOptionsGb.Controls.Add(this.encoderQualityCb);
|
||||
this.lameOptionsGb.Controls.Add(this.maxSampleRateCb);
|
||||
this.lameOptionsGb.Controls.Add(this.lameDownsampleMonoCbox);
|
||||
this.lameOptionsGb.Controls.Add(this.lameBitrateGb);
|
||||
this.lameOptionsGb.Controls.Add(this.label1);
|
||||
@@ -757,8 +765,7 @@
|
||||
//
|
||||
// lameDownsampleMonoCbox
|
||||
//
|
||||
this.lameDownsampleMonoCbox.AutoSize = true;
|
||||
this.lameDownsampleMonoCbox.Location = new System.Drawing.Point(234, 35);
|
||||
this.lameDownsampleMonoCbox.Location = new System.Drawing.Point(237, 30);
|
||||
this.lameDownsampleMonoCbox.Name = "lameDownsampleMonoCbox";
|
||||
this.lameDownsampleMonoCbox.Size = new System.Drawing.Size(184, 34);
|
||||
this.lameDownsampleMonoCbox.TabIndex = 1;
|
||||
@@ -776,9 +783,9 @@
|
||||
this.lameBitrateGb.Controls.Add(this.label11);
|
||||
this.lameBitrateGb.Controls.Add(this.label3);
|
||||
this.lameBitrateGb.Controls.Add(this.lameBitrateTb);
|
||||
this.lameBitrateGb.Location = new System.Drawing.Point(6, 84);
|
||||
this.lameBitrateGb.Location = new System.Drawing.Point(6, 104);
|
||||
this.lameBitrateGb.Name = "lameBitrateGb";
|
||||
this.lameBitrateGb.Size = new System.Drawing.Size(421, 101);
|
||||
this.lameBitrateGb.Size = new System.Drawing.Size(421, 102);
|
||||
this.lameBitrateGb.TabIndex = 0;
|
||||
this.lameBitrateGb.TabStop = false;
|
||||
this.lameBitrateGb.Text = "Bitrate";
|
||||
@@ -786,7 +793,7 @@
|
||||
// LameMatchSourceBRCbox
|
||||
//
|
||||
this.LameMatchSourceBRCbox.AutoSize = true;
|
||||
this.LameMatchSourceBRCbox.Location = new System.Drawing.Point(260, 77);
|
||||
this.LameMatchSourceBRCbox.Location = new System.Drawing.Point(275, 76);
|
||||
this.LameMatchSourceBRCbox.Name = "LameMatchSourceBRCbox";
|
||||
this.LameMatchSourceBRCbox.Size = new System.Drawing.Size(140, 19);
|
||||
this.LameMatchSourceBRCbox.TabIndex = 3;
|
||||
@@ -883,7 +890,7 @@
|
||||
this.label1.AutoSize = true;
|
||||
this.label1.Enabled = false;
|
||||
this.label1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Italic, System.Drawing.GraphicsUnit.Point);
|
||||
this.label1.Location = new System.Drawing.Point(6, 298);
|
||||
this.label1.Location = new System.Drawing.Point(6, 325);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(172, 15);
|
||||
this.label1.TabIndex = 1;
|
||||
@@ -904,9 +911,9 @@
|
||||
this.lameQualityGb.Controls.Add(this.label14);
|
||||
this.lameQualityGb.Controls.Add(this.label2);
|
||||
this.lameQualityGb.Controls.Add(this.lameVBRQualityTb);
|
||||
this.lameQualityGb.Location = new System.Drawing.Point(6, 186);
|
||||
this.lameQualityGb.Location = new System.Drawing.Point(6, 212);
|
||||
this.lameQualityGb.Name = "lameQualityGb";
|
||||
this.lameQualityGb.Size = new System.Drawing.Size(421, 109);
|
||||
this.lameQualityGb.Size = new System.Drawing.Size(421, 103);
|
||||
this.lameQualityGb.TabIndex = 0;
|
||||
this.lameQualityGb.TabStop = false;
|
||||
this.lameQualityGb.Text = "Quality";
|
||||
@@ -986,7 +993,7 @@
|
||||
// label13
|
||||
//
|
||||
this.label13.AutoSize = true;
|
||||
this.label13.Location = new System.Drawing.Point(376, 81);
|
||||
this.label13.Location = new System.Drawing.Point(376, 80);
|
||||
this.label13.Name = "label13";
|
||||
this.label13.Size = new System.Drawing.Size(39, 15);
|
||||
this.label13.TabIndex = 1;
|
||||
@@ -995,7 +1002,7 @@
|
||||
// label10
|
||||
//
|
||||
this.label10.AutoSize = true;
|
||||
this.label10.Location = new System.Drawing.Point(6, 81);
|
||||
this.label10.Location = new System.Drawing.Point(6, 80);
|
||||
this.label10.Name = "label10";
|
||||
this.label10.Size = new System.Drawing.Size(43, 15);
|
||||
this.label10.TabIndex = 1;
|
||||
@@ -1036,7 +1043,7 @@
|
||||
this.groupBox2.Controls.Add(this.lameTargetBitrateRb);
|
||||
this.groupBox2.Location = new System.Drawing.Point(6, 22);
|
||||
this.groupBox2.Name = "groupBox2";
|
||||
this.groupBox2.Size = new System.Drawing.Size(222, 56);
|
||||
this.groupBox2.Size = new System.Drawing.Size(214, 47);
|
||||
this.groupBox2.TabIndex = 0;
|
||||
this.groupBox2.TabStop = false;
|
||||
this.groupBox2.Text = "Target";
|
||||
@@ -1044,7 +1051,7 @@
|
||||
// lameTargetQualityRb
|
||||
//
|
||||
this.lameTargetQualityRb.AutoSize = true;
|
||||
this.lameTargetQualityRb.Location = new System.Drawing.Point(138, 23);
|
||||
this.lameTargetQualityRb.Location = new System.Drawing.Point(139, 22);
|
||||
this.lameTargetQualityRb.Name = "lameTargetQualityRb";
|
||||
this.lameTargetQualityRb.Size = new System.Drawing.Size(63, 19);
|
||||
this.lameTargetQualityRb.TabIndex = 0;
|
||||
@@ -1056,7 +1063,7 @@
|
||||
// lameTargetBitrateRb
|
||||
//
|
||||
this.lameTargetBitrateRb.AutoSize = true;
|
||||
this.lameTargetBitrateRb.Location = new System.Drawing.Point(6, 23);
|
||||
this.lameTargetBitrateRb.Location = new System.Drawing.Point(6, 22);
|
||||
this.lameTargetBitrateRb.Name = "lameTargetBitrateRb";
|
||||
this.lameTargetBitrateRb.Size = new System.Drawing.Size(59, 19);
|
||||
this.lameTargetBitrateRb.TabIndex = 0;
|
||||
@@ -1112,6 +1119,42 @@
|
||||
this.createCueSheetCbox.UseVisualStyleBackColor = true;
|
||||
this.createCueSheetCbox.CheckedChanged += new System.EventHandler(this.allowLibationFixupCbox_CheckedChanged);
|
||||
//
|
||||
// maxSampleRateCb
|
||||
//
|
||||
this.maxSampleRateCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.maxSampleRateCb.FormattingEnabled = true;
|
||||
this.maxSampleRateCb.Location = new System.Drawing.Point(119, 75);
|
||||
this.maxSampleRateCb.Name = "maxSampleRateCb";
|
||||
this.maxSampleRateCb.Size = new System.Drawing.Size(101, 23);
|
||||
this.maxSampleRateCb.TabIndex = 2;
|
||||
//
|
||||
// encoderQualityCb
|
||||
//
|
||||
this.encoderQualityCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.encoderQualityCb.FormattingEnabled = true;
|
||||
this.encoderQualityCb.Location = new System.Drawing.Point(337, 75);
|
||||
this.encoderQualityCb.Name = "encoderQualityCb";
|
||||
this.encoderQualityCb.Size = new System.Drawing.Size(90, 23);
|
||||
this.encoderQualityCb.TabIndex = 2;
|
||||
//
|
||||
// label20
|
||||
//
|
||||
this.label20.AutoSize = true;
|
||||
this.label20.Location = new System.Drawing.Point(12, 78);
|
||||
this.label20.Name = "label20";
|
||||
this.label20.Size = new System.Drawing.Size(101, 15);
|
||||
this.label20.TabIndex = 3;
|
||||
this.label20.Text = "Max Sample Rate:";
|
||||
//
|
||||
// label21
|
||||
//
|
||||
this.label21.AutoSize = true;
|
||||
this.label21.Location = new System.Drawing.Point(239, 78);
|
||||
this.label21.Name = "label21";
|
||||
this.label21.Size = new System.Drawing.Size(94, 15);
|
||||
this.label21.TabIndex = 3;
|
||||
this.label21.Text = "Encoder Quality:";
|
||||
//
|
||||
// SettingsDialog
|
||||
//
|
||||
this.AcceptButton = this.saveBtn;
|
||||
@@ -1253,5 +1296,9 @@
|
||||
private System.Windows.Forms.ComboBox clipsBookmarksFormatCb;
|
||||
private System.Windows.Forms.CheckBox downloadClipsBookmarksCbox;
|
||||
private System.Windows.Forms.CheckBox moveMoovAtomCbox;
|
||||
private System.Windows.Forms.ComboBox encoderQualityCb;
|
||||
private System.Windows.Forms.ComboBox maxSampleRateCb;
|
||||
private System.Windows.Forms.Label label21;
|
||||
private System.Windows.Forms.Label label20;
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ namespace LibationWinForms.Dialogs
|
||||
},
|
||||
Configuration.KnownDirectories.UserProfile,
|
||||
"Books");
|
||||
booksSelectControl.SelectDirectory(config.Books);
|
||||
booksSelectControl.SelectDirectory(config.Books.PathWithoutPrefix);
|
||||
|
||||
saveEpisodesToSeriesFolderCbox.Checked = config.SavePodcastsToParentFolder;
|
||||
betaOptInCbox.Checked = config.BetaOptIn;
|
||||
|
||||
14
Source/LibationWinForms/Form1.Designer.cs
generated
14
Source/LibationWinForms/Form1.Designer.cs
generated
@@ -62,6 +62,8 @@
|
||||
this.setDownloadedAutoToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.removeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.toolStripSeparator4 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.launchHangoverToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.locateAudiobooksToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.accountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
@@ -378,6 +380,8 @@
|
||||
this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.accountsToolStripMenuItem,
|
||||
this.basicSettingsToolStripMenuItem,
|
||||
this.toolStripSeparator4,
|
||||
this.launchHangoverToolStripMenuItem,
|
||||
this.toolStripSeparator2,
|
||||
this.aboutToolStripMenuItem});
|
||||
this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
|
||||
@@ -516,6 +520,7 @@
|
||||
this.productsDisplay.VisibleCountChanged += new System.EventHandler<int>(this.productsDisplay_VisibleCountChanged);
|
||||
this.productsDisplay.RemovableCountChanged += new System.EventHandler<int>(this.productsDisplay_RemovableCountChanged);
|
||||
this.productsDisplay.LiberateClicked += new System.EventHandler<DataLayer.LibraryBook>(this.ProductsDisplay_LiberateClicked);
|
||||
this.productsDisplay.ConvertToMp3Clicked += new System.EventHandler<DataLayer.LibraryBook>(this.ProductsDisplay_ConvertToMp3Clicked);
|
||||
this.productsDisplay.InitialLoaded += new System.EventHandler(this.productsDisplay_InitialLoaded);
|
||||
//
|
||||
// toggleQueueHideBtn
|
||||
@@ -572,6 +577,13 @@
|
||||
this.locateAudiobooksToolStripMenuItem.Text = "L&ocate Audiobooks";
|
||||
this.locateAudiobooksToolStripMenuItem.Click += new System.EventHandler(this.locateAudiobooksToolStripMenuItem_Click);
|
||||
//
|
||||
// launchHangoverToolStripMenuItem
|
||||
//
|
||||
this.launchHangoverToolStripMenuItem.Name = "launchHangoverToolStripMenuItem";
|
||||
this.launchHangoverToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
|
||||
this.launchHangoverToolStripMenuItem.Text = "Launch &Hangover";
|
||||
this.launchHangoverToolStripMenuItem.Click += new System.EventHandler(this.launchHangoverToolStripMenuItem_Click);
|
||||
//
|
||||
// toolStripSeparator3
|
||||
//
|
||||
this.toolStripSeparator3.Name = "toolStripSeparator3";
|
||||
@@ -648,6 +660,8 @@
|
||||
private System.Windows.Forms.ToolStripMenuItem removeToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
|
||||
private System.Windows.Forms.ToolStripMenuItem locateAudiobooksToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator4;
|
||||
private System.Windows.Forms.ToolStripMenuItem launchHangoverToolStripMenuItem;
|
||||
private LibationWinForms.FormattableToolStripMenuItem liberateVisibleToolStripMenuItem_LiberateMenu;
|
||||
private System.Windows.Forms.SplitContainer splitContainer1;
|
||||
private LibationWinForms.ProcessQueue.ProcessQueueControl processBookQueue1;
|
||||
|
||||
@@ -56,6 +56,23 @@ namespace LibationWinForms
|
||||
}
|
||||
}
|
||||
|
||||
private void ProductsDisplay_ConvertToMp3Clicked(object sender, LibraryBook libraryBook)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (libraryBook.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", libraryBook);
|
||||
SetQueueCollapseState(false);
|
||||
processBookQueue1.AddConvertMp3(libraryBook);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "An error occurred while handling the stop light button click for {libraryBook}", libraryBook);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetQueueCollapseState(bool collapsed)
|
||||
{
|
||||
if (collapsed && !splitContainer1.Panel2Collapsed)
|
||||
|
||||
@@ -14,5 +14,17 @@ namespace LibationWinForms
|
||||
|
||||
private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
=> MessageBox.Show($"Libation {AppScaffolding.LibationScaffolding.Variety}{Environment.NewLine}Version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}");
|
||||
}
|
||||
|
||||
private void launchHangoverToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start("Hangover.exe");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, "Failed to launch Hangover");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
this.productsGrid.TabIndex = 0;
|
||||
this.productsGrid.VisibleCountChanged += new System.EventHandler<int>(this.productsGrid_VisibleCountChanged);
|
||||
this.productsGrid.LiberateClicked += new LibationWinForms.GridView.LibraryBookEntryClickedEventHandler(this.productsGrid_LiberateClicked);
|
||||
this.productsGrid.ConvertToMp3Clicked += new LibationWinForms.GridView.LibraryBookEntryClickedEventHandler(this.productsGrid_ConvertToMp3Clicked);
|
||||
this.productsGrid.CoverClicked += new LibationWinForms.GridView.GridEntryClickedEventHandler(this.productsGrid_CoverClicked);
|
||||
this.productsGrid.DetailsClicked += new LibationWinForms.GridView.LibraryBookEntryClickedEventHandler(this.productsGrid_DetailsClicked);
|
||||
this.productsGrid.DescriptionClicked += new LibationWinForms.GridView.GridEntryRectangleClickedEventHandler(this.productsGrid_DescriptionClicked);
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace LibationWinForms.GridView
|
||||
public event EventHandler<int> VisibleCountChanged;
|
||||
public event EventHandler<int> RemovableCountChanged;
|
||||
public event EventHandler<LibraryBook> LiberateClicked;
|
||||
public event EventHandler<LibraryBook> ConvertToMp3Clicked;
|
||||
public event EventHandler InitialLoaded;
|
||||
|
||||
private bool hasBeenDisplayed;
|
||||
@@ -204,6 +205,12 @@ namespace LibationWinForms.GridView
|
||||
LiberateClicked?.Invoke(this, liveGridEntry.LibraryBook);
|
||||
}
|
||||
|
||||
private void productsGrid_ConvertToMp3Clicked(LibraryBookEntry liveGridEntry)
|
||||
{
|
||||
if (liveGridEntry.LibraryBook.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error)
|
||||
ConvertToMp3Clicked?.Invoke(this, liveGridEntry.LibraryBook);
|
||||
}
|
||||
|
||||
private void productsGrid_RemovableCountChanged(object sender, EventArgs e)
|
||||
{
|
||||
RemovableCountChanged?.Invoke(sender, productsGrid.GetAllBookEntries().Count(lbe => lbe.Remove is RemoveStatus.Removed));
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
@@ -21,6 +22,7 @@ namespace LibationWinForms.GridView
|
||||
/// <summary>Number of visible rows has changed</summary>
|
||||
public event EventHandler<int> VisibleCountChanged;
|
||||
public event LibraryBookEntryClickedEventHandler LiberateClicked;
|
||||
public event LibraryBookEntryClickedEventHandler ConvertToMp3Clicked;
|
||||
public event GridEntryClickedEventHandler CoverClicked;
|
||||
public event LibraryBookEntryClickedEventHandler DetailsClicked;
|
||||
public event GridEntryRectangleClickedEventHandler DescriptionClicked;
|
||||
@@ -122,7 +124,7 @@ namespace LibationWinForms.GridView
|
||||
{
|
||||
var dgv = (DataGridView)sender;
|
||||
var text = dgv[e.ColumnIndex, e.RowIndex].FormattedValue.ToString();
|
||||
InteropFactory.Create().CopyTextToClipboard(text);
|
||||
Clipboard.SetDataObject(text, false, 5, 150);
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
@@ -152,7 +154,7 @@ namespace LibationWinForms.GridView
|
||||
setNotDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.NotLiberated);
|
||||
|
||||
var removeMenuItem = new ToolStripMenuItem() { Text = "&Remove from library" };
|
||||
removeMenuItem.Click += (_, __) => LibraryCommands.RemoveBook(entry.AudibleProductId);
|
||||
removeMenuItem.Click += async (_, __) => await Task.Run(() => LibraryCommands.RemoveBook(entry.AudibleProductId));
|
||||
|
||||
var locateFileMenuItem = new ToolStripMenuItem() { Text = "&Locate file..." };
|
||||
locateFileMenuItem.Click += (_, __) =>
|
||||
@@ -175,6 +177,13 @@ namespace LibationWinForms.GridView
|
||||
}
|
||||
};
|
||||
|
||||
var convertToMp3MenuItem = new ToolStripMenuItem
|
||||
{
|
||||
Text = "&Convert to Mp3",
|
||||
Enabled = entry.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated
|
||||
};
|
||||
convertToMp3MenuItem.Click += (_, e) => ConvertToMp3Clicked?.Invoke(entry as LibraryBookEntry);
|
||||
|
||||
var bookRecordMenuItem = new ToolStripMenuItem { Text = "View &Bookmarks/Clips" };
|
||||
bookRecordMenuItem.Click += (_, _) => new BookRecordsDialog(entry.LibraryBook).ShowDialog(this);
|
||||
|
||||
@@ -183,6 +192,7 @@ namespace LibationWinForms.GridView
|
||||
stopLightContextMenu.Items.Add(setNotDownloadMenuItem);
|
||||
stopLightContextMenu.Items.Add(removeMenuItem);
|
||||
stopLightContextMenu.Items.Add(locateFileMenuItem);
|
||||
stopLightContextMenu.Items.Add(convertToMp3MenuItem);
|
||||
stopLightContextMenu.Items.Add(new ToolStripSeparator());
|
||||
stopLightContextMenu.Items.Add(bookRecordMenuItem);
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autoupdater.NET.Official" Version="1.7.6" />
|
||||
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="7.2.1.1" />
|
||||
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="7.2.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace LibationWinForms
|
||||
|
||||
ApplicationConfiguration.Initialize();
|
||||
|
||||
AppScaffolding.LibationScaffolding.SetReleaseIdentifier(AppScaffolding.ReleaseIdentifier.WindowsClassic);
|
||||
AppScaffolding.LibationScaffolding.SetReleaseIdentifier(AppScaffolding.Variety.Classic);
|
||||
|
||||
//***********************************************//
|
||||
// //
|
||||
@@ -92,7 +92,7 @@ namespace LibationWinForms
|
||||
// check for existing settings in default location
|
||||
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
|
||||
if (Configuration.SettingsFileIsValid(defaultSettingsFile))
|
||||
config.SetLibationFiles(defaultLibationFilesDir);
|
||||
Configuration.SetLibationFiles(defaultLibationFilesDir);
|
||||
|
||||
if (config.LibationSettingsAreValid)
|
||||
return;
|
||||
@@ -112,7 +112,7 @@ namespace LibationWinForms
|
||||
}
|
||||
|
||||
if (setupDialog.IsNewUser)
|
||||
config.SetLibationFiles(defaultLibationFilesDir);
|
||||
Configuration.SetLibationFiles(defaultLibationFilesDir);
|
||||
else if (setupDialog.IsReturningUser)
|
||||
{
|
||||
var libationFilesDialog = new LibationFilesDialog();
|
||||
@@ -123,7 +123,7 @@ namespace LibationWinForms
|
||||
return;
|
||||
}
|
||||
|
||||
config.SetLibationFiles(libationFilesDialog.SelectedDirectory);
|
||||
Configuration.SetLibationFiles(libationFilesDialog.SelectedDirectory);
|
||||
if (config.LibationSettingsAreValid)
|
||||
return;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[Desktop Entry]
|
||||
Name=Libation
|
||||
Exec=libation
|
||||
Exec=/usr/bin/libation
|
||||
Icon=libation
|
||||
Comment=Liberate your Audiobooks
|
||||
Terminal=false
|
||||
@@ -34,4 +34,16 @@
|
||||
<ProjectReference Include="..\..\AppScaffolding\AppScaffolding.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
<ItemGroup>
|
||||
<None Update="libation_glass.svg">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Libation.desktop">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="runasroot.sh">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,14 +1,71 @@
|
||||
using LibationFileManager;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace LinuxConfigApp
|
||||
{
|
||||
internal class LinuxInterop : IInteropFunctions
|
||||
{
|
||||
public LinuxInterop() { }
|
||||
public LinuxInterop(params object[] values) { }
|
||||
internal class LinuxInterop : IInteropFunctions
|
||||
{
|
||||
//Different terminal apps possibly installed on a linux system
|
||||
// [0] console executable
|
||||
// [1] argument to set the concole's title
|
||||
// [2] argument to pass a command to be executed to the terminal
|
||||
static readonly string[][] consoleCommands =
|
||||
{
|
||||
new[] {"konsole", "--title", "-e"},
|
||||
new[] {"gnome-terminal", "--title", "--"},
|
||||
new[] {"mate-terminal", "--title", "-x"},
|
||||
new[] {"xterm", "-T", "-e"},
|
||||
};
|
||||
|
||||
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
|
||||
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
|
||||
public void CopyTextToClipboard(string text) => throw new PlatformNotSupportedException();
|
||||
}
|
||||
public LinuxInterop() { }
|
||||
public LinuxInterop(params object[] values) { }
|
||||
|
||||
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
|
||||
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
|
||||
|
||||
//only run the auto updater if the current app was installed from the
|
||||
//.deb package. Try to detect this by checking if the symlink exists.
|
||||
public bool CanUpdate => Directory.Exists("/usr/lib/libation");
|
||||
public void InstallUpdate(string updateBundle)
|
||||
{
|
||||
RunAsRoot("apt", $"install '{updateBundle}'");
|
||||
}
|
||||
|
||||
public Process RunAsRoot(string exe, string args)
|
||||
{
|
||||
//cribbed this script from VirtualBox's guest additions installer.
|
||||
//It's designed to launch the system's gui superuser password
|
||||
//prompt across multiple distributions and desktop environments.
|
||||
const string runasroot = "runasroot.sh";
|
||||
|
||||
string command = $"{exe ?? ""} {args ?? ""}".Trim();
|
||||
|
||||
foreach (var console in consoleCommands)
|
||||
{
|
||||
ProcessStartInfo psi = new()
|
||||
{
|
||||
FileName = console[0],
|
||||
UseShellExecute = false,
|
||||
ArgumentList =
|
||||
{
|
||||
console[1],
|
||||
$"Running '{exe}' as root", // console title
|
||||
console[2],
|
||||
"/bin/sh",
|
||||
Path.Combine(Configuration.ProcessDirectory, runasroot), //script file
|
||||
"Installing libation.deb", //command title
|
||||
command, // command to execute vis /bin/sh
|
||||
$"Please run '{command}' manually" // error message to display in the terminal
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
return Process.Start(psi);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
throw new PlatformNotSupportedException($"Could not start any of the supported terminals: {string.Join(", ", consoleCommands.Select(c => c[0]))}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
using AppScaffolding;
|
||||
|
||||
namespace LinuxConfigApp
|
||||
namespace LinuxConfigApp
|
||||
{
|
||||
class Program : OSConfigBase
|
||||
class Program
|
||||
{
|
||||
public override Type InteropFunctionsType => typeof(LinuxInterop);
|
||||
|
||||
static void Main() => new Program().Run();
|
||||
static void Main() { }
|
||||
}
|
||||
}
|
||||
|
||||
28
Source/LoadByOS/LinuxConfigApp/libation_glass.svg
Normal file
28
Source/LoadByOS/LinuxConfigApp/libation_glass.svg
Normal file
@@ -0,0 +1,28 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256.0px" height="256.0px" viewBox="0 0 512 512" enable-background="new 0 0 512 512">
|
||||
<path id="glass" d=
|
||||
"M139,2
|
||||
A 192,200 0 0 0 103,84
|
||||
A 222,334 41 0 0 241,320
|
||||
V478
|
||||
H160
|
||||
A 16,16 0 0 0 160,510
|
||||
H352
|
||||
A16 16 0 0 0 352,478
|
||||
H271
|
||||
V320
|
||||
A 222,334 -41 0 0 409,84
|
||||
A 192,200 0 0 0 373,2
|
||||
M355,32
|
||||
A 192,200 0 0 1 381,127
|
||||
A 187.5,334 -35 0 1 256,286
|
||||
A 187.5,334 35 0 1 131,127
|
||||
A 192,200 0 0 1 157,32
|
||||
H355
|
||||
z" />
|
||||
|
||||
<path id="wine-level" d=
|
||||
"M146,128
|
||||
A 168,300 35 0 0 256,270
|
||||
A 168,300 -35 0 0 366,128
|
||||
z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 618 B |
188
Source/LoadByOS/LinuxConfigApp/runasroot.sh
Normal file
188
Source/LoadByOS/LinuxConfigApp/runasroot.sh
Normal file
@@ -0,0 +1,188 @@
|
||||
#!/bin/sh
|
||||
# $Id: runasroot.sh 153224 2022-08-22 17:43:14Z klaus $
|
||||
## @file
|
||||
# VirtualBox privileged execution helper script for Linux and Solaris
|
||||
#
|
||||
|
||||
#
|
||||
# Copyright (C) 2009-2022 Oracle and/or its affiliates.
|
||||
#
|
||||
# This file is part of VirtualBox base platform packages, as
|
||||
# available from https://www.virtualbox.org.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation, in version 3 of the
|
||||
# License.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, see <https://www.gnu.org/licenses>.
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
#
|
||||
|
||||
# Deal with differing "which" semantics
|
||||
mywhich() {
|
||||
which "$1" 2>/dev/null | grep -v "no $1"
|
||||
}
|
||||
|
||||
# Get the name and execute switch for a useful terminal emulator
|
||||
#
|
||||
# Sets $gxtpath to the emulator path or empty
|
||||
# Sets $gxttitle to the "title" switch for that emulator
|
||||
# Sets $gxtexec to the "execute" switch for that emulator
|
||||
# May clobber $gtx*
|
||||
# Calls mywhich
|
||||
getxterm() {
|
||||
# gnome-terminal uses -e differently to other emulators
|
||||
for gxti in "konsole --title -e" "gnome-terminal --title -x" "xterm -T -e"; do
|
||||
set $gxti
|
||||
gxtpath="`mywhich $1`"
|
||||
case "$gxtpath" in ?*)
|
||||
gxttitle=$2
|
||||
gxtexec=$3
|
||||
return
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Quotes its argument by inserting '\' in front of every character save
|
||||
# for 'A-Za-z0-9/'. Prints the result to stdout.
|
||||
quotify() {
|
||||
echo "$1" | sed -e 's/\([^a-zA-Z0-9/]\)/\\\1/g'
|
||||
}
|
||||
|
||||
ostype=`uname -s`
|
||||
if test "$ostype" != "Linux" && test "$ostype" != "SunOS" ; then
|
||||
echo "Linux/Solaris not detected."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
HAS_TERMINAL=""
|
||||
case "$1" in "--has-terminal")
|
||||
shift
|
||||
HAS_TERMINAL="yes"
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$#" in "2"|"3")
|
||||
;;
|
||||
*)
|
||||
echo "Usage: `basename $0` DESCRIPTION COMMAND [ADVICE]" >&2
|
||||
echo >&2
|
||||
echo "Attempt to execute COMMAND with root privileges, displaying DESCRIPTION if" >&2
|
||||
echo "possible and displaying ADVICE if possible if no su(1)-like tool is available." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
DESCRIPTION=$1
|
||||
COMMAND=$2
|
||||
ADVICE=$3
|
||||
PATH=$PATH:/bin:/usr/bin:/usr/local/bin:/sbin:/usr/sbin:/usr/local/sbin:/usr/X11/bin
|
||||
|
||||
case "$ostype" in SunOS)
|
||||
PATH=$PATH:/usr/sfw/bin:/usr/gnu/bin:/usr/xpg4/bin:/usr/xpg6/bin:/usr/openwin/bin:/usr/ucb
|
||||
GKSU_SWITCHES="-au root"
|
||||
;;
|
||||
*)
|
||||
GKSU_SWITCHES=""
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$HAS_TERMINAL" in "")
|
||||
case "$DISPLAY" in ?*)
|
||||
KDESUDO="`mywhich kdesudo`"
|
||||
case "$KDESUDO" in ?*)
|
||||
eval "`quotify "$KDESUDO"` --comment `quotify "$DESCRIPTION"` -- $COMMAND"
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
KDESU="`mywhich kdesu`"
|
||||
case "$KDESU" in ?*)
|
||||
"$KDESU" -c "$COMMAND"
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
GKSU="`mywhich gksu`"
|
||||
case "$GKSU" in ?*)
|
||||
# Older gksu does not grok --description nor '--' and multiple args.
|
||||
# @todo which versions do?
|
||||
# "$GKSU" --description "$DESCRIPTION" -- "$@"
|
||||
# Note that $GKSU_SWITCHES is NOT quoted in the following
|
||||
"$GKSU" $GKSU_SWITCHES "$COMMAND"
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac # $DISPLAY
|
||||
;;
|
||||
esac # ! $HAS_TERMINAL
|
||||
|
||||
# pkexec may work for ssh console sessions as well if the right agents
|
||||
# are installed. However it is very generic and does not allow for any
|
||||
# custom messages. Thus it comes after gksu.
|
||||
## @todo should we insist on either a display or a terminal?
|
||||
# case "$DISPLAY$HAS_TERMINAL" in ?*)
|
||||
PKEXEC="`mywhich pkexec`"
|
||||
case "$PKEXEC" in ?*)
|
||||
eval "\"$PKEXEC\" $COMMAND"
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
# ;;S
|
||||
#esac
|
||||
|
||||
case "$HAS_TERMINAL" in ?*)
|
||||
USE_SUDO=
|
||||
grep -q Ubuntu /etc/lsb-release 2>/dev/null && USE_SUDO=true
|
||||
# On Ubuntu we need sudo instead of su. Assume this works, and is only
|
||||
# needed for Ubuntu until proven wrong.
|
||||
case $USE_SUDO in true)
|
||||
SUDO_COMMAND="`quotify "$SUDO"` -- $COMMAND"
|
||||
eval "$SUDO_COMMAND"
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
SU="`mywhich su`"
|
||||
case "$SU" in ?*)
|
||||
"$SU" - root -c "$COMMAND"
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
|
||||
# The ultimate fallback is running 'su -' within an xterm. We use the
|
||||
# title of the xterm to tell what is going on.
|
||||
case "$DISPLAY" in ?*)
|
||||
SU="`mywhich su`"
|
||||
case "$SU" in ?*)
|
||||
getxterm
|
||||
case "$gxtpath" in ?*)
|
||||
"$gxtpath" "$gxttitle" "$DESCRIPTION - su" "$gxtexec" su - root -c "$COMMAND"
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
esac
|
||||
esac # $DISPLAY
|
||||
|
||||
# Failure...
|
||||
case "$DISPLAY" in ?*)
|
||||
echo "Unable to locate 'pkexec', 'gksu' or 'su+xterm'. $ADVICE" >&2
|
||||
;;
|
||||
*)
|
||||
echo "Unable to locate 'pkexec'. $ADVICE" >&2
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 1
|
||||
35
Source/LoadByOS/MacOSConfigApp/Info.plist
Normal file
35
Source/LoadByOS/MacOSConfigApp/Info.plist
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Libation</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Libation</string>
|
||||
<key>LSArchitecturePriority</key>
|
||||
<string>ARCHITECTURE_STRING</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.15.0</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.libation.macos</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>libation.icns</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>VERSION_STRING</string>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-executable-page-protection</key>
|
||||
<true/>
|
||||
<key>com.apple.security.automation.apple-events</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -34,4 +34,13 @@
|
||||
<ProjectReference Include="..\..\AppScaffolding\AppScaffolding.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Info.plist">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="libation.icns">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,14 +1,78 @@
|
||||
using LibationFileManager;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace MacOSConfigApp
|
||||
{
|
||||
internal class MacOSInterop : IInteropFunctions
|
||||
{
|
||||
public MacOSInterop() { }
|
||||
{
|
||||
private const string AppPath = "/Applications/Libation.app";
|
||||
public MacOSInterop() { }
|
||||
public MacOSInterop(params object[] values) { }
|
||||
|
||||
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
|
||||
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
|
||||
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
|
||||
public void CopyTextToClipboard(string text) => throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
//I haven't figured out how to find the app bundle's directory from within
|
||||
//the running process, so don't update unless it's "installed" in /Applications
|
||||
public bool CanUpdate => Directory.Exists(AppPath);
|
||||
|
||||
public void InstallUpdate(string updateBundle)
|
||||
{
|
||||
Serilog.Log.Information($"Extracting update bundle to {AppPath}");
|
||||
|
||||
//tar wil overwrite existing without elevated privileges
|
||||
Process.Start("tar", $"-xf \"{updateBundle}\" -C \"/Applications\"").WaitForExit();
|
||||
|
||||
//For now, it seems like this step is unnecessary. We can overwrite and
|
||||
//run Libation without needing to re-add the exception. This is insurance.
|
||||
RunAsRoot(null, $"""
|
||||
sudo spctl --master-disable
|
||||
sudo spctl --add --label 'Libation' {AppPath}
|
||||
open {AppPath}
|
||||
sudo spctl --master-enable
|
||||
""");
|
||||
}
|
||||
|
||||
//Using osascript -e '[script]' works from the terminal, but I haven't figured
|
||||
//out the syntax for it to work from create_process, so write to stdin instead.
|
||||
public Process RunAsRoot(string _, string command)
|
||||
{
|
||||
const string osascript = "osascript";
|
||||
var fullCommand = $"do shell script \"{command}\" with administrator privileges";
|
||||
|
||||
var psi = new ProcessStartInfo()
|
||||
{
|
||||
FileName = osascript,
|
||||
UseShellExecute = false,
|
||||
Arguments = "-",
|
||||
RedirectStandardError= true,
|
||||
RedirectStandardOutput= true,
|
||||
RedirectStandardInput= true,
|
||||
};
|
||||
|
||||
Serilog.Log.Logger.Information($"running {osascript} as root: {{script}}", fullCommand);
|
||||
|
||||
var proc = Process.Start(psi);
|
||||
proc.ErrorDataReceived += Proc_ErrorDataReceived;
|
||||
proc.OutputDataReceived += Proc_OutputDataReceived;
|
||||
proc.BeginErrorReadLine();
|
||||
proc.BeginOutputReadLine();
|
||||
proc.StandardInput.WriteLine(fullCommand);
|
||||
proc.StandardInput.Close();
|
||||
|
||||
return proc;
|
||||
}
|
||||
|
||||
private void Proc_OutputDataReceived(object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
if (e.Data != null)
|
||||
Serilog.Log.Logger.Information("stderr: {data}", e.Data);
|
||||
}
|
||||
|
||||
private void Proc_ErrorDataReceived(object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
if (e.Data!= null)
|
||||
Serilog.Log.Logger.Information("stderr: {data}", e.Data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
using AppScaffolding;
|
||||
|
||||
namespace MacOSConfigApp
|
||||
namespace MacOSConfigApp
|
||||
{
|
||||
class Program : OSConfigBase
|
||||
class Program
|
||||
{
|
||||
public override Type InteropFunctionsType => typeof(MacOSInterop);
|
||||
|
||||
static void Main() => new Program().Run();
|
||||
static void Main() { }
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Source/LoadByOS/MacOSConfigApp/libation.icns
Normal file
BIN
Source/LoadByOS/MacOSConfigApp/libation.icns
Normal file
Binary file not shown.
@@ -1,18 +1,7 @@
|
||||
using AppScaffolding;
|
||||
|
||||
namespace WindowsConfigApp
|
||||
{
|
||||
class Program : OSConfigBase
|
||||
class Program
|
||||
{
|
||||
public override Type InteropFunctionsType => typeof(WinInterop);
|
||||
public override Type[] ReferencedTypes => new Type[]
|
||||
{
|
||||
typeof(Bitmap),
|
||||
typeof(Dinah.Core.WindowsDesktop.GitClient),
|
||||
typeof(Accessibility.IAccIdentity),
|
||||
typeof(Microsoft.Win32.SystemEvents)
|
||||
};
|
||||
|
||||
static void Main() => new Program().Run();
|
||||
static void Main() { }
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -35,8 +36,31 @@ namespace WindowsConfigApp
|
||||
|
||||
public void DeleteFolderIcon(string directory)
|
||||
=> new DirectoryInfo(directory)?.DeleteIcon();
|
||||
public bool CanUpdate => true;
|
||||
public void InstallUpdate(string updateBundle)
|
||||
{
|
||||
var thisExe = Environment.ProcessPath;
|
||||
var thisDir = Path.GetDirectoryName(thisExe);
|
||||
var zipExtractor = Path.Combine(Path.GetTempPath(), "ZipExtractor.exe");
|
||||
|
||||
public void CopyTextToClipboard(string text)
|
||||
=> Clipboard.SetText(text);
|
||||
File.Copy("ZipExtractor.exe", zipExtractor, overwrite: true);
|
||||
|
||||
RunAsRoot(zipExtractor, $"--input \"{updateBundle}\" --output \"{thisDir}\" --executable \"{thisExe}\"");
|
||||
}
|
||||
|
||||
public Process RunAsRoot(string exe, string args)
|
||||
{
|
||||
var psi = new ProcessStartInfo()
|
||||
{
|
||||
FileName = exe,
|
||||
UseShellExecute = true,
|
||||
Verb = "runas",
|
||||
WindowStyle = ProcessWindowStyle.Normal,
|
||||
CreateNoWindow = true,
|
||||
Arguments = args
|
||||
};
|
||||
|
||||
return Process.Start(psi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +33,17 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="7.2.1.1" />
|
||||
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="7.2.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\AppScaffolding\AppScaffolding.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="ZipExtractor.exe">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -19,7 +19,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="7.2.1.1" />
|
||||
<PackageReference Include="Dinah.Core" Version="7.2.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.10.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.10.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.2.0">
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.10.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.2.0">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user