mirror of
https://github.com/rmcrackan/Libation.git
synced 2025-12-24 06:28:02 -05:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f44c26b57 | ||
|
|
03534773ab | ||
|
|
37f223fb77 | ||
|
|
f0dc33a01e | ||
|
|
315d76e061 | ||
|
|
6e78145adc | ||
|
|
200a334f86 | ||
|
|
4dd4a1495a | ||
|
|
b3ce0e0af0 | ||
|
|
1299d91d08 | ||
|
|
ad3a767057 | ||
|
|
a59c73caf8 | ||
|
|
442a688b85 | ||
|
|
0c85ea4d11 | ||
|
|
03ed8e6b57 | ||
|
|
3eca508a26 | ||
|
|
770adf33f3 | ||
|
|
1087ffb150 | ||
|
|
f620234e7d | ||
|
|
2b6b5d082e | ||
|
|
cbbc45c3c5 | ||
|
|
28de1a6cb6 | ||
|
|
1615c6ef77 | ||
|
|
6961bd72fa | ||
|
|
68846a90e5 | ||
|
|
d60ec0702c | ||
|
|
1c55c8533a | ||
|
|
6fa69b603e | ||
|
|
3df8a97463 | ||
|
|
0bd7bd80b9 | ||
|
|
13bb4238b4 | ||
|
|
d5021e4f74 | ||
|
|
5e1458cfb4 | ||
|
|
e1d4533887 | ||
|
|
c1bd1d983b | ||
|
|
b567c38a98 | ||
|
|
348ec22465 | ||
|
|
90bb4d9176 | ||
|
|
7944154ea6 | ||
|
|
01fc7f3fb9 | ||
|
|
b70f973994 | ||
|
|
98d3f85579 | ||
|
|
bdae155af6 | ||
|
|
c8b44193ac | ||
|
|
9545b3a874 | ||
|
|
e932c9fab9 | ||
|
|
c8f4c1e751 | ||
|
|
0303db153f | ||
|
|
a7e9479eab | ||
|
|
d339dbc906 | ||
|
|
5fe6f931ad | ||
|
|
ca9fe9fc32 | ||
|
|
986dbd678f | ||
|
|
ea3716f48a | ||
|
|
426d5a87b4 | ||
|
|
c893bbe52e | ||
|
|
ad5a9874af | ||
|
|
3b70c08439 | ||
|
|
a230605ed5 | ||
|
|
d48ce39773 | ||
|
|
368e695214 | ||
|
|
9c3881c67d | ||
|
|
4c5fdf05f5 | ||
|
|
4bd491f5b9 | ||
|
|
c34b1e752e | ||
|
|
fa30c10435 | ||
|
|
cdb91ae2ca | ||
|
|
7852067b81 | ||
|
|
3708515df9 | ||
|
|
530aca4f4d | ||
|
|
cf571148bc | ||
|
|
2c2a720ba9 | ||
|
|
b577ef7187 | ||
|
|
ffbb3c3516 | ||
|
|
2a6cf38677 | ||
|
|
d8104a4d7c | ||
|
|
af85ea9219 | ||
|
|
c30e149a36 | ||
|
|
050a4867b7 | ||
|
|
2bf6f7a4f2 | ||
|
|
788a768271 | ||
|
|
022a6e979d | ||
|
|
9fc5a7d834 |
2
.github/workflows/build-linux.yml
vendored
2
.github/workflows/build-linux.yml
vendored
@@ -75,7 +75,7 @@ jobs:
|
||||
artifact=$(ls ./bundle)
|
||||
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ steps.bundle.outputs.artifact }}
|
||||
path: ./bundle/${{ steps.bundle.outputs.artifact }}
|
||||
|
||||
4
.github/workflows/build-mac.yml
vendored
4
.github/workflows/build-mac.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
RUNTIME_ID: "osx-${{ inputs.architecture }}"
|
||||
WAIT_FOR_NOTARIZE: ${{ vars.WAIT_FOR_NOTARIZE == 'true' }}
|
||||
steps:
|
||||
- uses: apple-actions/import-codesign-certs@v5
|
||||
- uses: apple-actions/import-codesign-certs@v6
|
||||
if: ${{ inputs.sign-app }}
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.DISTRIBUTION_SIGNING_CERT }}
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
xcrun stapler staple "./bundle/${{ steps.bundle.outputs.artifact }}"
|
||||
fi
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ steps.bundle.outputs.artifact }}
|
||||
path: ./bundle/${{ steps.bundle.outputs.artifact }}
|
||||
|
||||
15
.github/workflows/build-windows.yml
vendored
15
.github/workflows/build-windows.yml
vendored
@@ -21,16 +21,21 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Windows-${{ matrix.release_name }}-x64"
|
||||
name: "Windows-${{ matrix.release_name }}-${{ matrix.architecture }} (${{ matrix.ui }})"
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
ui: [Avalonia]
|
||||
architecture: [x64]
|
||||
release_name: [chardonnay]
|
||||
include:
|
||||
- ui: WinForms
|
||||
- architecture: x64
|
||||
ui: WinForms
|
||||
release_name: classic
|
||||
prefix: Classic-
|
||||
- architecture: arm64
|
||||
ui: Avalonia
|
||||
release_name: chardonnay
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
@@ -48,7 +53,7 @@ jobs:
|
||||
working-directory: ./Source
|
||||
run: |
|
||||
$PUBLISH_ARGS=@(
|
||||
"--runtime", "win-x64",
|
||||
"--runtime", "win-${{ matrix.architecture }}",
|
||||
"--configuration", "Release",
|
||||
"--output", "../bin",
|
||||
"-p:PublishProtocol=FileSystem",
|
||||
@@ -70,11 +75,11 @@ jobs:
|
||||
"WindowsConfigApp.deps.json")
|
||||
|
||||
foreach ($file in $delfiles){ if (test-path $file){ Remove-Item $file } }
|
||||
$artifact="${{ matrix.prefix }}Libation.${{ inputs.libation-version }}-windows-${{ matrix.release_name }}-x64.zip"
|
||||
$artifact="${{ matrix.prefix }}Libation.${{ inputs.libation-version }}-windows-${{ matrix.release_name }}-${{ matrix.architecture }}.zip"
|
||||
"artifact=$artifact" >> $env:GITHUB_OUTPUT
|
||||
Compress-Archive -Path * -DestinationPath "$artifact"
|
||||
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ steps.zip.outputs.artifact }}
|
||||
path: ./bin/${{ steps.zip.outputs.artifact }}
|
||||
|
||||
67
.github/workflows/deploy.yml
vendored
Normal file
67
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Deploy VitePress site to Pages
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the `main` branch. Change this to `master` if you're
|
||||
# using the `master` branch as the default branch.
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- .github/workflows/deploy.yml
|
||||
- docs/**
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Build job
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0 # Not needed if lastUpdated is not enabled
|
||||
# - uses: pnpm/action-setup@v4 # Uncomment this block if you're using pnpm
|
||||
# with:
|
||||
# version: 9 # Not needed if you've set "packageManager" in package.json
|
||||
# - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm # or pnpm / yarn
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Install dependencies
|
||||
run: npm ci # or pnpm install / yarn install / bun install
|
||||
- name: Build with VitePress
|
||||
run: npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: .vitepress/dist
|
||||
|
||||
# Deployment job
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: artifacts
|
||||
pattern: "*(Classic-)Libation.*"
|
||||
|
||||
6
.github/workflows/validate.yml
vendored
6
.github/workflows/validate.yml
vendored
@@ -6,8 +6,14 @@ name: validate
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- Source/**
|
||||
- .github/workflows/**
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- Source/**
|
||||
- .github/workflows/**
|
||||
|
||||
jobs:
|
||||
get_version:
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -376,4 +376,9 @@ FodyWeavers.xsd
|
||||
.DS_Store
|
||||
|
||||
# JetBrains Rider Settings
|
||||
**/.idea/
|
||||
**/.idea/
|
||||
|
||||
# VitePress
|
||||
node_modules
|
||||
.vitepress/cache
|
||||
.vitepress/dist
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"WindowsClassic": "Classic-Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-win(?:dows)?-classic-x64\\.zip",
|
||||
"WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-win(?:dows)?-chardonnay-x64\\.zip",
|
||||
"WindowsAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-win(?:dows)?-chardonnay-arm64\\.zip",
|
||||
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-amd64\\.deb",
|
||||
"LinuxAvalonia_RPM": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-amd64\\.rpm",
|
||||
"MacOSAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-macOS-chardonnay-x64\\.dmg",
|
||||
|
||||
90
.vitepress/config.js
Normal file
90
.vitepress/config.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import { defineConfig } from "vitepress";
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title: "Libation",
|
||||
description: "Libation: Liberate your Library - A free application for downloading your Audible audiobooks",
|
||||
head: [["link", { rel: "icon", href: "/favicon.ico" }]],
|
||||
cleanUrls: true,
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
logo: {
|
||||
light: "/libation_logo_light.svg",
|
||||
dark: "/libation_logo_dark.svg",
|
||||
},
|
||||
|
||||
footer: {
|
||||
message: "Released under the GPLv3 License",
|
||||
},
|
||||
|
||||
editLink: {
|
||||
pattern: "https://github.com/rmcrackan/Libation/edit/main/:path",
|
||||
},
|
||||
|
||||
lastUpdated: true,
|
||||
|
||||
nav: [
|
||||
{ text: "Getting Started", link: "/docs/getting-started" },
|
||||
{ text: "Docs", link: "/docs/index" },
|
||||
{ text: "Download", link: "https://github.com/rmcrackan/Libation/releases/latest" },
|
||||
{ text: "Issues & Requests", link: "https://github.com/rmcrackan/Libation/issues" },
|
||||
{ text: "Donate", link: "https://www.paypal.com/paypalme/mcrackan" },
|
||||
],
|
||||
sidebar: [
|
||||
{
|
||||
items: [
|
||||
{ text: "Overview", link: "/docs/index"},
|
||||
{ text: "Getting Started", link: "/docs/getting-started" },
|
||||
{ text: "FAQ", link: "/docs/frequently-asked-questions" },
|
||||
{
|
||||
text: "Issues & Requests",
|
||||
link: "https://github.com/rmcrackan/Libation/issues",
|
||||
},
|
||||
{ text: "Donate", link: "https://www.paypal.com/paypalme/mcrackan" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Installation",
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: "Linux", link: "/docs/installation/linux" },
|
||||
{ text: "Mac", link: "/docs/installation/mac" },
|
||||
{ text: "Docker", link: "/docs/installation/docker" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Features",
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: "Audio File Formats", link: "/docs/features/audio-file-formats" },
|
||||
{ text: "Naming Templates", link: "/docs/features/naming-templates" },
|
||||
{
|
||||
text: "Searching & Filtering",
|
||||
link: "/docs/features/searching-and-filtering",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Advanced",
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: "Advanced Topics", link: "/docs/advanced/advanced" },
|
||||
{
|
||||
text: "Linux Development Setup",
|
||||
link: "/docs/advanced/linux-development-setup-using-nix",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
outline: {
|
||||
level: "deep",
|
||||
},
|
||||
|
||||
socialLinks: [{ icon: "github", link: "https://github.com/rmcrackan/Libation" }],
|
||||
|
||||
search: {
|
||||
provider: "local",
|
||||
},
|
||||
},
|
||||
});
|
||||
15
.vitepress/theme/custom.css
Normal file
15
.vitepress/theme/custom.css
Normal file
@@ -0,0 +1,15 @@
|
||||
/* Custom styles for Libation documentation */
|
||||
|
||||
/* Hide certain nav items on tablet devices to prevent horizontal scroll */
|
||||
@media (min-width: 640px) and (max-width: 959px) {
|
||||
/* Target specific nav items by their position */
|
||||
/* Hide "Issues & Requests" and "Donate" links on tablet */
|
||||
.VPNav .VPNavBar .nav .VPNavBarMenu .VPMenu:nth-child(3) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Alternative: Use a more specific selector if needed */
|
||||
.VPNavBarMenuLink[href*="issues"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
4
.vitepress/theme/index.js
Normal file
4
.vitepress/theme/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import './custom.css'
|
||||
|
||||
export default DefaultTheme
|
||||
5
Directory.Build.props
Normal file
5
Directory.Build.props
Normal file
@@ -0,0 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<EnableMSTestRunner>true</EnableMSTestRunner>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -1,56 +0,0 @@
|
||||
## [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 [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**.
|
||||
|
||||
|
||||
|
||||
# Frequently Asked Questions
|
||||
|
||||
## Q: Where can I get help for my specific problem?
|
||||
|
||||
**A:** [You can open an issue here](https://github.com/rmcrackan/Libation/issues) for bug reports, feature requests, or specialized help.
|
||||
|
||||
## Q: What's the difference between 'Classic' and 'Chardonnay'?
|
||||
|
||||
**A:** First and most importantly: Classic and Chardonnay have the exact same features.
|
||||
|
||||
* **Classic** is Windows only. Its older 'grey boxes' look has a compact design which allows for more information on the screen. Notably, Classic was written using an older, more mature technology which has built-in support for screenreaders.
|
||||
|
||||
* **Chardonnay** is available for Windows, Mac, and Linux. Its modern design has a more open look and feel.
|
||||
|
||||
## Q: Now that I've downloaded my books, how can I listen to them?
|
||||
|
||||
**A:** You can use any app which plays m4b files (or mp3 files if you used that setting). Here are just a few ideas. Disclaimer: I have no affiliation with any of these companies:
|
||||
|
||||
* iOS: [BookPlayer](https://apps.apple.com/us/app/bookplayer/id1138219998)
|
||||
* iOS: [Bound](https://apps.apple.com/us/app/bound-audiobook-player/id1041727137)
|
||||
* Android: [Smart AudioBook Player](https://play.google.com/store/apps/details?id=ak.alizandro.smartaudiobookplayer&hl=en_US&gl=US)
|
||||
* Android: [Listen](https://play.google.com/store/apps/details?id=ru.litres.android.audio&hl=en_US&gl=US)
|
||||
* Desktop: [VLC](https://www.videolan.org/)
|
||||
* Windows Desktop: [Audibly](https://github.com/rstewa/Audibly) -- a desktop player build specifically for audiobooks
|
||||
|
||||
Self-hosting online:
|
||||
|
||||
* [audiobookshelf](https://www.audiobookshelf.org). On [reddit](https://www.reddit.com/r/audiobookshelf/)
|
||||
* [plex](https://www.plex.tv/). Listen with [Prologue](https://prologue.audio/) (iOS)
|
||||
|
||||
## Q: I'm having trouble playing my non-spatial audiobook, how can I fix this?
|
||||
|
||||
**A:** If you enabled the [Request xHE-AAC Codec](AudioFileFormats.md#request-xhe-aac-codec) option in settings, then the audiobook is being downloaded in the [xHE-AAC codec](AudioFileFormats.md#xhe-aac) which isn't widely supported. You have two options:
|
||||
1. Use a media player which supports the xHE-AAC codec. [See an incomplete list of media players which support xHE-AAC](AudioFileFormats.md#supported-media-players).
|
||||
2. Disable the [Request xHE-AAC Codec](AudioFileFormats.md#request-xhe-aac-codec) option in settings and re-download the audiobook. This will cause Libation to download audiobooks in the [AAC-LC codec](AudioFileFormats.md#aac-lc), which enjoys near-universal media player support.
|
||||
|
||||
## Q: I'm having trouble playing my book with 4D, spatial audio, or Dolby Atmos, how can I fix this?
|
||||
|
||||
**A:** Spatial audiobooks are delivered in two formats: [E-AC-3](AudioFileFormats.md#e-ac-3) and [AC-4](AudioFileFormats.md#ac-4). [See an incomplete list of media players which support those codecs](AudioFileFormats.md#supported-media-players).
|
||||
|
||||
## Q: I'm having trouble loggin into my Brazil account.
|
||||
|
||||
**A:** For reasons known only to Jeff Bezos and God, amazon and audible brazil handle logins slightly differently. The external browser login option is not possible for Brazil. [See this ticket for more details.](https://github.com/rmcrackan/Libation/issues/1103)
|
||||
|
||||
## Q: How do I use Libation with a South Africa account?
|
||||
|
||||
**A:** Like many countries, amazon gives South Africa it's own amazon site. [Unlike many other regions](https://www.audible.com/ep/country-selector) there is not South Africa specific audible site. Use `US` for your region -- ie: audible.com.
|
||||
|
||||
(Not exactly a *frequently* asked question but it's come up more than once)
|
||||
@@ -1,67 +0,0 @@
|
||||
## [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 [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**.
|
||||
|
||||
|
||||
## Packaging status
|
||||
|
||||
[](https://repology.org/project/libation/versions)
|
||||
|
||||
New Libation releases are automatically packed into `.deb` and `.rpm` package and are available from the [Libation repository's releases page](https://github.com/rmcrackan/Libation/releases).
|
||||
|
||||
Run these commands in your terminal to download and install Libation. **Make sure you replace** `X.X.X` with the latest Libation version and `ARCH` with your CPU's architechture (either `amd64` or `arm64`).
|
||||
|
||||
### Debian
|
||||
```Console
|
||||
wget -O libation.deb https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay-ARCH.deb
|
||||
sudo apt install ./libation.deb
|
||||
```
|
||||
### Redhat and CentOS
|
||||
```Console
|
||||
wget -O libation.rpm https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay-ARCH.rpm
|
||||
sudo yum install ./libation.rpm
|
||||
```
|
||||
### Fedora
|
||||
```Console
|
||||
wget -O libation.rpm https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay-ARCH.rpm
|
||||
sudo dnf5 install ./libation.rpm
|
||||
```
|
||||
---
|
||||
### Arch Linux
|
||||
```Console
|
||||
yay -S libation
|
||||
```
|
||||
This package is available on [Arch User Repository](https://aur.archlinux.org/packages/libation), install via your choice of [AUR helpers](https://wiki.archlinux.org/title/AUR_helpers).
|
||||
|
||||
Thanks to [mhdi](https://aur.archlinux.org/account/mhdi) for taking care of AUR package maintenance.
|
||||
### NixOS
|
||||
- Install via `nix-shell`
|
||||
```Console
|
||||
nix-shell -p libation
|
||||
```
|
||||
A `nix-shell` will temporarily modify your $PATH environment variable. This can be used to try a piece of software before deciding to permanently install it.
|
||||
- Install via NixOS configuration
|
||||
```Console
|
||||
environment.systemPackages = [
|
||||
pkgs.libation
|
||||
];
|
||||
```
|
||||
Add the following Nix code to your NixOS Configuration, usually located in `/etc/nixos/configuration.nix`
|
||||
- On NixOS via via `nix-env`
|
||||
```Console
|
||||
nix-env -iA nixos.libation
|
||||
```
|
||||
- On Non NixOS via `nix-env`
|
||||
```Console
|
||||
nix-env -iA nixpkgs.libation
|
||||
```
|
||||
Warning: Using `nix-env` permanently modifies a local profile of installed packages. This must be updated and maintained by the user in the same way as with a traditional package manager.
|
||||
|
||||
Thanks to [TomaSajt](https://github.com/tomasajt) for taking care of Nix package maintenance.
|
||||
|
||||
If your desktop uses gtk, you should now see Libation among your applications.
|
||||
|
||||
Additionally, you may launch Libation, LibationCli, and Hangover (the Libation recovery app) via the command line using 'libation, libationcli', and 'hangover' aliases respectively.
|
||||
|
||||
Report bugs to https://github.com/rmcrackan/Libation/issues
|
||||
@@ -1,82 +0,0 @@
|
||||
## [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 [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.
|
||||
|
||||
## Supports macOS 13 (Ventura) and above
|
||||
|
||||
## Install Libation
|
||||
|
||||
- Download the file from the latest release and extract it.
|
||||
- Apple Silicon (M1, M2, ...): `Libation.x.x.x-macOS-chardonnay-`**arm64**`.tgz`
|
||||
- Intel: `Libation.x.x.x-macOS-chardonnay-`**x64**`.tgz`
|
||||
- Move the extracted Libation app bundle to your applications folder.
|
||||
- Right-click on Libation and then click on open
|
||||
- The first time, it will not immediately show you an option to open it. Just dismiss the dialog and do the same thing again (right-click -> open) then you will get an option to run the unsigned application. This takes about 10 seconds.
|
||||
|
||||
## If this doesn't work
|
||||
|
||||
You can add Libation as a safe app without touching Gatekeeper.
|
||||
|
||||
- Copy/paste/run the following command. Adjust the file path to the Libation.app on your computer if necessary.
|
||||
|
||||
```Console
|
||||
xattr -r -d com.apple.quarantine ~/Downloads/Libation.app
|
||||
```
|
||||
- Close the terminal and use Libation!
|
||||
|
||||
## If this still doesn't work
|
||||
|
||||
- Copy/paste/run the following command (you'll be prompted to enter your Mac 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!
|
||||
|
||||
## "Apple can't check app for malicious software"
|
||||
|
||||
From: [How to Open Anyway](https://support.apple.com/guide/mac-help/apple-cant-check-app-for-malicious-software-mchleab3a043/mac):
|
||||
|
||||
* On your Mac, choose Apple menu > System Settings, then click Privacy & Security in the sidebar. (You may need to scroll down.)
|
||||
* Go to Security, then click Open.
|
||||
* Click Open Anyway. This button is available for about an hour after you try to open the app.
|
||||
* Enter your login password, then click OK.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If Libation fails to start after completing the above steps, try the following:
|
||||
|
||||
1. Right-click the Libation app in your applications folder and select _Show Package Contents_
|
||||
2. Open the `Contents` folder and then the `MacOS` folder.
|
||||
3. Find the file named `Libation`, right-click it, and then select _Open_.
|
||||
|
||||
Libation _should_ launch, and you should now be able to open Libation by just double-clicking the app bundle in your applications folder.
|
||||
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## Running LibationCli
|
||||
|
||||
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`
|
||||
|
||||
Then use `./LibationCli` to execute a command.
|
||||
|
||||
## Get Libation running on Mac
|
||||
|
||||
[Run Libation on MacOS](https://user-images.githubusercontent.com/37587114/219271379-a922e4e1-48a0-48e4-bd81-48aa1226a4f5.mp4)
|
||||
@@ -1,178 +0,0 @@
|
||||
## [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 [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**.
|
||||
|
||||
|
||||
|
||||
# Naming Templates
|
||||
File and Folder names can be customized using Libation's built-in tag template naming engine. To edit how folder and file names are created, go to Settings \> Download/Decrypt and edit the naming templates. If you're splitting your audiobook into multiple files by chapter, you can also use a custom template to set each chapter's title metadata tag by editing the template in Settings \> Audio File Options.
|
||||
|
||||
These templates apply to both GUI and CLI.
|
||||
|
||||
# Table of Contents
|
||||
|
||||
- [Template Tags](#template-tags)
|
||||
- [Property Tags](#property-tags)
|
||||
- [Conditional Tags](#conditional-tags)
|
||||
- [Tag Formatters](#tag-formatters)
|
||||
- [Text Formatters](#text-formatters)
|
||||
- [Series Formatters](#series-formatters)
|
||||
- [Series List Formatters](#series-list-formatters)
|
||||
- [Name Formatters](#name-formatters)
|
||||
- [Name List Formatters](#name-list-formatters)
|
||||
- [Number Formatters](#number-formatters)
|
||||
- [Date Formatters](#date-formatters)
|
||||
|
||||
|
||||
# Template Tags
|
||||
|
||||
These are the naming template tags currently supported by Libation.
|
||||
|
||||
## Property Tags
|
||||
These tags will be replaced in the template with the audiobook's values.
|
||||
|
||||
|Tag|Description|Type|
|
||||
|-|-|-|
|
||||
|\<id\> **†**|Audible book ID (ASIN)|Text|
|
||||
|\<title\>|Full title with subtitle|[Text](#text-formatters)|
|
||||
|\<title short\>|Title. Stop at first colon|[Text](#text-formatters)|
|
||||
|\<audible title\>|Audible's title (does not include subtitle)|[Text](#text-formatters)|
|
||||
|\<audible subtitle\>|Audible's subtitle|[Text](#text-formatters)|
|
||||
|\<author\>|Author(s)|[Name List](#name-list-formatters)|
|
||||
|\<first author\>|First author|[Name](#name-formatters)|
|
||||
|\<narrator\>|Narrator(s)|[Name List](#name-list-formatters)|
|
||||
|\<first narrator\>|First narrator|[Name](#name-formatters)|
|
||||
|\<series\>|All series to which the book belongs (if any)|[Series List](#series-list-formatters)|
|
||||
|\<first series\>|First series|[Series](#series-formatters)|
|
||||
|\<series#\>|Number order in series (alias for \<first series[{#}]\>|[Number](#number-formatters)|
|
||||
|\<bitrate\>|Bitrate (kbps) of the last downloaded audiobook|[Number](#number-formatters)|
|
||||
|\<samplerate\>|Sample rate (Hz) of the last downloaded audiobook|[Number](#number-formatters)|
|
||||
|\<channels\>|Number of audio channels in the last downloaded audiobook|[Number](#number-formatters)|
|
||||
|\<codec\>|Audio codec of the last downloaded audiobook|[Text](#text-formatters)|
|
||||
|\<file version\>|Audible's file version number of the last downloaded audiobook|[Text](#text-formatters)|
|
||||
|\<libation version\>|Libation version used during last download of the audiobook|[Text](#text-formatters)|
|
||||
|\<account\>|Audible account of this book|[Text](#text-formatters)|
|
||||
|\<account nickname\>|Audible account nickname of this book|[Text](#text-formatters)|
|
||||
|\<locale\>|Region/country|[Text](#text-formatters)|
|
||||
|\<year\>|Year published|[Number](#number-formatters)|
|
||||
|\<language\>|Book's language|[Text](#text-formatters)|
|
||||
|\<language short\> **†**|Book's language abbreviated. Eg: ENG|Text|
|
||||
|\<file date\>|File creation date/time.|[DateTime](#date-formatters)|
|
||||
|\<pub date\>|Audiobook publication date|[DateTime](#date-formatters)|
|
||||
|\<date added\>|Date the book added to your Audible account|[DateTime](#date-formatters)|
|
||||
|\<ch count\> **‡**|Number of chapters|[Number](#number-formatters)|
|
||||
|\<ch title\> **‡**|Chapter title|[Text](#text-formatters)|
|
||||
|\<ch#\> **‡**|Chapter number|[Number](#number-formatters)|
|
||||
|\<ch# 0\> **‡**|Chapter number with leading zeros|[Number](#number-formatters)|
|
||||
|
||||
**†** Does not support custom formatting
|
||||
|
||||
**‡** Only valid for Chapter Filename and Chapter Tile Metadata
|
||||
|
||||
To change how these properties are displayed, [read about custom formatters](#tag-formatters)
|
||||
|
||||
## Conditional Tags
|
||||
Anything between the opening tag (`<tagname->`) and closing tag (`<-tagname>`) will only appear in the name if the condition evaluates to true.
|
||||
|
||||
|Tag|Description|Type|
|
||||
|-|-|-|
|
||||
|\<if series-\>...\<-if series\>|Only include if part of a book series or podcast|Conditional|
|
||||
|\<if podcast-\>...\<-if podcast\>|Only include if part of a podcast|Conditional|
|
||||
|\<if bookseries-\>...\<-if bookseries\>|Only include if part of a book series|Conditional|
|
||||
|\<if podcastparent-\>...\<-if podcastparent\>**†**|Only include if item is a podcast series parent|Conditional|
|
||||
|\<has PROPERTY-\>...\<-has\>|Only include if the PROPERTY has a value (i.e. not null or empty)|Conditional|
|
||||
|
||||
**†** Only affects the podcast series folder naming if "Save all podcast episodes to the series parent folder" option is checked.
|
||||
|
||||
For example, `<if podcast-><series><-if podcast>` will evaluate to the podcast's series name if the file is a podcast. For audiobooks that are not podcasts, that tag will be blank.
|
||||
|
||||
You can invert the condition (instead of displaying the text when the condition is true, display the text when it is false) by playing a `!` symbol before the opening tag name.
|
||||
|
||||
|Inverted Tag|Description|Type|
|
||||
|-|-|-|
|
||||
|\<!if series-\>...\<-if series\>|Only include if *not* part of a book series or podcast|Conditional|
|
||||
|\<!if podcast-\>...\<-if podcast\>|Only include if *not* part of a podcast|Conditional|
|
||||
|\<!if bookseries-\>...\<-if bookseries\>|Only include if *not* part of a book series|Conditional|
|
||||
|\<!if podcastparent-\>...\<-if podcastparent\>**†**|Only include if item is *not* a podcast series parent|Conditional|
|
||||
|\<!has PROPERTY-\>...\<-has\>|Only include if the PROPERTY *does not* have a value (i.e. is null or empty)|Conditional|
|
||||
|
||||
**†** Only affects the podcast series folder naming if "Save all podcast episodes to the series parent folder" option is checked.
|
||||
|
||||
As an example, this folder template will place all Liberated podcasts into a "Podcasts" folder and all liberated books (not podcasts) into a "Books" folder.
|
||||
|
||||
`<if podcast->Podcasts<-if podcast><!if podcast->Books<-if podcast>\<title>`
|
||||
|
||||
This example will add a number if the `<series#\>` tag has a value:
|
||||
|
||||
`<has series#><series#><-has>`
|
||||
|
||||
This example will put non-series books in a "Standalones" folder:
|
||||
|
||||
`<!if series->Standalones/<-if series>`
|
||||
|
||||
And this example will customize the title based on whether the book has a subtitle:
|
||||
|
||||
`<audible title><has audible subtitle->-<audible subtitle><-has>`
|
||||
|
||||
# Tag Formatters
|
||||
**Text**, **Name List**, **Number**, and **DateTime** tags can be optionally formatted using format text in square brackets after the tag name. Below is a list of supported formatters for each tag type.
|
||||
|
||||
## Text Formatters
|
||||
|Formatter|Description|Example Usage|Example Result|
|
||||
|-|-|-|-|
|
||||
|L|Converts text to lowercase|\<title[L]\>|a study in scarlet꞉ a sherlock holmes novel|
|
||||
|U|Converts text to uppercase|\<title short[U]\>|A STUDY IN SCARLET|
|
||||
|
||||
## Series Formatters
|
||||
|Formatter|Description|Example Usage|Example Result|
|
||||
|-|-|-|-|
|
||||
|\{N \| # \| ID\}|Formats the series using<br>the series part tags.<br>\{N\} = Series Name<br>\{#\} = Number order in series<br>\{#:[Number_Formatter](#number-formatters)\} = Number order in series, formatted<br>\{ID\} = Audible Series ID<br><br>Default is \{N\}|`<first series>`<hr>`<first series[{N}]>`<hr>`<first series[{N}, {#}, {ID}]>`<hr>`<first series[{N}, {ID}, {#:00.0}]>`|Sherlock Holmes<hr>Sherlock Holmes<hr>Sherlock Holmes, 1-6, B08376S3R2<hr>Sherlock Holmes, B08376S3R2, 01.0-06.0|
|
||||
|
||||
## Series List Formatters
|
||||
|Formatter|Description|Example Usage|Example Result|
|
||||
|-|-|-|-|
|
||||
|separator()|Speficy the text used to join<br>multiple series names.<br><br>Default is ", "|`<series[separator(; )]>`|Sherlock Holmes; Some Other Series|
|
||||
|format(\{N \| # \| ID\})|Formats the series properties<br>using the name series tags.<br>See [Series Formatter Usage](#series-formatters) above.|`<series[format({N}, {#})`<br>`separator(; )]>`<hr>`<series[format({ID}-{N}, {#:00.0})]>`|Sherlock Holmes, 1-6; Book Collection, 1<hr>B08376S3R2-Sherlock Holmes, 01.0-06.0, B000000000-Book Collection, 01.0|
|
||||
|max(#)|Only use the first # of series<br><br>Default is all series|`<series[max(1)]>`|Sherlock Holmes|
|
||||
|
||||
## Name Formatters
|
||||
|Formatter|Description|Example Usage|Example Result|
|
||||
|-|-|-|-|
|
||||
|\{T \| F \| M \| L \| S \| ID\}|Formats the human name using<br>the name part tags.<br>\{T\} = Title (e.g. "Dr.")<br>\{F\} = First name<br>\{M\} = Middle name<br>\{L\} = Last Name<br>\{S\} = Suffix (e.g. "PhD")<br>\{ID\} = Audible Contributor ID<br><br>Default is \{P\} \{F\} \{M\} \{L\} \{S\}|`<first narrator[{L}, {F}]>`<hr>`<first author[{L}, {F} _{ID}_]>`|Fry, Stephen<hr>Doyle, Arthur \_B000AQ43GQ\_;<br>Fry, Stephen \_B000APAGVS\_|
|
||||
|
||||
## Name List Formatters
|
||||
|Formatter|Description|Example Usage|Example Result|
|
||||
|-|-|-|-|
|
||||
|separator()|Speficy the text used to join<br>multiple people's names.<br><br>Default is ", "|`<author[separator(; )]>`|Arthur Conan Doyle; Stephen Fry|
|
||||
|format(\{T \| F \| M \| L \| S \| ID\})|Formats the human name using<br>the name part tags.<br>See [Name Formatter Usage](#name-formatters) above.|`<author[format({L}, {F})`<br>`separator(; )]>`<hr>`<author[format({L}, {F}`<br>`_{ID}_) separator(; )]>`|Doyle, Arthur; Fry, Stephen<hr>Doyle, Arthur \_B000AQ43GQ\_;<br>Fry, Stephen \_B000APAGVS\_|
|
||||
|sort(F \| M \| L)|Sorts the names by first, middle,<br>or last name<br><br>Default is unsorted|`<author[sort(M)]>`|Stephen Fry, Arthur Conan Doyle|
|
||||
|max(#)|Only use the first # of names<br><br>Default is all names|`<author[max(1)]>`|Arthur Conan Doyle|
|
||||
|
||||
## Number Formatters
|
||||
For more custom formatters and examples, [see this guide from Microsoft](https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-numeric-format-strings).
|
||||
|Formatter|Description|Example Usage|Example Result|
|
||||
|-|-|-|-|
|
||||
|\[integer\]|Zero-pads the number|\<bitrate\[4\]\><br>\<series#\[3\]\><br>\<samplerate\[6\]\>|0128<br>001<br>044100|
|
||||
|0|Replaces the zero with the corresponding digit if one<br>is present; otherwise, zero appears in the result string.|\<series#\[000.0\]\>|001.0|
|
||||
|#|Replaces the "#" symbol with the corresponding digit if one<br> is present; otherwise, no digit appears in the result string|\<series#\[00.##\]\>|01|
|
||||
|
||||
## Date Formatters
|
||||
Form more standard formatters, [see this guide from Microsoft](https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings).
|
||||
### Standard DateTime Formatters
|
||||
|Formatter|Description|Example Usage|Example Result|
|
||||
|-|-|-|-|
|
||||
|s|Sortable date/time pattern.|\<file date[s]\>|2023-02-14T13:45:30|
|
||||
|Y|Year month pattern.|\<file date[Y]\>|February 2023|
|
||||
|
||||
### Custom DateTime Formatters
|
||||
You can use custom formatters to construct customized DateTime string. For more custom formatters and examples, [see this guide from Microsoft](https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings).
|
||||
|Formatter|Description|Example Usage|Example Result|
|
||||
|-|-|-|-|
|
||||
|yyyy|4-digit year|\<file date[yyyy]\>|2023|
|
||||
|yy|2-digit year|\<file date[yy]\>|23|
|
||||
|MM|2-digit month|\<file date[MM]\>|02|
|
||||
|dd|2-digit day of the month|\<file date[yyyy-MM-dd]\>|2023-02-14|
|
||||
|HH<br>mm|The hour, using a 24-hour clock from 00 to 23<br>The minute, from 00 through 59.|\<file date[HH:mm]\>|14:45|
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
77
README.md
77
README.md
@@ -3,73 +3,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 [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**.
|
||||
|
||||
## Getting started with Libation
|
||||
|
||||
All documentation has been moved to our new site: [getlibation.com](https://getlibation.com). Or jump to the important bits:
|
||||
|
||||
# Table of Contents
|
||||
|
||||
- [Audible audiobook manager](#audible-audiobook-manager)
|
||||
- [The good](#the-good)
|
||||
- [The bad](#the-bad)
|
||||
- [The ugly](#the-ugly)
|
||||
- [Getting started](Documentation/GettingStarted.md)
|
||||
- [Download Libation](Documentation/GettingStarted.md#download-libation-1)
|
||||
- [Installation](Documentation/GettingStarted.md#installation)
|
||||
- [Create Accounts](Documentation/GettingStarted.md#create-accounts)
|
||||
- [Import your library](Documentation/GettingStarted.md#import-your-library)
|
||||
- [Download your books -- DRM-free!](Documentation/GettingStarted.md#download-your-books----drm-free)
|
||||
- [Download PDF attachments](Documentation/GettingStarted.md#download-pdf-attachments)
|
||||
- [Details of downloaded files](Documentation/GettingStarted.md#details-of-downloaded-files)
|
||||
- [Export your library](Documentation/GettingStarted.md#export-your-library)
|
||||
- If you still need help, [you can open an issue here](https://github.com/rmcrackan/Libation/issues) for bug reports, feature requests, or specialized help.
|
||||
- [Searching and filtering](Documentation/SearchingAndFiltering.md)
|
||||
- [Tags](Documentation/SearchingAndFiltering.md#tags)
|
||||
- [Searches](Documentation/SearchingAndFiltering.md#searches)
|
||||
- [Search examples](Documentation/SearchingAndFiltering.md#search-examples)
|
||||
- [Filters](Documentation/SearchingAndFiltering.md#filters)
|
||||
- [Advanced](Documentation/Advanced.md)
|
||||
- [Files and folders](Documentation/Advanced.md#files-and-folders)
|
||||
- [Settings](Documentation/Advanced.md#settings)
|
||||
- [Custom File Naming](Documentation/NamingTemplates.md)
|
||||
- [Command Line Interface](Documentation/Advanced.md#command-line-interface)
|
||||
- [Custom Theme Colors](Documentation/Advanced.md#custom-theme-colors) (Chardonnay Only)
|
||||
- [Audio Formats (Dolby Atmos, Widevine, Spacial Audio)](Documentation/AudioFileFormats.md)
|
||||
- [Docker](Documentation/Docker.md)
|
||||
- [Frequently Asked Questions](Documentation/FrequentlyAskedQuestions.md)
|
||||
|
||||
## Getting started
|
||||
|
||||
* [Getting Started](https://getlibation.com/docs/getting-started)
|
||||
* [Download](https://github.com/rmcrackan/Libation/releases/latest)
|
||||
* [Step-by-step walk-through](Documentation/GettingStarted.md)
|
||||
* [Issues, bugs, and requests](https://github.com/rmcrackan/Libation/issues)
|
||||
* [Documentation](https://getlibation.com/docs/index)
|
||||
|
||||
## Audible audiobook manager
|
||||
## Development
|
||||
|
||||
### The good
|
||||
### Documentation
|
||||
|
||||
* Import library from audible, including cover art
|
||||
* Download and remove DRM from all books
|
||||
* Download accompanying PDFs
|
||||
* Add tags to books for better organization
|
||||
* Powerful advanced search built on the Lucene search engine
|
||||
* Customizable saved filters for common searches
|
||||
* Open source
|
||||
* Supports most regions: US, UK, Canada, Germany, France, Australia, Japan, India, and Spain
|
||||
* Fully supported in Windows, Mac, and Linux
|
||||
The documentation is built with [VitePress](https://vitepress.dev/) and located in the `docs` directory. For more information like [markdown syntax](https://vitepress.dev/guide/markdown#advanced-configuration) and [routing](https://vitepress.dev/guide/routing) or other features, refer [VitePress documentation](https://vitepress.dev/guide).
|
||||
|
||||
<a name="theBad"/>
|
||||
**Prerequisites**: Node.js 18+
|
||||
|
||||
### The bad
|
||||
**Commands**:
|
||||
|
||||
* Large file size
|
||||
* Made by a programmer, not a designer so the goals are function rather than beauty. And it shows
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
### The ugly
|
||||
# Start local dev server (http://localhost:5173)
|
||||
npm run docs:dev
|
||||
|
||||
* Documentation? Yer lookin' at it
|
||||
* This is a single-developer personal passion project. Support, response, updates, enhancements, bug fixes etc are as my free time allows
|
||||
* I have a full-time job, a life, and a finite attention span. Therefore a lot of time can potentially go by with no improvements of any kind
|
||||
# Build for production (output: docs/.vitepress/dist)
|
||||
npm run docs:build
|
||||
|
||||
Disclaimer: I've made every good-faith effort to include nothing insecure, malicious, anti-privacy, or destructive. That said: use at your own risk.
|
||||
# Preview production build
|
||||
npm run docs:preview
|
||||
```
|
||||
|
||||
I made this for myself and I want to share it with the great programming and audible/audiobook communities which have been so generous with their time and help.
|
||||
**Note**: New pages are automatically routed based on their folder structure (e.g., `docs/docs/index.md` maps to `/docs/index`). To add them to the sidebar, update the `sidebar` configuration in `.vitepress/config.js`.
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AAXClean.Codecs" Version="2.1.0.1" />
|
||||
<PackageReference Include="AAXClean.Codecs" Version="2.1.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -26,7 +26,17 @@ namespace AaxDecrypter
|
||||
protected string OutputDirectory { get; }
|
||||
public IDownloadOptions DownloadOptions { get; }
|
||||
protected NetworkFileStream InputFileStream => NfsPersister.NetworkFileStream;
|
||||
protected virtual long InputFilePosition => InputFileStream.Position;
|
||||
protected virtual long InputFilePosition
|
||||
{
|
||||
get
|
||||
{
|
||||
//Use try/catch instread of checking CanRead to avoid
|
||||
//a race with the background download completing
|
||||
//between the check and the Position call.
|
||||
try { return InputFileStream.Position; }
|
||||
catch { return InputFileStream.Length; }
|
||||
}
|
||||
}
|
||||
private bool downloadFinished;
|
||||
|
||||
private NetworkFileStreamPersister? m_nfsPersister;
|
||||
|
||||
@@ -209,6 +209,12 @@ namespace AaxDecrypter
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Don't throw from DownloadTask.
|
||||
//This task gets awaited in Dispose() and we don't want to have an unhandled exception there.
|
||||
Serilog.Log.Error(ex, "An error was encountered during the download process.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writeFile.Dispose();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Version>12.7.2.1</Version>
|
||||
<Version>13.0.0.1</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Octokit" Version="14.0.0" />
|
||||
|
||||
@@ -24,7 +24,8 @@ namespace AppScaffolding
|
||||
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
|
||||
MacOSAvalonia_Arm64 = OS.MacOS | Variety.Chardonnay | Architecture.Arm64,
|
||||
WindowsAvalonia_Arm64 = OS.Windows | 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
|
||||
@@ -90,6 +91,7 @@ namespace AppScaffolding
|
||||
{
|
||||
config.LoadPersistentSettings(config.LibationFiles.SettingsFilePath);
|
||||
}
|
||||
DeleteOpenSqliteFiles(config);
|
||||
AudibleApiStorage.EnsureAccountsSettingsFileExists();
|
||||
|
||||
//
|
||||
@@ -102,6 +104,39 @@ namespace AppScaffolding
|
||||
Migrations.migrate_to_v12_0_1(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete shared memory and write-ahead log SQLite database files which may prevent access to the database.
|
||||
/// These file may or may not cause libation to hang on CreateContext,
|
||||
/// so try our luck by swallowing any exceptions and continuing.
|
||||
/// </summary>
|
||||
private static void DeleteOpenSqliteFiles(Configuration config)
|
||||
{
|
||||
var walFile = SqliteStorage.DatabasePath + "-wal";
|
||||
var shmFile = SqliteStorage.DatabasePath + "-shm";
|
||||
if (File.Exists(walFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
FileManager.FileUtility.SaferDelete(walFile);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
Log.Logger.Warning(ex, "Could not delete SQLite WAL file: {@WalFile}", walFile);
|
||||
}
|
||||
}
|
||||
if (File.Exists(shmFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
FileManager.FileUtility.SaferDelete(shmFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Logger.Warning(ex, "Could not delete SQLite SHM file: {@ShmFile}", shmFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Initialize logging. Wire-up events. Run after migration</summary>
|
||||
public static void RunPostMigrationScaffolding(Variety variety, Configuration config)
|
||||
{
|
||||
|
||||
@@ -299,7 +299,7 @@ namespace ApplicationServices
|
||||
|
||||
try
|
||||
{
|
||||
var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryOptions, Configuration.Instance.ImportEpisodes);
|
||||
var dtoItems = await apiExtended.GetLibraryValidatedAsync(libraryOptions);
|
||||
|
||||
logTime($"post scanAccountAsync {account.AccountName} qty: {dtoItems.Count}");
|
||||
|
||||
@@ -586,7 +586,7 @@ namespace ApplicationServices
|
||||
|
||||
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those...
|
||||
|
||||
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int booksUnavailable, int pdfsDownloaded, int pdfsNotDownloaded, int pdfsUnavailable)
|
||||
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int booksUnavailable, int pdfsDownloaded, int pdfsNotDownloaded, int pdfsUnavailable, IEnumerable<LibraryBook> LibraryBooks)
|
||||
{
|
||||
public int PendingBooks => booksNoProgress + booksDownloadedOnly;
|
||||
public bool HasPendingBooks => PendingBooks > 0;
|
||||
@@ -655,7 +655,7 @@ namespace ApplicationServices
|
||||
|
||||
Log.Logger.Information("PDF counts. {@DebugInfo}", new { total = pdfResults.Count, pdfsDownloaded, pdfsNotDownloaded, pdfsUnavailable });
|
||||
|
||||
return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError, booksUnavailable, pdfsDownloaded, pdfsNotDownloaded, pdfsUnavailable);
|
||||
return new(booksFullyBackedUp, booksDownloadedOnly, booksNoProgress, booksError, booksUnavailable, pdfsDownloaded, pdfsNotDownloaded, pdfsUnavailable, libraryBooks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using Dinah.Core;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
using System.Threading;
|
||||
using LibationFileManager;
|
||||
|
||||
#nullable enable
|
||||
namespace AudibleUtilities
|
||||
@@ -72,16 +73,16 @@ namespace AudibleUtilities
|
||||
// 2 retries == 3 total
|
||||
.RetryAsync(2);
|
||||
|
||||
public Task<List<Item>> GetLibraryValidatedAsync(LibraryOptions libraryOptions, bool importEpisodes = true)
|
||||
public Task<List<Item>> GetLibraryValidatedAsync(LibraryOptions libraryOptions)
|
||||
{
|
||||
// bug on audible's side. the 1st time after a long absence, a query to get library will return without titles or authors. a subsequent identical query will be successful. this is true whether or not tokens are refreshed
|
||||
// worse, this 1st dummy call doesn't seem to help:
|
||||
// var page = await api.GetLibraryAsync(new AudibleApi.LibraryOptions { NumberOfResultPerPage = 1, PageNumber = 1, PurchasedAfter = DateTime.Now.AddYears(-20), ResponseGroups = AudibleApi.LibraryOptions.ResponseGroupOptions.ALL_OPTIONS });
|
||||
// i don't want to incur the cost of making a full dummy call every time because it fails sometimes
|
||||
return policy.ExecuteAsync(() => getItemsAsync(libraryOptions, importEpisodes));
|
||||
return policy.ExecuteAsync(() => getItemsAsync(libraryOptions));
|
||||
}
|
||||
|
||||
private async Task<List<Item>> getItemsAsync(LibraryOptions libraryOptions, bool importEpisodes)
|
||||
private async Task<List<Item>> getItemsAsync(LibraryOptions libraryOptions)
|
||||
{
|
||||
Serilog.Log.Logger.Debug("Beginning library scan.");
|
||||
|
||||
@@ -95,12 +96,12 @@ namespace AudibleUtilities
|
||||
|
||||
//Scan the library for all added books.
|
||||
//Get relationship asins from episode-type items and write them to episodeChannel where they will be batched and queried.
|
||||
await foreach (var item in Api.GetLibraryItemsPagesAsync(libraryOptions, BatchSize, semaphore))
|
||||
await foreach (var itemsBatch in Api.GetLibraryItemsPagesAsync(libraryOptions, BatchSize, semaphore))
|
||||
{
|
||||
if (importEpisodes)
|
||||
if (Configuration.Instance.ImportEpisodes)
|
||||
{
|
||||
var episodes = item.Where(i => i.IsEpisodes).ToList();
|
||||
var series = item.Where(i => i.IsSeriesParent).ToList();
|
||||
var episodes = itemsBatch.Where(i => i.IsEpisodes).ToList();
|
||||
var series = itemsBatch.Where(i => i.IsSeriesParent).ToList();
|
||||
|
||||
var parentAsins = episodes
|
||||
.SelectMany(i => i.Relationships)
|
||||
@@ -119,7 +120,11 @@ namespace AudibleUtilities
|
||||
items.AddRange(series);
|
||||
}
|
||||
|
||||
items.AddRange(item.Where(i => !i.IsSeriesParent && !i.IsEpisodes));
|
||||
var booksInBatch
|
||||
= itemsBatch
|
||||
.Where(i => !i.IsSeriesParent && !i.IsEpisodes)
|
||||
.Where(i => i.IsAyce is not true || Configuration.Instance.ImportPlusTitles);
|
||||
items.AddRange(booksInBatch);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AudibleApi" Version="10.1.0.1" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.33.1" />
|
||||
<PackageReference Include="AudibleApi" Version="10.1.2.1" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.33.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace DataLayer.Postgres
|
||||
namespace DataLayer.Sqlite
|
||||
{
|
||||
public class SqliteContextFactory : IDesignTimeDbContextFactory<LibationContext>
|
||||
{
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace DataLayer
|
||||
/// <summary>True if IsLiberated or Error. False if NotLiberated</summary>
|
||||
public bool AudioExists => book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated or LiberatedStatus.Error;
|
||||
/// <summary>True if exists and IsLiberated. Else false</summary>
|
||||
public bool PdfExists => book.UserDefinedItem.PdfStatus == LiberatedStatus.NotLiberated;
|
||||
public bool PdfExists => book.UserDefinedItem.PdfStatus is LiberatedStatus.Liberated;
|
||||
/// <summary> Whether the book has any supplements </summary>
|
||||
public bool HasPdf => book.Supplements.Any();
|
||||
|
||||
|
||||
@@ -16,9 +16,10 @@ namespace FileLiberator
|
||||
/// Path: directory nested inside of Books directory
|
||||
/// File name: n/a
|
||||
/// </summary>
|
||||
public static string GetDestinationDirectory(this AudioFileStorage _, LibraryBook libraryBook)
|
||||
public static string GetDestinationDirectory(this AudioFileStorage _, LibraryBook libraryBook, Configuration config = null)
|
||||
{
|
||||
if (libraryBook.Book.IsEpisodeChild() && Configuration.Instance.SavePodcastsToParentFolder)
|
||||
config ??= Configuration.Instance;
|
||||
if (libraryBook.Book.IsEpisodeChild() && config.SavePodcastsToParentFolder)
|
||||
{
|
||||
var series = libraryBook.Book.SeriesLink.SingleOrDefault();
|
||||
if (series is not null)
|
||||
|
||||
@@ -13,7 +13,7 @@ using LibationFileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public class ConvertToMp3 : AudioDecodable
|
||||
public class ConvertToMp3 : AudioDecodable, IProcessable<ConvertToMp3>
|
||||
{
|
||||
public override string Name => "Convert to Mp3";
|
||||
private Mp4Operation Mp4Operation;
|
||||
@@ -72,15 +72,14 @@ namespace FileLiberator
|
||||
OnNarratorsDiscovered(m4bBook.AppleTags.Narrator);
|
||||
OnCoverImageDiscovered(m4bBook.AppleTags.Cover);
|
||||
|
||||
var config = Configuration.Instance;
|
||||
var lameConfig = DownloadOptions.GetLameOptions(config);
|
||||
var lameConfig = DownloadOptions.GetLameOptions(Configuration);
|
||||
var chapters = m4bBook.GetChaptersFromMetadata();
|
||||
//Finishing configuring lame encoder.
|
||||
AaxDecrypter.MpegUtil.ConfigureLameOptions(
|
||||
m4bBook,
|
||||
lameConfig,
|
||||
config.LameDownsampleMono,
|
||||
config.LameMatchSourceBR,
|
||||
Configuration.LameDownsampleMono,
|
||||
Configuration.LameMatchSourceBR,
|
||||
chapters);
|
||||
|
||||
if (m4bBook.AppleTags.Tracks is (int trackNum, int trackCount))
|
||||
@@ -108,9 +107,9 @@ namespace FileLiberator
|
||||
= FileUtility.SaferMoveToValidPath(
|
||||
tempPath,
|
||||
entry.proposedMp3Path,
|
||||
Configuration.Instance.ReplacementCharacters,
|
||||
Configuration.ReplacementCharacters,
|
||||
extension: "mp3",
|
||||
Configuration.Instance.OverwriteExisting);
|
||||
Configuration.OverwriteExisting);
|
||||
|
||||
SetFileTime(libraryBook, realMp3Path);
|
||||
SetDirectoryTime(libraryBook, Path.GetDirectoryName(realMp3Path));
|
||||
@@ -169,5 +168,7 @@ namespace FileLiberator
|
||||
TotalBytesToReceive = totalInputSize
|
||||
});
|
||||
}
|
||||
public static ConvertToMp3 Create(Configuration config) => new() { Configuration = config };
|
||||
private ConvertToMp3() { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ using System.Threading.Tasks;
|
||||
#nullable enable
|
||||
namespace FileLiberator
|
||||
{
|
||||
public class DownloadDecryptBook : AudioDecodable
|
||||
public class DownloadDecryptBook : AudioDecodable, IProcessable<DownloadDecryptBook>
|
||||
{
|
||||
public override string Name => "Download & Decrypt";
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
@@ -50,8 +50,10 @@ namespace FileLiberator
|
||||
|
||||
var api = await libraryBook.GetApiAsync();
|
||||
|
||||
LicenseInfo ??= await DownloadOptions.GetDownloadLicenseAsync(api, libraryBook, Configuration.Instance, cancellationToken);
|
||||
using var downloadOptions = DownloadOptions.BuildDownloadOptions(libraryBook, Configuration.Instance, LicenseInfo);
|
||||
//Processable instances are reusable, so don't set LicenseInfo
|
||||
//override from within a DownloadDecryptBook instance.
|
||||
var license = LicenseInfo ?? await DownloadOptions.GetDownloadLicenseAsync(api, libraryBook, Configuration, cancellationToken);
|
||||
using var downloadOptions = DownloadOptions.BuildDownloadOptions(libraryBook, Configuration, license);
|
||||
var result = await DownloadAudiobookAsync(api, downloadOptions, cancellationToken);
|
||||
|
||||
if (!result.Success || getFirstAudioFile(result.ResultFiles) is not TempFile audioFile)
|
||||
@@ -62,7 +64,7 @@ namespace FileLiberator
|
||||
return new StatusHandler { "Decrypt failed" };
|
||||
}
|
||||
|
||||
if (Configuration.Instance.RetainAaxFile)
|
||||
if (Configuration.RetainAaxFile)
|
||||
{
|
||||
//Add the cached aaxc and key files to the entries list to be moved to the Books directory.
|
||||
result.ResultFiles.AddRange(getAaxcFiles(result.CacheFiles));
|
||||
@@ -256,7 +258,7 @@ namespace FileLiberator
|
||||
|
||||
private void AaxcDownloader_RetrievedCoverArt(object? sender, byte[]? e)
|
||||
{
|
||||
if (Configuration.Instance.AllowLibationFixup && sender is AaxcDownloadConvertBase downloader)
|
||||
if (Configuration.AllowLibationFixup && sender is AaxcDownloadConvertBase downloader)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -345,15 +347,15 @@ namespace FileLiberator
|
||||
destinationDir,
|
||||
entry.Extension,
|
||||
entry.PartProperties,
|
||||
Configuration.Instance.OverwriteExisting);
|
||||
Configuration.OverwriteExisting);
|
||||
|
||||
var realDest
|
||||
= FileUtility.SaferMoveToValidPath(
|
||||
entry.FilePath,
|
||||
destFileName,
|
||||
Configuration.Instance.ReplacementCharacters,
|
||||
Configuration.ReplacementCharacters,
|
||||
entry.Extension,
|
||||
Configuration.Instance.OverwriteExisting);
|
||||
Configuration.OverwriteExisting);
|
||||
|
||||
#region File Move Progress
|
||||
totalBytesMoved += new FileInfo(realDest).Length;
|
||||
@@ -403,7 +405,7 @@ namespace FileLiberator
|
||||
options.LibraryBook,
|
||||
destinationDir,
|
||||
extension: ".jpg",
|
||||
returnFirstExisting: Configuration.Instance.OverwriteExisting);
|
||||
returnFirstExisting: Configuration.OverwriteExisting);
|
||||
|
||||
if (File.Exists(coverPath))
|
||||
FileUtility.SaferDelete(coverPath);
|
||||
@@ -440,7 +442,7 @@ namespace FileLiberator
|
||||
options.LibraryBook,
|
||||
destinationDir,
|
||||
extension: formatExtension,
|
||||
returnFirstExisting: Configuration.Instance.OverwriteExisting);
|
||||
returnFirstExisting: Configuration.OverwriteExisting);
|
||||
|
||||
if (File.Exists(recordsPath))
|
||||
FileUtility.SaferDelete(recordsPath);
|
||||
@@ -487,7 +489,7 @@ namespace FileLiberator
|
||||
options.LibraryBook,
|
||||
destinationDir,
|
||||
extension: ".metadata.json",
|
||||
returnFirstExisting: Configuration.Instance.OverwriteExisting);
|
||||
returnFirstExisting: Configuration.OverwriteExisting);
|
||||
|
||||
if (File.Exists(metadataPath))
|
||||
FileUtility.SaferDelete(metadataPath);
|
||||
@@ -512,10 +514,10 @@ namespace FileLiberator
|
||||
#endregion
|
||||
|
||||
#region Macros
|
||||
private static string getDestinationDirectory(LibraryBook libraryBook)
|
||||
private string getDestinationDirectory(LibraryBook libraryBook)
|
||||
{
|
||||
Serilog.Log.Verbose("Getting destination directory for {@Book}", libraryBook.LogFriendly());
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook);
|
||||
var destinationDir = AudibleFileStorage.Audio.GetDestinationDirectory(libraryBook, Configuration);
|
||||
Serilog.Log.Verbose("Got destination directory for {@Book}. {@Directory}", libraryBook.LogFriendly(), destinationDir);
|
||||
if (!Directory.Exists(destinationDir))
|
||||
{
|
||||
@@ -533,5 +535,8 @@ namespace FileLiberator
|
||||
private static IEnumerable<TempFile> getAaxcFiles(IEnumerable<TempFile> entries)
|
||||
=> entries.Where(f => File.Exists(f.FilePath) && (getFileType(f) is FileType.AAXC || f.Extension.Equals(".key", StringComparison.OrdinalIgnoreCase)));
|
||||
#endregion
|
||||
|
||||
public static DownloadDecryptBook Create(Configuration config) => new() { Configuration = config };
|
||||
private DownloadDecryptBook() { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ public partial class DownloadOptions
|
||||
//try to request a widevine content license using the user's audio settings
|
||||
var aacCodecChoice = config.Request_xHE_AAC ? Codecs.xHE_AAC : Codecs.AAC_LC;
|
||||
//Always use the ec+3 codec if converting to mp3
|
||||
var spatialCodecChoice = config.SpatialAudioCodec is Configuration.SpatialCodec.AC_4 && !config.DecryptToLossy ? Codecs.AC_4 : Codecs.EC_3;
|
||||
var spatialCodecChoice = config.SpatialAudioCodec is Configuration.SpatialCodec.AC_4 ? Codecs.AC_4 : Codecs.EC_3;
|
||||
|
||||
var contentLic
|
||||
= await api.GetDownloadLicenseAsync(
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace FileLiberator
|
||||
//If DrmType is not Adrm or Widevine, the delivered file is an unencrypted mp3.
|
||||
OutputFormat
|
||||
= licInfo.DrmType is not AudibleApi.Common.DrmType.Adrm and not AudibleApi.Common.DrmType.Widevine ||
|
||||
(config.AllowLibationFixup && config.DecryptToLossy && licInfo.ContentMetadata.ContentReference.Codec != AudibleApi.Codecs.AC_4)
|
||||
(config.AllowLibationFixup && config.DecryptToLossy)
|
||||
? OutputFormat.Mp3
|
||||
: OutputFormat.M4b;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ using LibationFileManager;
|
||||
|
||||
namespace FileLiberator
|
||||
{
|
||||
public class DownloadPdf : Processable
|
||||
public class DownloadPdf : Processable, IProcessable<DownloadPdf>
|
||||
{
|
||||
public override string Name => "Download Pdf";
|
||||
public override bool Validate(LibraryBook libraryBook)
|
||||
@@ -89,5 +89,8 @@ namespace FileLiberator
|
||||
=> !File.Exists(actualDownloadedFilePath)
|
||||
? new StatusHandler { "Downloaded PDF cannot be found" }
|
||||
: new StatusHandler();
|
||||
|
||||
public static DownloadPdf Create(Configuration config) => new() { Configuration = config };
|
||||
private DownloadPdf() { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,24 +9,36 @@ using Dinah.Core.ErrorHandling;
|
||||
using Dinah.Core.Net.Http;
|
||||
using LibationFileManager;
|
||||
|
||||
#nullable enable
|
||||
namespace FileLiberator
|
||||
{
|
||||
public abstract class Processable
|
||||
public interface IProcessable<T> where T : IProcessable<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new instance of the Processable which uses a specific Configuration
|
||||
/// </summary>
|
||||
/// <param name="config">The <see cref="Configuration"/> this <typeparamref name="T"/> will use</param>
|
||||
static abstract T Create(Configuration config);
|
||||
}
|
||||
public abstract class Processable
|
||||
{
|
||||
public abstract string Name { get; }
|
||||
public event EventHandler<LibraryBook> Begin;
|
||||
public event EventHandler<LibraryBook>? Begin;
|
||||
|
||||
/// <summary>General string message to display. DON'T rely on this for success, failure, or control logic</summary>
|
||||
public event EventHandler<string> StatusUpdate;
|
||||
public event EventHandler<string>? StatusUpdate;
|
||||
/// <summary>Fired when a file is successfully saved to disk</summary>
|
||||
public event EventHandler<(string id, string path)> FileCreated;
|
||||
public event EventHandler<DownloadProgress> StreamingProgressChanged;
|
||||
public event EventHandler<TimeSpan> StreamingTimeRemaining;
|
||||
public event EventHandler<(string id, string path)>? FileCreated;
|
||||
public event EventHandler<DownloadProgress>? StreamingProgressChanged;
|
||||
public event EventHandler<TimeSpan>? StreamingTimeRemaining;
|
||||
|
||||
public event EventHandler<LibraryBook> Completed;
|
||||
public event EventHandler<LibraryBook>? Completed;
|
||||
|
||||
/// <returns>True == Valid</returns>
|
||||
public abstract bool Validate(LibraryBook libraryBook);
|
||||
public required Configuration Configuration{ get; init; }
|
||||
protected Processable() { }
|
||||
|
||||
/// <returns>True == Valid</returns>
|
||||
public abstract bool Validate(LibraryBook libraryBook);
|
||||
|
||||
/// <returns>True == success</returns>
|
||||
public abstract Task<StatusHandler> ProcessAsync(LibraryBook libraryBook);
|
||||
@@ -35,7 +47,7 @@ namespace FileLiberator
|
||||
public IEnumerable<LibraryBook> GetValidLibraryBooks(IEnumerable<LibraryBook> library)
|
||||
=> library.Where(libraryBook =>
|
||||
Validate(libraryBook)
|
||||
&& (!libraryBook.Book.IsEpisodeChild() || Configuration.Instance.DownloadEpisodes)
|
||||
&& (!libraryBook.Book.IsEpisodeChild() || Configuration.DownloadEpisodes)
|
||||
);
|
||||
|
||||
public async Task<StatusHandler> ProcessSingleAsync(LibraryBook libraryBook, bool validate)
|
||||
@@ -86,12 +98,12 @@ namespace FileLiberator
|
||||
|
||||
protected void OnStreamingProgressChanged(DownloadProgress progress)
|
||||
=> OnStreamingProgressChanged(null, progress);
|
||||
protected void OnStreamingProgressChanged(object _, DownloadProgress progress)
|
||||
protected void OnStreamingProgressChanged(object? _, DownloadProgress progress)
|
||||
=> StreamingProgressChanged?.Invoke(this, progress);
|
||||
|
||||
protected void OnStreamingTimeRemaining(TimeSpan timeRemaining)
|
||||
=> OnStreamingTimeRemaining(null, timeRemaining);
|
||||
protected void OnStreamingTimeRemaining(object _, TimeSpan timeRemaining)
|
||||
protected void OnStreamingTimeRemaining(object? _, TimeSpan timeRemaining)
|
||||
=> StreamingTimeRemaining?.Invoke(this, timeRemaining);
|
||||
|
||||
protected void OnCompleted(LibraryBook libraryBook)
|
||||
@@ -100,17 +112,17 @@ namespace FileLiberator
|
||||
Completed?.Invoke(this, libraryBook);
|
||||
}
|
||||
|
||||
protected static void SetFileTime(LibraryBook libraryBook, string file)
|
||||
protected void SetFileTime(LibraryBook libraryBook, string file)
|
||||
=> setFileSystemTime(libraryBook, new FileInfo(file));
|
||||
protected static void SetDirectoryTime(LibraryBook libraryBook, string file)
|
||||
protected void SetDirectoryTime(LibraryBook libraryBook, string file)
|
||||
=> setFileSystemTime(libraryBook, new DirectoryInfo(file));
|
||||
|
||||
private static void setFileSystemTime(LibraryBook libraryBook, FileSystemInfo fileInfo)
|
||||
private void setFileSystemTime(LibraryBook libraryBook, FileSystemInfo fileInfo)
|
||||
{
|
||||
if (!fileInfo.Exists) return;
|
||||
|
||||
fileInfo.CreationTimeUtc = getTimeValue(Configuration.Instance.CreationTime) ?? fileInfo.CreationTimeUtc;
|
||||
fileInfo.LastWriteTimeUtc = getTimeValue(Configuration.Instance.LastWriteTime) ?? fileInfo.LastWriteTimeUtc;
|
||||
fileInfo.CreationTimeUtc = getTimeValue(Configuration.CreationTime) ?? fileInfo.CreationTimeUtc;
|
||||
fileInfo.LastWriteTimeUtc = getTimeValue(Configuration.LastWriteTime) ?? fileInfo.LastWriteTimeUtc;
|
||||
|
||||
DateTime? getTimeValue(Configuration.DateTimeSource source) => source switch
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dinah.Core" Version="10.0.0.1" />
|
||||
<PackageReference Include="Polly" Version="8.6.4" />
|
||||
<PackageReference Include="Polly" Version="8.6.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
||||
@@ -56,15 +56,18 @@ namespace FileManager
|
||||
|
||||
fileExtension = GetStandardizedExtension(fileExtension);
|
||||
|
||||
// remove invalid chars
|
||||
path = GetSafePath(path, replacements);
|
||||
var pathStr = removeInvalidWhitespace(path.Path);
|
||||
var pathWithoutExtension = pathStr.EndsWithInsensitive(fileExtension)
|
||||
? pathStr[..^fileExtension.Length]
|
||||
: path.Path;
|
||||
|
||||
// remove invalid chars, but leave file extension untouched
|
||||
pathWithoutExtension = GetSafePath(pathWithoutExtension, replacements);
|
||||
|
||||
// ensure uniqueness and check lengths
|
||||
var dir = Path.GetDirectoryName(path)?.TruncateFilename(LongPath.MaxDirectoryLength) ?? string.Empty;
|
||||
var dir = Path.GetDirectoryName(pathWithoutExtension)?.TruncateFilename(LongPath.MaxDirectoryLength) ?? string.Empty;
|
||||
|
||||
var fileName = Path.GetFileName(path);
|
||||
var extIndex = fileName.LastIndexOf(fileExtension, StringComparison.OrdinalIgnoreCase);
|
||||
var filenameWithoutExtension = extIndex >= 0 ? fileName.Remove(extIndex, fileExtension.Length) : fileName;
|
||||
var filenameWithoutExtension = Path.GetFileName(pathWithoutExtension);
|
||||
var fileStem
|
||||
= Path.Combine(dir, filenameWithoutExtension.TruncateFilename(LongPath.MaxFilenameLength - fileExtension.Length))
|
||||
.TruncateFilename(LongPath.MaxPathLength - fileExtension.Length);
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace FileManager;
|
||||
|
||||
public interface IJsonBackedDictionary
|
||||
{
|
||||
JObject GetJObject();
|
||||
bool Exists(string propertyName);
|
||||
string? GetString(string propertyName, string? defaultValue = null);
|
||||
T? GetNonString<T>(string propertyName, T? defaultValue = default);
|
||||
|
||||
@@ -273,5 +273,7 @@ namespace FileManager
|
||||
{
|
||||
File.WriteAllText(Filepath, "{}");
|
||||
}
|
||||
}
|
||||
|
||||
public JObject GetJObject() => readFile();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,12 +136,12 @@ public class App : Application
|
||||
|
||||
[PropertyChangeFilter(nameof(ThemeVariant))]
|
||||
private static void ThemeVariant_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
|
||||
=> OpenAndApplyTheme(e.NewValue as string);
|
||||
=> OpenAndApplyTheme(e.NewValue as Configuration.Theme? ?? Configuration.Theme.System);
|
||||
|
||||
private static void OnActualThemeVariantChanged(object? sender, EventArgs e)
|
||||
=> OpenAndApplyTheme(Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)));
|
||||
=> OpenAndApplyTheme(Configuration.Instance.ThemeVariant);
|
||||
|
||||
private static void OpenAndApplyTheme(string? themeVariant)
|
||||
private static void OpenAndApplyTheme(Configuration.Theme themeVariant)
|
||||
{
|
||||
using ChardonnayThemePersister? themePersister = ChardonnayThemePersister.Create();
|
||||
themePersister?.Target.ApplyTheme(themeVariant);
|
||||
|
||||
@@ -92,7 +92,6 @@
|
||||
Margin="5,0,0,0"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
SelectionChanged="SpatialCodec_SelectionChanged"
|
||||
ItemsSource="{CompiledBinding SpatialAudioCodecs}"
|
||||
SelectedItem="{CompiledBinding SpatialAudioCodec}"/>
|
||||
</Grid>
|
||||
|
||||
@@ -23,15 +23,6 @@ namespace LibationAvalonia.Controls.Settings
|
||||
}
|
||||
}
|
||||
|
||||
private void SpatialCodec_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_viewModel.SpatialAudioCodec.Value is Configuration.SpatialCodec.AC_4 && _viewModel.DecryptToLossy)
|
||||
{
|
||||
_viewModel.SpatialAudioCodec = _viewModel.SpatialAudioCodecs[0];
|
||||
_viewModel.RaisePropertyChanged(nameof(AudioSettingsVM.SpatialAudioCodec));
|
||||
}
|
||||
}
|
||||
|
||||
private async void UseWidevine_IsCheckedChanged(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (sender is CheckBox cbox && cbox.IsChecked is true)
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
<TextBlock Text="{CompiledBinding ImportEpisodesText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding ImportPlusTitles, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding ImportPlusTitlesText}" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{CompiledBinding DownloadEpisodes, Mode=TwoWay}">
|
||||
<TextBlock Text="{CompiledBinding DownloadEpisodesText}" />
|
||||
</CheckBox>
|
||||
|
||||
@@ -52,7 +52,8 @@ namespace LibationAvalonia.Controls.Settings
|
||||
var parent = ThemeComboBox.Parent as Panel;
|
||||
if (parent?.Children.Remove(ThemeComboBox) ?? false)
|
||||
{
|
||||
Configuration.Instance.SetString(ViewModel?.ThemeVariant, nameof(ViewModel.ThemeVariant));
|
||||
|
||||
Configuration.Instance.ThemeVariant = ViewModel?.ThemeVariant.Value ?? Configuration.Theme.System;
|
||||
parent.Children.Add(ThemeComboBox);
|
||||
}
|
||||
ThemeComboBox.SelectionChanged += ThemeComboBox_SelectionChanged;
|
||||
|
||||
@@ -2,6 +2,7 @@ using Avalonia.Controls;
|
||||
using DataLayer;
|
||||
using Dinah.Core.ErrorHandling;
|
||||
using LibationAvalonia.ViewModels;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase.ProcessQueue;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -32,11 +33,11 @@ public partial class ThemePreviewControl : UserControl
|
||||
MainVM.Configure_NonUI();
|
||||
}
|
||||
|
||||
QueuedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Queued };
|
||||
WorkingBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Working };
|
||||
CompletedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Completed };
|
||||
CancelledBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Cancelled };
|
||||
FailedBook = new ProcessBookViewModel(sampleEntries[0]) { Status = ProcessBookStatus.Failed };
|
||||
QueuedBook = new ProcessBookViewModel(sampleEntries[0], Configuration.Instance) { Status = ProcessBookStatus.Queued };
|
||||
WorkingBook = new ProcessBookViewModel(sampleEntries[0], Configuration.Instance) { Status = ProcessBookStatus.Working };
|
||||
CompletedBook = new ProcessBookViewModel(sampleEntries[0], Configuration.Instance) { Status = ProcessBookStatus.Completed };
|
||||
CancelledBook = new ProcessBookViewModel(sampleEntries[0], Configuration.Instance) { Status = ProcessBookStatus.Cancelled };
|
||||
FailedBook = new ProcessBookViewModel(sampleEntries[0], Configuration.Instance) { Status = ProcessBookStatus.Failed };
|
||||
|
||||
//Set the current processable so that the empty queue doesn't try to advance.
|
||||
QueuedBook.AddDownloadPdf();
|
||||
|
||||
@@ -73,8 +73,8 @@ namespace LibationAvalonia.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> SaveAndClose();
|
||||
public async void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
=> await SaveAndCloseAsync();
|
||||
public class liberatedComboBoxItem
|
||||
{
|
||||
public LiberatedStatus Status { get; set; }
|
||||
|
||||
@@ -29,11 +29,11 @@ namespace LibationAvalonia.Dialogs
|
||||
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
var themeVariant = Configuration.CreateMockInstance().GetString(propertyName: nameof(ThemeVariant));
|
||||
var themeVariant = Configuration.CreateMockInstance().ThemeVariant;
|
||||
RequestedThemeVariant = themeVariant switch
|
||||
{
|
||||
nameof(ThemeVariant.Dark) => ThemeVariant.Dark,
|
||||
nameof(ThemeVariant.Light) => ThemeVariant.Light,
|
||||
Configuration.Theme.Dark => ThemeVariant.Dark,
|
||||
Configuration.Theme.Light => ThemeVariant.Light,
|
||||
_ => ThemeVariant.Default,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using AudibleUtilities;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Threading;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase.Forms;
|
||||
using System;
|
||||
@@ -67,8 +68,17 @@ namespace LibationAvalonia.Dialogs.Login
|
||||
{
|
||||
if (dialog.TryGetCookieManager() is NativeWebViewCookieManager cookieManager)
|
||||
{
|
||||
foreach (System.Net.Cookie c in shoiceIn.SignInCookies)
|
||||
cookieManager.AddOrUpdateCookie(c);
|
||||
foreach (System.Net.Cookie c in shoiceIn.SignInCookies ?? [])
|
||||
{
|
||||
try
|
||||
{
|
||||
cookieManager.AddOrUpdateCookie(c);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, $"Failed to set cookie {c.Name} for domain {c.Domain}");
|
||||
}
|
||||
}
|
||||
}
|
||||
//Set the source only after loading cookies
|
||||
dialog.Source = new Uri(shoiceIn.LoginUrl);
|
||||
|
||||
@@ -11,10 +11,11 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Platform;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia
|
||||
{
|
||||
|
||||
public class MessageBox
|
||||
{
|
||||
public static Task<DialogResult> Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton)
|
||||
@@ -27,15 +28,15 @@ namespace LibationAvalonia
|
||||
=> ShowCoreAsync(null, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
public static Task<DialogResult> Show(string text)
|
||||
=> ShowCoreAsync(null, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
|
||||
public static Task<DialogResult> Show(Window? owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
|
||||
=> ShowCoreAsync(owner, text, caption, buttons, icon, defaultButton, saveAndRestorePosition);
|
||||
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon)
|
||||
public static Task<DialogResult> Show(Window? owner, string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon)
|
||||
=> ShowCoreAsync(owner, text, caption, buttons, icon, MessageBoxDefaultButton.Button1);
|
||||
public static Task<DialogResult> Show(Window owner, string text, string caption, MessageBoxButtons buttons)
|
||||
public static Task<DialogResult> Show(Window? owner, string text, string caption, MessageBoxButtons buttons)
|
||||
=> ShowCoreAsync(owner, text, caption, buttons, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
public static Task<DialogResult> Show(Window owner, string text, string caption)
|
||||
public static Task<DialogResult> Show(Window? owner, string text, string caption)
|
||||
=> ShowCoreAsync(owner, text, caption, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
public static Task<DialogResult> Show(Window owner, string text)
|
||||
public static Task<DialogResult> Show(Window? owner, string text)
|
||||
=> ShowCoreAsync(owner, text, string.Empty, MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1);
|
||||
|
||||
public static async Task VerboseLoggingWarning_ShowIfTrue()
|
||||
@@ -58,7 +59,7 @@ namespace LibationAvalonia
|
||||
/// <summary>
|
||||
/// Note: the format field should use {0} and NOT use the `$` string interpolation. Formatting is done inside this method.
|
||||
/// </summary>
|
||||
public static async Task<DialogResult> ShowConfirmationDialog(Window owner, IEnumerable<LibraryBook> libraryBooks, string format, string title, MessageBoxDefaultButton defaultButton = MessageBoxDefaultButton.Button1)
|
||||
public static async Task<DialogResult> ShowConfirmationDialog(Window? owner, IEnumerable<LibraryBook> libraryBooks, string format, string title, MessageBoxDefaultButton defaultButton = MessageBoxDefaultButton.Button1)
|
||||
{
|
||||
if (libraryBooks is null || !libraryBooks.Any())
|
||||
return DialogResult.Cancel;
|
||||
@@ -88,7 +89,8 @@ namespace LibationAvalonia
|
||||
/// <param name="text">The text to display in the message box.</param>
|
||||
/// <param name="caption">The text to display in the title bar of the message box.</param>
|
||||
/// <param name="exception">Exception to log.</param>
|
||||
public static async Task ShowAdminAlert(Window owner, string text, string caption, Exception exception)
|
||||
public static async Task ShowAdminAlert(Window? owner, string text, string caption, Exception exception)
|
||||
=> await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
// for development and debugging, show me what broke!
|
||||
if (System.Diagnostics.Debugger.IsAttached)
|
||||
@@ -104,9 +106,9 @@ namespace LibationAvalonia
|
||||
var form = new MessageBoxAlertAdminDialog(text, caption, exception);
|
||||
|
||||
await DisplayWindow(form, owner);
|
||||
}
|
||||
});
|
||||
|
||||
private static async Task<DialogResult> ShowCoreAsync(Window owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
|
||||
private static async Task<DialogResult> ShowCoreAsync(Window? owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
|
||||
=> await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
owner = owner?.IsLoaded is true ? owner : null;
|
||||
@@ -114,10 +116,8 @@ namespace LibationAvalonia
|
||||
return await DisplayWindow(dialog, owner);
|
||||
});
|
||||
|
||||
private static MessageBoxWindow CreateMessageBox(Window owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
|
||||
private static MessageBoxWindow CreateMessageBox(Window? owner, string message, string caption, MessageBoxButtons buttons, MessageBoxIcon icon, MessageBoxDefaultButton defaultButton, bool saveAndRestorePosition = true)
|
||||
{
|
||||
owner ??= (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).MainWindow;
|
||||
|
||||
var dialog = new MessageBoxWindow(saveAndRestorePosition);
|
||||
|
||||
var vm = new MessageBoxViewModel(message, caption, buttons, icon, defaultButton);
|
||||
@@ -125,18 +125,12 @@ namespace LibationAvalonia
|
||||
dialog.ControlToFocusOnShow = dialog.FindControl<Control>(defaultButton.ToString());
|
||||
dialog.CanResize = false;
|
||||
dialog.WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
var tbx = dialog.FindControl<TextBlock>("messageTextBlock");
|
||||
var tbx = dialog.messageTextBlock;
|
||||
|
||||
tbx.MinWidth = vm.TextBlockMinWidth;
|
||||
tbx.Text = message;
|
||||
|
||||
var thisScreen = owner.Screens?.ScreenFromVisual(owner);
|
||||
|
||||
var maxSize
|
||||
= thisScreen is null ? owner.ClientSize
|
||||
: new Size(0.20 * thisScreen.Bounds.Width, 0.9 * thisScreen.Bounds.Height - 55);
|
||||
|
||||
var desiredMax = new Size(maxSize.Width, maxSize.Height);
|
||||
var desiredMax = GetMaxMessageBoxSizeFromOwner(owner);
|
||||
|
||||
tbx.Measure(desiredMax);
|
||||
|
||||
@@ -152,13 +146,34 @@ namespace LibationAvalonia
|
||||
dialog.Width = dialog.MinWidth;
|
||||
return dialog;
|
||||
}
|
||||
private static async Task<DialogResult> DisplayWindow(DialogWindow toDisplay, Window owner)
|
||||
|
||||
private static Size GetMaxMessageBoxSizeFromOwner(TopLevel? owner)
|
||||
{
|
||||
if (owner is null && App.Current is IClassicDesktopStyleApplicationLifetime lt)
|
||||
{
|
||||
//The Windows enumeration will only contain active (non-disposed) windows.
|
||||
//If none are available, the last disposed window may still be in MainWindow
|
||||
//Just be careful what you use it for. It will still have Screens, but
|
||||
//ScreenFromTopLevel can't be used on macOS.
|
||||
owner = lt.Windows.FirstOrDefault() ?? lt.MainWindow;
|
||||
}
|
||||
if (owner?.Screens is Screens screens)
|
||||
{
|
||||
var mainScreen = owner?.PlatformImpl is null ? screens.Primary : screens.ScreenFromTopLevel(owner);
|
||||
if (mainScreen is not null)
|
||||
return new Size(0.20 * mainScreen.Bounds.Width, 0.9 * mainScreen.Bounds.Height - 55);
|
||||
}
|
||||
|
||||
return owner?.ClientSize ?? new Size(800, 600);
|
||||
}
|
||||
|
||||
private static async Task<DialogResult> DisplayWindow(DialogWindow toDisplay, Window? owner)
|
||||
{
|
||||
if (owner is null)
|
||||
{
|
||||
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
if (desktop.MainWindow.IsLoaded)
|
||||
if (desktop.MainWindow?.IsLoaded is true)
|
||||
return await toDisplay.ShowDialog<DialogResult>(desktop.MainWindow);
|
||||
else
|
||||
{
|
||||
@@ -185,7 +200,6 @@ namespace LibationAvalonia
|
||||
window.Close();
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@ public class ChardonnayTheme : IUpdatable, ICloneable
|
||||
/// <summary> Invoke <see cref="IUpdatable.Updated"/> </summary>
|
||||
public void Save() => Updated?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
public Color GetColor(string? themeVariant, string itemName)
|
||||
public Color GetColor(LibationFileManager.Configuration.Theme themeVariant, string itemName)
|
||||
=> GetColor(FromVariantName(themeVariant), itemName);
|
||||
|
||||
public Color GetColor(ThemeVariant themeVariant, string itemName)
|
||||
@@ -46,7 +46,7 @@ public class ChardonnayTheme : IUpdatable, ICloneable
|
||||
return ThemeColors[themeVariant].TryGetValue(itemName, out var color) ? color : default;
|
||||
}
|
||||
|
||||
public ChardonnayTheme SetColor(string? themeVariant, Expression<Func<ColorPaletteResources, Color>> colorSelector, Color color)
|
||||
public ChardonnayTheme SetColor(LibationFileManager.Configuration.Theme themeVariant, Expression<Func<ColorPaletteResources, Color>> colorSelector, Color color)
|
||||
=> SetColor(FromVariantName(themeVariant), colorSelector, color);
|
||||
|
||||
public ChardonnayTheme SetColor(ThemeVariant themeVariant, Expression<Func<ColorPaletteResources, Color>> colorSelector, Color color)
|
||||
@@ -59,7 +59,7 @@ public class ChardonnayTheme : IUpdatable, ICloneable
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChardonnayTheme SetColor(string? themeVariant, string itemName, Color itemColor)
|
||||
public ChardonnayTheme SetColor(LibationFileManager.Configuration.Theme themeVariant, string itemName, Color itemColor)
|
||||
=> SetColor(FromVariantName(themeVariant), itemName, itemColor);
|
||||
|
||||
public ChardonnayTheme SetColor(ThemeVariant themeVariant, string itemName, Color itemColor)
|
||||
@@ -69,7 +69,7 @@ public class ChardonnayTheme : IUpdatable, ICloneable
|
||||
return this;
|
||||
}
|
||||
|
||||
public FrozenDictionary<string, Color> GetThemeColors(string? themeVariant)
|
||||
public FrozenDictionary<string, Color> GetThemeColors(LibationFileManager.Configuration.Theme themeVariant)
|
||||
=> GetThemeColors(FromVariantName(themeVariant));
|
||||
|
||||
public FrozenDictionary<string, Color> GetThemeColors(ThemeVariant themeVariant)
|
||||
@@ -78,7 +78,7 @@ public class ChardonnayTheme : IUpdatable, ICloneable
|
||||
return ThemeColors[themeVariant].ToFrozenDictionary();
|
||||
}
|
||||
|
||||
public void ApplyTheme(string? themeVariant)
|
||||
public void ApplyTheme(LibationFileManager.Configuration.Theme themeVariant)
|
||||
=> ApplyTheme(FromVariantName(themeVariant));
|
||||
|
||||
public void ApplyTheme(ThemeVariant themeVariant)
|
||||
@@ -195,11 +195,11 @@ public class ChardonnayTheme : IUpdatable, ICloneable
|
||||
throw new InvalidOperationException("FluentTheme.Palettes only supports Light and Dark variants.");
|
||||
}
|
||||
|
||||
private static ThemeVariant FromVariantName(string? variantName)
|
||||
private static ThemeVariant FromVariantName(LibationFileManager.Configuration.Theme variantName)
|
||||
=> variantName switch
|
||||
{
|
||||
nameof(ThemeVariant.Dark) => ThemeVariant.Dark,
|
||||
nameof(ThemeVariant.Light) => ThemeVariant.Light,
|
||||
LibationFileManager.Configuration.Theme.Dark => ThemeVariant.Dark,
|
||||
LibationFileManager.Configuration.Theme.Light => ThemeVariant.Light,
|
||||
// "System"
|
||||
_ => ThemeVariant.Default
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using ApplicationServices;
|
||||
using Avalonia.Threading;
|
||||
using DataLayer;
|
||||
using LibationFileManager;
|
||||
using ReactiveUI;
|
||||
@@ -11,7 +10,7 @@ namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
partial class MainVM
|
||||
{
|
||||
private Task<LibraryCommands.LibraryStats>? updateCountsTask;
|
||||
private System.ComponentModel.BackgroundWorker updateCountsBw = new();
|
||||
|
||||
/// <summary> The "Begin Book and PDF Backup" menu item header text </summary>
|
||||
public string BookBackupsToolStripText { get; private set; } = "Begin Book and PDF Backups: 0";
|
||||
@@ -46,20 +45,40 @@ namespace LibationAvalonia.ViewModels
|
||||
//Pass null to the setup count to get the whole library.
|
||||
LibraryCommands.BookUserDefinedItemCommitted += async (_, _)
|
||||
=> await SetBackupCountsAsync(null);
|
||||
|
||||
updateCountsBw.DoWork += UpdateCountsBw_DoWork;
|
||||
updateCountsBw.RunWorkerCompleted += UpdateCountsBw_Completed; ;
|
||||
}
|
||||
|
||||
|
||||
private bool runBackupCountsAgain;
|
||||
|
||||
public async Task SetBackupCountsAsync(IEnumerable<LibraryBook>? libraryBooks)
|
||||
{
|
||||
if (updateCountsTask?.IsCompleted ?? true)
|
||||
{
|
||||
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts(libraryBooks));
|
||||
var stats = await updateCountsTask;
|
||||
await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats);
|
||||
runBackupCountsAgain = true;
|
||||
|
||||
if (Configuration.Instance.AutoDownloadEpisodes
|
||||
&& stats.PendingBooks + stats.pdfsNotDownloaded > 0)
|
||||
await Dispatcher.UIThread.InvokeAsync(BackupAllBooks);
|
||||
if (!updateCountsBw.IsBusy)
|
||||
updateCountsBw.RunWorkerAsync(libraryBooks);
|
||||
}
|
||||
|
||||
private void UpdateCountsBw_DoWork(object? sender, System.ComponentModel.DoWorkEventArgs e)
|
||||
{
|
||||
while (runBackupCountsAgain)
|
||||
{
|
||||
runBackupCountsAgain = false;
|
||||
e.Result = LibraryCommands.GetCounts(e.Argument as IEnumerable<LibraryBook>);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateCountsBw_Completed(object? sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
|
||||
{
|
||||
if (e.Result is not LibraryCommands.LibraryStats stats)
|
||||
return;
|
||||
LibraryStats = stats;
|
||||
|
||||
if (Configuration.Instance.AutoDownloadEpisodes
|
||||
&& stats.PendingBooks + stats.pdfsNotDownloaded > 0)
|
||||
BackupAllBooks(stats.LibraryBooks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using DataLayer;
|
||||
using LibationUiBase.Forms;
|
||||
using LibationUiBase;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Threading;
|
||||
|
||||
#nullable enable
|
||||
namespace LibationAvalonia.ViewModels
|
||||
@@ -15,14 +16,24 @@ namespace LibationAvalonia.ViewModels
|
||||
{
|
||||
public void Configure_Liberate() { }
|
||||
|
||||
/// <summary> This gets called by the "Begin Book and PDF Backups" menu item. </summary>
|
||||
public async Task BackupAllBooks()
|
||||
{
|
||||
var books = await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking());
|
||||
BackupAllBooks(books);
|
||||
}
|
||||
|
||||
private void BackupAllBooks(IEnumerable<LibraryBook> books)
|
||||
{
|
||||
try
|
||||
{
|
||||
var unliberated = await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking().UnLiberated().ToArray());
|
||||
var unliberated = books.UnLiberated().ToArray();
|
||||
|
||||
if (ProcessQueue.QueueDownloadDecrypt(unliberated))
|
||||
setQueueCollapseState(false);
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
if (ProcessQueue.QueueDownloadDecrypt(unliberated))
|
||||
setQueueCollapseState(false);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -30,9 +41,11 @@ namespace LibationAvalonia.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> This gets called by the "Begin PDF Only Backups" menu item. </summary>
|
||||
public async Task BackupAllPdfs()
|
||||
{
|
||||
if (ProcessQueue.QueueDownloadPdf(await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking())))
|
||||
var books = await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking());
|
||||
if (ProcessQueue.QueueDownloadPdf(books))
|
||||
setQueueCollapseState(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,13 +32,13 @@ namespace LibationAvalonia.ViewModels
|
||||
setQueueCollapseState(collapseState);
|
||||
}
|
||||
|
||||
public async void LiberateClicked(LibraryBook[] libraryBooks)
|
||||
public async void LiberateClicked(System.Collections.Generic.IList<LibraryBook> libraryBooks, Configuration config)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ProcessQueue.QueueDownloadDecrypt(libraryBooks))
|
||||
if (ProcessQueue.QueueDownloadDecrypt(libraryBooks, config))
|
||||
setQueueCollapseState(false);
|
||||
else if (libraryBooks.Length == 1 && libraryBooks[0].Book.AudioExists)
|
||||
else if (libraryBooks.Count == 1 && libraryBooks[0].Book.AudioExists)
|
||||
{
|
||||
// liberated: open explorer to file
|
||||
var filePath = AudibleFileStorage.Audio.GetPath(libraryBooks[0].Book.AudibleProductId);
|
||||
|
||||
@@ -63,11 +63,11 @@ namespace LibationAvalonia.ViewModels
|
||||
public async void ProductsDisplay_VisibleCountChanged(object? sender, int qty)
|
||||
{
|
||||
setVisibleCount(qty);
|
||||
await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
||||
await setLiberatedVisibleMenuItemAsync();
|
||||
}
|
||||
|
||||
private async void setLiberatedVisibleMenuItemAsync(object? _, object __)
|
||||
=> await Dispatcher.UIThread.InvokeAsync(setLiberatedVisibleMenuItem);
|
||||
=> await setLiberatedVisibleMenuItemAsync();
|
||||
|
||||
|
||||
public void LiberateVisible()
|
||||
@@ -191,10 +191,11 @@ namespace LibationAvalonia.ViewModels
|
||||
await visibleLibraryBooks.RemoveBooksAsync();
|
||||
}
|
||||
|
||||
private void setLiberatedVisibleMenuItem()
|
||||
private async Task setLiberatedVisibleMenuItemAsync()
|
||||
{
|
||||
var libraryStats = LibraryCommands.GetCounts(ProductsDisplay.GetVisibleBookEntries());
|
||||
setVisibleNotLiberatedCount(libraryStats.PendingBooks);
|
||||
var visible = ProductsDisplay.GetVisibleBookEntries();
|
||||
var libraryStats = await Task.Run(() => LibraryCommands.GetCounts(visible));
|
||||
await Dispatcher.UIThread.InvokeAsync(() => setVisibleNotLiberatedCount(libraryStats.PendingBooks));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,18 +145,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public string StripAudibleBrandAudioTip => Configuration.GetHelpText(nameof(StripAudibleBrandAudio));
|
||||
public bool StripUnabridged { get; set; }
|
||||
public string StripUnabridgedTip => Configuration.GetHelpText(nameof(StripUnabridged));
|
||||
public bool DecryptToLossy {
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref field, value);
|
||||
if (DecryptToLossy && SpatialAudioCodec.Value is Configuration.SpatialCodec.AC_4)
|
||||
{
|
||||
SpatialAudioCodec = SpatialAudioCodecs[0];
|
||||
this.RaisePropertyChanged(nameof(SpatialAudioCodec));
|
||||
}
|
||||
}
|
||||
}
|
||||
public bool DecryptToLossy { get => field; set => this.RaiseAndSetIfChanged(ref field, value); }
|
||||
public string DecryptToLossyTip => Configuration.GetHelpText(nameof(DecryptToLossy));
|
||||
public bool MoveMoovToBeginning { get; set; }
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
AutoScan = config.AutoScan;
|
||||
ShowImportedStats = config.ShowImportedStats;
|
||||
ImportEpisodes = config.ImportEpisodes;
|
||||
ImportPlusTitles = config.ImportPlusTitles;
|
||||
DownloadEpisodes = config.DownloadEpisodes;
|
||||
AutoDownloadEpisodes = config.AutoDownloadEpisodes;
|
||||
}
|
||||
@@ -19,6 +20,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
config.AutoScan = AutoScan;
|
||||
config.ShowImportedStats = ShowImportedStats;
|
||||
config.ImportEpisodes = ImportEpisodes;
|
||||
config.ImportPlusTitles = ImportPlusTitles;
|
||||
config.DownloadEpisodes = DownloadEpisodes;
|
||||
config.AutoDownloadEpisodes = AutoDownloadEpisodes;
|
||||
}
|
||||
@@ -26,12 +28,14 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public string AutoScanText { get; } = Configuration.GetDescription(nameof(Configuration.AutoScan));
|
||||
public string ShowImportedStatsText { get; } = Configuration.GetDescription(nameof(Configuration.ShowImportedStats));
|
||||
public string ImportEpisodesText { get; } = Configuration.GetDescription(nameof(Configuration.ImportEpisodes));
|
||||
public string ImportPlusTitlesText { get; } = Configuration.GetDescription(nameof(Configuration.ImportPlusTitles));
|
||||
public string DownloadEpisodesText { get; } = Configuration.GetDescription(nameof(Configuration.DownloadEpisodes));
|
||||
public string AutoDownloadEpisodesText { get; } = Configuration.GetDescription(nameof(Configuration.AutoDownloadEpisodes));
|
||||
|
||||
public bool AutoScan { get; set; }
|
||||
public bool ShowImportedStats { get; set; }
|
||||
public bool ImportEpisodes { get; set; }
|
||||
public bool ImportPlusTitles { get; set; }
|
||||
public bool DownloadEpisodes { get; set; }
|
||||
public bool AutoDownloadEpisodes { get; set; }
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
{
|
||||
public class ImportantSettingsVM : ViewModelBase
|
||||
{
|
||||
private string themeVariant;
|
||||
private string initialThemeVariant;
|
||||
private EnumDisplay<Configuration.Theme> themeVariant;
|
||||
private readonly Configuration config;
|
||||
|
||||
public ImportantSettingsVM(Configuration config)
|
||||
@@ -30,9 +29,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
GridScaleFactor = scaleFactorToLinearRange(config.GridScaleFactor);
|
||||
GridFontScaleFactor = scaleFactorToLinearRange(config.GridFontScaleFactor);
|
||||
|
||||
themeVariant = initialThemeVariant = config.GetString(propertyName: nameof(ThemeVariant)) ?? "";
|
||||
if (string.IsNullOrWhiteSpace(initialThemeVariant))
|
||||
themeVariant = initialThemeVariant = "System";
|
||||
themeVariant = Themes.Single(v => v.Value == config.ThemeVariant);
|
||||
}
|
||||
|
||||
public void SaveSettings(Configuration config)
|
||||
@@ -91,7 +88,10 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public string GridScaleFactorText { get; } = Configuration.GetDescription(nameof(Configuration.GridScaleFactor));
|
||||
public string GridFontScaleFactorText { get; } = Configuration.GetDescription(nameof(Configuration.GridFontScaleFactor));
|
||||
public string BetaOptInText { get; } = Configuration.GetDescription(nameof(Configuration.BetaOptIn));
|
||||
public string[] Themes { get; } = { "System", nameof(Avalonia.Styling.ThemeVariant.Light), nameof(Avalonia.Styling.ThemeVariant.Dark) };
|
||||
public EnumDisplay<Configuration.Theme>[] Themes { get; }
|
||||
= Enum.GetValues<Configuration.Theme>()
|
||||
.Select(v => new EnumDisplay<Configuration.Theme>(v))
|
||||
.ToArray();
|
||||
|
||||
public string BooksDirectory { get; set; }
|
||||
public bool SavePodcastsToParentFolder { get; set; }
|
||||
@@ -103,7 +103,7 @@ namespace LibationAvalonia.ViewModels.Settings
|
||||
public bool UseWebView { get; set; }
|
||||
public Serilog.Events.LogEventLevel LoggingLevel { get; set; }
|
||||
|
||||
public string ThemeVariant
|
||||
public EnumDisplay<Configuration.Theme> ThemeVariant
|
||||
{
|
||||
get => themeVariant;
|
||||
set => this.RaiseAndSetIfChanged(ref themeVariant, value);
|
||||
|
||||
@@ -173,7 +173,7 @@ namespace LibationAvalonia.Views
|
||||
await ViewModel.BindToGridTask;
|
||||
}
|
||||
|
||||
public void ProductsDisplay_LiberateClicked(object _, LibraryBook[] libraryBook) => ViewModel.LiberateClicked(libraryBook);
|
||||
public void ProductsDisplay_LiberateClicked(object _, IList<LibraryBook> libraryBook, Configuration config) => ViewModel.LiberateClicked(libraryBook, config);
|
||||
public void ProductsDisplay_LiberateSeriesClicked(object _, SeriesEntry series) => ViewModel.LiberateSeriesClicked(series);
|
||||
public void ProductsDisplay_ConvertToMp3Clicked(object _, LibraryBook[] libraryBook) => ViewModel.ConvertToMp3Clicked(libraryBook);
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace LibationAvalonia.Views
|
||||
if (Design.IsDesignMode)
|
||||
{
|
||||
ViewModels.MainVM.Configure_NonUI();
|
||||
DataContext = new ProcessBookViewModel(MockLibraryBook.CreateBook());
|
||||
DataContext = new ProcessBookViewModel(MockLibraryBook.CreateBook(), LibationFileManager.Configuration.Instance);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,42 +38,42 @@ namespace LibationAvalonia.Views
|
||||
var trialBook = MockLibraryBook.CreateBook();
|
||||
List<ProcessBookViewModel> testList = new()
|
||||
{
|
||||
new ProcessBookViewModel(trialBook)
|
||||
new ProcessBookViewModel(trialBook, Configuration.Instance)
|
||||
{
|
||||
Result = ProcessBookResult.FailedAbort,
|
||||
Status = ProcessBookStatus.Failed,
|
||||
},
|
||||
new ProcessBookViewModel(trialBook)
|
||||
new ProcessBookViewModel(trialBook, Configuration.Instance)
|
||||
{
|
||||
Result = ProcessBookResult.FailedSkip,
|
||||
Status = ProcessBookStatus.Failed,
|
||||
},
|
||||
new ProcessBookViewModel(trialBook)
|
||||
new ProcessBookViewModel(trialBook, Configuration.Instance)
|
||||
{
|
||||
Result = ProcessBookResult.FailedRetry,
|
||||
Status = ProcessBookStatus.Failed,
|
||||
},
|
||||
new ProcessBookViewModel(trialBook)
|
||||
new ProcessBookViewModel(trialBook, Configuration.Instance)
|
||||
{
|
||||
Result = ProcessBookResult.ValidationFail,
|
||||
Status = ProcessBookStatus.Failed,
|
||||
},
|
||||
new ProcessBookViewModel(trialBook)
|
||||
new ProcessBookViewModel(trialBook, Configuration.Instance)
|
||||
{
|
||||
Result = ProcessBookResult.Cancelled,
|
||||
Status = ProcessBookStatus.Cancelled,
|
||||
},
|
||||
new ProcessBookViewModel(trialBook)
|
||||
new ProcessBookViewModel(trialBook, Configuration.Instance)
|
||||
{
|
||||
Result = ProcessBookResult.Success,
|
||||
Status = ProcessBookStatus.Completed,
|
||||
},
|
||||
new ProcessBookViewModel(trialBook)
|
||||
new ProcessBookViewModel(trialBook, Configuration.Instance)
|
||||
{
|
||||
Result = ProcessBookResult.None,
|
||||
Status = ProcessBookStatus.Working,
|
||||
},
|
||||
new ProcessBookViewModel(trialBook)
|
||||
new ProcessBookViewModel(trialBook, Configuration.Instance)
|
||||
{
|
||||
Result = ProcessBookResult.None,
|
||||
Status = ProcessBookStatus.Queued,
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace LibationAvalonia.Views
|
||||
{
|
||||
public partial class ProductsDisplay : UserControl
|
||||
{
|
||||
public event EventHandler<LibraryBook[]>? LiberateClicked;
|
||||
public event LiberateClickedHandler? LiberateClicked;
|
||||
public event EventHandler<SeriesEntry>? LiberateSeriesClicked;
|
||||
public event EventHandler<LibraryBook[]>? ConvertToMp3Clicked;
|
||||
public event EventHandler<LibraryBook>? TagsButtonClicked;
|
||||
@@ -298,10 +298,29 @@ namespace LibationAvalonia.Views
|
||||
args.ContextMenuItems.Add(new MenuItem
|
||||
{
|
||||
Header = ctx.DownloadSelectedText,
|
||||
Command = ReactiveCommand.Create(() => LiberateClicked?.Invoke(this, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray()))
|
||||
Command = ReactiveCommand.Create(() => LiberateClicked?.Invoke(this, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray(), Configuration.Instance))
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region Download split by chapters
|
||||
if (entries.Length == 1 && entries[0] is LibraryBookEntry entry3_a)
|
||||
{
|
||||
args.ContextMenuItems.Add(new MenuItem()
|
||||
{
|
||||
Header = ctx.DownloadAsChapters,
|
||||
IsEnabled = ctx.DownloadAsChaptersEnabled,
|
||||
Command = ReactiveCommand.Create(() =>
|
||||
{
|
||||
var config = Configuration.Instance.CreateEphemeralCopy();
|
||||
config.AllowLibationFixup = config.SplitFilesByChapter = true;
|
||||
var books = ctx.LibraryBookEntries.Select(e => e.LibraryBook).Where(lb => lb.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error).ToList();
|
||||
//No need to persist BookStatus changes. They only needs to last long for the files to start downloading
|
||||
books.ForEach(b => b.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated);
|
||||
LiberateClicked?.Invoke(this, [entry3_a.LibraryBook], config);
|
||||
})
|
||||
});
|
||||
}
|
||||
#endregion
|
||||
#region Convert to Mp3
|
||||
|
||||
@@ -329,7 +348,7 @@ namespace LibationAvalonia.Views
|
||||
entry4.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated;
|
||||
if (entry4.Book.HasPdf)
|
||||
entry4.Book.UserDefinedItem.SetPdfStatus(LiberatedStatus.NotLiberated);
|
||||
LiberateClicked?.Invoke(this, [entry4.LibraryBook]);
|
||||
LiberateClicked?.Invoke(this, [entry4.LibraryBook], Configuration.Instance);
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -512,7 +531,7 @@ namespace LibationAvalonia.Views
|
||||
}
|
||||
else if (button.DataContext is LibraryBookEntry lbEntry)
|
||||
{
|
||||
LiberateClicked?.Invoke(this, [lbEntry.LibraryBook]);
|
||||
LiberateClicked?.Invoke(this, [lbEntry.LibraryBook], Configuration.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ namespace LibationCli
|
||||
|
||||
[Option(shortName: 'f', longName: "force", Required = false, Default = false, HelpText = "Force the book to re-download")]
|
||||
public bool Force { get; set; }
|
||||
|
||||
|
||||
[Option(shortName: 'l', longName: "license", Required = false, Default = null, HelpText = "A license file from the get-license command. Either a file path or dash ('-') to read from standard input.")]
|
||||
public string? LicenseInput { get; set; }
|
||||
|
||||
protected override async Task ProcessAsync()
|
||||
{
|
||||
@@ -32,40 +36,9 @@ namespace LibationCli
|
||||
return;
|
||||
}
|
||||
|
||||
if (Console.IsInputRedirected)
|
||||
if (LicenseInput is string licenseInput)
|
||||
{
|
||||
Console.WriteLine("Reading license file from standard input.");
|
||||
using var reader = new StreamReader(Console.OpenStandardInput());
|
||||
var stdIn = await reader.ReadToEndAsync();
|
||||
try
|
||||
{
|
||||
|
||||
var jsonSettings = new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
Converters = [new StringEnumConverter(), new ByteArrayHexConverter()]
|
||||
};
|
||||
var licenseInfo = JsonConvert.DeserializeObject<DownloadOptions.LicenseInfo>(stdIn, jsonSettings);
|
||||
|
||||
if (licenseInfo?.ContentMetadata?.ContentReference?.Asin is not string asin)
|
||||
{
|
||||
Console.Error.WriteLine("Error: License file is missing ASIN information.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (DbContexts.GetLibraryBook_Flat_NoTracking(asin) is not LibraryBook libraryBook)
|
||||
{
|
||||
Console.Error.WriteLine($"Book not found with asin={asin}");
|
||||
return;
|
||||
}
|
||||
|
||||
SetDownloadedStatus(libraryBook);
|
||||
await ProcessOneAsync(GetProcessable(licenseInfo), libraryBook, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.Error.WriteLine("Error: Failed to read license file from standard input. Please ensure the input is a valid license file in JSON format.");
|
||||
}
|
||||
await LiberateFromLicense(licenseInput);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -73,6 +46,87 @@ namespace LibationCli
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LiberateFromLicense(string licPath)
|
||||
{
|
||||
var licenseInfo = licPath is "-" ? ReadLicenseFromStdIn()
|
||||
: ReadLicenseFromFile(licPath);
|
||||
|
||||
if (licenseInfo is null)
|
||||
return;
|
||||
|
||||
if (licenseInfo?.ContentMetadata?.ContentReference?.Asin is not string asin)
|
||||
{
|
||||
Console.Error.WriteLine("Error: License file is missing ASIN information.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (DbContexts.GetLibraryBook_Flat_NoTracking(asin) is not LibraryBook libraryBook)
|
||||
{
|
||||
Console.Error.WriteLine($"Book not found with asin={asin}");
|
||||
return;
|
||||
}
|
||||
|
||||
SetDownloadedStatus(libraryBook);
|
||||
await ProcessOneAsync(GetProcessable(licenseInfo), libraryBook, true);
|
||||
}
|
||||
|
||||
private static DownloadOptions.LicenseInfo? ReadLicenseFromFile(string licFile)
|
||||
{
|
||||
if (!File.Exists(licFile))
|
||||
{
|
||||
Console.Error.WriteLine("File does not exist: " + licFile);
|
||||
return null;
|
||||
}
|
||||
|
||||
Console.WriteLine("Reading license from file.");
|
||||
try
|
||||
{
|
||||
var serializer = CreateLicenseInfoSerializer();
|
||||
using var reader = new JsonTextReader(new StreamReader(licFile));
|
||||
return serializer.Deserialize<DownloadOptions.LicenseInfo>(reader);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Failed to read license file: {@LicenseFile}", licFile);
|
||||
Console.Error.WriteLine("Error: Failed to read license file. Please ensure the file is a valid license file in JSON format.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static DownloadOptions.LicenseInfo? ReadLicenseFromStdIn()
|
||||
{
|
||||
if (!Console.IsInputRedirected)
|
||||
{
|
||||
Console.Error.WriteLine("Ther is nothing in standard input to read.");
|
||||
return null;
|
||||
}
|
||||
|
||||
Console.WriteLine("Reading license from standard input.");
|
||||
try
|
||||
{
|
||||
var serializer = CreateLicenseInfoSerializer();
|
||||
using var reader = new JsonTextReader(new StreamReader(Console.OpenStandardInput()));
|
||||
return serializer.Deserialize<DownloadOptions.LicenseInfo>(reader);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "Failed to read license from standard input");
|
||||
Console.Error.WriteLine("Error: Failed to read license file from standard input. Please ensure the input is a valid license file in JSON format.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static JsonSerializer CreateLicenseInfoSerializer()
|
||||
{
|
||||
var jsonSettings = new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
Converters = [new StringEnumConverter(), new ByteArrayHexConverter()]
|
||||
};
|
||||
|
||||
return JsonSerializer.Create(jsonSettings);
|
||||
}
|
||||
|
||||
private Processable GetProcessable(DownloadOptions.LicenseInfo? licenseInfo = null)
|
||||
=> PdfOnly ? CreateProcessable<DownloadPdf>() : CreateBackupBook(licenseInfo);
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ namespace LibationCli
|
||||
public IEnumerable<string>? Asins { get; set; }
|
||||
|
||||
protected static TProcessable CreateProcessable<TProcessable>(EventHandler<LibraryBook>? completedAction = null)
|
||||
where TProcessable : Processable, new()
|
||||
where TProcessable : Processable, IProcessable<TProcessable>
|
||||
{
|
||||
var progressBar = new ConsoleProgressBar(Console.Out);
|
||||
var strProc = new TProcessable();
|
||||
var strProc = TProcessable.Create(Configuration.Instance);
|
||||
LibraryBook? currentLibraryBook = null;
|
||||
|
||||
strProc.Begin += (o, e) =>
|
||||
|
||||
@@ -138,15 +138,35 @@ namespace LibationFileManager
|
||||
protected override LongPath? GetFilePathCustom(string productId)
|
||||
=> GetFilePathsCustom(productId).FirstOrDefault();
|
||||
|
||||
//GetFilePathsCustom gets called for every book during LibraryCommands.GetCounts().
|
||||
//Cache the results for a short time to avoid excessive file system hits.
|
||||
private DateTime lastDlInProgressEnumeration;
|
||||
private static TimeSpan dlInProgressCacheTime = TimeSpan.FromSeconds(10);
|
||||
private IEnumerable<LongPath>? dlInProgressFilesCache;
|
||||
|
||||
protected override List<LongPath> GetFilePathsCustom(string productId)
|
||||
{
|
||||
if (DownloadsInProgressDirectory is not LongPath dlFolder)
|
||||
return [];
|
||||
|
||||
var regex = GetBookSearchRegex(productId);
|
||||
return FileUtility
|
||||
.SaferEnumerateFiles(dlFolder, "*.*", SearchOption.AllDirectories)
|
||||
.Where(s => regex.IsMatch(s)).ToList();
|
||||
|
||||
if (DateTime.UtcNow - lastDlInProgressEnumeration > dlInProgressCacheTime)
|
||||
{
|
||||
dlInProgressFilesCache = null;
|
||||
}
|
||||
|
||||
if (dlInProgressFilesCache is null)
|
||||
{
|
||||
dlInProgressFilesCache
|
||||
= FileUtility
|
||||
.SaferEnumerateFiles(dlFolder, "*.*", SearchOption.AllDirectories)
|
||||
.ToArray();
|
||||
|
||||
lastDlInProgressEnumeration = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
return dlInProgressFilesCache.Where(s => regex.IsMatch(s)).ToList();
|
||||
}
|
||||
|
||||
public bool Exists(string productId) => GetFilePath(productId) is not null;
|
||||
|
||||
@@ -86,8 +86,6 @@ namespace LibationFileManager
|
||||
The Dolby Digital Plus (E-AC-3) codec is more widely
|
||||
supported than the AC-4 codec, but E-AC-3 files are
|
||||
much larger than AC-4 files.
|
||||
|
||||
AC-4 cannot be converted to MP3.
|
||||
""" },
|
||||
{nameof(UseWidevine), """
|
||||
Some audiobooks are only delivered in the highest
|
||||
|
||||
@@ -154,6 +154,9 @@ namespace LibationFileManager
|
||||
set => SetString(value);
|
||||
}
|
||||
|
||||
[Description("Libation's display color theme")]
|
||||
public Theme ThemeVariant { get => GetNonString(defaultValue: Theme.System); set => SetNonString(value); }
|
||||
|
||||
[Description("Allow Libation to fix up audiobook metadata")]
|
||||
public bool AllowLibationFixup { get => GetNonString(defaultValue: true); set => SetNonString(value); }
|
||||
|
||||
@@ -261,6 +264,14 @@ namespace LibationFileManager
|
||||
Ignore = 3
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum Theme
|
||||
{
|
||||
System = 0,
|
||||
Light = 1,
|
||||
Dark = 2
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum DateTimeSource
|
||||
{
|
||||
@@ -319,6 +330,9 @@ namespace LibationFileManager
|
||||
[Description("Import episodes? (eg: podcasts) When unchecked, episodes will not be imported into Libation.")]
|
||||
public bool ImportEpisodes { get => GetNonString(defaultValue: true); set => SetNonString(value); }
|
||||
|
||||
[Description("Import Audible Plus books (books you do not own)? When unchecked, Audible Plus books will not be imported into Libation.")]
|
||||
public bool ImportPlusTitles { get => GetNonString(defaultValue: true); set => SetNonString(value); }
|
||||
|
||||
[Description("Download episodes? (eg: podcasts). When unchecked, episodes already in Libation will not be downloaded.")]
|
||||
public bool DownloadEpisodes { get => GetNonString(defaultValue: true); set => SetNonString(value); }
|
||||
|
||||
|
||||
@@ -32,7 +32,14 @@ namespace LibationFileManager
|
||||
}
|
||||
private static readonly Configuration s_SingletonInstance = new();
|
||||
public static Configuration Instance { get; private set; } = s_SingletonInstance;
|
||||
public bool IsEphemeralInstance => JsonBackedDictionary is EphemeralDictionary;
|
||||
|
||||
public Configuration CreateEphemeralCopy()
|
||||
{
|
||||
var copy = new Configuration();
|
||||
copy.LoadEphemeralSettings(Settings.GetJObject());
|
||||
return copy;
|
||||
}
|
||||
|
||||
private Configuration() { }
|
||||
#endregion
|
||||
|
||||
@@ -17,6 +17,7 @@ internal class EphemeralDictionary : IJsonBackedDictionary
|
||||
JsonObject = dataStore;
|
||||
}
|
||||
|
||||
public JObject GetJObject() => (JObject)JsonObject.DeepClone();
|
||||
public bool Exists(string propertyName)
|
||||
=> JsonObject.ContainsKey(propertyName);
|
||||
public string? GetString(string propertyName, string? defaultValue = null)
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace LibationFileManager
|
||||
public static class SqliteStorage
|
||||
{
|
||||
// not customizable. don't move to config
|
||||
private static string databasePath => Path.Combine(Configuration.Instance.LibationFiles.Location, "LibationContext.db");
|
||||
public static string ConnectionString => $"Data Source={databasePath};Foreign Keys=False;Pooling=False;";
|
||||
public static string DatabasePath => Path.Combine(Configuration.Instance.LibationFiles.Location, "LibationContext.db");
|
||||
public static string ConnectionString => $"Data Source={DatabasePath};Foreign Keys=False;Pooling=False;";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace LibationUiBase.GridView;
|
||||
|
||||
public delegate void LiberateClickedHandler(object sender, System.Collections.Generic.IList<LibraryBook> libraryBooks, Configuration config);
|
||||
public class GridContextMenu
|
||||
{
|
||||
public string CopyCellText => $"{Accelerator}Copy Cell Contents";
|
||||
@@ -20,6 +21,7 @@ public class GridContextMenu
|
||||
public string LocateFileDialogTitle => $"Locate the audio file for '{GridEntries[0].Book.TitleWithSubtitle}'";
|
||||
public string LocateFileErrorMessage => "Error saving book's location";
|
||||
public string ConvertToMp3Text => $"{Accelerator}Convert to Mp3";
|
||||
public string DownloadAsChapters => $"Download {Accelerator}split by chapters";
|
||||
public string ReDownloadText => "Re-download this audiobook";
|
||||
public string DownloadSelectedText => "Download selected audiobooks";
|
||||
public string EditTemplatesText => "Edit Templates";
|
||||
@@ -33,6 +35,7 @@ public class GridContextMenu
|
||||
public bool SetDownloadedEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated || ge.Liberate.IsSeries);
|
||||
public bool SetNotDownloadedEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus != LiberatedStatus.NotLiberated || ge.Liberate.IsSeries);
|
||||
public bool ConvertToMp3Enabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
|
||||
public bool DownloadAsChaptersEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error);
|
||||
public bool ReDownloadEnabled => LibraryBookEntries.Any(ge => ge.Book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated);
|
||||
|
||||
private GridEntry[] GridEntries { get; }
|
||||
|
||||
@@ -43,6 +43,7 @@ public enum ProcessBookStatus
|
||||
public class ProcessBookViewModel : ReactiveObject
|
||||
{
|
||||
public LibraryBook LibraryBook { get; protected set; }
|
||||
public Configuration Configuration { get; }
|
||||
|
||||
#region Properties exposed to the view
|
||||
public ProcessBookResult Result { get => field; set { RaiseAndSetIfChanged(ref field, value); RaisePropertyChanged(nameof(StatusText)); } }
|
||||
@@ -95,9 +96,10 @@ public class ProcessBookViewModel : ReactiveObject
|
||||
/// <summary> A series of Processable actions to perform on this book </summary>
|
||||
protected Queue<Func<Processable>> Processes { get; } = new();
|
||||
|
||||
public ProcessBookViewModel(LibraryBook libraryBook)
|
||||
public ProcessBookViewModel(LibraryBook libraryBook, Configuration configuration)
|
||||
{
|
||||
LibraryBook = libraryBook;
|
||||
Configuration = configuration;
|
||||
|
||||
Title = LibraryBook.Book.TitleWithSubtitle;
|
||||
Author = LibraryBook.Book.AuthorNames;
|
||||
@@ -203,9 +205,9 @@ public class ProcessBookViewModel : ReactiveObject
|
||||
public ProcessBookViewModel AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
|
||||
public ProcessBookViewModel AddConvertToMp3() => AddProcessable<ConvertToMp3>();
|
||||
|
||||
private ProcessBookViewModel AddProcessable<T>() where T : Processable, new()
|
||||
private ProcessBookViewModel AddProcessable<T>() where T : Processable, IProcessable<T>
|
||||
{
|
||||
Processes.Enqueue(() => new T());
|
||||
Processes.Enqueue(() => T.Create(Configuration));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -260,7 +262,7 @@ public class ProcessBookViewModel : ReactiveObject
|
||||
private byte[] AudioDecodable_RequestCoverArt(object? sender, EventArgs e)
|
||||
{
|
||||
var quality
|
||||
= Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
|
||||
= Configuration.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
|
||||
? new PictureDefinition(LibraryBook.Book.PictureLarge, PictureSize.Native)
|
||||
: new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500);
|
||||
|
||||
@@ -345,7 +347,7 @@ public class ProcessBookViewModel : ReactiveObject
|
||||
const DialogResult SkipResult = DialogResult.Ignore;
|
||||
LogError($"ERROR. All books have not been processed. Book failed: {libraryBook.Book}");
|
||||
|
||||
DialogResult? dialogResult = Configuration.Instance.BadBook switch
|
||||
DialogResult? dialogResult = Configuration.BadBook switch
|
||||
{
|
||||
Configuration.BadBookAction.Abort => DialogResult.Abort,
|
||||
Configuration.BadBookAction.Retry => DialogResult.Retry,
|
||||
|
||||
@@ -27,7 +27,7 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
{
|
||||
Queue.QueuedCountChanged += Queue_QueuedCountChanged;
|
||||
Queue.CompletedCountChanged += Queue_CompletedCountChanged;
|
||||
SpeedLimit = LibationFileManager.Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
|
||||
SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
|
||||
}
|
||||
|
||||
public int CompletedCount { get => field; private set { RaiseAndSetIfChanged(ref field, value); RaisePropertyChanged(nameof(AnyCompleted)); } }
|
||||
@@ -48,7 +48,7 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
set
|
||||
{
|
||||
var newValue = Math.Min(999 * 1024 * 1024, (long)Math.Ceiling(value * 1024 * 1024));
|
||||
var config = LibationFileManager.Configuration.Instance;
|
||||
var config = Configuration.Instance;
|
||||
config.DownloadSpeedLimit = newValue;
|
||||
|
||||
_speedLimit
|
||||
@@ -57,6 +57,8 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
: 0;
|
||||
|
||||
config.DownloadSpeedLimit = (long)(_speedLimit * 1024 * 1024);
|
||||
if (Queue.Current is ProcessBookViewModel currentBook)
|
||||
currentBook.Configuration.DownloadSpeedLimit = config.DownloadSpeedLimit;
|
||||
|
||||
SpeedLimitIncrement = _speedLimit > 100 ? 10
|
||||
: _speedLimit > 10 ? 1
|
||||
@@ -89,24 +91,26 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
|
||||
#region Add Books to Queue
|
||||
|
||||
public bool QueueDownloadPdf(IList<LibraryBook> libraryBooks)
|
||||
public bool QueueDownloadPdf(IList<LibraryBook> libraryBooks, Configuration? config = null)
|
||||
{
|
||||
if (!IsBooksDirectoryValid())
|
||||
config ??= Configuration.Instance;
|
||||
if (!IsBooksDirectoryValid(config))
|
||||
return false;
|
||||
|
||||
var needsPdf = libraryBooks.Where(lb => lb.NeedsPdfDownload()).ToArray();
|
||||
if (needsPdf.Length > 0)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin download {count} pdfs", needsPdf.Length);
|
||||
AddDownloadPdf(needsPdf);
|
||||
AddDownloadPdf(needsPdf, config);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool QueueConvertToMp3(IList<LibraryBook> libraryBooks)
|
||||
public bool QueueConvertToMp3(IList<LibraryBook> libraryBooks, Configuration? config = null)
|
||||
{
|
||||
if (!IsBooksDirectoryValid())
|
||||
config ??= Configuration.Instance;
|
||||
if (!IsBooksDirectoryValid(config))
|
||||
return false;
|
||||
|
||||
//Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing.
|
||||
@@ -116,15 +120,16 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
if (preLiberated.Length == 1)
|
||||
RemoveCompleted(preLiberated[0]);
|
||||
Serilog.Log.Logger.Information("Begin convert {count} books to mp3", preLiberated.Length);
|
||||
AddConvertMp3(preLiberated);
|
||||
AddConvertMp3(preLiberated, config);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool QueueDownloadDecrypt(IList<LibraryBook> libraryBooks)
|
||||
public bool QueueDownloadDecrypt(IList<LibraryBook> libraryBooks, Configuration? config = null)
|
||||
{
|
||||
if (!IsBooksDirectoryValid())
|
||||
config ??= Configuration.Instance;
|
||||
if (!IsBooksDirectoryValid(config))
|
||||
return false;
|
||||
|
||||
if (libraryBooks.Count == 1)
|
||||
@@ -137,14 +142,14 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
{
|
||||
RemoveCompleted(item);
|
||||
Serilog.Log.Logger.Information("Begin single library book backup of {libraryBook}", item);
|
||||
AddDownloadDecrypt([item]);
|
||||
AddDownloadDecrypt([item], config);
|
||||
return true;
|
||||
}
|
||||
else if (item.NeedsPdfDownload())
|
||||
{
|
||||
RemoveCompleted(item);
|
||||
Serilog.Log.Logger.Information("Begin single pdf backup of {libraryBook}", item);
|
||||
AddDownloadPdf([item]);
|
||||
AddDownloadPdf([item], config);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -155,16 +160,16 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
if (toLiberate.Length > 0)
|
||||
{
|
||||
Serilog.Log.Logger.Information("Begin backup of {count} library books", toLiberate.Length);
|
||||
AddDownloadDecrypt(toLiberate);
|
||||
AddDownloadDecrypt(toLiberate, config);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsBooksDirectoryValid()
|
||||
private bool IsBooksDirectoryValid(Configuration config)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Configuration.Instance.Books))
|
||||
if (string.IsNullOrWhiteSpace(config.Books))
|
||||
{
|
||||
Serilog.Log.Logger.Error("Books location is not set in configuration.");
|
||||
MessageBoxBase.Show(
|
||||
@@ -176,9 +181,9 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
}
|
||||
else if (AudibleFileStorage.BooksDirectory is null)
|
||||
{
|
||||
Serilog.Log.Logger.Error("Failed to create books directory: {@booksDir}", Configuration.Instance.Books);
|
||||
Serilog.Log.Logger.Error("Failed to create books directory: {@booksDir}", config.Books);
|
||||
MessageBoxBase.Show(
|
||||
$"Libation was unable to create the \"Books location\" folder at:\n{Configuration.Instance.Books}\n\nPlease change the Books location in the settings menu.",
|
||||
$"Libation was unable to create the \"Books location\" folder at:\n{config.Books}\n\nPlease change the Books location in the settings menu.",
|
||||
"Failed to Create Books Directory",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
@@ -186,9 +191,9 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
}
|
||||
else if (AudibleFileStorage.DownloadsInProgressDirectory is null)
|
||||
{
|
||||
Serilog.Log.Logger.Error("Failed to create DownloadsInProgressDirectory in {@InProgress}", Configuration.Instance.InProgress);
|
||||
Serilog.Log.Logger.Error("Failed to create DownloadsInProgressDirectory in {@InProgress}", config.InProgress);
|
||||
MessageBoxBase.Show(
|
||||
$"Libation was unable to create the \"Downloads In Progress\" folder in:\n{Configuration.Instance.InProgress}\n\nPlease change the In Progress location in the settings menu.",
|
||||
$"Libation was unable to create the \"Downloads In Progress\" folder in:\n{config.InProgress}\n\nPlease change the In Progress location in the settings menu.",
|
||||
"Failed to Create Downloads In Progress Directory",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
@@ -196,9 +201,9 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
}
|
||||
else if (AudibleFileStorage.DecryptInProgressDirectory is null)
|
||||
{
|
||||
Serilog.Log.Logger.Error("Failed to create DecryptInProgressDirectory in {@InProgress}", Configuration.Instance.InProgress);
|
||||
Serilog.Log.Logger.Error("Failed to create DecryptInProgressDirectory in {@InProgress}", config.InProgress);
|
||||
MessageBoxBase.Show(
|
||||
$"Libation was unable to create the \"Decrypt In Progress\" folder in:\n{Configuration.Instance.InProgress}\n\nPlease change the In Progress location in the settings menu.",
|
||||
$"Libation was unable to create the \"Decrypt In Progress\" folder in:\n{config.InProgress}\n\nPlease change the In Progress location in the settings menu.",
|
||||
"Failed to Create Decrypt In Progress Directory",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
@@ -218,34 +223,34 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
&& entry.Status is ProcessBookStatus.Completed
|
||||
&& Queue.RemoveCompleted(entry);
|
||||
|
||||
private void AddDownloadPdf(IList<LibraryBook> entries)
|
||||
private void AddDownloadPdf(IList<LibraryBook> entries, Configuration config)
|
||||
{
|
||||
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
|
||||
Serilog.Log.Logger.Information("Queueing {count} books for PDF-only download", procs.Length);
|
||||
AddToQueue(procs);
|
||||
|
||||
ProcessBookViewModel Create(LibraryBook entry)
|
||||
=> new ProcessBookViewModel(entry).AddDownloadPdf();
|
||||
=> new ProcessBookViewModel(entry, config).AddDownloadPdf();
|
||||
}
|
||||
|
||||
private void AddDownloadDecrypt(IList<LibraryBook> entries)
|
||||
private void AddDownloadDecrypt(IList<LibraryBook> entries, Configuration config)
|
||||
{
|
||||
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
|
||||
Serilog.Log.Logger.Information("Queueing {count} books ofr download/decrypt", procs.Length);
|
||||
AddToQueue(procs);
|
||||
|
||||
ProcessBookViewModel Create(LibraryBook entry)
|
||||
=> new ProcessBookViewModel(entry).AddDownloadDecryptBook().AddDownloadPdf();
|
||||
=> new ProcessBookViewModel(entry, config).AddDownloadDecryptBook().AddDownloadPdf();
|
||||
}
|
||||
|
||||
private void AddConvertMp3(IList<LibraryBook> entries)
|
||||
private void AddConvertMp3(IList<LibraryBook> entries, Configuration config)
|
||||
{
|
||||
var procs = entries.Where(e => !IsBookInQueue(e)).Select(Create).ToArray();
|
||||
Serilog.Log.Logger.Information("Queueing {count} books for mp3 conversion", procs.Length);
|
||||
AddToQueue(procs);
|
||||
|
||||
ProcessBookViewModel Create(LibraryBook entry)
|
||||
=> new ProcessBookViewModel(entry).AddConvertToMp3();
|
||||
=> new ProcessBookViewModel(entry, config).AddConvertToMp3();
|
||||
}
|
||||
|
||||
private void AddToQueue(IList<ProcessBookViewModel> pbook)
|
||||
@@ -282,7 +287,7 @@ public class ProcessQueueViewModel : ReactiveObject
|
||||
}
|
||||
|
||||
Serilog.Log.Logger.Information("Begin processing queued item: '{item_LibraryBook}'", nextBook.LibraryBook);
|
||||
|
||||
SpeedLimit = nextBook.Configuration.DownloadSpeedLimit / 1024m / 1024;
|
||||
var result = await nextBook.ProcessOneAsync();
|
||||
|
||||
Serilog.Log.Logger.Information("Completed processing queued item: '{item_LibraryBook}' with result: {result}", nextBook.LibraryBook, result);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using System.Windows.Forms;
|
||||
using LibationWinForms.GridView;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
public class AccessibleDataGridViewButtonCell : DataGridViewButtonCell
|
||||
public class AccessibleDataGridViewButtonCell : DataGridViewButtonCell
|
||||
{
|
||||
protected string AccessibilityName { get; }
|
||||
|
||||
@@ -24,7 +25,8 @@ namespace LibationWinForms
|
||||
public AccessibleDataGridViewButtonCell(string accessibilityName) : base()
|
||||
{
|
||||
AccessibilityName = accessibilityName;
|
||||
}
|
||||
FlatStyle = Application.IsDarkModeEnabled ? FlatStyle.Flat : FlatStyle.System;
|
||||
}
|
||||
|
||||
protected class ButtonCellAccessibilityObject : DataGridViewButtonCellAccessibleObject
|
||||
{
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms
|
||||
{
|
||||
public class AccessibleDataGridViewComboBoxCell : DataGridViewComboBoxCell
|
||||
{
|
||||
protected string AccessibilityName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get or set description for accessibility. eg: screen readers. Also sets the ToolTipText
|
||||
/// </summary>
|
||||
protected string AccessibilityDescription
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
ToolTipText = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override AccessibleObject CreateAccessibilityInstance() => new ComboBoxCellAccessibilityObject(this, name: AccessibilityName, description: AccessibilityDescription);
|
||||
|
||||
public AccessibleDataGridViewComboBoxCell(string accessibilityName) : base()
|
||||
{
|
||||
FlatStyle = Application.IsDarkModeEnabled ? FlatStyle.Flat : FlatStyle.Standard;
|
||||
AccessibilityName = accessibilityName;
|
||||
}
|
||||
|
||||
protected class ComboBoxCellAccessibilityObject : DataGridViewComboBoxCellAccessibleObject
|
||||
{
|
||||
private string _name;
|
||||
public override string Name => _name;
|
||||
|
||||
private string _description;
|
||||
public override string Description => _description;
|
||||
|
||||
public ComboBoxCellAccessibilityObject(DataGridViewCell owner, string name, string description) : base(owner)
|
||||
{
|
||||
_name = name;
|
||||
_description = description;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,18 @@ namespace LibationWinForms
|
||||
}
|
||||
}
|
||||
|
||||
public int SelectionStart
|
||||
{
|
||||
get => textBox1.SelectionStart;
|
||||
set => textBox1.SelectionStart = value;
|
||||
}
|
||||
|
||||
protected override void OnGotFocus(EventArgs e)
|
||||
{
|
||||
base.OnGotFocus(e);
|
||||
textBox1.Focus();
|
||||
}
|
||||
|
||||
public ClearableTextBox()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace LibationWinForms.Dialogs
|
||||
InitializeComponent();
|
||||
this.SetLibationIcon();
|
||||
releaseNotesLbl.Text = $"Libation {AppScaffolding.LibationScaffolding.Variety} v{AppScaffolding.LibationScaffolding.BuildVersion}";
|
||||
|
||||
pictureBox1.Image = Application.IsDarkModeEnabled ? Properties.Resources.cheers_dark : Properties.Resources.cheers;
|
||||
rmcrackanLbl.Tag = LibationContributor.PrimaryContributors.Single(c => c.Name == rmcrackanLbl.Text);
|
||||
MBucariLbl.Tag = LibationContributor.PrimaryContributors.Single(c => c.Name == MBucariLbl.Text);
|
||||
|
||||
@@ -22,8 +22,13 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
var label = new LinkLabel { Tag = contributor, Text = contributor.Name, AutoSize = true };
|
||||
label.LinkClicked += ContributorLabel_LinkClicked;
|
||||
label.SetLinkLabelColors();
|
||||
flowLayoutPanel1.Controls.Add(label);
|
||||
}
|
||||
rmcrackanLbl.SetLinkLabelColors();
|
||||
MBucariLbl.SetLinkLabelColors();
|
||||
releaseNotesLbl.SetLinkLabelColors();
|
||||
getLibationLbl.SetLinkLabelColors();
|
||||
|
||||
var toolTip = new ToolTip();
|
||||
toolTip.SetToolTip(releaseNotesLbl, "View Release Notes");
|
||||
|
||||
@@ -31,11 +31,11 @@
|
||||
this.cancelBtn = new System.Windows.Forms.Button();
|
||||
this.saveBtn = new System.Windows.Forms.Button();
|
||||
this.dataGridView1 = new System.Windows.Forms.DataGridView();
|
||||
this.DeleteAccount = new System.Windows.Forms.DataGridViewButtonColumn();
|
||||
this.ExportAccount = new System.Windows.Forms.DataGridViewButtonColumn();
|
||||
this.DeleteAccount = new DeleteColumn();
|
||||
this.ExportAccount = new ExportColumn();
|
||||
this.LibraryScan = new System.Windows.Forms.DataGridViewCheckBoxColumn();
|
||||
this.AccountId = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.Locale = new System.Windows.Forms.DataGridViewComboBoxColumn();
|
||||
this.Locale = new LocaleColumn();
|
||||
this.AccountName = new System.Windows.Forms.DataGridViewTextBoxColumn();
|
||||
this.importBtn = new System.Windows.Forms.Button();
|
||||
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
|
||||
@@ -165,11 +165,11 @@
|
||||
private System.Windows.Forms.Button saveBtn;
|
||||
private System.Windows.Forms.DataGridView dataGridView1;
|
||||
private System.Windows.Forms.Button importBtn;
|
||||
private System.Windows.Forms.DataGridViewButtonColumn DeleteAccount;
|
||||
private System.Windows.Forms.DataGridViewButtonColumn ExportAccount;
|
||||
private DeleteColumn DeleteAccount;
|
||||
private ExportColumn ExportAccount;
|
||||
private System.Windows.Forms.DataGridViewCheckBoxColumn LibraryScan;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn AccountId;
|
||||
private System.Windows.Forms.DataGridViewComboBoxColumn Locale;
|
||||
private LocaleColumn Locale;
|
||||
private System.Windows.Forms.DataGridViewTextBoxColumn AccountName;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ namespace LibationWinForms.Dialogs
|
||||
public AccountsDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
dataGridView1.EnableHeadersVisualStyles = !Application.IsDarkModeEnabled;
|
||||
dataGridView1.Columns[COL_AccountName].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
|
||||
|
||||
populateDropDown();
|
||||
@@ -299,5 +299,55 @@ namespace LibationWinForms.Dialogs
|
||||
ex);
|
||||
}
|
||||
}
|
||||
#region Accessable Columns
|
||||
|
||||
public class DeleteColumn : DataGridViewButtonColumn
|
||||
{
|
||||
public DeleteColumn() : base()
|
||||
{
|
||||
this.CellTemplate = new DeleteColumnCell();
|
||||
}
|
||||
}
|
||||
|
||||
public class ExportColumn : DataGridViewButtonColumn
|
||||
{
|
||||
public ExportColumn() : base()
|
||||
{
|
||||
this.CellTemplate = new ExportColumnCell();
|
||||
}
|
||||
}
|
||||
|
||||
public class LocaleColumn : DataGridViewComboBoxColumn
|
||||
{
|
||||
public LocaleColumn() : base()
|
||||
{
|
||||
this.CellTemplate = new LocaleColumnCell();
|
||||
}
|
||||
}
|
||||
|
||||
public class DeleteColumnCell : AccessibleDataGridViewButtonCell
|
||||
{
|
||||
public DeleteColumnCell() : base("Delete account from Libation")
|
||||
{
|
||||
ToolTipText = AccessibilityName;
|
||||
}
|
||||
}
|
||||
|
||||
public class LocaleColumnCell : AccessibleDataGridViewComboBoxCell
|
||||
{
|
||||
public LocaleColumnCell() : base("Select Audible account region")
|
||||
{
|
||||
ToolTipText = AccessibilityName;
|
||||
}
|
||||
}
|
||||
|
||||
public class ExportColumnCell : AccessibleDataGridViewButtonCell
|
||||
{
|
||||
public ExportColumnCell() : base("Export account to mkb79/audible-cli format")
|
||||
{
|
||||
ToolTipText = AccessibilityName;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using Dinah.Core;
|
||||
using LibationFileManager;
|
||||
@@ -20,26 +21,34 @@ namespace LibationWinForms.Dialogs
|
||||
public LiberatedStatus BookLiberatedStatus { get; private set; }
|
||||
public LiberatedStatus? PdfLiberatedStatus { get; private set; }
|
||||
|
||||
private LibraryBook _libraryBook { get; }
|
||||
private Book Book => _libraryBook.Book;
|
||||
private Book Book => LibraryBook.Book;
|
||||
|
||||
public BookDetailsDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.SetLibationIcon();
|
||||
audibleLink.SetLinkLabelColors();
|
||||
}
|
||||
public BookDetailsDialog(LibraryBook libraryBook) : this()
|
||||
|
||||
public LibraryBook LibraryBook
|
||||
{
|
||||
_libraryBook = ArgumentValidator.EnsureNotNull(libraryBook, nameof(libraryBook));
|
||||
initDetails();
|
||||
initTags();
|
||||
initLiberated();
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
initDetails();
|
||||
initTags();
|
||||
initLiberated();
|
||||
}
|
||||
}
|
||||
|
||||
// 1st draft: lazily cribbed from GridEntry.ctor()
|
||||
private void initDetails()
|
||||
{
|
||||
audibleLink.LinkVisited = false;
|
||||
this.Text = Book.TitleWithSubtitle;
|
||||
dolbyAtmosPb.Visible = Book.IsSpatial;
|
||||
dolbyAtmosPb.Image = Application.IsDarkModeEnabled ? Properties.Resources.Dolby_Atmos_Vertical_80_dark : Properties.Resources.Dolby_Atmos_Vertical_80;
|
||||
|
||||
(_, var picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80));
|
||||
this.coverPb.Image = WinFormsUtil.TryLoadImageOrDefault(picture, PictureSize._80x80);
|
||||
@@ -51,7 +60,7 @@ namespace LibationWinForms.Dialogs
|
||||
Narrator(s): {Book.NarratorNames}
|
||||
Length: {(Book.LengthInMinutes == 0 ? "" : $"{Book.LengthInMinutes / 60} hr {Book.LengthInMinutes % 60} min")}
|
||||
Category: {string.Join(", ", Book.LowestCategoryNames())}
|
||||
Purchase Date: {_libraryBook.DateAdded:d}
|
||||
Purchase Date: {LibraryBook.DateAdded:d}
|
||||
Language: {Book.Language}
|
||||
Audible ID: {Book.AudibleProductId}
|
||||
""";
|
||||
@@ -75,7 +84,7 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
{
|
||||
var status = Book.UserDefinedItem.BookStatus;
|
||||
|
||||
this.bookLiberatedCb.Items.Clear();
|
||||
this.bookLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.Liberated, Text = "Downloaded" });
|
||||
this.bookLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" });
|
||||
|
||||
@@ -88,10 +97,9 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
{
|
||||
var status = Book.UserDefinedItem.PdfStatus;
|
||||
|
||||
if (status is null)
|
||||
this.pdfLiberatedCb.Enabled = false;
|
||||
else
|
||||
this.pdfLiberatedCb.Items.Clear();
|
||||
this.pdfLiberatedCb.Enabled = status is not null;
|
||||
if (status is not null)
|
||||
{
|
||||
this.pdfLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.Liberated, Text = "Downloaded" });
|
||||
this.pdfLiberatedCb.Items.Add(new liberatedComboBoxItem { Status = LiberatedStatus.NotLiberated, Text = "Not Downloaded" });
|
||||
@@ -115,16 +123,17 @@ namespace LibationWinForms.Dialogs
|
||||
comboBox.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private void saveBtn_Click(object sender, EventArgs e)
|
||||
private async void saveBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
NewTags = this.newTagsTb.Text;
|
||||
|
||||
BookLiberatedStatus = ((liberatedComboBoxItem)this.bookLiberatedCb.SelectedItem).Status;
|
||||
|
||||
if (this.pdfLiberatedCb.Enabled)
|
||||
PdfLiberatedStatus = ((liberatedComboBoxItem)this.pdfLiberatedCb.SelectedItem).Status;
|
||||
|
||||
this.DialogResult = DialogResult.OK;
|
||||
Invoke(() => saveBtn.Enabled = cancelBtn.Enabled = false);
|
||||
await LibraryBook.UpdateUserDefinedItemAsync(NewTags, BookLiberatedStatus, PdfLiberatedStatus);
|
||||
Invoke(() => saveBtn.Enabled = cancelBtn.Enabled = true);
|
||||
}
|
||||
|
||||
private void cancelBtn_Click(object sender, EventArgs e)
|
||||
@@ -135,9 +144,10 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
private void audibleLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
|
||||
{
|
||||
var locale = AudibleApi.Localization.Get(_libraryBook.Book.Locale);
|
||||
var locale = AudibleApi.Localization.Get(Book.Locale);
|
||||
var link = $"https://www.audible.{locale.TopDomain}/pd/{Book.AudibleProductId}";
|
||||
Go.To.Url(link);
|
||||
e.Link.Visited = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace LibationWinForms.Dialogs
|
||||
public BookRecordsDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
dataGridView1.EnableHeadersVisualStyles = !Application.IsDarkModeEnabled;
|
||||
if (!DesignMode)
|
||||
{
|
||||
//Prevent the designer from auto-generating columns
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace LibationWinForms.Dialogs
|
||||
public EditQuickFilters()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
dataGridView1.EnableHeadersVisualStyles = !Application.IsDarkModeEnabled;
|
||||
dataGridView1.Columns[COL_Filter].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
|
||||
|
||||
populateGridValues();
|
||||
|
||||
@@ -149,6 +149,7 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
templateTb.Text = text.Insert(selStart, itemText);
|
||||
templateTb.SelectionStart = selStart + itemText.Length;
|
||||
templateTb.Focus();
|
||||
}
|
||||
|
||||
private void llblGoToWiki_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
|
||||
|
||||
@@ -41,7 +41,14 @@ namespace LibationWinForms.Login
|
||||
//Load init cookies
|
||||
foreach (System.Net.Cookie cookie in choiceIn.SignInCookies ?? [])
|
||||
{
|
||||
webView.CoreWebView2.CookieManager.AddOrUpdateCookie(webView.CoreWebView2.CookieManager.CreateCookieWithSystemNetCookie(cookie));
|
||||
try
|
||||
{
|
||||
webView.CoreWebView2.CookieManager.AddOrUpdateCookie(webView.CoreWebView2.CookieManager.CreateCookieWithSystemNetCookie(cookie));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Error(ex, $"Failed to set cookie {cookie.Name} for domain {cookie.Domain}");
|
||||
}
|
||||
}
|
||||
|
||||
webView.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded;
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
label3.Location = new System.Drawing.Point(4, 18);
|
||||
label3.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
label3.Name = "label3";
|
||||
label3.Size = new System.Drawing.Size(218, 120);
|
||||
label3.Size = new System.Drawing.Size(218, 150);
|
||||
label3.TabIndex = 2;
|
||||
label3.Text = resources.GetString("label3.Text");
|
||||
//
|
||||
@@ -155,6 +155,7 @@
|
||||
lboxIdFields.Name = "lboxIdFields";
|
||||
lboxIdFields.Size = new System.Drawing.Size(220, 305);
|
||||
lboxIdFields.TabIndex = 0;
|
||||
lboxIdFields.DoubleClick += lboxFields_DoubleClick;
|
||||
//
|
||||
// label9
|
||||
//
|
||||
@@ -194,6 +195,7 @@
|
||||
lboxBoolFields.Name = "lboxBoolFields";
|
||||
lboxBoolFields.Size = new System.Drawing.Size(220, 365);
|
||||
lboxBoolFields.TabIndex = 0;
|
||||
lboxBoolFields.DoubleClick += lboxFields_DoubleClick;
|
||||
//
|
||||
// label8
|
||||
//
|
||||
@@ -229,10 +231,11 @@
|
||||
//
|
||||
lboxNumberFields.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
lboxNumberFields.FormattingEnabled = true;
|
||||
lboxNumberFields.Location = new System.Drawing.Point(3, 141);
|
||||
lboxNumberFields.Location = new System.Drawing.Point(3, 171);
|
||||
lboxNumberFields.Name = "lboxNumberFields";
|
||||
lboxNumberFields.Size = new System.Drawing.Size(220, 275);
|
||||
lboxNumberFields.Size = new System.Drawing.Size(220, 245);
|
||||
lboxNumberFields.TabIndex = 0;
|
||||
lboxNumberFields.DoubleClick += lboxFields_DoubleClick;
|
||||
//
|
||||
// label7
|
||||
//
|
||||
@@ -272,6 +275,7 @@
|
||||
lboxStringFields.Name = "lboxStringFields";
|
||||
lboxStringFields.Size = new System.Drawing.Size(220, 350);
|
||||
lboxStringFields.TabIndex = 0;
|
||||
lboxStringFields.DoubleClick += lboxFields_DoubleClick;
|
||||
//
|
||||
// label6
|
||||
//
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using LibationSearchEngine;
|
||||
using System.ComponentModel;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace LibationWinForms.Dialogs
|
||||
{
|
||||
public partial class SearchSyntaxDialog : Form
|
||||
{
|
||||
public event EventHandler<string> TagDoubleClicked;
|
||||
public SearchSyntaxDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -23,5 +24,13 @@ namespace LibationWinForms.Dialogs
|
||||
base.OnFormClosing(e);
|
||||
this.SaveSizeAndLocation(LibationFileManager.Configuration.Instance);
|
||||
}
|
||||
|
||||
private void lboxFields_DoubleClick(object sender, EventArgs e)
|
||||
{
|
||||
if (sender is ListBox { SelectedItem: string tagName })
|
||||
{
|
||||
TagDoubleClicked?.Invoke(this, tagName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,12 +178,6 @@ namespace LibationWinForms.Dialogs
|
||||
moveMoovAtomCbox.Enabled = convertLosslessRb.Checked;
|
||||
lameOptionsGb.Enabled = !convertLosslessRb.Checked;
|
||||
|
||||
if (convertLossyRb.Checked && requestSpatialCbox.Checked)
|
||||
{
|
||||
// Only E-AC-3 can be converted to mp3
|
||||
spatialAudioCodecCb.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
lameTargetRb_CheckedChanged(sender, e);
|
||||
LameMatchSourceBRCbox_CheckedChanged(sender, e);
|
||||
}
|
||||
@@ -205,14 +199,6 @@ namespace LibationWinForms.Dialogs
|
||||
}
|
||||
}
|
||||
|
||||
private void spatialAudioCodecCb_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (spatialAudioCodecCb.SelectedIndex == 1 && convertLossyRb.Checked)
|
||||
{
|
||||
// Only E-AC-3 can be converted to mp3
|
||||
spatialAudioCodecCb.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
private void requestSpatialCbox_CheckedChanged(object sender, EventArgs e)
|
||||
{
|
||||
spatialAudioCodecCb.Enabled = requestSpatialCbox.Checked && useWidevineCbox.Checked;
|
||||
|
||||
@@ -44,13 +44,14 @@
|
||||
allowLibationFixupCbox = new System.Windows.Forms.CheckBox();
|
||||
convertLossyRb = new System.Windows.Forms.RadioButton();
|
||||
convertLosslessRb = new System.Windows.Forms.RadioButton();
|
||||
inProgressSelectControl = new DirectorySelectControl();
|
||||
logsBtn = new System.Windows.Forms.Button();
|
||||
booksSelectControl = new DirectoryOrCustomSelectControl();
|
||||
loggingLevelLbl = new System.Windows.Forms.Label();
|
||||
loggingLevelCb = new System.Windows.Forms.ComboBox();
|
||||
tabControl = new System.Windows.Forms.TabControl();
|
||||
tab1ImportantSettings = new System.Windows.Forms.TabPage();
|
||||
themeLbl = new System.Windows.Forms.Label();
|
||||
themeCb = new System.Windows.Forms.ComboBox();
|
||||
label22 = new System.Windows.Forms.Label();
|
||||
groupBox1 = new System.Windows.Forms.GroupBox();
|
||||
applyDisplaySettingsBtn = new System.Windows.Forms.Button();
|
||||
gridScaleFactorLbl = new System.Windows.Forms.Label();
|
||||
@@ -58,6 +59,7 @@
|
||||
gridFontScaleFactorLbl = new System.Windows.Forms.Label();
|
||||
gridFontScaleFactorTbar = new System.Windows.Forms.TrackBar();
|
||||
booksGb = new System.Windows.Forms.GroupBox();
|
||||
booksSelectControl = new DirectoryOrCustomSelectControl();
|
||||
lastWriteTimeCb = new System.Windows.Forms.ComboBox();
|
||||
creationTimeCb = new System.Windows.Forms.ComboBox();
|
||||
lastWriteTimeLbl = new System.Windows.Forms.Label();
|
||||
@@ -65,6 +67,7 @@
|
||||
overwriteExistingCbox = new System.Windows.Forms.CheckBox();
|
||||
saveEpisodesToSeriesFolderCbox = new System.Windows.Forms.CheckBox();
|
||||
tab2ImportLibrary = new System.Windows.Forms.TabPage();
|
||||
importPlusTitlesCb = new System.Windows.Forms.CheckBox();
|
||||
autoDownloadEpisodesCb = new System.Windows.Forms.CheckBox();
|
||||
autoScanCb = new System.Windows.Forms.CheckBox();
|
||||
showImportedStatsCb = new System.Windows.Forms.CheckBox();
|
||||
@@ -72,6 +75,7 @@
|
||||
saveMetadataToFileCbox = new System.Windows.Forms.CheckBox();
|
||||
useCoverAsFolderIconCb = new System.Windows.Forms.CheckBox();
|
||||
inProgressFilesGb = new System.Windows.Forms.GroupBox();
|
||||
inProgressSelectControl = new DirectoryOrCustomSelectControl();
|
||||
customFileNamingGb = new System.Windows.Forms.GroupBox();
|
||||
editCharreplacementBtn = new System.Windows.Forms.Button();
|
||||
chapterFileTemplateBtn = new System.Windows.Forms.Button();
|
||||
@@ -172,7 +176,7 @@
|
||||
// inProgressDescLbl
|
||||
//
|
||||
inProgressDescLbl.AutoSize = true;
|
||||
inProgressDescLbl.Location = new System.Drawing.Point(7, 19);
|
||||
inProgressDescLbl.Location = new System.Drawing.Point(7, 17);
|
||||
inProgressDescLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
||||
inProgressDescLbl.Name = "inProgressDescLbl";
|
||||
inProgressDescLbl.Size = new System.Drawing.Size(100, 45);
|
||||
@@ -182,8 +186,8 @@
|
||||
// saveBtn
|
||||
//
|
||||
saveBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
saveBtn.Location = new System.Drawing.Point(668, 499);
|
||||
saveBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
saveBtn.Location = new System.Drawing.Point(668, 501);
|
||||
saveBtn.Margin = new System.Windows.Forms.Padding(4, 1, 4, 1);
|
||||
saveBtn.Name = "saveBtn";
|
||||
saveBtn.Size = new System.Drawing.Size(88, 27);
|
||||
saveBtn.TabIndex = 98;
|
||||
@@ -195,8 +199,8 @@
|
||||
//
|
||||
cancelBtn.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
cancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
cancelBtn.Location = new System.Drawing.Point(786, 499);
|
||||
cancelBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
|
||||
cancelBtn.Location = new System.Drawing.Point(785, 501);
|
||||
cancelBtn.Margin = new System.Windows.Forms.Padding(1);
|
||||
cancelBtn.Name = "cancelBtn";
|
||||
cancelBtn.Size = new System.Drawing.Size(88, 27);
|
||||
cancelBtn.TabIndex = 99;
|
||||
@@ -217,10 +221,10 @@
|
||||
// downloadEpisodesCb
|
||||
//
|
||||
downloadEpisodesCb.AutoSize = true;
|
||||
downloadEpisodesCb.Location = new System.Drawing.Point(6, 81);
|
||||
downloadEpisodesCb.Location = new System.Drawing.Point(6, 106);
|
||||
downloadEpisodesCb.Name = "downloadEpisodesCb";
|
||||
downloadEpisodesCb.Size = new System.Drawing.Size(163, 19);
|
||||
downloadEpisodesCb.TabIndex = 4;
|
||||
downloadEpisodesCb.TabIndex = 5;
|
||||
downloadEpisodesCb.Text = "[download episodes desc]";
|
||||
downloadEpisodesCb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
@@ -341,15 +345,6 @@
|
||||
convertLosslessRb.UseVisualStyleBackColor = true;
|
||||
convertLosslessRb.CheckedChanged += convertFormatRb_CheckedChanged;
|
||||
//
|
||||
// inProgressSelectControl
|
||||
//
|
||||
inProgressSelectControl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
inProgressSelectControl.Location = new System.Drawing.Point(6, 85);
|
||||
inProgressSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
|
||||
inProgressSelectControl.Name = "inProgressSelectControl";
|
||||
inProgressSelectControl.Size = new System.Drawing.Size(830, 49);
|
||||
inProgressSelectControl.TabIndex = 19;
|
||||
//
|
||||
// logsBtn
|
||||
//
|
||||
logsBtn.Location = new System.Drawing.Point(256, 424);
|
||||
@@ -360,15 +355,6 @@
|
||||
logsBtn.UseVisualStyleBackColor = true;
|
||||
logsBtn.Click += logsBtn_Click;
|
||||
//
|
||||
// booksSelectControl
|
||||
//
|
||||
booksSelectControl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
booksSelectControl.Location = new System.Drawing.Point(6, 37);
|
||||
booksSelectControl.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
|
||||
booksSelectControl.Name = "booksSelectControl";
|
||||
booksSelectControl.Size = new System.Drawing.Size(832, 102);
|
||||
booksSelectControl.TabIndex = 2;
|
||||
//
|
||||
// loggingLevelLbl
|
||||
//
|
||||
loggingLevelLbl.AutoSize = true;
|
||||
@@ -397,12 +383,16 @@
|
||||
tabControl.Location = new System.Drawing.Point(12, 12);
|
||||
tabControl.Name = "tabControl";
|
||||
tabControl.SelectedIndex = 0;
|
||||
tabControl.Size = new System.Drawing.Size(864, 481);
|
||||
tabControl.Size = new System.Drawing.Size(864, 485);
|
||||
tabControl.TabIndex = 100;
|
||||
//
|
||||
// tab1ImportantSettings
|
||||
//
|
||||
tab1ImportantSettings.AutoScroll = true;
|
||||
tab1ImportantSettings.BackColor = System.Drawing.SystemColors.Window;
|
||||
tab1ImportantSettings.Controls.Add(themeLbl);
|
||||
tab1ImportantSettings.Controls.Add(themeCb);
|
||||
tab1ImportantSettings.Controls.Add(label22);
|
||||
tab1ImportantSettings.Controls.Add(groupBox1);
|
||||
tab1ImportantSettings.Controls.Add(booksGb);
|
||||
tab1ImportantSettings.Controls.Add(logsBtn);
|
||||
@@ -411,10 +401,37 @@
|
||||
tab1ImportantSettings.Location = new System.Drawing.Point(4, 24);
|
||||
tab1ImportantSettings.Name = "tab1ImportantSettings";
|
||||
tab1ImportantSettings.Padding = new System.Windows.Forms.Padding(3);
|
||||
tab1ImportantSettings.Size = new System.Drawing.Size(856, 453);
|
||||
tab1ImportantSettings.Size = new System.Drawing.Size(856, 457);
|
||||
tab1ImportantSettings.TabIndex = 0;
|
||||
tab1ImportantSettings.Text = "Important settings";
|
||||
tab1ImportantSettings.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// themeLbl
|
||||
//
|
||||
themeLbl.AutoSize = true;
|
||||
themeLbl.Location = new System.Drawing.Point(190, 393);
|
||||
themeLbl.Name = "themeLbl";
|
||||
themeLbl.Size = new System.Drawing.Size(296, 15);
|
||||
themeLbl.TabIndex = 12;
|
||||
themeLbl.Text = "You must restart Libation for this change to take effect.";
|
||||
//
|
||||
// themeCb
|
||||
//
|
||||
themeCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
themeCb.FormattingEnabled = true;
|
||||
themeCb.Location = new System.Drawing.Point(63, 390);
|
||||
themeCb.Name = "themeCb";
|
||||
themeCb.Size = new System.Drawing.Size(121, 23);
|
||||
themeCb.TabIndex = 11;
|
||||
themeCb.SelectedIndexChanged += themeCb_SelectedIndexChanged;
|
||||
//
|
||||
// label22
|
||||
//
|
||||
label22.AutoSize = true;
|
||||
label22.Location = new System.Drawing.Point(4, 393);
|
||||
label22.Name = "label22";
|
||||
label22.Size = new System.Drawing.Size(44, 15);
|
||||
label22.TabIndex = 10;
|
||||
label22.Text = "Theme";
|
||||
//
|
||||
// groupBox1
|
||||
//
|
||||
@@ -434,7 +451,7 @@
|
||||
// applyDisplaySettingsBtn
|
||||
//
|
||||
applyDisplaySettingsBtn.Anchor = System.Windows.Forms.AnchorStyles.Right;
|
||||
applyDisplaySettingsBtn.Location = new System.Drawing.Point(689, 26);
|
||||
applyDisplaySettingsBtn.Location = new System.Drawing.Point(672, 26);
|
||||
applyDisplaySettingsBtn.Name = "applyDisplaySettingsBtn";
|
||||
applyDisplaySettingsBtn.Size = new System.Drawing.Size(148, 34);
|
||||
applyDisplaySettingsBtn.TabIndex = 9;
|
||||
@@ -487,13 +504,13 @@
|
||||
// booksGb
|
||||
//
|
||||
booksGb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
booksGb.Controls.Add(booksSelectControl);
|
||||
booksGb.Controls.Add(lastWriteTimeCb);
|
||||
booksGb.Controls.Add(creationTimeCb);
|
||||
booksGb.Controls.Add(lastWriteTimeLbl);
|
||||
booksGb.Controls.Add(creationTimeLbl);
|
||||
booksGb.Controls.Add(overwriteExistingCbox);
|
||||
booksGb.Controls.Add(saveEpisodesToSeriesFolderCbox);
|
||||
booksGb.Controls.Add(booksSelectControl);
|
||||
booksGb.Controls.Add(booksLocationDescLbl);
|
||||
booksGb.Location = new System.Drawing.Point(6, 6);
|
||||
booksGb.Name = "booksGb";
|
||||
@@ -502,6 +519,14 @@
|
||||
booksGb.TabStop = false;
|
||||
booksGb.Text = "Books location";
|
||||
//
|
||||
// booksSelectControl
|
||||
//
|
||||
booksSelectControl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
booksSelectControl.Location = new System.Drawing.Point(8, 37);
|
||||
booksSelectControl.Name = "booksSelectControl";
|
||||
booksSelectControl.Size = new System.Drawing.Size(830, 80);
|
||||
booksSelectControl.TabIndex = 6;
|
||||
//
|
||||
// lastWriteTimeCb
|
||||
//
|
||||
lastWriteTimeCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
@@ -563,6 +588,8 @@
|
||||
// tab2ImportLibrary
|
||||
//
|
||||
tab2ImportLibrary.AutoScroll = true;
|
||||
tab2ImportLibrary.BackColor = System.Drawing.SystemColors.Window;
|
||||
tab2ImportLibrary.Controls.Add(importPlusTitlesCb);
|
||||
tab2ImportLibrary.Controls.Add(autoDownloadEpisodesCb);
|
||||
tab2ImportLibrary.Controls.Add(autoScanCb);
|
||||
tab2ImportLibrary.Controls.Add(showImportedStatsCb);
|
||||
@@ -571,18 +598,27 @@
|
||||
tab2ImportLibrary.Location = new System.Drawing.Point(4, 24);
|
||||
tab2ImportLibrary.Name = "tab2ImportLibrary";
|
||||
tab2ImportLibrary.Padding = new System.Windows.Forms.Padding(3);
|
||||
tab2ImportLibrary.Size = new System.Drawing.Size(856, 453);
|
||||
tab2ImportLibrary.Size = new System.Drawing.Size(856, 457);
|
||||
tab2ImportLibrary.TabIndex = 1;
|
||||
tab2ImportLibrary.Text = "Import library";
|
||||
tab2ImportLibrary.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// importPlusTitlesCb
|
||||
//
|
||||
importPlusTitlesCb.AutoSize = true;
|
||||
importPlusTitlesCb.Location = new System.Drawing.Point(6, 81);
|
||||
importPlusTitlesCb.Name = "importPlusTitlesCb";
|
||||
importPlusTitlesCb.Size = new System.Drawing.Size(199, 19);
|
||||
importPlusTitlesCb.TabIndex = 4;
|
||||
importPlusTitlesCb.Text = "[import audible plus books desc]";
|
||||
importPlusTitlesCb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// autoDownloadEpisodesCb
|
||||
//
|
||||
autoDownloadEpisodesCb.AutoSize = true;
|
||||
autoDownloadEpisodesCb.Location = new System.Drawing.Point(6, 106);
|
||||
autoDownloadEpisodesCb.Location = new System.Drawing.Point(6, 131);
|
||||
autoDownloadEpisodesCb.Name = "autoDownloadEpisodesCb";
|
||||
autoDownloadEpisodesCb.Size = new System.Drawing.Size(190, 19);
|
||||
autoDownloadEpisodesCb.TabIndex = 5;
|
||||
autoDownloadEpisodesCb.TabIndex = 6;
|
||||
autoDownloadEpisodesCb.Text = "[auto download episodes desc]";
|
||||
autoDownloadEpisodesCb.UseVisualStyleBackColor = true;
|
||||
//
|
||||
@@ -609,6 +645,7 @@
|
||||
// tab3DownloadDecrypt
|
||||
//
|
||||
tab3DownloadDecrypt.AutoScroll = true;
|
||||
tab3DownloadDecrypt.BackColor = System.Drawing.SystemColors.Window;
|
||||
tab3DownloadDecrypt.Controls.Add(saveMetadataToFileCbox);
|
||||
tab3DownloadDecrypt.Controls.Add(useCoverAsFolderIconCb);
|
||||
tab3DownloadDecrypt.Controls.Add(inProgressFilesGb);
|
||||
@@ -617,15 +654,15 @@
|
||||
tab3DownloadDecrypt.Location = new System.Drawing.Point(4, 24);
|
||||
tab3DownloadDecrypt.Name = "tab3DownloadDecrypt";
|
||||
tab3DownloadDecrypt.Padding = new System.Windows.Forms.Padding(3);
|
||||
tab3DownloadDecrypt.Size = new System.Drawing.Size(856, 453);
|
||||
tab3DownloadDecrypt.Size = new System.Drawing.Size(856, 457);
|
||||
tab3DownloadDecrypt.TabIndex = 2;
|
||||
tab3DownloadDecrypt.Text = "Download/Decrypt";
|
||||
tab3DownloadDecrypt.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// saveMetadataToFileCbox
|
||||
//
|
||||
saveMetadataToFileCbox.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right;
|
||||
saveMetadataToFileCbox.AutoSize = true;
|
||||
saveMetadataToFileCbox.Location = new System.Drawing.Point(482, 428);
|
||||
saveMetadataToFileCbox.Location = new System.Drawing.Point(481, 435);
|
||||
saveMetadataToFileCbox.Name = "saveMetadataToFileCbox";
|
||||
saveMetadataToFileCbox.Size = new System.Drawing.Size(166, 19);
|
||||
saveMetadataToFileCbox.TabIndex = 22;
|
||||
@@ -634,8 +671,9 @@
|
||||
//
|
||||
// useCoverAsFolderIconCb
|
||||
//
|
||||
useCoverAsFolderIconCb.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left;
|
||||
useCoverAsFolderIconCb.AutoSize = true;
|
||||
useCoverAsFolderIconCb.Location = new System.Drawing.Point(7, 428);
|
||||
useCoverAsFolderIconCb.Location = new System.Drawing.Point(6, 435);
|
||||
useCoverAsFolderIconCb.Name = "useCoverAsFolderIconCb";
|
||||
useCoverAsFolderIconCb.Size = new System.Drawing.Size(180, 19);
|
||||
useCoverAsFolderIconCb.TabIndex = 22;
|
||||
@@ -644,16 +682,24 @@
|
||||
//
|
||||
// inProgressFilesGb
|
||||
//
|
||||
inProgressFilesGb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
inProgressFilesGb.Controls.Add(inProgressDescLbl);
|
||||
inProgressFilesGb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
inProgressFilesGb.Controls.Add(inProgressSelectControl);
|
||||
inProgressFilesGb.Controls.Add(inProgressDescLbl);
|
||||
inProgressFilesGb.Location = new System.Drawing.Point(6, 281);
|
||||
inProgressFilesGb.Name = "inProgressFilesGb";
|
||||
inProgressFilesGb.Size = new System.Drawing.Size(842, 141);
|
||||
inProgressFilesGb.Size = new System.Drawing.Size(842, 148);
|
||||
inProgressFilesGb.TabIndex = 21;
|
||||
inProgressFilesGb.TabStop = false;
|
||||
inProgressFilesGb.Text = "In progress files";
|
||||
//
|
||||
// inProgressSelectControl
|
||||
//
|
||||
inProgressSelectControl.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
inProgressSelectControl.Location = new System.Drawing.Point(6, 65);
|
||||
inProgressSelectControl.Name = "inProgressSelectControl";
|
||||
inProgressSelectControl.Size = new System.Drawing.Size(830, 80);
|
||||
inProgressSelectControl.TabIndex = 19;
|
||||
//
|
||||
// customFileNamingGb
|
||||
//
|
||||
customFileNamingGb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
@@ -775,6 +821,7 @@
|
||||
// tab4AudioFileOptions
|
||||
//
|
||||
tab4AudioFileOptions.AutoScroll = true;
|
||||
tab4AudioFileOptions.BackColor = System.Drawing.SystemColors.Window;
|
||||
tab4AudioFileOptions.Controls.Add(request_xHE_AAC_Cbox);
|
||||
tab4AudioFileOptions.Controls.Add(requestSpatialCbox);
|
||||
tab4AudioFileOptions.Controls.Add(useWidevineCbox);
|
||||
@@ -798,10 +845,9 @@
|
||||
tab4AudioFileOptions.Location = new System.Drawing.Point(4, 24);
|
||||
tab4AudioFileOptions.Name = "tab4AudioFileOptions";
|
||||
tab4AudioFileOptions.Padding = new System.Windows.Forms.Padding(3);
|
||||
tab4AudioFileOptions.Size = new System.Drawing.Size(856, 453);
|
||||
tab4AudioFileOptions.Size = new System.Drawing.Size(856, 457);
|
||||
tab4AudioFileOptions.TabIndex = 3;
|
||||
tab4AudioFileOptions.Text = "Audio File Options";
|
||||
tab4AudioFileOptions.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// request_xHE_AAC_Cbox
|
||||
//
|
||||
@@ -851,7 +897,6 @@
|
||||
spatialAudioCodecCb.Name = "spatialAudioCodecCb";
|
||||
spatialAudioCodecCb.Size = new System.Drawing.Size(173, 23);
|
||||
spatialAudioCodecCb.TabIndex = 5;
|
||||
spatialAudioCodecCb.SelectedIndexChanged += spatialAudioCodecCb_SelectedIndexChanged;
|
||||
//
|
||||
// moveMoovAtomCbox
|
||||
//
|
||||
@@ -1183,6 +1228,7 @@
|
||||
//
|
||||
label19.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label19.AutoSize = true;
|
||||
label19.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label19.Location = new System.Drawing.Point(332, 47);
|
||||
label19.Name = "label19";
|
||||
label19.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1193,6 +1239,7 @@
|
||||
//
|
||||
label18.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label18.AutoSize = true;
|
||||
label18.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label18.Location = new System.Drawing.Point(291, 47);
|
||||
label18.Name = "label18";
|
||||
label18.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1203,6 +1250,7 @@
|
||||
//
|
||||
label17.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label17.AutoSize = true;
|
||||
label17.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label17.Location = new System.Drawing.Point(251, 47);
|
||||
label17.Name = "label17";
|
||||
label17.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1213,6 +1261,7 @@
|
||||
//
|
||||
label16.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label16.AutoSize = true;
|
||||
label16.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label16.Location = new System.Drawing.Point(212, 47);
|
||||
label16.Name = "label16";
|
||||
label16.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1223,6 +1272,7 @@
|
||||
//
|
||||
label12.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label12.AutoSize = true;
|
||||
label12.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label12.Location = new System.Drawing.Point(170, 47);
|
||||
label12.Name = "label12";
|
||||
label12.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1233,6 +1283,7 @@
|
||||
//
|
||||
label15.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label15.AutoSize = true;
|
||||
label15.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label15.Location = new System.Drawing.Point(130, 47);
|
||||
label15.Name = "label15";
|
||||
label15.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1243,6 +1294,7 @@
|
||||
//
|
||||
label9.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label9.AutoSize = true;
|
||||
label9.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label9.Location = new System.Drawing.Point(89, 47);
|
||||
label9.Name = "label9";
|
||||
label9.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1253,6 +1305,7 @@
|
||||
//
|
||||
label8.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label8.AutoSize = true;
|
||||
label8.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label8.Location = new System.Drawing.Point(371, 47);
|
||||
label8.Name = "label8";
|
||||
label8.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1281,6 +1334,7 @@
|
||||
//
|
||||
label14.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label14.AutoSize = true;
|
||||
label14.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label14.Location = new System.Drawing.Point(50, 47);
|
||||
label14.Name = "label14";
|
||||
label14.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1291,6 +1345,7 @@
|
||||
//
|
||||
label2.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
label2.AutoSize = true;
|
||||
label2.BackColor = System.Drawing.SystemColors.ControlLightLight;
|
||||
label2.Location = new System.Drawing.Point(10, 47);
|
||||
label2.Name = "label2";
|
||||
label2.Size = new System.Drawing.Size(20, 15);
|
||||
@@ -1464,8 +1519,6 @@
|
||||
public System.Windows.Forms.Button saveBtn;
|
||||
public System.Windows.Forms.Button cancelBtn;
|
||||
private System.Windows.Forms.CheckBox allowLibationFixupCbox;
|
||||
private DirectoryOrCustomSelectControl booksSelectControl;
|
||||
private DirectorySelectControl inProgressSelectControl;
|
||||
private System.Windows.Forms.RadioButton convertLossyRb;
|
||||
private System.Windows.Forms.RadioButton convertLosslessRb;
|
||||
private System.Windows.Forms.Button logsBtn;
|
||||
@@ -1568,5 +1621,11 @@
|
||||
private System.Windows.Forms.CheckBox useWidevineCbox;
|
||||
private System.Windows.Forms.CheckBox requestSpatialCbox;
|
||||
private System.Windows.Forms.CheckBox request_xHE_AAC_Cbox;
|
||||
private DirectoryOrCustomSelectControl inProgressSelectControl;
|
||||
private DirectoryOrCustomSelectControl booksSelectControl;
|
||||
private System.Windows.Forms.Label label22;
|
||||
private System.Windows.Forms.ComboBox themeCb;
|
||||
private System.Windows.Forms.Label themeLbl;
|
||||
private System.Windows.Forms.CheckBox importPlusTitlesCb;
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,14 @@ namespace LibationWinForms.Dialogs
|
||||
this.autoScanCb.Text = desc(nameof(config.AutoScan));
|
||||
this.showImportedStatsCb.Text = desc(nameof(config.ShowImportedStats));
|
||||
this.importEpisodesCb.Text = desc(nameof(config.ImportEpisodes));
|
||||
this.importPlusTitlesCb.Text = desc(nameof(config.ImportPlusTitles));
|
||||
this.downloadEpisodesCb.Text = desc(nameof(config.DownloadEpisodes));
|
||||
this.autoDownloadEpisodesCb.Text = desc(nameof(config.AutoDownloadEpisodes));
|
||||
|
||||
autoScanCb.Checked = config.AutoScan;
|
||||
showImportedStatsCb.Checked = config.ShowImportedStats;
|
||||
importEpisodesCb.Checked = config.ImportEpisodes;
|
||||
importPlusTitlesCb.Checked = config.ImportPlusTitles;
|
||||
downloadEpisodesCb.Checked = config.DownloadEpisodes;
|
||||
autoDownloadEpisodesCb.Checked = config.AutoDownloadEpisodes;
|
||||
}
|
||||
@@ -26,6 +28,7 @@ namespace LibationWinForms.Dialogs
|
||||
config.AutoScan = autoScanCb.Checked;
|
||||
config.ShowImportedStats = showImportedStatsCb.Checked;
|
||||
config.ImportEpisodes = importEpisodesCb.Checked;
|
||||
config.ImportPlusTitles = importPlusTitlesCb.Checked;
|
||||
config.DownloadEpisodes = downloadEpisodesCb.Checked;
|
||||
config.AutoDownloadEpisodes = autoDownloadEpisodesCb.Checked;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Dinah.Core;
|
||||
using DocumentFormat.OpenXml.Drawing;
|
||||
using DocumentFormat.OpenXml.Office2013.Theme;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
@@ -19,6 +21,8 @@ namespace LibationWinForms.Dialogs
|
||||
else
|
||||
Go.To.Folder(Configuration.Instance.LibationFiles.Location.ShortPathName);
|
||||
}
|
||||
private Configuration.Theme themeVariant;
|
||||
private Configuration.Theme initialThemeVariant;
|
||||
|
||||
private void Load_Important(Configuration config)
|
||||
{
|
||||
@@ -44,6 +48,10 @@ namespace LibationWinForms.Dialogs
|
||||
creationTimeCb.SelectedItem = dateTimeSources.SingleOrDefault(v => v.Value == config.CreationTime) ?? dateTimeSources[0];
|
||||
lastWriteTimeCb.SelectedItem = dateTimeSources.SingleOrDefault(v => v.Value == config.LastWriteTime) ?? dateTimeSources[0];
|
||||
|
||||
themeVariant = initialThemeVariant = config.ThemeVariant;
|
||||
var themes = Enum.GetValues<Configuration.Theme>().Select(v => new EnumDisplay<Configuration.Theme>(v)).ToArray();
|
||||
themeCb.Items.AddRange(themes);
|
||||
themeCb.SelectedItem = themes.SingleOrDefault(v => v.Value == themeVariant) ?? themes[0];
|
||||
|
||||
booksSelectControl.SetSearchTitle("books location");
|
||||
booksSelectControl.SetDirectoryItems(
|
||||
@@ -110,6 +118,7 @@ namespace LibationWinForms.Dialogs
|
||||
|
||||
config.CreationTime = (creationTimeCb.SelectedItem as EnumDisplay<Configuration.DateTimeSource>)?.Value ?? Configuration.DateTimeSource.File;
|
||||
config.LastWriteTime = (lastWriteTimeCb.SelectedItem as EnumDisplay<Configuration.DateTimeSource>)?.Value ?? Configuration.DateTimeSource.File;
|
||||
config.ThemeVariant = (themeCb.SelectedItem as EnumDisplay<Configuration.Theme>)?.Value ?? Configuration.Theme.System;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -123,5 +132,15 @@ namespace LibationWinForms.Dialogs
|
||||
config.GridFontScaleFactor = linearRangeToScaleFactor(gridFontScaleFactorTbar.Value);
|
||||
config.GridScaleFactor = linearRangeToScaleFactor(gridScaleFactorTbar.Value);
|
||||
}
|
||||
|
||||
private void themeCb_SelectedIndexChanged(object? sender, EventArgs e)
|
||||
{
|
||||
var selected = themeCb.SelectedItem as EnumDisplay<Configuration.Theme>;
|
||||
if (selected != null)
|
||||
{
|
||||
themeVariant = selected.Value;
|
||||
themeLbl.Visible = themeVariant != initialThemeVariant;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace LibationWinForms
|
||||
{
|
||||
protected void Configure_Filter() { }
|
||||
|
||||
private void filterHelpBtn_Click(object sender, EventArgs e) => new SearchSyntaxDialog().ShowDialog();
|
||||
private void filterHelpBtn_Click(object sender, EventArgs e) => ShowSearchSyntaxDialog();
|
||||
|
||||
private void filterSearchTb_TextCleared(object sender, EventArgs e)
|
||||
{
|
||||
@@ -45,5 +45,32 @@ namespace LibationWinForms
|
||||
performFilter(lastGoodFilter);
|
||||
}
|
||||
}
|
||||
|
||||
public SearchSyntaxDialog ShowSearchSyntaxDialog()
|
||||
{
|
||||
var dialog = new SearchSyntaxDialog();
|
||||
dialog.TagDoubleClicked += Dialog_TagDoubleClicked;
|
||||
dialog.FormClosed += Dialog_Closed;
|
||||
filterHelpBtn.Enabled = false;
|
||||
dialog.Show(this);
|
||||
return dialog;
|
||||
|
||||
void Dialog_Closed(object sender, FormClosedEventArgs e)
|
||||
{
|
||||
dialog.TagDoubleClicked -= Dialog_TagDoubleClicked;
|
||||
filterHelpBtn.Enabled = true;
|
||||
}
|
||||
void Dialog_TagDoubleClicked(object sender, string tag)
|
||||
{
|
||||
if (string.IsNullOrEmpty(tag)) return;
|
||||
|
||||
var text = filterSearchTb.Text;
|
||||
var selStart = Math.Min(Math.Max(0, filterSearchTb.SelectionStart), text.Length);
|
||||
|
||||
filterSearchTb.Text = text.Insert(selStart, tag);
|
||||
filterSearchTb.SelectionStart = selStart + tag.Length;
|
||||
filterSearchTb.Focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using DataLayer;
|
||||
using LibationUiBase;
|
||||
using ApplicationServices;
|
||||
using DataLayer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
@@ -13,12 +14,21 @@ namespace LibationWinForms
|
||||
|
||||
//GetLibrary_Flat_NoTracking() may take a long time on a hugh library. so run in new thread
|
||||
private async void beginBookBackupsToolStripMenuItem_Click(object _ = null, EventArgs __ = null)
|
||||
{
|
||||
var library = await Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking());
|
||||
BackupAllBooks(library);
|
||||
}
|
||||
|
||||
private void BackupAllBooks(IEnumerable<LibraryBook> books)
|
||||
{
|
||||
try
|
||||
{
|
||||
var unliberated = await Task.Run(() => ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking().UnLiberated().ToArray());
|
||||
if (processBookQueue1.ViewModel.QueueDownloadDecrypt(unliberated))
|
||||
SetQueueCollapseState(false);
|
||||
var unliberated = books.UnLiberated().ToArray();
|
||||
Invoke(() =>
|
||||
{
|
||||
if (processBookQueue1.ViewModel.QueueDownloadDecrypt(unliberated))
|
||||
SetQueueCollapseState(false);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -25,13 +25,13 @@ namespace LibationWinForms
|
||||
this.Width = width;
|
||||
}
|
||||
|
||||
private void ProductsDisplay_LiberateClicked(object sender, LibraryBook[] libraryBooks)
|
||||
private void ProductsDisplay_LiberateClicked(object sender, System.Collections.Generic.IList<LibraryBook> libraryBooks, Configuration config)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (processBookQueue1.ViewModel.QueueDownloadDecrypt(libraryBooks))
|
||||
if (processBookQueue1.ViewModel.QueueDownloadDecrypt(libraryBooks, config))
|
||||
SetQueueCollapseState(false);
|
||||
else if (libraryBooks.Length == 1 && libraryBooks[0].Book.AudioExists)
|
||||
else if (libraryBooks.Count == 1 && libraryBooks[0].Book.AudioExists)
|
||||
{
|
||||
// liberated: open explorer to file
|
||||
var filePath = AudibleFileStorage.Audio.GetPath(libraryBooks[0].Book.AudibleProductId);
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace LibationWinForms
|
||||
scanLibraryOfAllAccountsToolStripMenuItem.Enabled = false;
|
||||
scanLibraryOfSomeAccountsToolStripMenuItem.Enabled = false;
|
||||
|
||||
this.scanningToolStripMenuItem.Image = System.Windows.Forms.Application.IsDarkModeEnabled ? Properties.Resources.import_16x16_dark : Properties.Resources.import_16x16;
|
||||
this.scanningToolStripMenuItem.Visible = true;
|
||||
this.scanningToolStripMenuItem.Text
|
||||
= (accountsLength == 1)
|
||||
|
||||
@@ -22,22 +22,27 @@ namespace LibationWinForms
|
||||
PictureStorage.SetDefaultImage(PictureSize.Native, Properties.Resources.default_cover_500x500.ToBytes(format));
|
||||
|
||||
BaseUtil.SetLoadImageDelegate(WinFormsUtil.TryLoadImageOrDefault);
|
||||
BaseUtil.SetLoadResourceImageDelegate(Properties.Resources.ResourceManager.GetObject);
|
||||
BaseUtil.SetLoadResourceImageDelegate(LoadResourceImage);
|
||||
|
||||
// wire-up event to automatically download after scan.
|
||||
// winforms only. this should NOT be allowed in cli
|
||||
updateCountsBw.RunWorkerCompleted += (object sender, System.ComponentModel.RunWorkerCompletedEventArgs e) =>
|
||||
{
|
||||
if (!Configuration.Instance.AutoDownloadEpisodes)
|
||||
if (!Configuration.Instance.AutoDownloadEpisodes || e.Result is not LibraryCommands.LibraryStats libraryStats)
|
||||
return;
|
||||
|
||||
var libraryStats = e.Result as LibraryCommands.LibraryStats;
|
||||
|
||||
if ((libraryStats.PendingBooks + libraryStats.pdfsNotDownloaded) > 0)
|
||||
Invoke(() => beginBookBackupsToolStripMenuItem_Click(null, System.EventArgs.Empty));
|
||||
BackupAllBooks(libraryStats.LibraryBooks);
|
||||
};
|
||||
}
|
||||
|
||||
private static object LoadResourceImage(string resourceName)
|
||||
{
|
||||
if (Application.IsDarkModeEnabled)
|
||||
resourceName += "_dark";
|
||||
return Properties.Resources.ResourceManager.GetObject(resourceName);
|
||||
}
|
||||
|
||||
private void AudibleApiStorage_LoadError(object sender, AccountSettingsLoadErrorEventArgs e)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace LibationWinForms.GridView
|
||||
{
|
||||
public EditTagsDataGridViewImageButtonCell() : base("Edit Tags button") { }
|
||||
|
||||
private static Image ButtonImage { get; } = Properties.Resources.edit_25x25;
|
||||
private static Image ButtonImage => Application.IsDarkModeEnabled ? Properties.Resources.edit_25x25_dark : Properties.Resources.edit_25x25;
|
||||
|
||||
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
|
||||
{
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace LibationWinForms.GridView
|
||||
private static readonly Brush DISABLED_GRAY = new SolidBrush(Color.FromArgb(0x60, Color.LightGray));
|
||||
private static readonly Color HiddenForeColor = Color.LightGray;
|
||||
private static readonly Color SERIES_BG_COLOR = Color.FromArgb(230, 255, 230);
|
||||
private static readonly Color SERIES_BG_COLOR_DARK = Color.FromArgb(76, 82, 93);
|
||||
private static Color SeriesBgColor => Application.IsDarkModeEnabled ? SERIES_BG_COLOR_DARK:SERIES_BG_COLOR;
|
||||
|
||||
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object? value, object? formattedValue, string? errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
|
||||
{
|
||||
@@ -32,7 +34,7 @@ namespace LibationWinForms.GridView
|
||||
//Don't paint the button graphic
|
||||
paintParts ^= DataGridViewPaintParts.ContentBackground | DataGridViewPaintParts.ContentForeground | DataGridViewPaintParts.SelectionBackground;
|
||||
|
||||
row.DefaultCellStyle.BackColor = status.IsEpisode ? SERIES_BG_COLOR : grid.DefaultCellStyle.BackColor;
|
||||
row.DefaultCellStyle.BackColor = status.IsEpisode ? SeriesBgColor : grid.DefaultCellStyle.BackColor;
|
||||
row.DefaultCellStyle.ForeColor = status.Opacity == 1 ? grid.DefaultCellStyle.ForeColor : HiddenForeColor;
|
||||
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
|
||||
namespace LibationWinForms.GridView
|
||||
{
|
||||
public partial class ProductsDisplay : UserControl
|
||||
@@ -21,7 +22,7 @@ namespace LibationWinForms.GridView
|
||||
/// <summary>Number of visible rows has changed</summary>
|
||||
public event EventHandler<int> VisibleCountChanged;
|
||||
public event EventHandler<int> RemovableCountChanged;
|
||||
public event EventHandler<LibraryBook[]> LiberateClicked;
|
||||
public event LiberateClickedHandler LiberateClicked;
|
||||
public event EventHandler<SeriesEntry> LiberateSeriesClicked;
|
||||
public event EventHandler<LibraryBook[]> ConvertToMp3Clicked;
|
||||
public event EventHandler InitialLoaded;
|
||||
@@ -68,7 +69,7 @@ namespace LibationWinForms.GridView
|
||||
PictureStorage.PictureCached -= PictureCached;
|
||||
|
||||
if (!imageDisplay.Visible)
|
||||
imageDisplay.Show(null);
|
||||
imageDisplay.Show(this);
|
||||
}
|
||||
|
||||
private void productsGrid_DescriptionClicked(GridEntry liveGridEntry, Rectangle cellRectangle)
|
||||
@@ -90,11 +91,25 @@ namespace LibationWinForms.GridView
|
||||
displayWindow.Show(this);
|
||||
}
|
||||
|
||||
private async void productsGrid_DetailsClicked(LibraryBookEntry liveGridEntry)
|
||||
private BookDetailsDialog bookDetailsForm;
|
||||
private void productsGrid_DetailsClicked(LibraryBookEntry liveGridEntry)
|
||||
{
|
||||
var bookDetailsForm = new BookDetailsDialog(liveGridEntry.LibraryBook);
|
||||
if (bookDetailsForm.ShowDialog() == DialogResult.OK)
|
||||
await liveGridEntry.LibraryBook.UpdateUserDefinedItemAsync(bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
|
||||
if (bookDetailsForm is null || bookDetailsForm.IsDisposed || !bookDetailsForm.Visible)
|
||||
{
|
||||
bookDetailsForm = new();
|
||||
bookDetailsForm.RestoreSizeAndLocation(Configuration.Instance);
|
||||
bookDetailsForm.FormClosed += bookDetailsForm_FormClosed;
|
||||
}
|
||||
|
||||
bookDetailsForm.LibraryBook = liveGridEntry.LibraryBook;
|
||||
if (!bookDetailsForm.Visible)
|
||||
bookDetailsForm.Show(this);
|
||||
|
||||
async void bookDetailsForm_FormClosed(object sender, FormClosedEventArgs e)
|
||||
{
|
||||
bookDetailsForm.FormClosed -= bookDetailsForm_FormClosed;
|
||||
bookDetailsForm.SaveSizeAndLocation(Configuration.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -185,10 +200,31 @@ namespace LibationWinForms.GridView
|
||||
ctxMenu.Items.Add(downloadSelectedMenuItem);
|
||||
downloadSelectedMenuItem.Click += (s, _) =>
|
||||
{
|
||||
LiberateClicked?.Invoke(s, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray());
|
||||
LiberateClicked?.Invoke(s, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray(), Configuration.Instance);
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region Download split by chapters
|
||||
|
||||
if (ctx.LibraryBookEntries.Length > 0)
|
||||
{
|
||||
var downloadChaptersMenuItem = new ToolStripMenuItem
|
||||
{
|
||||
Text = ctx.DownloadAsChapters,
|
||||
Enabled = ctx.DownloadAsChaptersEnabled
|
||||
};
|
||||
downloadChaptersMenuItem.Click += (_, e) =>
|
||||
{
|
||||
var config = Configuration.Instance.CreateEphemeralCopy();
|
||||
config.AllowLibationFixup = config.SplitFilesByChapter = true;
|
||||
var books = ctx.LibraryBookEntries.Select(e => e.LibraryBook).Where(lb => lb.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error).ToList();
|
||||
books.ForEach(b => b.Book.UserDefinedItem.BookStatus = LiberatedStatus.NotLiberated);
|
||||
LiberateClicked?.Invoke(this, books, config);
|
||||
};
|
||||
ctxMenu.Items.Add(downloadChaptersMenuItem);
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region Convert to Mp3
|
||||
|
||||
@@ -201,7 +237,6 @@ namespace LibationWinForms.GridView
|
||||
};
|
||||
convertToMp3MenuItem.Click += (_, e) => ConvertToMp3Clicked?.Invoke(this, ctx.LibraryBookEntries.Select(e => e.LibraryBook).ToArray());
|
||||
ctxMenu.Items.Add(convertToMp3MenuItem);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -222,7 +257,7 @@ namespace LibationWinForms.GridView
|
||||
if (entry4.Book.HasPdf)
|
||||
entry4.Book.UserDefinedItem.SetPdfStatus(LiberatedStatus.NotLiberated);
|
||||
|
||||
LiberateClicked?.Invoke(s, [entry4.LibraryBook]);
|
||||
LiberateClicked?.Invoke(s, [entry4.LibraryBook], Configuration.Instance);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -398,7 +433,7 @@ namespace LibationWinForms.GridView
|
||||
{
|
||||
if (liveGridEntry.LibraryBook.Book.UserDefinedItem.BookStatus is not LiberatedStatus.Error
|
||||
&& !liveGridEntry.Liberate.IsUnavailable)
|
||||
LiberateClicked?.Invoke(this, [liveGridEntry.LibraryBook]);
|
||||
LiberateClicked?.Invoke(this, [liveGridEntry.LibraryBook], Configuration.Instance);
|
||||
}
|
||||
|
||||
private void productsGrid_RemovableCountChanged(object sender, EventArgs e)
|
||||
|
||||
@@ -53,6 +53,7 @@ namespace LibationWinForms.GridView
|
||||
setGridScale(Configuration.Instance.GridScaleFactor);
|
||||
Configuration.Instance.PropertyChanged += Configuration_ScaleChanged;
|
||||
Configuration.Instance.PropertyChanged += Configuration_FontScaleChanged;
|
||||
gridEntryDataGridView.EnableHeadersVisualStyles = !Application.IsDarkModeEnabled;
|
||||
|
||||
gridEntryDataGridView.Disposed += (_, _) =>
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user