Compare commits
No commits in common. "master" and "v3.0.2" have entirely different histories.
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"CdmUrls": [
|
|
||||||
"https://ollj0gz40d.execute-api.us-west-2.amazonaws.com/default/AudibleCdm"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
43
.github/ISSUE_TEMPLATE/bug_report.md
vendored
43
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,43 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve Libation
|
|
||||||
title: ''
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
PLEASE FILL OUT THE FOLLOWING. Bug reports with limited information or lacking an attached log file may get limited or delayed help.
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
## Describe the bug
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
## To Reproduce
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
## Expected behavior
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
## Platform
|
|
||||||
[e.g. Windows 10, Windows 11, Mac, Linux (State distribution)]
|
|
||||||
|
|
||||||
## Log Files
|
|
||||||
Attach your Libation log file here. If your user folder contains the file "LibationCrash.log", attach that also.
|
|
||||||
|
|
||||||
**Default Log File Locations**
|
|
||||||
|Platform|Folder|
|
|
||||||
|-|-|
|
|
||||||
|Windows|`%userprofile%\Libation`|
|
|
||||||
|macOS|`~/Library/Application Support/Libation`|
|
|
||||||
|Linux|`~/.local/share/Libation`|
|
|
||||||
|
|
||||||
Alternative, you may open the log file folder from within Libation. Open Libation's settings, and on the first tab in Settings you can click the button 'Open log folder'.
|
|
||||||
31
.github/ISSUE_TEMPLATE/feature_request.md
vendored
31
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,31 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: enhancement
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
**No-go ideas**
|
|
||||||
There are lots of great ideas and many are beyond what we intend to do for Libation. Some good ideas which we do not intend to pursue:
|
|
||||||
|
|
||||||
* comprehensive api/cli
|
|
||||||
* aax/audiobook import
|
|
||||||
* bulk rename of existing files
|
|
||||||
* general metadata/tag editor
|
|
||||||
* playback features
|
|
||||||
* web gui
|
|
||||||
* supporting non-audible vendors
|
|
||||||
* official docker support
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
version: 2
|
|
||||||
updates:
|
|
||||||
# Maintain dependencies for GitHub Actions
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "daily"
|
|
||||||
132
.github/workflows/build-linux.yml
vendored
132
.github/workflows/build-linux.yml
vendored
@ -1,132 +0,0 @@
|
|||||||
# build-linux.yml
|
|
||||||
# Reusable workflow that builds the Linux and MacOS (x64 and arm64) versions of Libation.
|
|
||||||
---
|
|
||||||
name: build
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
version_override:
|
|
||||||
type: string
|
|
||||||
description: "Version number override"
|
|
||||||
required: false
|
|
||||||
run_unit_tests:
|
|
||||||
type: boolean
|
|
||||||
description: "Skip running unit tests"
|
|
||||||
required: false
|
|
||||||
default: true
|
|
||||||
runs_on:
|
|
||||||
type: string
|
|
||||||
description: "The GitHub hosted runner to use"
|
|
||||||
required: true
|
|
||||||
OS:
|
|
||||||
type: string
|
|
||||||
description: >
|
|
||||||
The operating system targeted by the build.
|
|
||||||
|
|
||||||
There must be a corresponding Bundle_$OS.sh script file in ./Scripts
|
|
||||||
required: true
|
|
||||||
architecture:
|
|
||||||
type: string
|
|
||||||
description: "CPU architecture targeted by the build."
|
|
||||||
required: true
|
|
||||||
|
|
||||||
env:
|
|
||||||
DOTNET_CONFIGURATION: "Release"
|
|
||||||
DOTNET_VERSION: "9.0.x"
|
|
||||||
RELEASE_NAME: "chardonnay"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: "${{ inputs.OS }}-${{ inputs.architecture }}"
|
|
||||||
runs-on: ${{ inputs.runs_on }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- name: Setup .NET
|
|
||||||
uses: actions/setup-dotnet@v5
|
|
||||||
with:
|
|
||||||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
|
||||||
env:
|
|
||||||
NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Get version
|
|
||||||
id: get_version
|
|
||||||
run: |
|
|
||||||
inputVersion="${{ inputs.version_override }}"
|
|
||||||
if [[ "${#inputVersion}" -gt 0 ]]
|
|
||||||
then
|
|
||||||
version="${inputVersion}"
|
|
||||||
else
|
|
||||||
version="$(grep -Eio -m 1 '<Version>.*</Version>' ./Source/AppScaffolding/AppScaffolding.csproj | sed -r 's/<\/?Version>//g')"
|
|
||||||
fi
|
|
||||||
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
|
||||||
|
|
||||||
- name: Unit test
|
|
||||||
if: ${{ inputs.run_unit_tests }}
|
|
||||||
working-directory: ./Source
|
|
||||||
run: dotnet test
|
|
||||||
|
|
||||||
- name: Publish
|
|
||||||
id: publish
|
|
||||||
working-directory: ./Source
|
|
||||||
run: |
|
|
||||||
if [[ "${{ inputs.OS }}" == "MacOS" ]]
|
|
||||||
then
|
|
||||||
display_os="macOS"
|
|
||||||
RUNTIME_ID="osx-${{ inputs.architecture }}"
|
|
||||||
else
|
|
||||||
display_os="Linux"
|
|
||||||
RUNTIME_ID="linux-${{ inputs.architecture }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
OUTPUT="bin/Publish/${display_os}-${{ inputs.architecture }}-${{ env.RELEASE_NAME }}"
|
|
||||||
|
|
||||||
echo "display_os=${display_os}" >> $GITHUB_OUTPUT
|
|
||||||
echo "Runtime Identifier: $RUNTIME_ID"
|
|
||||||
echo "Output Directory: $OUTPUT"
|
|
||||||
|
|
||||||
dotnet publish \
|
|
||||||
LibationAvalonia/LibationAvalonia.csproj \
|
|
||||||
--runtime $RUNTIME_ID \
|
|
||||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
|
||||||
--output $OUTPUT \
|
|
||||||
-p:PublishProfile=LibationAvalonia/Properties/PublishProfiles/${display_os}Profile.pubxml
|
|
||||||
dotnet publish \
|
|
||||||
LoadByOS/${display_os}ConfigApp/${display_os}ConfigApp.csproj \
|
|
||||||
--runtime $RUNTIME_ID \
|
|
||||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
|
||||||
--output $OUTPUT \
|
|
||||||
-p:PublishProfile=LoadByOS/Properties/${display_os}ConfigApp/PublishProfiles/${display_os}Profile.pubxml
|
|
||||||
dotnet publish \
|
|
||||||
LibationCli/LibationCli.csproj \
|
|
||||||
--runtime $RUNTIME_ID \
|
|
||||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
|
||||||
--output $OUTPUT \
|
|
||||||
-p:PublishProfile=LibationCli/Properties/PublishProfiles/${display_os}Profile.pubxml
|
|
||||||
dotnet publish \
|
|
||||||
HangoverAvalonia/HangoverAvalonia.csproj \
|
|
||||||
--runtime $RUNTIME_ID \
|
|
||||||
--configuration ${{ env.DOTNET_CONFIGURATION }} \
|
|
||||||
--output $OUTPUT \
|
|
||||||
-p:PublishProfile=HangoverAvalonia/Properties/PublishProfiles/${display_os}Profile.pubxml
|
|
||||||
|
|
||||||
- name: Build bundle
|
|
||||||
id: bundle
|
|
||||||
working-directory: ./Source/bin/Publish/${{ steps.publish.outputs.display_os }}-${{ inputs.architecture }}-${{ env.RELEASE_NAME }}
|
|
||||||
run: |
|
|
||||||
BUNDLE_DIR=$(pwd)
|
|
||||||
echo "Bundle dir: ${BUNDLE_DIR}"
|
|
||||||
cd ..
|
|
||||||
SCRIPT=../../../Scripts/Bundle_${{ inputs.OS }}.sh
|
|
||||||
chmod +rx ${SCRIPT}
|
|
||||||
${SCRIPT} "${BUNDLE_DIR}" "${{ steps.get_version.outputs.version }}" "${{ inputs.architecture }}"
|
|
||||||
artifact=$(ls ./bundle)
|
|
||||||
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
|
|
||||||
|
|
||||||
- name: Publish bundle
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ${{ steps.bundle.outputs.artifact }}
|
|
||||||
path: ./Source/bin/Publish/bundle/${{ steps.bundle.outputs.artifact }}
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 7
|
|
||||||
118
.github/workflows/build-windows.yml
vendored
118
.github/workflows/build-windows.yml
vendored
@ -1,118 +0,0 @@
|
|||||||
# build-windows.yml
|
|
||||||
# Reusable workflow that builds the Windows versions of Libation.
|
|
||||||
---
|
|
||||||
name: build
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
version_override:
|
|
||||||
type: string
|
|
||||||
description: "Version number override"
|
|
||||||
required: false
|
|
||||||
run_unit_tests:
|
|
||||||
type: boolean
|
|
||||||
description: "Skip running unit tests"
|
|
||||||
required: false
|
|
||||||
default: true
|
|
||||||
architecture:
|
|
||||||
type: string
|
|
||||||
description: "CPU architecture targeted by the build."
|
|
||||||
required: true
|
|
||||||
|
|
||||||
env:
|
|
||||||
DOTNET_CONFIGURATION: "Release"
|
|
||||||
DOTNET_VERSION: "9.0.x"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: "${{ matrix.os }}-${{ matrix.release_name }}-${{ inputs.architecture }}"
|
|
||||||
runs-on: windows-latest
|
|
||||||
env:
|
|
||||||
OUTPUT_NAME: "${{ matrix.os }}-${{ matrix.release_name }}-${{ inputs.architecture }}"
|
|
||||||
RUNTIME_ID: "win-${{ inputs.architecture }}"
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [Windows]
|
|
||||||
ui: [Avalonia]
|
|
||||||
release_name: [chardonnay]
|
|
||||||
include:
|
|
||||||
- os: Windows
|
|
||||||
ui: WinForms
|
|
||||||
release_name: classic
|
|
||||||
prefix: Classic-
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- name: Setup .NET
|
|
||||||
uses: actions/setup-dotnet@v5
|
|
||||||
with:
|
|
||||||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
|
||||||
env:
|
|
||||||
NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Get version
|
|
||||||
id: get_version
|
|
||||||
run: |
|
|
||||||
if ("${{ inputs.version_override }}".length -gt 0) {
|
|
||||||
$version = "${{ inputs.version_override }}"
|
|
||||||
} else {
|
|
||||||
$version = (Select-Xml -Path "./Source/AppScaffolding/AppScaffolding.csproj" -XPath "/Project/PropertyGroup/Version").Node.InnerXML.Trim()
|
|
||||||
}
|
|
||||||
"version=$version" >> $env:GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Unit test
|
|
||||||
if: ${{ inputs.run_unit_tests }}
|
|
||||||
working-directory: ./Source
|
|
||||||
run: dotnet test
|
|
||||||
|
|
||||||
- name: Publish
|
|
||||||
working-directory: ./Source
|
|
||||||
run: |
|
|
||||||
dotnet publish `
|
|
||||||
Libation${{ matrix.ui }}/Libation${{ matrix.ui }}.csproj `
|
|
||||||
--runtime ${{ env.RUNTIME_ID }} `
|
|
||||||
--configuration ${{ env.DOTNET_CONFIGURATION }} `
|
|
||||||
--output bin/Publish/${{ env.OUTPUT_NAME }} `
|
|
||||||
-p:PublishProfile=Libation${{ matrix.ui }}/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
|
||||||
dotnet publish `
|
|
||||||
LoadByOS/${{ matrix.os }}ConfigApp/${{ matrix.os }}ConfigApp.csproj `
|
|
||||||
--runtime ${{ env.RUNTIME_ID }} `
|
|
||||||
--configuration ${{ env.DOTNET_CONFIGURATION }} `
|
|
||||||
--output bin/Publish/${{ env.OUTPUT_NAME }} `
|
|
||||||
-p:PublishProfile=LoadByOS/${{ matrix.os }}ConfigApp/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
|
||||||
dotnet publish `
|
|
||||||
LibationCli/LibationCli.csproj `
|
|
||||||
--runtime ${{ env.RUNTIME_ID }} `
|
|
||||||
--configuration ${{ env.DOTNET_CONFIGURATION }} `
|
|
||||||
--output bin/Publish/${{ env.OUTPUT_NAME }} `
|
|
||||||
-p:DefineConstants="${{ matrix.release_name }}" `
|
|
||||||
-p:PublishProfile=LibationCli/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
|
||||||
dotnet publish `
|
|
||||||
Hangover${{ matrix.ui }}/Hangover${{ matrix.ui }}.csproj `
|
|
||||||
--runtime ${{ env.RUNTIME_ID }} `
|
|
||||||
--configuration ${{ env.DOTNET_CONFIGURATION }} `
|
|
||||||
--output bin/Publish/${{ env.OUTPUT_NAME }} `
|
|
||||||
-p:PublishProfile=Hangover${{ matrix.ui }}/Properties/PublishProfiles/${{ matrix.os }}Profile.pubxml
|
|
||||||
|
|
||||||
- name: Zip artifact
|
|
||||||
id: zip
|
|
||||||
working-directory: ./Source/bin/Publish
|
|
||||||
run: |
|
|
||||||
$bin_dir = "${{ env.OUTPUT_NAME }}\"
|
|
||||||
$delfiles = @(
|
|
||||||
"WindowsConfigApp.exe",
|
|
||||||
"WindowsConfigApp.runtimeconfig.json",
|
|
||||||
"WindowsConfigApp.deps.json"
|
|
||||||
)
|
|
||||||
foreach ($file in $delfiles){ if (test-path $bin_dir$file){ Remove-Item $bin_dir$file } }
|
|
||||||
$artifact="${{ matrix.prefix }}Libation.${{ steps.get_version.outputs.version }}-" + "${{ matrix.os }}".ToLower() + "-${{ matrix.release_name }}-${{ inputs.architecture }}"
|
|
||||||
"artifact=$artifact" >> $env:GITHUB_OUTPUT
|
|
||||||
Compress-Archive -Path "${bin_dir}*" -DestinationPath "$artifact.zip"
|
|
||||||
|
|
||||||
- name: Publish artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ${{ steps.zip.outputs.artifact }}.zip
|
|
||||||
path: ./Source/bin/Publish/${{ steps.zip.outputs.artifact }}.zip
|
|
||||||
if-no-files-found: error
|
|
||||||
retention-days: 7
|
|
||||||
53
.github/workflows/build.yml
vendored
53
.github/workflows/build.yml
vendored
@ -1,53 +0,0 @@
|
|||||||
# build.yml
|
|
||||||
# Reusable workflow that builds Libation for all platforms.
|
|
||||||
---
|
|
||||||
name: build
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
version_override:
|
|
||||||
type: string
|
|
||||||
description: "Version number override"
|
|
||||||
required: false
|
|
||||||
run_unit_tests:
|
|
||||||
type: boolean
|
|
||||||
description: "Skip running unit tests"
|
|
||||||
required: false
|
|
||||||
default: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
windows:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
architecture: [x64]
|
|
||||||
uses: ./.github/workflows/build-windows.yml
|
|
||||||
with:
|
|
||||||
version_override: ${{ inputs.version_override }}
|
|
||||||
run_unit_tests: ${{ inputs.run_unit_tests }}
|
|
||||||
architecture: ${{ matrix.architecture }}
|
|
||||||
|
|
||||||
linux:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
OS: [Redhat, Debian]
|
|
||||||
architecture: [x64, arm64]
|
|
||||||
uses: ./.github/workflows/build-linux.yml
|
|
||||||
with:
|
|
||||||
version_override: ${{ inputs.version_override }}
|
|
||||||
runs_on: ubuntu-latest
|
|
||||||
OS: ${{ matrix.OS }}
|
|
||||||
architecture: ${{ matrix.architecture }}
|
|
||||||
run_unit_tests: ${{ inputs.run_unit_tests }}
|
|
||||||
|
|
||||||
macos:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
architecture: [x64, arm64]
|
|
||||||
uses: ./.github/workflows/build-linux.yml
|
|
||||||
with:
|
|
||||||
version_override: ${{ inputs.version_override }}
|
|
||||||
runs_on: macos-latest
|
|
||||||
OS: MacOS
|
|
||||||
architecture: ${{ matrix.architecture }}
|
|
||||||
run_unit_tests: ${{ inputs.run_unit_tests }}
|
|
||||||
63
.github/workflows/docker.yml
vendored
63
.github/workflows/docker.yml
vendored
@ -1,63 +0,0 @@
|
|||||||
# docker.yml
|
|
||||||
# Reusable workflow that builds a docker image for Libation.
|
|
||||||
---
|
|
||||||
name: docker
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
type: string
|
|
||||||
description: "Version number"
|
|
||||||
required: true
|
|
||||||
release:
|
|
||||||
type: boolean
|
|
||||||
description: "Is this a release build?"
|
|
||||||
required: true
|
|
||||||
secrets:
|
|
||||||
docker_username:
|
|
||||||
required: true
|
|
||||||
docker_token:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build_and_push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
if: ${{ inputs.release }}
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.docker_username }}
|
|
||||||
password: ${{ secrets.docker_token }}
|
|
||||||
|
|
||||||
- name: Generate docker image tags
|
|
||||||
id: metadata
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
flavor: |
|
|
||||||
latest=true
|
|
||||||
images: |
|
|
||||||
name=${{ secrets.docker_username }}/libation
|
|
||||||
tags: |
|
|
||||||
type=raw,value=${{ inputs.version }},enable=${{ inputs.release }}
|
|
||||||
|
|
||||||
- name: Build and push image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: ${{ steps.metadata.outputs.tags != ''}}
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
tags: ${{ steps.metadata.outputs.tags }}
|
|
||||||
labels: ${{ steps.metadata.outputs.labels }}
|
|
||||||
58
.github/workflows/release.yml
vendored
58
.github/workflows/release.yml
vendored
@ -1,58 +0,0 @@
|
|||||||
# release.yml
|
|
||||||
# Builds and creates the release on any tags starting with a `v`
|
|
||||||
---
|
|
||||||
name: release
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
jobs:
|
|
||||||
prerelease:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
version: ${{ steps.get_version.outputs.version }}
|
|
||||||
steps:
|
|
||||||
- name: Get tag version
|
|
||||||
id: get_version
|
|
||||||
run: |
|
|
||||||
export TAG="${{ github.ref_name }}"
|
|
||||||
echo "version=${TAG#v}" >> "${GITHUB_OUTPUT}"
|
|
||||||
|
|
||||||
docker:
|
|
||||||
needs: [prerelease]
|
|
||||||
uses: ./.github/workflows/docker.yml
|
|
||||||
with:
|
|
||||||
version: ${{ needs.prerelease.outputs.version }}
|
|
||||||
release: true
|
|
||||||
secrets:
|
|
||||||
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
build:
|
|
||||||
needs: [prerelease]
|
|
||||||
uses: ./.github/workflows/build.yml
|
|
||||||
with:
|
|
||||||
version_override: ${{ needs.prerelease.outputs.version }}
|
|
||||||
run_unit_tests: false
|
|
||||||
|
|
||||||
release:
|
|
||||||
needs: [prerelease, build]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Download artifacts
|
|
||||||
uses: actions/download-artifact@v5
|
|
||||||
with:
|
|
||||||
path: artifacts
|
|
||||||
pattern: "*(Classic-)Libation.*"
|
|
||||||
|
|
||||||
- name: Release
|
|
||||||
id: release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
name: Libation ${{ needs.prerelease.outputs.version }}
|
|
||||||
body: <Put a body here>
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
draft: true
|
|
||||||
prerelease: false
|
|
||||||
files: |
|
|
||||||
artifacts/*/*
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
name: Validate MetaInfo
|
|
||||||
"on":
|
|
||||||
pull_request:
|
|
||||||
branches: ["master"]
|
|
||||||
paths:
|
|
||||||
- .github/workflows/validate-appstream-metainfo.yml
|
|
||||||
- Source/LoadByOS/LinuxConfigApp/com.getlibation.Libation.metainfo.xml
|
|
||||||
push:
|
|
||||||
branches: ["master"]
|
|
||||||
paths:
|
|
||||||
- .github/workflows/validate-appstream-metainfo.yml
|
|
||||||
- Source/LoadByOS/LinuxConfigApp/com.getlibation.Libation.metainfo.xml
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
validate-appstream-metainfo:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: ghcr.io/flathub/flatpak-builder-lint:latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- name: Check the MetaInfo file
|
|
||||||
run: flatpak-builder-lint appstream Source/LoadByOS/LinuxConfigApp/com.getlibation.Libation.metainfo.xml
|
|
||||||
21
.github/workflows/validate-desktop-file.yaml
vendored
21
.github/workflows/validate-desktop-file.yaml
vendored
@ -1,21 +0,0 @@
|
|||||||
name: Check desktop file
|
|
||||||
"on":
|
|
||||||
pull_request:
|
|
||||||
branches: ["master"]
|
|
||||||
paths:
|
|
||||||
- .github/workflows/validate-desktop-file.yml
|
|
||||||
- Source/LoadByOS/LinuxConfigApp/Libation.desktop
|
|
||||||
push:
|
|
||||||
branches: ["master"]
|
|
||||||
paths:
|
|
||||||
- .github/workflows/validate-desktop-file.yml
|
|
||||||
- Source/LoadByOS/LinuxConfigApp/Libation.desktop
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
validate-desktop-file:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- run: sudo apt --yes install desktop-file-utils
|
|
||||||
- name: Check the desktop file
|
|
||||||
run: desktop-file-validate Source/LoadByOS/LinuxConfigApp/Libation.desktop
|
|
||||||
22
.github/workflows/validate.yml
vendored
22
.github/workflows/validate.yml
vendored
@ -1,22 +0,0 @@
|
|||||||
# validate.yml
|
|
||||||
# Validates that Libation will build on a pull request or push to master.
|
|
||||||
---
|
|
||||||
name: validate
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
uses: ./.github/workflows/build.yml
|
|
||||||
docker:
|
|
||||||
uses: ./.github/workflows/docker.yml
|
|
||||||
with:
|
|
||||||
version: ${GITHUB_SHA}
|
|
||||||
release: false
|
|
||||||
secrets:
|
|
||||||
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
77
.gitignore
vendored
77
.gitignore
vendored
@ -4,7 +4,6 @@
|
|||||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||||
|
|
||||||
# User-specific files
|
# User-specific files
|
||||||
*.rsuser
|
|
||||||
*.suo
|
*.suo
|
||||||
*.user
|
*.user
|
||||||
*.userosscache
|
*.userosscache
|
||||||
@ -13,9 +12,6 @@
|
|||||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
*.userprefs
|
*.userprefs
|
||||||
|
|
||||||
# Mono auto generated files
|
|
||||||
mono_crash.*
|
|
||||||
|
|
||||||
# Build results
|
# Build results
|
||||||
[Dd]ebug/
|
[Dd]ebug/
|
||||||
[Dd]ebugPublic/
|
[Dd]ebugPublic/
|
||||||
@ -23,15 +19,10 @@ mono_crash.*
|
|||||||
[Rr]eleases/
|
[Rr]eleases/
|
||||||
x64/
|
x64/
|
||||||
x86/
|
x86/
|
||||||
[Ww][Ii][Nn]32/
|
|
||||||
[Aa][Rr][Mm]/
|
|
||||||
[Aa][Rr][Mm]64/
|
|
||||||
bld/
|
bld/
|
||||||
[Bb]in/
|
[Bb]in/
|
||||||
[Oo]bj/
|
[Oo]bj/
|
||||||
[Oo]ut/
|
|
||||||
[Ll]og/
|
[Ll]og/
|
||||||
[Ll]ogs/
|
|
||||||
|
|
||||||
# Visual Studio 2015/2017 cache/options directory
|
# Visual Studio 2015/2017 cache/options directory
|
||||||
.vs/
|
.vs/
|
||||||
@ -45,10 +36,9 @@ Generated\ Files/
|
|||||||
[Tt]est[Rr]esult*/
|
[Tt]est[Rr]esult*/
|
||||||
[Bb]uild[Ll]og.*
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
# NUnit
|
# NUNIT
|
||||||
*.VisualState.xml
|
*.VisualState.xml
|
||||||
TestResult.xml
|
TestResult.xml
|
||||||
nunit-*.xml
|
|
||||||
|
|
||||||
# Build Results of an ATL Project
|
# Build Results of an ATL Project
|
||||||
[Dd]ebugPS/
|
[Dd]ebugPS/
|
||||||
@ -62,9 +52,7 @@ BenchmarkDotNet.Artifacts/
|
|||||||
project.lock.json
|
project.lock.json
|
||||||
project.fragment.lock.json
|
project.fragment.lock.json
|
||||||
artifacts/
|
artifacts/
|
||||||
|
**/Properties/launchSettings.json
|
||||||
# ASP.NET Scaffolding
|
|
||||||
ScaffoldingReadMe.txt
|
|
||||||
|
|
||||||
# StyleCop
|
# StyleCop
|
||||||
StyleCopReport.xml
|
StyleCopReport.xml
|
||||||
@ -72,7 +60,7 @@ StyleCopReport.xml
|
|||||||
# Files built by Visual Studio
|
# Files built by Visual Studio
|
||||||
*_i.c
|
*_i.c
|
||||||
*_p.c
|
*_p.c
|
||||||
*_h.h
|
*_i.h
|
||||||
*.ilk
|
*.ilk
|
||||||
*.meta
|
*.meta
|
||||||
*.obj
|
*.obj
|
||||||
@ -89,7 +77,6 @@ StyleCopReport.xml
|
|||||||
*.tlh
|
*.tlh
|
||||||
*.tmp
|
*.tmp
|
||||||
*.tmp_proj
|
*.tmp_proj
|
||||||
*_wpftmp.csproj
|
|
||||||
*.log
|
*.log
|
||||||
*.vspscc
|
*.vspscc
|
||||||
*.vssscc
|
*.vssscc
|
||||||
@ -132,6 +119,9 @@ _ReSharper*/
|
|||||||
*.[Rr]e[Ss]harper
|
*.[Rr]e[Ss]harper
|
||||||
*.DotSettings.user
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# JustCode is a .NET coding add-in
|
||||||
|
.JustCode
|
||||||
|
|
||||||
# TeamCity is a build add-in
|
# TeamCity is a build add-in
|
||||||
_TeamCity*
|
_TeamCity*
|
||||||
|
|
||||||
@ -142,11 +132,6 @@ _TeamCity*
|
|||||||
.axoCover/*
|
.axoCover/*
|
||||||
!.axoCover/settings.json
|
!.axoCover/settings.json
|
||||||
|
|
||||||
# Coverlet is a free, cross platform Code Coverage Tool
|
|
||||||
coverage*.json
|
|
||||||
coverage*.xml
|
|
||||||
coverage*.info
|
|
||||||
|
|
||||||
# Visual Studio code coverage results
|
# Visual Studio code coverage results
|
||||||
*.coverage
|
*.coverage
|
||||||
*.coveragexml
|
*.coveragexml
|
||||||
@ -184,7 +169,7 @@ publish/
|
|||||||
*.azurePubxml
|
*.azurePubxml
|
||||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||||
# but database connection strings (with potential passwords) will be unencrypted
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
#*.pubxml
|
*.pubxml
|
||||||
*.publishproj
|
*.publishproj
|
||||||
|
|
||||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||||
@ -194,8 +179,6 @@ PublishScripts/
|
|||||||
|
|
||||||
# NuGet Packages
|
# NuGet Packages
|
||||||
*.nupkg
|
*.nupkg
|
||||||
# NuGet Symbol Packages
|
|
||||||
*.snupkg
|
|
||||||
# The packages folder can be ignored because of Package Restore
|
# The packages folder can be ignored because of Package Restore
|
||||||
**/[Pp]ackages/*
|
**/[Pp]ackages/*
|
||||||
# except build/, which is used as an MSBuild target.
|
# except build/, which is used as an MSBuild target.
|
||||||
@ -220,14 +203,12 @@ BundleArtifacts/
|
|||||||
Package.StoreAssociation.xml
|
Package.StoreAssociation.xml
|
||||||
_pkginfo.txt
|
_pkginfo.txt
|
||||||
*.appx
|
*.appx
|
||||||
*.appxbundle
|
|
||||||
*.appxupload
|
|
||||||
|
|
||||||
# Visual Studio cache files
|
# Visual Studio cache files
|
||||||
# files ending in .cache can be ignored
|
# files ending in .cache can be ignored
|
||||||
*.[Cc]ache
|
*.[Cc]ache
|
||||||
# but keep track of directories ending in .cache
|
# but keep track of directories ending in .cache
|
||||||
!?*.[Cc]ache/
|
!*.[Cc]ache/
|
||||||
|
|
||||||
# Others
|
# Others
|
||||||
ClientBin/
|
ClientBin/
|
||||||
@ -240,7 +221,7 @@ ClientBin/
|
|||||||
*.publishsettings
|
*.publishsettings
|
||||||
orleans.codegen.cs
|
orleans.codegen.cs
|
||||||
|
|
||||||
# Including strong name files can present a security risk
|
# Including strong name files can present a security risk
|
||||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||||
#*.snk
|
#*.snk
|
||||||
|
|
||||||
@ -271,9 +252,6 @@ ServiceFabricBackup/
|
|||||||
*.bim.layout
|
*.bim.layout
|
||||||
*.bim_*.settings
|
*.bim_*.settings
|
||||||
*.rptproj.rsuser
|
*.rptproj.rsuser
|
||||||
*- [Bb]ackup.rdl
|
|
||||||
*- [Bb]ackup ([0-9]).rdl
|
|
||||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
|
||||||
|
|
||||||
# Microsoft Fakes
|
# Microsoft Fakes
|
||||||
FakesAssemblies/
|
FakesAssemblies/
|
||||||
@ -309,8 +287,12 @@ paket-files/
|
|||||||
# FAKE - F# Make
|
# FAKE - F# Make
|
||||||
.fake/
|
.fake/
|
||||||
|
|
||||||
# CodeRush personal settings
|
# JetBrains Rider
|
||||||
.cr/personal
|
.idea/
|
||||||
|
*.sln.iml
|
||||||
|
|
||||||
|
# CodeRush
|
||||||
|
.cr/
|
||||||
|
|
||||||
# Python Tools for Visual Studio (PTVS)
|
# Python Tools for Visual Studio (PTVS)
|
||||||
__pycache__/
|
__pycache__/
|
||||||
@ -335,7 +317,7 @@ __pycache__/
|
|||||||
# OpenCover UI analysis results
|
# OpenCover UI analysis results
|
||||||
OpenCover/
|
OpenCover/
|
||||||
|
|
||||||
# Azure Stream Analytics local run output
|
# Azure Stream Analytics local run output
|
||||||
ASALocalRun/
|
ASALocalRun/
|
||||||
|
|
||||||
# MSBuild Binary and Structured Log
|
# MSBuild Binary and Structured Log
|
||||||
@ -344,30 +326,5 @@ ASALocalRun/
|
|||||||
# NVidia Nsight GPU debugger configuration file
|
# NVidia Nsight GPU debugger configuration file
|
||||||
*.nvuser
|
*.nvuser
|
||||||
|
|
||||||
# MFractors (Xamarin productivity tool) working folder
|
# MFractors (Xamarin productivity tool) working folder
|
||||||
.mfractor/
|
.mfractor/
|
||||||
|
|
||||||
# Local History for Visual Studio
|
|
||||||
.localhistory/
|
|
||||||
|
|
||||||
# BeatPulse healthcheck temp database
|
|
||||||
healthchecksdb
|
|
||||||
|
|
||||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
|
||||||
MigrationBackup/
|
|
||||||
|
|
||||||
# Ionide (cross platform F# VS Code tools) working folder
|
|
||||||
.ionide/
|
|
||||||
|
|
||||||
# Fody - auto-generated XML schema
|
|
||||||
FodyWeavers.xsd
|
|
||||||
|
|
||||||
|
|
||||||
### manually ignored files
|
|
||||||
|
|
||||||
# Windows shortcuts
|
|
||||||
*.lnk
|
|
||||||
|
|
||||||
/__TODO.txt
|
|
||||||
/DataLayer/LibationContext.db
|
|
||||||
*/bin-Avalonia
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"WindowsClassic": "Classic-Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-win(?:dows)?-classic-x64\\.zip",
|
|
||||||
"WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-win(?:dows)?-chardonnay-x64\\.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\\.tgz",
|
|
||||||
"LinuxAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-arm64\\.deb",
|
|
||||||
"LinuxAvalonia_Arm64_RPM": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-linux-chardonnay-arm64\\.rpm",
|
|
||||||
"MacOSAvalonia_Arm64": "Libation\\.\\d+\\.\\d+\\.\\d+(?:\\.\\d+)?-macOS-chardonnay-arm64\\.tgz"
|
|
||||||
}
|
|
||||||
32
.vscode/launch.json
vendored
32
.vscode/launch.json
vendored
@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
|
|
||||||
{
|
|
||||||
"name": ".NET Core Launch (console) Windows",
|
|
||||||
"type": "coreclr",
|
|
||||||
"request": "launch",
|
|
||||||
"preLaunchTask": "build",
|
|
||||||
"program": "${workspaceFolder}/Source/bin/Avalonia/Debug/Libation.dll",
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}",
|
|
||||||
"stopAtEntry": false,
|
|
||||||
"console": "internalConsole"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": ".NET Core Launch (console) Linux",
|
|
||||||
"type": "coreclr",
|
|
||||||
"request": "launch",
|
|
||||||
"preLaunchTask": "build_linux",
|
|
||||||
"program": "${workspaceFolder}/Source/bin/Avalonia/Debug/Libation.dll",
|
|
||||||
"args": [],
|
|
||||||
"cwd": "${workspaceFolder}",
|
|
||||||
"stopAtEntry": false,
|
|
||||||
"console": "internalConsole"
|
|
||||||
}
|
|
||||||
|
|
||||||
]
|
|
||||||
}
|
|
||||||
59
.vscode/tasks.json
vendored
59
.vscode/tasks.json
vendored
@ -1,59 +0,0 @@
|
|||||||
{
|
|
||||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
|
||||||
// for the documentation about the tasks.json format
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"label": "build",
|
|
||||||
"dependsOn": [
|
|
||||||
"build_libation",
|
|
||||||
"build_linuxconfigapp"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "build_libation",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "dotnet",
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"${workspaceFolder}/Source/LibationAvalonia/LibationAvalonia.csproj"
|
|
||||||
],
|
|
||||||
"group": "build",
|
|
||||||
"presentation": {
|
|
||||||
//"reveal": "silent"
|
|
||||||
},
|
|
||||||
"problemMatcher": "$msCompile"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "build_linuxconfigapp",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "dotnet",
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"${workspaceFolder}/Source/LoadByOS/LinuxConfigApp/LinuxConfigApp.csproj"
|
|
||||||
],
|
|
||||||
"group": "build",
|
|
||||||
"presentation": {
|
|
||||||
//"reveal": "silent"
|
|
||||||
},
|
|
||||||
"problemMatcher": "$msCompile"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "build_linux",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "dotnet",
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"${workspaceFolder}/Source/LibationAvalonia/LibationAvalonia.csproj",
|
|
||||||
"-p:TargetFramework=net9.0",
|
|
||||||
"-p:TargetFrameworks=net9.0",
|
|
||||||
"-p:RuntimeIdentifier=linux-x64"
|
|
||||||
],
|
|
||||||
"group": "build",
|
|
||||||
"presentation": {
|
|
||||||
//"reveal": "silent"
|
|
||||||
},
|
|
||||||
"problemMatcher": "$msCompile"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
123
AaxDecrypter/AaxDecrypter.csproj
Normal file
123
AaxDecrypter/AaxDecrypter.csproj
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="taglib-sharp">
|
||||||
|
<HintPath>lib\taglib-sharp.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="..\..\..\..\..\..\Dinah%2527s folder\coding\_NET\Visual Studio 2019\Libation\AaxDecrypter\UNTESTED\BytesCrackerLib\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\Dinah.Core\Dinah.Core\Dinah.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="BytesCrackerLib\alglib1.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="BytesCrackerLib\audible_byte#4-4_0_10000x789935_0.rtc">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="BytesCrackerLib\audible_byte#4-4_1_10000x791425_0.rtc">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="BytesCrackerLib\audible_byte#4-4_2_10000x790991_0.rtc">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="BytesCrackerLib\audible_byte#4-4_3_10000x792120_0.rtc">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="BytesCrackerLib\audible_byte#4-4_4_10000x790743_0.rtc">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="BytesCrackerLib\audible_byte#4-4_5_10000x790568_0.rtc">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="BytesCrackerLib\audible_byte#4-4_6_10000x791458_0.rtc">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="BytesCrackerLib\audible_byte#4-4_7_10000x791707_0.rtc">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="BytesCrackerLib\audible_byte#4-4_8_10000x790202_0.rtc">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="BytesCrackerLib\audible_byte#4-4_9_10000x791022_0.rtc">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="BytesCrackerLib\ffmpeg.exe">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="BytesCrackerLib\ffprobe.exe">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="BytesCrackerLib\rcrack.exe">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="DecryptLib\AtomicParsley.exe">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="DecryptLib\avcodec-57.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="DecryptLib\avdevice-57.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="DecryptLib\avfilter-6.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="DecryptLib\avformat-57.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="DecryptLib\avutil-55.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="DecryptLib\cygcrypto-1.0.0.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="DecryptLib\cyggcc_s-1.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="DecryptLib\cygmp4v2-2.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="DecryptLib\cygstdc++-6.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="DecryptLib\cygwin1.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="DecryptLib\cygz.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="DecryptLib\ffmpeg.exe">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="DecryptLib\ffprobe.exe">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="DecryptLib\mp4trackdump.exe">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="DecryptLib\postproc-54.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="DecryptLib\swresample-2.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="DecryptLib\swscale-4.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="DecryptLib\taglib-sharp.dll">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
BIN
AaxDecrypter/BytesCrackerLib/alglib1.dll
Normal file
BIN
AaxDecrypter/BytesCrackerLib/alglib1.dll
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
AaxDecrypter/BytesCrackerLib/ffmpeg.exe
Normal file
BIN
AaxDecrypter/BytesCrackerLib/ffmpeg.exe
Normal file
Binary file not shown.
BIN
AaxDecrypter/BytesCrackerLib/ffprobe.exe
Normal file
BIN
AaxDecrypter/BytesCrackerLib/ffprobe.exe
Normal file
Binary file not shown.
BIN
AaxDecrypter/BytesCrackerLib/rcrack.exe
Normal file
BIN
AaxDecrypter/BytesCrackerLib/rcrack.exe
Normal file
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/AtomicParsley.exe
Normal file
BIN
AaxDecrypter/DecryptLib/AtomicParsley.exe
Normal file
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/avcodec-57.dll
Normal file
BIN
AaxDecrypter/DecryptLib/avcodec-57.dll
Normal file
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/avdevice-57.dll
Normal file
BIN
AaxDecrypter/DecryptLib/avdevice-57.dll
Normal file
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/avfilter-6.dll
Normal file
BIN
AaxDecrypter/DecryptLib/avfilter-6.dll
Normal file
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/avformat-57.dll
Normal file
BIN
AaxDecrypter/DecryptLib/avformat-57.dll
Normal file
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/avutil-55.dll
Normal file
BIN
AaxDecrypter/DecryptLib/avutil-55.dll
Normal file
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/cygcrypto-1.0.0.dll
Normal file
BIN
AaxDecrypter/DecryptLib/cygcrypto-1.0.0.dll
Normal file
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/cyggcc_s-1.dll
Normal file
BIN
AaxDecrypter/DecryptLib/cyggcc_s-1.dll
Normal file
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/cygmp4v2-2.dll
Normal file
BIN
AaxDecrypter/DecryptLib/cygmp4v2-2.dll
Normal file
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/cygstdc++-6.dll
Normal file
BIN
AaxDecrypter/DecryptLib/cygstdc++-6.dll
Normal file
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/cygwin1.dll
Normal file
BIN
AaxDecrypter/DecryptLib/cygwin1.dll
Normal file
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/cygz.dll
Normal file
BIN
AaxDecrypter/DecryptLib/cygz.dll
Normal file
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/ffmpeg.exe
Normal file
BIN
AaxDecrypter/DecryptLib/ffmpeg.exe
Normal file
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/ffprobe.exe
Normal file
BIN
AaxDecrypter/DecryptLib/ffprobe.exe
Normal file
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/mp4trackdump.exe
Normal file
BIN
AaxDecrypter/DecryptLib/mp4trackdump.exe
Normal file
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/postproc-54.dll
Normal file
BIN
AaxDecrypter/DecryptLib/postproc-54.dll
Normal file
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/swresample-2.dll
Normal file
BIN
AaxDecrypter/DecryptLib/swresample-2.dll
Normal file
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/swscale-4.dll
Normal file
BIN
AaxDecrypter/DecryptLib/swscale-4.dll
Normal file
Binary file not shown.
BIN
AaxDecrypter/DecryptLib/taglib-sharp.dll
Normal file
BIN
AaxDecrypter/DecryptLib/taglib-sharp.dll
Normal file
Binary file not shown.
355
AaxDecrypter/UNTESTED/AaxToM4bConverter.cs
Normal file
355
AaxDecrypter/UNTESTED/AaxToM4bConverter.cs
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dinah.Core;
|
||||||
|
using Dinah.Core.Diagnostics;
|
||||||
|
using Dinah.Core.IO;
|
||||||
|
using Dinah.Core.StepRunner;
|
||||||
|
|
||||||
|
namespace AaxDecrypter
|
||||||
|
{
|
||||||
|
public interface ISimpleAaxToM4bConverter
|
||||||
|
{
|
||||||
|
event EventHandler<int> DecryptProgressUpdate;
|
||||||
|
|
||||||
|
bool Run();
|
||||||
|
|
||||||
|
string AppName { get; set; }
|
||||||
|
string inputFileName { get; }
|
||||||
|
byte[] coverBytes { get; }
|
||||||
|
string outDir { get; }
|
||||||
|
string outputFileName { get; }
|
||||||
|
|
||||||
|
Chapters chapters { get; }
|
||||||
|
Tags tags { get; }
|
||||||
|
EncodingInfo encodingInfo { get; }
|
||||||
|
|
||||||
|
void SetOutputFilename(string outFileName);
|
||||||
|
}
|
||||||
|
public interface IAdvancedAaxToM4bConverter : ISimpleAaxToM4bConverter
|
||||||
|
{
|
||||||
|
bool Step1_CreateDir();
|
||||||
|
bool Step2_DecryptAax();
|
||||||
|
bool Step3_Chapterize();
|
||||||
|
bool Step4_InsertCoverArt();
|
||||||
|
bool Step5_Cleanup();
|
||||||
|
bool Step6_AddTags();
|
||||||
|
bool End_CreateCue();
|
||||||
|
bool End_CreateNfo();
|
||||||
|
}
|
||||||
|
/// <summary>full c# app. integrated logging. no UI</summary>
|
||||||
|
public class AaxToM4bConverter : IAdvancedAaxToM4bConverter
|
||||||
|
{
|
||||||
|
public event EventHandler<int> DecryptProgressUpdate;
|
||||||
|
|
||||||
|
public string inputFileName { get; }
|
||||||
|
public string decryptKey { get; private set; }
|
||||||
|
|
||||||
|
private StepSequence steps { get; }
|
||||||
|
public byte[] coverBytes { get; private set; }
|
||||||
|
|
||||||
|
public string AppName { get; set; } = nameof(AaxToM4bConverter);
|
||||||
|
|
||||||
|
public string outDir { get; private set; }
|
||||||
|
public string outputFileName { get; private set; }
|
||||||
|
|
||||||
|
public Chapters chapters { get; private set; }
|
||||||
|
public Tags tags { get; private set; }
|
||||||
|
public EncodingInfo encodingInfo { get; private set; }
|
||||||
|
|
||||||
|
public static async Task<AaxToM4bConverter> CreateAsync(string inputFile, string decryptKey)
|
||||||
|
{
|
||||||
|
var converter = new AaxToM4bConverter(inputFile, decryptKey);
|
||||||
|
await converter.prelimProcessing();
|
||||||
|
converter.printPrelim();
|
||||||
|
|
||||||
|
return converter;
|
||||||
|
}
|
||||||
|
private AaxToM4bConverter(string inputFile, string decryptKey)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(inputFile)) throw new ArgumentNullException(nameof(inputFile), "Input file may not be null or whitespace");
|
||||||
|
if (!File.Exists(inputFile)) throw new ArgumentNullException(nameof(inputFile), "File does not exist");
|
||||||
|
|
||||||
|
steps = new StepSequence
|
||||||
|
{
|
||||||
|
Name = "Convert Aax To M4b",
|
||||||
|
|
||||||
|
["Step 1: Create Dir"] = Step1_CreateDir,
|
||||||
|
["Step 2: Decrypt Aax"] = Step2_DecryptAax,
|
||||||
|
["Step 3: Chapterize and tag"] = Step3_Chapterize,
|
||||||
|
["Step 4: Insert Cover Art"] = Step4_InsertCoverArt,
|
||||||
|
["Step 5: Cleanup"] = Step5_Cleanup,
|
||||||
|
["Step 6: Add Tags"] = Step6_AddTags,
|
||||||
|
["End: Create Cue"] = End_CreateCue,
|
||||||
|
["End: Create Nfo"] = End_CreateNfo
|
||||||
|
};
|
||||||
|
|
||||||
|
this.inputFileName = inputFile;
|
||||||
|
this.decryptKey = decryptKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task prelimProcessing()
|
||||||
|
{
|
||||||
|
this.tags = new Tags(this.inputFileName);
|
||||||
|
this.encodingInfo = new EncodingInfo(this.inputFileName);
|
||||||
|
this.chapters = new Chapters(this.inputFileName, this.tags.duration.TotalSeconds);
|
||||||
|
|
||||||
|
var defaultFilename = Path.Combine(
|
||||||
|
Path.GetDirectoryName(this.inputFileName),
|
||||||
|
getASCIITag(this.tags.author),
|
||||||
|
getASCIITag(this.tags.title) + ".m4b"
|
||||||
|
);
|
||||||
|
SetOutputFilename(defaultFilename);
|
||||||
|
|
||||||
|
await Task.Run(() => saveCover(inputFileName));
|
||||||
|
}
|
||||||
|
private string getASCIITag(string property)
|
||||||
|
{
|
||||||
|
foreach (char ch in new string(Path.GetInvalidFileNameChars()) + new string(Path.GetInvalidPathChars()))
|
||||||
|
property = property.Replace(ch.ToString(), "");
|
||||||
|
return property;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveCover(string aaxFile)
|
||||||
|
{
|
||||||
|
using var file = TagLib.File.Create(aaxFile, "audio/mp4", TagLib.ReadStyle.Average);
|
||||||
|
this.coverBytes = file.Tag.Pictures[0].Data.Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void printPrelim()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Audible Book ID = " + tags.id);
|
||||||
|
|
||||||
|
Console.WriteLine("Book: " + tags.title);
|
||||||
|
Console.WriteLine("Author: " + tags.author);
|
||||||
|
Console.WriteLine("Narrator: " + tags.narrator);
|
||||||
|
Console.WriteLine("Year: " + tags.year);
|
||||||
|
Console.WriteLine("Total Time: "
|
||||||
|
+ tags.duration.GetTotalTimeFormatted()
|
||||||
|
+ " in " + chapters.Count() + " chapters");
|
||||||
|
Console.WriteLine("WARNING-Source is "
|
||||||
|
+ encodingInfo.originalBitrate + " kbits @ "
|
||||||
|
+ encodingInfo.sampleRate + "Hz, "
|
||||||
|
+ encodingInfo.channels + " channels");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Run()
|
||||||
|
{
|
||||||
|
var (IsSuccess, Elapsed) = steps.Run();
|
||||||
|
|
||||||
|
if (!IsSuccess)
|
||||||
|
{
|
||||||
|
Console.WriteLine("WARNING-Conversion failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var speedup = (int)(tags.duration.TotalSeconds / (long)Elapsed.TotalSeconds);
|
||||||
|
Console.WriteLine("Speedup is " + speedup + "x realtime.");
|
||||||
|
Console.WriteLine("Done");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetOutputFilename(string outFileName)
|
||||||
|
{
|
||||||
|
this.outputFileName = outFileName;
|
||||||
|
|
||||||
|
if (Path.GetExtension(this.outputFileName) != ".m4b")
|
||||||
|
this.outputFileName = outputFileWithNewExt(".m4b");
|
||||||
|
|
||||||
|
this.outDir = Path.GetDirectoryName(this.outputFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string outputFileWithNewExt(string extension)
|
||||||
|
=> Path.Combine(this.outDir, Path.GetFileNameWithoutExtension(this.outputFileName) + '.' + extension.Trim('.'));
|
||||||
|
|
||||||
|
public bool Step1_CreateDir()
|
||||||
|
{
|
||||||
|
ProcessRunner.WorkingDir = this.outDir;
|
||||||
|
Directory.CreateDirectory(this.outDir);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Step2_DecryptAax()
|
||||||
|
{
|
||||||
|
DecryptProgressUpdate?.Invoke(this, 0);
|
||||||
|
|
||||||
|
var tempRipFile = Path.Combine(this.outDir, "funny.aac");
|
||||||
|
|
||||||
|
var fail = "WARNING-Decrypt failure. ";
|
||||||
|
|
||||||
|
int returnCode;
|
||||||
|
if (string.IsNullOrWhiteSpace(decryptKey))
|
||||||
|
{
|
||||||
|
returnCode = getKey_decrypt(tempRipFile);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
returnCode = decrypt(tempRipFile);
|
||||||
|
if (returnCode == -99)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{fail}Incorrect decrypt key: {decryptKey}");
|
||||||
|
this.decryptKey = null;
|
||||||
|
returnCode = getKey_decrypt(tempRipFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (returnCode == 100)
|
||||||
|
Console.WriteLine($"{fail}Thread completed without changing return code. This shouldn't be possible");
|
||||||
|
else if (returnCode == 0)
|
||||||
|
{
|
||||||
|
// success!
|
||||||
|
FileExt.SafeMove(tempRipFile, outputFileWithNewExt(".mp4"));
|
||||||
|
DecryptProgressUpdate?.Invoke(this, 100);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (returnCode == -99)
|
||||||
|
Console.WriteLine($"{fail}Incorrect decrypt key: {decryptKey}");
|
||||||
|
else // any other returnCode
|
||||||
|
Console.WriteLine($"{fail}Unknown failure code: {returnCode}");
|
||||||
|
|
||||||
|
FileExt.SafeDelete(tempRipFile);
|
||||||
|
DecryptProgressUpdate?.Invoke(this, 0);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getKey_decrypt(string tempRipFile)
|
||||||
|
{
|
||||||
|
getKey();
|
||||||
|
return decrypt(tempRipFile);
|
||||||
|
}
|
||||||
|
private void getKey()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Discovering decrypt key");
|
||||||
|
|
||||||
|
Console.WriteLine("Getting file hash");
|
||||||
|
var checksum = BytesCracker.GetChecksum(inputFileName);
|
||||||
|
Console.WriteLine("File hash calculated: " + checksum);
|
||||||
|
|
||||||
|
Console.WriteLine("Cracking activation bytes");
|
||||||
|
var activation_bytes = BytesCracker.GetActivationBytes(checksum);
|
||||||
|
this.decryptKey = activation_bytes;
|
||||||
|
Console.WriteLine("Activation bytes cracked. Decrypt key: " + activation_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int decrypt(string tempRipFile)
|
||||||
|
{
|
||||||
|
FileExt.SafeDelete(tempRipFile);
|
||||||
|
|
||||||
|
Console.WriteLine("Decrypting with key " + decryptKey);
|
||||||
|
|
||||||
|
var returnCode = 100;
|
||||||
|
var thread = new Thread(() => returnCode = this.ngDecrypt());
|
||||||
|
thread.Start();
|
||||||
|
|
||||||
|
double fileLen = new FileInfo(this.inputFileName).Length;
|
||||||
|
while (thread.IsAlive && returnCode == 100)
|
||||||
|
{
|
||||||
|
Thread.Sleep(500);
|
||||||
|
if (File.Exists(tempRipFile))
|
||||||
|
{
|
||||||
|
double tempLen = new FileInfo(tempRipFile).Length;
|
||||||
|
var percentProgress = tempLen / fileLen * 100.0;
|
||||||
|
DecryptProgressUpdate?.Invoke(this, (int)percentProgress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int ngDecrypt()
|
||||||
|
{
|
||||||
|
var info = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = DecryptSupportLibraries.mp4trackdumpPath,
|
||||||
|
Arguments = "-c " + this.encodingInfo.channels + " -r " + this.encodingInfo.sampleRate + " \"" + this.inputFileName + "\""
|
||||||
|
};
|
||||||
|
info.EnvironmentVariables["VARIABLE"] = decryptKey;
|
||||||
|
|
||||||
|
var (output, exitCode) = info.RunHidden();
|
||||||
|
|
||||||
|
// bad checksum -- bad decrypt key
|
||||||
|
if (output.Contains("checksums mismatch, aborting!"))
|
||||||
|
return -99;
|
||||||
|
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// temp file names for steps 3, 4, 5
|
||||||
|
string tempChapsPath => Path.Combine(this.outDir, "tempChaps.mp4");
|
||||||
|
string mp4_file => outputFileWithNewExt(".mp4");
|
||||||
|
string ff_txt_file => mp4_file + ".ff.txt";
|
||||||
|
|
||||||
|
public bool Step3_Chapterize()
|
||||||
|
{
|
||||||
|
string str1 = "";
|
||||||
|
if (this.chapters.FirstChapterStart != 0.0)
|
||||||
|
{
|
||||||
|
str1 = " -ss " + this.chapters.FirstChapterStart.ToString("0.000", CultureInfo.InvariantCulture) + " -t " + (this.chapters.LastChapterStart - 1.0).ToString("0.000", CultureInfo.InvariantCulture) + " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
string ffmpegTags = this.tags.GenerateFfmpegTags();
|
||||||
|
string ffmpegChapters = this.chapters.GenerateFfmpegChapters();
|
||||||
|
File.WriteAllText(ff_txt_file, ffmpegTags + ffmpegChapters);
|
||||||
|
|
||||||
|
var tagAndChapterInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = DecryptSupportLibraries.ffmpegPath,
|
||||||
|
Arguments = "-y -i \"" + mp4_file + "\" -f ffmetadata -i \"" + ff_txt_file + "\" -map_metadata 1 -bsf:a aac_adtstoasc -c:a copy" + str1 + " -map 0 \"" + tempChapsPath + "\""
|
||||||
|
};
|
||||||
|
tagAndChapterInfo.RunHidden();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Step4_InsertCoverArt()
|
||||||
|
{
|
||||||
|
// save cover image as temp file
|
||||||
|
var coverPath = Path.Combine(this.outDir, "cover-" + Guid.NewGuid() + ".jpg");
|
||||||
|
FileExt.CreateFile(coverPath, this.coverBytes);
|
||||||
|
|
||||||
|
var insertCoverArtInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = DecryptSupportLibraries.atomicParsleyPath,
|
||||||
|
Arguments = "\"" + tempChapsPath + "\" --encodingTool \"" + AppName + "\" --artwork \"" + coverPath + "\" --overWrite"
|
||||||
|
};
|
||||||
|
insertCoverArtInfo.RunHidden();
|
||||||
|
|
||||||
|
// delete temp file
|
||||||
|
FileExt.SafeDelete(coverPath);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Step5_Cleanup()
|
||||||
|
{
|
||||||
|
FileExt.SafeDelete(mp4_file);
|
||||||
|
FileExt.SafeDelete(ff_txt_file);
|
||||||
|
FileExt.SafeMove(tempChapsPath, this.outputFileName);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Step6_AddTags()
|
||||||
|
{
|
||||||
|
this.tags.AddAppleTags(this.outputFileName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool End_CreateCue()
|
||||||
|
{
|
||||||
|
File.WriteAllText(outputFileWithNewExt(".cue"), this.chapters.GetCuefromChapters(Path.GetFileName(this.outputFileName)));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool End_CreateNfo()
|
||||||
|
{
|
||||||
|
File.WriteAllText(outputFileWithNewExt(".nfo"), NFO.CreateNfoContents(AppName, this.tags, this.encodingInfo, this.chapters));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
AaxDecrypter/UNTESTED/BytesCracker.cs
Normal file
53
AaxDecrypter/UNTESTED/BytesCracker.cs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dinah.Core;
|
||||||
|
using Dinah.Core.Diagnostics;
|
||||||
|
|
||||||
|
namespace AaxDecrypter
|
||||||
|
{
|
||||||
|
public static class BytesCracker
|
||||||
|
{
|
||||||
|
public static string GetChecksum(string aaxPath)
|
||||||
|
{
|
||||||
|
var info = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = BytesCrackerSupportLibraries.ffprobePath,
|
||||||
|
Arguments = aaxPath.SurroundWithQuotes(),
|
||||||
|
WorkingDirectory = Directory.GetCurrentDirectory()
|
||||||
|
};
|
||||||
|
|
||||||
|
// checksum is in the debug info. ffprobe's debug info is written to stderr, not stdout
|
||||||
|
var readErrorOutput = true;
|
||||||
|
var ffprobeStderr = info.RunHidden(readErrorOutput).Output;
|
||||||
|
|
||||||
|
// example checksum line:
|
||||||
|
// ... [aax] file checksum == 0c527840c4f18517157eb0b4f9d6f9317ce60cd1
|
||||||
|
var checksum = ffprobeStderr.ExtractString("file checksum == ", 40);
|
||||||
|
|
||||||
|
return checksum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>use checksum to get activation bytes. activation bytes are unique per audible customer. only have to do this 1x/customer</summary>
|
||||||
|
public static string GetActivationBytes(string checksum)
|
||||||
|
{
|
||||||
|
var info = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = BytesCrackerSupportLibraries.rcrackPath,
|
||||||
|
Arguments = @". -h " + checksum,
|
||||||
|
WorkingDirectory = Directory.GetCurrentDirectory()
|
||||||
|
};
|
||||||
|
|
||||||
|
var rcrackStdout = info.RunHidden().Output;
|
||||||
|
|
||||||
|
// example result
|
||||||
|
// 0c527840c4f18517157eb0b4f9d6f9317ce60cd1 \xbd\x89X\x09 hex:bd895809
|
||||||
|
var activation_bytes = rcrackStdout.ExtractString("hex:", 8);
|
||||||
|
|
||||||
|
return activation_bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
AaxDecrypter/UNTESTED/BytesCrackerSupportLibraries.cs
Normal file
28
AaxDecrypter/UNTESTED/BytesCrackerSupportLibraries.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace AaxDecrypter
|
||||||
|
{
|
||||||
|
public static class BytesCrackerSupportLibraries
|
||||||
|
{
|
||||||
|
// GetActivationBytes dependencies
|
||||||
|
// rcrack.exe
|
||||||
|
// alglib1.dll
|
||||||
|
// RainbowCrack files to recover your own Audible activation data (activation_bytes) in an offline manner
|
||||||
|
// audible_byte#4-4_0_10000x789935_0.rtc
|
||||||
|
// audible_byte#4-4_1_10000x791425_0.rtc
|
||||||
|
// audible_byte#4-4_2_10000x790991_0.rtc
|
||||||
|
// audible_byte#4-4_3_10000x792120_0.rtc
|
||||||
|
// audible_byte#4-4_4_10000x790743_0.rtc
|
||||||
|
// audible_byte#4-4_5_10000x790568_0.rtc
|
||||||
|
// audible_byte#4-4_6_10000x791458_0.rtc
|
||||||
|
// audible_byte#4-4_7_10000x791707_0.rtc
|
||||||
|
// audible_byte#4-4_8_10000x790202_0.rtc
|
||||||
|
// audible_byte#4-4_9_10000x791022_0.rtc
|
||||||
|
|
||||||
|
private static string appPath_ { get; } = Path.GetDirectoryName(Dinah.Core.Exe.FileLocationOnDisk);
|
||||||
|
private static string bytesCrackerLib_ { get; } = Path.Combine(appPath_, "BytesCrackerLib");
|
||||||
|
|
||||||
|
public static string ffprobePath { get; } = Path.Combine(bytesCrackerLib_, "ffprobe.exe");
|
||||||
|
public static string rcrackPath { get; } = Path.Combine(bytesCrackerLib_, "rcrack.exe");
|
||||||
|
}
|
||||||
|
}
|
||||||
95
AaxDecrypter/UNTESTED/Chapters.cs
Normal file
95
AaxDecrypter/UNTESTED/Chapters.cs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Dinah.Core.Diagnostics;
|
||||||
|
|
||||||
|
namespace AaxDecrypter
|
||||||
|
{
|
||||||
|
public class Chapters
|
||||||
|
{
|
||||||
|
private List<double> markers { get; }
|
||||||
|
|
||||||
|
public double FirstChapterStart => markers[0];
|
||||||
|
public double LastChapterStart => markers[markers.Count - 1];
|
||||||
|
|
||||||
|
public Chapters(string file, double totalTime)
|
||||||
|
{
|
||||||
|
this.markers = getAAXChapters(file);
|
||||||
|
|
||||||
|
// add end time
|
||||||
|
this.markers.Add(totalTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<double> getAAXChapters(string file)
|
||||||
|
{
|
||||||
|
var info = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = DecryptSupportLibraries.ffprobePath,
|
||||||
|
Arguments = "-loglevel panic -show_chapters -print_format xml \"" + file + "\""
|
||||||
|
};
|
||||||
|
var xml = info.RunHidden().Output;
|
||||||
|
|
||||||
|
var xmlDocument = new System.Xml.XmlDocument();
|
||||||
|
xmlDocument.LoadXml(xml);
|
||||||
|
var chapters = xmlDocument.SelectNodes("/ffprobe/chapters/chapter")
|
||||||
|
.Cast<System.Xml.XmlNode>()
|
||||||
|
.Select(xmlNode => double.Parse(xmlNode.Attributes["start_time"].Value.Replace(",", "."), CultureInfo.InvariantCulture))
|
||||||
|
.ToList();
|
||||||
|
return chapters;
|
||||||
|
}
|
||||||
|
|
||||||
|
// subtract 1 b/c end time marker is a real entry but isn't a real chapter
|
||||||
|
public int Count() => this.markers.Count - 1;
|
||||||
|
|
||||||
|
public string GetCuefromChapters(string fileName)
|
||||||
|
{
|
||||||
|
var stringBuilder = new StringBuilder();
|
||||||
|
if (fileName != "")
|
||||||
|
{
|
||||||
|
stringBuilder.Append("FILE \"" + fileName + "\" MP4\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < Count(); i++)
|
||||||
|
{
|
||||||
|
var chapter = i + 1;
|
||||||
|
|
||||||
|
var timeSpan = TimeSpan.FromSeconds(this.markers[i]);
|
||||||
|
var minutes = Math.Floor(timeSpan.TotalMinutes).ToString();
|
||||||
|
var seconds = timeSpan.Seconds.ToString("D2");
|
||||||
|
var milliseconds = (timeSpan.Milliseconds / 10).ToString("D2");
|
||||||
|
string str = minutes + ":" + seconds + ":" + milliseconds;
|
||||||
|
|
||||||
|
stringBuilder.Append("TRACK " + chapter + " AUDIO\n");
|
||||||
|
stringBuilder.Append(" TITLE \"Chapter " + chapter.ToString("D2") + "\"\n");
|
||||||
|
stringBuilder.Append(" INDEX 01 " + str + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringBuilder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GenerateFfmpegChapters()
|
||||||
|
{
|
||||||
|
var stringBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
for (var i = 0; i < Count(); i++)
|
||||||
|
{
|
||||||
|
var chapter = i + 1;
|
||||||
|
|
||||||
|
var start = this.markers[i] * 1000.0;
|
||||||
|
var end = this.markers[i + 1] * 1000.0;
|
||||||
|
var chapterName = chapter.ToString("D3");
|
||||||
|
|
||||||
|
stringBuilder.Append("[CHAPTER]\n");
|
||||||
|
stringBuilder.Append("TIMEBASE=1/1000\n");
|
||||||
|
stringBuilder.Append("START=" + start + "\n");
|
||||||
|
stringBuilder.Append("END=" + end + "\n");
|
||||||
|
stringBuilder.Append("title=" + chapterName + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringBuilder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
AaxDecrypter/UNTESTED/DecryptSupportLibraries.cs
Normal file
21
AaxDecrypter/UNTESTED/DecryptSupportLibraries.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace AaxDecrypter
|
||||||
|
{
|
||||||
|
public static class DecryptSupportLibraries
|
||||||
|
{
|
||||||
|
// OTHER EXTERNAL DEPENDENCIES
|
||||||
|
// ffprobe has these pre-req.s as I'm using it:
|
||||||
|
// avcodec-57.dll, avdevice-57.dll, avfilter-6.dll, avformat-57.dll, avutil-55.dll, postproc-54.dll, swresample-2.dll, swscale-4.dll, taglib-sharp.dll
|
||||||
|
//
|
||||||
|
// something else needs the cygwin files (cyg*.dll)
|
||||||
|
|
||||||
|
private static string appPath_ { get; } = Path.GetDirectoryName(Dinah.Core.Exe.FileLocationOnDisk);
|
||||||
|
private static string decryptLib_ { get; } = Path.Combine(appPath_, "DecryptLib");
|
||||||
|
|
||||||
|
public static string ffmpegPath { get; } = Path.Combine(decryptLib_, "ffmpeg.exe");
|
||||||
|
public static string ffprobePath { get; } = Path.Combine(decryptLib_, "ffprobe.exe");
|
||||||
|
public static string atomicParsleyPath { get; } = Path.Combine(decryptLib_, "AtomicParsley.exe");
|
||||||
|
public static string mp4trackdumpPath { get; } = Path.Combine(decryptLib_, "mp4trackdump.exe");
|
||||||
|
}
|
||||||
|
}
|
||||||
41
AaxDecrypter/UNTESTED/EncodingInfo.cs
Normal file
41
AaxDecrypter/UNTESTED/EncodingInfo.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Dinah.Core.Diagnostics;
|
||||||
|
|
||||||
|
namespace AaxDecrypter
|
||||||
|
{
|
||||||
|
public class EncodingInfo
|
||||||
|
{
|
||||||
|
public int sampleRate { get; } = 44100;
|
||||||
|
public int channels { get; } = 2;
|
||||||
|
public int originalBitrate { get; }
|
||||||
|
|
||||||
|
public EncodingInfo(string file)
|
||||||
|
{
|
||||||
|
var info = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = DecryptSupportLibraries.ffprobePath,
|
||||||
|
Arguments = "-loglevel panic -show_streams -print_format flat \"" + file + "\""
|
||||||
|
};
|
||||||
|
var end = info.RunHidden().Output;
|
||||||
|
|
||||||
|
foreach (string str2 in end.Split('\n'))
|
||||||
|
{
|
||||||
|
string[] strArray = str2.Split('=');
|
||||||
|
switch (strArray[0])
|
||||||
|
{
|
||||||
|
case "streams.stream.0.channels":
|
||||||
|
this.channels = int.Parse(strArray[1].Replace("\"", "").TrimEnd('\r', '\n'));
|
||||||
|
break;
|
||||||
|
case "streams.stream.0.sample_rate":
|
||||||
|
this.sampleRate = int.Parse(strArray[1].Replace("\"", "").TrimEnd('\r', '\n'));
|
||||||
|
break;
|
||||||
|
case "streams.stream.0.bit_rate":
|
||||||
|
string s = strArray[1].Replace("\"", "").TrimEnd('\r', '\n');
|
||||||
|
this.originalBitrate = (int)Math.Round(double.Parse(s) / 1000.0, MidpointRounding.AwayFromZero);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
AaxDecrypter/UNTESTED/NFO.cs
Normal file
56
AaxDecrypter/UNTESTED/NFO.cs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
namespace AaxDecrypter
|
||||||
|
{
|
||||||
|
public static class NFO
|
||||||
|
{
|
||||||
|
public static string CreateNfoContents(string ripper, Tags tags, EncodingInfo encodingInfo, Chapters chapters)
|
||||||
|
{
|
||||||
|
int _hours = (int)tags.duration.TotalHours;
|
||||||
|
string myDuration
|
||||||
|
= (_hours > 0 ? _hours + " hours, " : "")
|
||||||
|
+ tags.duration.Minutes + " minutes, "
|
||||||
|
+ tags.duration.Seconds + " seconds";
|
||||||
|
|
||||||
|
string str4
|
||||||
|
= "General Information\r\n"
|
||||||
|
+ "===================\r\n"
|
||||||
|
+ " Title: " + tags.title + "\r\n"
|
||||||
|
+ " Author: " + tags.author + "\r\n"
|
||||||
|
+ " Read By: " + tags.narrator + "\r\n"
|
||||||
|
+ " Copyright: " + tags.year + "\r\n"
|
||||||
|
+ " Audiobook Copyright: " + tags.year + "\r\n";
|
||||||
|
if (tags.genre != "")
|
||||||
|
{
|
||||||
|
str4 = str4 + " Genre: " + tags.genre + "\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
string s
|
||||||
|
= str4
|
||||||
|
+ " Publisher: " + tags.publisher + "\r\n"
|
||||||
|
+ " Duration: " + myDuration + "\r\n"
|
||||||
|
+ " Chapters: " + chapters.Count() + "\r\n"
|
||||||
|
+ "\r\n"
|
||||||
|
+ "\r\n"
|
||||||
|
+ "Media Information\r\n"
|
||||||
|
+ "=================\r\n"
|
||||||
|
+ " Source Format: Audible AAX\r\n"
|
||||||
|
+ " Source Sample Rate: " + encodingInfo.sampleRate + " Hz\r\n"
|
||||||
|
+ " Source Channels: " + encodingInfo.channels + "\r\n"
|
||||||
|
+ " Source Bitrate: " + encodingInfo.originalBitrate + " kbits\r\n"
|
||||||
|
+ "\r\n"
|
||||||
|
+ " Lossless Encode: Yes\r\n"
|
||||||
|
+ " Encoded Codec: AAC / M4B\r\n"
|
||||||
|
+ " Encoded Sample Rate: " + encodingInfo.sampleRate + " Hz\r\n"
|
||||||
|
+ " Encoded Channels: " + encodingInfo.channels + "\r\n"
|
||||||
|
+ " Encoded Bitrate: " + encodingInfo.originalBitrate + " kbits\r\n"
|
||||||
|
+ "\r\n"
|
||||||
|
+ " Ripper: " + ripper + "\r\n"
|
||||||
|
+ "\r\n"
|
||||||
|
+ "\r\n"
|
||||||
|
+ "Book Description\r\n"
|
||||||
|
+ "================\r\n"
|
||||||
|
+ tags.comments;
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
AaxDecrypter/UNTESTED/Tags.cs
Normal file
74
AaxDecrypter/UNTESTED/Tags.cs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using TagLib;
|
||||||
|
using TagLib.Mpeg4;
|
||||||
|
using Dinah.Core;
|
||||||
|
|
||||||
|
namespace AaxDecrypter
|
||||||
|
{
|
||||||
|
public class Tags
|
||||||
|
{
|
||||||
|
public string title { get; }
|
||||||
|
public string album { get; }
|
||||||
|
public string author { get; }
|
||||||
|
public string comments { get; }
|
||||||
|
public string narrator { get; }
|
||||||
|
public string year { get; }
|
||||||
|
public string publisher { get; }
|
||||||
|
public string id { get; }
|
||||||
|
public string genre { get; }
|
||||||
|
public TimeSpan duration { get; }
|
||||||
|
|
||||||
|
public Tags(string file)
|
||||||
|
{
|
||||||
|
using TagLib.File tagLibFile = TagLib.File.Create(file, "audio/mp4", ReadStyle.Average);
|
||||||
|
this.title = tagLibFile.Tag.Title.Replace(" (Unabridged)", "");
|
||||||
|
this.album = tagLibFile.Tag.Album.Replace(" (Unabridged)", "");
|
||||||
|
this.author = tagLibFile.Tag.FirstPerformer;
|
||||||
|
this.year = tagLibFile.Tag.Year.ToString();
|
||||||
|
this.comments = tagLibFile.Tag.Comment;
|
||||||
|
this.duration = tagLibFile.Properties.Duration;
|
||||||
|
this.genre = tagLibFile.Tag.FirstGenre;
|
||||||
|
|
||||||
|
var tag = tagLibFile.GetTag(TagTypes.Apple, true);
|
||||||
|
this.publisher = tag.Publisher;
|
||||||
|
this.narrator = string.IsNullOrWhiteSpace(tagLibFile.Tag.FirstComposer) ? tag.Narrator : tagLibFile.Tag.FirstComposer;
|
||||||
|
this.comments = !string.IsNullOrWhiteSpace(tag.LongDescription) ? tag.LongDescription : tag.Description;
|
||||||
|
this.id = tag.AudibleCDEK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddAppleTags(string file)
|
||||||
|
{
|
||||||
|
using var file1 = TagLib.File.Create(file, "audio/mp4", ReadStyle.Average);
|
||||||
|
var tag = (AppleTag)file1.GetTag(TagTypes.Apple, true);
|
||||||
|
tag.Publisher = this.publisher;
|
||||||
|
tag.LongDescription = this.comments;
|
||||||
|
tag.Description = this.comments;
|
||||||
|
file1.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GenerateFfmpegTags()
|
||||||
|
{
|
||||||
|
StringBuilder stringBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
stringBuilder.Append(";FFMETADATA1\n");
|
||||||
|
stringBuilder.Append("major_brand=aax\n");
|
||||||
|
stringBuilder.Append("minor_version=1\n");
|
||||||
|
stringBuilder.Append("compatible_brands=aax M4B mp42isom\n");
|
||||||
|
stringBuilder.Append("date=" + this.year + "\n");
|
||||||
|
stringBuilder.Append("genre=" + this.genre + "\n");
|
||||||
|
stringBuilder.Append("title=" + this.title + "\n");
|
||||||
|
stringBuilder.Append("artist=" + this.author + "\n");
|
||||||
|
stringBuilder.Append("album=" + this.album + "\n");
|
||||||
|
stringBuilder.Append("composer=" + this.narrator + "\n");
|
||||||
|
stringBuilder.Append("comment=" + this.comments.Truncate(254) + "\n");
|
||||||
|
stringBuilder.Append("description=" + this.comments + "\n");
|
||||||
|
|
||||||
|
return stringBuilder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
ApplicationServices/ApplicationServices.csproj
Normal file
15
ApplicationServices/ApplicationServices.csproj
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApiDTOs\AudibleApiDTOs.csproj" />
|
||||||
|
<ProjectReference Include="..\..\audible api\AudibleApi\AudibleApi\AudibleApi.csproj" />
|
||||||
|
<ProjectReference Include="..\DtoImporterService\DtoImporterService.csproj" />
|
||||||
|
<ProjectReference Include="..\InternalUtilities\InternalUtilities.csproj" />
|
||||||
|
<ProjectReference Include="..\LibationSearchEngine\LibationSearchEngine.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
40
ApplicationServices/UNTESTED/LibraryCommands.cs
Normal file
40
ApplicationServices/UNTESTED/LibraryCommands.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AudibleApi;
|
||||||
|
using DataLayer;
|
||||||
|
using DtoImporterService;
|
||||||
|
using InternalUtilities;
|
||||||
|
|
||||||
|
namespace ApplicationServices
|
||||||
|
{
|
||||||
|
public static class LibraryCommands
|
||||||
|
{
|
||||||
|
public static async Task<(int totalCount, int newCount)> IndexLibraryAsync(ILoginCallback callback)
|
||||||
|
{
|
||||||
|
var audibleApiActions = new AudibleApiActions();
|
||||||
|
var items = await audibleApiActions.GetAllLibraryItemsAsync(callback);
|
||||||
|
var totalCount = items.Count;
|
||||||
|
|
||||||
|
var libImporter = new LibraryImporter();
|
||||||
|
var newCount = await Task.Run(() => libImporter.Import(items));
|
||||||
|
|
||||||
|
await Task.Run(() => SearchEngineCommands.FullReIndex());
|
||||||
|
|
||||||
|
return (totalCount, newCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int IndexChangedTags(Book book)
|
||||||
|
{
|
||||||
|
// update disconnected entity
|
||||||
|
using var context = LibationContext.Create();
|
||||||
|
context.Update(book);
|
||||||
|
var qtyChanges = context.SaveChanges();
|
||||||
|
|
||||||
|
// this part is tags-specific
|
||||||
|
if (qtyChanges > 0)
|
||||||
|
SearchEngineCommands.UpdateBookTags(book);
|
||||||
|
|
||||||
|
return qtyChanges;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
ApplicationServices/UNTESTED/SearchEngineCommands.cs
Normal file
43
ApplicationServices/UNTESTED/SearchEngineCommands.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using DataLayer;
|
||||||
|
using LibationSearchEngine;
|
||||||
|
|
||||||
|
namespace ApplicationServices
|
||||||
|
{
|
||||||
|
public static class SearchEngineCommands
|
||||||
|
{
|
||||||
|
public static void FullReIndex()
|
||||||
|
{
|
||||||
|
var engine = new SearchEngine();
|
||||||
|
engine.CreateNewIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SearchResultSet Search(string searchString)
|
||||||
|
{
|
||||||
|
var engine = new SearchEngine();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return engine.Search(searchString);
|
||||||
|
}
|
||||||
|
catch (System.IO.FileNotFoundException)
|
||||||
|
{
|
||||||
|
FullReIndex();
|
||||||
|
return engine.Search(searchString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void UpdateBookTags(Book book)
|
||||||
|
{
|
||||||
|
var engine = new SearchEngine();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
engine.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags);
|
||||||
|
}
|
||||||
|
catch (System.IO.FileNotFoundException)
|
||||||
|
{
|
||||||
|
FullReIndex();
|
||||||
|
engine.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,39 +1,38 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFrameworks>netcoreapp3.0;netstandard2.1</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||||
|
<ApplicationIcon />
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
|
<StartupObject />
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dinah.Core" Version="9.0.3.1" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.0.0">
|
||||||
<PackageReference Include="Dinah.EntityFrameworkCore" Version="9.0.0.1" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.8" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.8">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.0.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
|
||||||
<DebugType>embedded</DebugType>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
|
||||||
<DebugType>embedded</DebugType>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="migrate.json">
|
<ProjectReference Include="..\..\Dinah.Core\Dinah.EntityFrameworkCore\Dinah.EntityFrameworkCore.csproj" />
|
||||||
|
<ProjectReference Include="..\FileManager\FileManager.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="appsettings.json">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@ -3,50 +3,54 @@ using System;
|
|||||||
using DataLayer;
|
using DataLayer;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
namespace DataLayer.Migrations
|
namespace DataLayer.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(LibationContext))]
|
[DbContext(typeof(LibationContext))]
|
||||||
[Migration("20191125182309_Fresh")]
|
[Migration("20191115193402_Fresh")]
|
||||||
partial class Fresh
|
partial class Fresh
|
||||||
{
|
{
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "3.0.0");
|
.HasAnnotation("ProductVersion", "3.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128)
|
||||||
|
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||||
|
|
||||||
modelBuilder.Entity("DataLayer.Book", b =>
|
modelBuilder.Entity("DataLayer.Book", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("BookId")
|
b.Property<int>("BookId")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int")
|
||||||
|
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||||
|
|
||||||
b.Property<string>("AudibleProductId")
|
b.Property<string>("AudibleProductId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
b.Property<int>("CategoryId")
|
b.Property<int>("CategoryId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<DateTime?>("DatePublished")
|
b.Property<DateTime?>("DatePublished")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
b.Property<string>("Description")
|
b.Property<string>("Description")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b.Property<bool>("IsAbridged")
|
b.Property<bool>("IsAbridged")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
b.Property<int>("LengthInMinutes")
|
b.Property<int>("LengthInMinutes")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<string>("PictureId")
|
b.Property<string>("PictureId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b.HasKey("BookId");
|
b.HasKey("BookId");
|
||||||
|
|
||||||
@ -60,16 +64,16 @@ namespace DataLayer.Migrations
|
|||||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("BookId")
|
b.Property<int>("BookId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<int>("ContributorId")
|
b.Property<int>("ContributorId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<int>("Role")
|
b.Property<int>("Role")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<byte>("Order")
|
b.Property<byte>("Order")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("tinyint");
|
||||||
|
|
||||||
b.HasKey("BookId", "ContributorId", "Role");
|
b.HasKey("BookId", "ContributorId", "Role");
|
||||||
|
|
||||||
@ -84,16 +88,17 @@ namespace DataLayer.Migrations
|
|||||||
{
|
{
|
||||||
b.Property<int>("CategoryId")
|
b.Property<int>("CategoryId")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int")
|
||||||
|
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||||
|
|
||||||
b.Property<string>("AudibleCategoryId")
|
b.Property<string>("AudibleCategoryId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b.Property<int?>("ParentCategoryCategoryId")
|
b.Property<int?>("ParentCategoryCategoryId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.HasKey("CategoryId");
|
b.HasKey("CategoryId");
|
||||||
|
|
||||||
@ -116,35 +121,29 @@ namespace DataLayer.Migrations
|
|||||||
{
|
{
|
||||||
b.Property<int>("ContributorId")
|
b.Property<int>("ContributorId")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int")
|
||||||
|
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||||
|
|
||||||
b.Property<string>("AudibleContributorId")
|
b.Property<string>("AudibleAuthorId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
b.HasKey("ContributorId");
|
b.HasKey("ContributorId");
|
||||||
|
|
||||||
b.HasIndex("Name");
|
b.HasIndex("Name");
|
||||||
|
|
||||||
b.ToTable("Contributors");
|
b.ToTable("Contributors");
|
||||||
|
|
||||||
b.HasData(
|
|
||||||
new
|
|
||||||
{
|
|
||||||
ContributorId = -1,
|
|
||||||
Name = ""
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("BookId")
|
b.Property<int>("BookId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<DateTime>("DateAdded")
|
b.Property<DateTime>("DateAdded")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
b.HasKey("BookId");
|
b.HasKey("BookId");
|
||||||
|
|
||||||
@ -155,13 +154,14 @@ namespace DataLayer.Migrations
|
|||||||
{
|
{
|
||||||
b.Property<int>("SeriesId")
|
b.Property<int>("SeriesId")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int")
|
||||||
|
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||||
|
|
||||||
b.Property<string>("AudibleSeriesId")
|
b.Property<string>("AudibleSeriesId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b.HasKey("SeriesId");
|
b.HasKey("SeriesId");
|
||||||
|
|
||||||
@ -173,13 +173,13 @@ namespace DataLayer.Migrations
|
|||||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("SeriesId")
|
b.Property<int>("SeriesId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<int>("BookId")
|
b.Property<int>("BookId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<float?>("Index")
|
b.Property<float?>("Index")
|
||||||
.HasColumnType("REAL");
|
.HasColumnType("real");
|
||||||
|
|
||||||
b.HasKey("SeriesId", "BookId");
|
b.HasKey("SeriesId", "BookId");
|
||||||
|
|
||||||
@ -201,16 +201,18 @@ namespace DataLayer.Migrations
|
|||||||
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
|
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<int>("BookId")
|
b1.Property<int>("BookId")
|
||||||
.HasColumnType("INTEGER");
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||||
|
|
||||||
b1.Property<float>("OverallRating")
|
b1.Property<float>("OverallRating")
|
||||||
.HasColumnType("REAL");
|
.HasColumnType("real");
|
||||||
|
|
||||||
b1.Property<float>("PerformanceRating")
|
b1.Property<float>("PerformanceRating")
|
||||||
.HasColumnType("REAL");
|
.HasColumnType("real");
|
||||||
|
|
||||||
b1.Property<float>("StoryRating")
|
b1.Property<float>("StoryRating")
|
||||||
.HasColumnType("REAL");
|
.HasColumnType("real");
|
||||||
|
|
||||||
b1.HasKey("BookId");
|
b1.HasKey("BookId");
|
||||||
|
|
||||||
@ -224,13 +226,14 @@ namespace DataLayer.Migrations
|
|||||||
{
|
{
|
||||||
b1.Property<int>("SupplementId")
|
b1.Property<int>("SupplementId")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int")
|
||||||
|
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||||
|
|
||||||
b1.Property<int>("BookId")
|
b1.Property<int>("BookId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b1.Property<string>("Url")
|
b1.Property<string>("Url")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b1.HasKey("SupplementId");
|
b1.HasKey("SupplementId");
|
||||||
|
|
||||||
@ -245,10 +248,10 @@ namespace DataLayer.Migrations
|
|||||||
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
|
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<int>("BookId")
|
b1.Property<int>("BookId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b1.Property<string>("Tags")
|
b1.Property<string>("Tags")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b1.HasKey("BookId");
|
b1.HasKey("BookId");
|
||||||
|
|
||||||
@ -260,16 +263,16 @@ namespace DataLayer.Migrations
|
|||||||
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
|
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
|
||||||
{
|
{
|
||||||
b2.Property<int>("UserDefinedItemBookId")
|
b2.Property<int>("UserDefinedItemBookId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b2.Property<float>("OverallRating")
|
b2.Property<float>("OverallRating")
|
||||||
.HasColumnType("REAL");
|
.HasColumnType("real");
|
||||||
|
|
||||||
b2.Property<float>("PerformanceRating")
|
b2.Property<float>("PerformanceRating")
|
||||||
.HasColumnType("REAL");
|
.HasColumnType("real");
|
||||||
|
|
||||||
b2.Property<float>("StoryRating")
|
b2.Property<float>("StoryRating")
|
||||||
.HasColumnType("REAL");
|
.HasColumnType("real");
|
||||||
|
|
||||||
b2.HasKey("UserDefinedItemBookId");
|
b2.HasKey("UserDefinedItemBookId");
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ namespace DataLayer.Migrations
|
|||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
CategoryId = table.Column<int>(nullable: false)
|
CategoryId = table.Column<int>(nullable: false)
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
AudibleCategoryId = table.Column<string>(nullable: true),
|
AudibleCategoryId = table.Column<string>(nullable: true),
|
||||||
Name = table.Column<string>(nullable: true),
|
Name = table.Column<string>(nullable: true),
|
||||||
ParentCategoryCategoryId = table.Column<int>(nullable: true)
|
ParentCategoryCategoryId = table.Column<int>(nullable: true)
|
||||||
@ -33,9 +33,9 @@ namespace DataLayer.Migrations
|
|||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
ContributorId = table.Column<int>(nullable: false)
|
ContributorId = table.Column<int>(nullable: false)
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
Name = table.Column<string>(nullable: true),
|
Name = table.Column<string>(nullable: true),
|
||||||
AudibleContributorId = table.Column<string>(nullable: true)
|
AudibleAuthorId = table.Column<string>(nullable: true)
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
{
|
{
|
||||||
@ -47,7 +47,7 @@ namespace DataLayer.Migrations
|
|||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
SeriesId = table.Column<int>(nullable: false)
|
SeriesId = table.Column<int>(nullable: false)
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
AudibleSeriesId = table.Column<string>(nullable: true),
|
AudibleSeriesId = table.Column<string>(nullable: true),
|
||||||
Name = table.Column<string>(nullable: true)
|
Name = table.Column<string>(nullable: true)
|
||||||
},
|
},
|
||||||
@ -61,7 +61,7 @@ namespace DataLayer.Migrations
|
|||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
BookId = table.Column<int>(nullable: false)
|
BookId = table.Column<int>(nullable: false)
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
AudibleProductId = table.Column<string>(nullable: true),
|
AudibleProductId = table.Column<string>(nullable: true),
|
||||||
Title = table.Column<string>(nullable: true),
|
Title = table.Column<string>(nullable: true),
|
||||||
Description = table.Column<string>(nullable: true),
|
Description = table.Column<string>(nullable: true),
|
||||||
@ -159,7 +159,7 @@ namespace DataLayer.Migrations
|
|||||||
columns: table => new
|
columns: table => new
|
||||||
{
|
{
|
||||||
SupplementId = table.Column<int>(nullable: false)
|
SupplementId = table.Column<int>(nullable: false)
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
BookId = table.Column<int>(nullable: false),
|
BookId = table.Column<int>(nullable: false),
|
||||||
Url = table.Column<string>(nullable: true)
|
Url = table.Column<string>(nullable: true)
|
||||||
},
|
},
|
||||||
@ -200,11 +200,6 @@ namespace DataLayer.Migrations
|
|||||||
columns: new[] { "CategoryId", "AudibleCategoryId", "Name", "ParentCategoryCategoryId" },
|
columns: new[] { "CategoryId", "AudibleCategoryId", "Name", "ParentCategoryCategoryId" },
|
||||||
values: new object[] { -1, "", "", null });
|
values: new object[] { -1, "", "", null });
|
||||||
|
|
||||||
migrationBuilder.InsertData(
|
|
||||||
table: "Contributors",
|
|
||||||
columns: new[] { "ContributorId", "AudibleContributorId", "Name" },
|
|
||||||
values: new object[] { -1, null, "" });
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_BookContributor_BookId",
|
name: "IX_BookContributor_BookId",
|
||||||
table: "BookContributor",
|
table: "BookContributor",
|
||||||
@ -3,53 +3,52 @@ using System;
|
|||||||
using DataLayer;
|
using DataLayer;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
namespace DataLayer.Migrations
|
namespace DataLayer.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(LibationContext))]
|
[DbContext(typeof(LibationContext))]
|
||||||
[Migration("20200812152646_AddLocaleAndAccount")]
|
partial class LibationContextModelSnapshot : ModelSnapshot
|
||||||
partial class AddLocaleAndAccount
|
|
||||||
{
|
{
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "3.1.7");
|
.HasAnnotation("ProductVersion", "3.0.0")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128)
|
||||||
|
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||||
|
|
||||||
modelBuilder.Entity("DataLayer.Book", b =>
|
modelBuilder.Entity("DataLayer.Book", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("BookId")
|
b.Property<int>("BookId")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int")
|
||||||
|
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||||
|
|
||||||
b.Property<string>("AudibleProductId")
|
b.Property<string>("AudibleProductId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
b.Property<int>("CategoryId")
|
b.Property<int>("CategoryId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<DateTime?>("DatePublished")
|
b.Property<DateTime?>("DatePublished")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
b.Property<string>("Description")
|
b.Property<string>("Description")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b.Property<bool>("IsAbridged")
|
b.Property<bool>("IsAbridged")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
b.Property<int>("LengthInMinutes")
|
b.Property<int>("LengthInMinutes")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<string>("Locale")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PictureId")
|
b.Property<string>("PictureId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b.HasKey("BookId");
|
b.HasKey("BookId");
|
||||||
|
|
||||||
@ -63,16 +62,16 @@ namespace DataLayer.Migrations
|
|||||||
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
modelBuilder.Entity("DataLayer.BookContributor", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("BookId")
|
b.Property<int>("BookId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<int>("ContributorId")
|
b.Property<int>("ContributorId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<int>("Role")
|
b.Property<int>("Role")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<byte>("Order")
|
b.Property<byte>("Order")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("tinyint");
|
||||||
|
|
||||||
b.HasKey("BookId", "ContributorId", "Role");
|
b.HasKey("BookId", "ContributorId", "Role");
|
||||||
|
|
||||||
@ -87,16 +86,17 @@ namespace DataLayer.Migrations
|
|||||||
{
|
{
|
||||||
b.Property<int>("CategoryId")
|
b.Property<int>("CategoryId")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int")
|
||||||
|
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||||
|
|
||||||
b.Property<string>("AudibleCategoryId")
|
b.Property<string>("AudibleCategoryId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b.Property<int?>("ParentCategoryCategoryId")
|
b.Property<int?>("ParentCategoryCategoryId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.HasKey("CategoryId");
|
b.HasKey("CategoryId");
|
||||||
|
|
||||||
@ -119,38 +119,29 @@ namespace DataLayer.Migrations
|
|||||||
{
|
{
|
||||||
b.Property<int>("ContributorId")
|
b.Property<int>("ContributorId")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int")
|
||||||
|
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||||
|
|
||||||
b.Property<string>("AudibleContributorId")
|
b.Property<string>("AudibleAuthorId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
b.HasKey("ContributorId");
|
b.HasKey("ContributorId");
|
||||||
|
|
||||||
b.HasIndex("Name");
|
b.HasIndex("Name");
|
||||||
|
|
||||||
b.ToTable("Contributors");
|
b.ToTable("Contributors");
|
||||||
|
|
||||||
b.HasData(
|
|
||||||
new
|
|
||||||
{
|
|
||||||
ContributorId = -1,
|
|
||||||
Name = ""
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
modelBuilder.Entity("DataLayer.LibraryBook", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("BookId")
|
b.Property<int>("BookId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<string>("Account")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("DateAdded")
|
b.Property<DateTime>("DateAdded")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
b.HasKey("BookId");
|
b.HasKey("BookId");
|
||||||
|
|
||||||
@ -161,13 +152,14 @@ namespace DataLayer.Migrations
|
|||||||
{
|
{
|
||||||
b.Property<int>("SeriesId")
|
b.Property<int>("SeriesId")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int")
|
||||||
|
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||||
|
|
||||||
b.Property<string>("AudibleSeriesId")
|
b.Property<string>("AudibleSeriesId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(450)");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b.HasKey("SeriesId");
|
b.HasKey("SeriesId");
|
||||||
|
|
||||||
@ -179,13 +171,13 @@ namespace DataLayer.Migrations
|
|||||||
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
modelBuilder.Entity("DataLayer.SeriesBook", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("SeriesId")
|
b.Property<int>("SeriesId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<int>("BookId")
|
b.Property<int>("BookId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<float?>("Index")
|
b.Property<float?>("Index")
|
||||||
.HasColumnType("REAL");
|
.HasColumnType("real");
|
||||||
|
|
||||||
b.HasKey("SeriesId", "BookId");
|
b.HasKey("SeriesId", "BookId");
|
||||||
|
|
||||||
@ -207,16 +199,18 @@ namespace DataLayer.Migrations
|
|||||||
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
|
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<int>("BookId")
|
b1.Property<int>("BookId")
|
||||||
.HasColumnType("INTEGER");
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||||
|
|
||||||
b1.Property<float>("OverallRating")
|
b1.Property<float>("OverallRating")
|
||||||
.HasColumnType("REAL");
|
.HasColumnType("real");
|
||||||
|
|
||||||
b1.Property<float>("PerformanceRating")
|
b1.Property<float>("PerformanceRating")
|
||||||
.HasColumnType("REAL");
|
.HasColumnType("real");
|
||||||
|
|
||||||
b1.Property<float>("StoryRating")
|
b1.Property<float>("StoryRating")
|
||||||
.HasColumnType("REAL");
|
.HasColumnType("real");
|
||||||
|
|
||||||
b1.HasKey("BookId");
|
b1.HasKey("BookId");
|
||||||
|
|
||||||
@ -230,13 +224,14 @@ namespace DataLayer.Migrations
|
|||||||
{
|
{
|
||||||
b1.Property<int>("SupplementId")
|
b1.Property<int>("SupplementId")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int")
|
||||||
|
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||||
|
|
||||||
b1.Property<int>("BookId")
|
b1.Property<int>("BookId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b1.Property<string>("Url")
|
b1.Property<string>("Url")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b1.HasKey("SupplementId");
|
b1.HasKey("SupplementId");
|
||||||
|
|
||||||
@ -251,10 +246,10 @@ namespace DataLayer.Migrations
|
|||||||
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
|
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
|
||||||
{
|
{
|
||||||
b1.Property<int>("BookId")
|
b1.Property<int>("BookId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b1.Property<string>("Tags")
|
b1.Property<string>("Tags")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b1.HasKey("BookId");
|
b1.HasKey("BookId");
|
||||||
|
|
||||||
@ -266,16 +261,16 @@ namespace DataLayer.Migrations
|
|||||||
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
|
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
|
||||||
{
|
{
|
||||||
b2.Property<int>("UserDefinedItemBookId")
|
b2.Property<int>("UserDefinedItemBookId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b2.Property<float>("OverallRating")
|
b2.Property<float>("OverallRating")
|
||||||
.HasColumnType("REAL");
|
.HasColumnType("real");
|
||||||
|
|
||||||
b2.Property<float>("PerformanceRating")
|
b2.Property<float>("PerformanceRating")
|
||||||
.HasColumnType("REAL");
|
.HasColumnType("real");
|
||||||
|
|
||||||
b2.Property<float>("StoryRating")
|
b2.Property<float>("StoryRating")
|
||||||
.HasColumnType("REAL");
|
.HasColumnType("real");
|
||||||
|
|
||||||
b2.HasKey("UserDefinedItemBookId");
|
b2.HasKey("UserDefinedItemBookId");
|
||||||
|
|
||||||
71
DataLayer/UNTESTED/Configurations/BookConfig.cs
Normal file
71
DataLayer/UNTESTED/Configurations/BookConfig.cs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace DataLayer.Configurations
|
||||||
|
{
|
||||||
|
internal class BookConfig : IEntityTypeConfiguration<Book>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Book> entity)
|
||||||
|
{
|
||||||
|
entity.HasKey(b => b.BookId);
|
||||||
|
entity.HasIndex(b => b.AudibleProductId);
|
||||||
|
|
||||||
|
entity.OwnsOne(b => b.Rating);
|
||||||
|
|
||||||
|
//
|
||||||
|
// CRUCIAL: ignore unmapped collections, even get-only
|
||||||
|
//
|
||||||
|
entity.Ignore(nameof(Book.Authors));
|
||||||
|
entity.Ignore(nameof(Book.Narrators));
|
||||||
|
//// these don't seem to matter
|
||||||
|
//entity.Ignore(nameof(Book.AuthorNames));
|
||||||
|
//entity.Ignore(nameof(Book.NarratorNames));
|
||||||
|
//entity.Ignore(nameof(Book.HasPdfs));
|
||||||
|
|
||||||
|
// OwnsMany: "Can only ever appear on navigation properties of other entity types.
|
||||||
|
// Are automatically loaded, and can only be tracked by a DbContext alongside their owner."
|
||||||
|
entity
|
||||||
|
.OwnsMany(b => b.Supplements, b_s =>
|
||||||
|
{
|
||||||
|
b_s.WithOwner(s => s.Book)
|
||||||
|
.HasForeignKey(s => s.BookId);
|
||||||
|
b_s.HasKey(s => s.SupplementId);
|
||||||
|
});
|
||||||
|
// even though it's owned, we need to map its backing field
|
||||||
|
entity
|
||||||
|
.Metadata
|
||||||
|
.FindNavigation(nameof(Book.Supplements))
|
||||||
|
.SetPropertyAccessMode(PropertyAccessMode.Field);
|
||||||
|
|
||||||
|
// owns it 1:1, store in separate table
|
||||||
|
entity
|
||||||
|
.OwnsOne(b => b.UserDefinedItem, b_udi =>
|
||||||
|
{
|
||||||
|
b_udi.WithOwner(udi => udi.Book)
|
||||||
|
.HasForeignKey(udi => udi.BookId);
|
||||||
|
b_udi.Property(udi => udi.BookId).ValueGeneratedNever();
|
||||||
|
b_udi.ToTable(nameof(Book.UserDefinedItem));
|
||||||
|
|
||||||
|
// owns it 1:1, store in same table
|
||||||
|
b_udi.OwnsOne(udi => udi.Rating);
|
||||||
|
});
|
||||||
|
|
||||||
|
entity
|
||||||
|
.Metadata
|
||||||
|
.FindNavigation(nameof(Book.ContributorsLink))
|
||||||
|
// PropertyAccessMode.Field : Contributions is a get-only property, not a field, so use its backing field
|
||||||
|
.SetPropertyAccessMode(PropertyAccessMode.Field);
|
||||||
|
|
||||||
|
entity
|
||||||
|
.Metadata
|
||||||
|
.FindNavigation(nameof(Book.SeriesLink))
|
||||||
|
// PropertyAccessMode.Field : Series is a get-only property, not a field, so use its backing field
|
||||||
|
.SetPropertyAccessMode(PropertyAccessMode.Field);
|
||||||
|
|
||||||
|
entity
|
||||||
|
.HasOne(b => b.Category)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(b => b.CategoryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,8 +9,8 @@ namespace DataLayer.Configurations
|
|||||||
{
|
{
|
||||||
entity.HasKey(bc => new { bc.BookId, bc.ContributorId, bc.Role });
|
entity.HasKey(bc => new { bc.BookId, bc.ContributorId, bc.Role });
|
||||||
|
|
||||||
entity.HasIndex(bc => bc.BookId);
|
entity.HasIndex(b => b.BookId);
|
||||||
entity.HasIndex(bc => bc.ContributorId);
|
entity.HasIndex(b => b.ContributorId);
|
||||||
|
|
||||||
entity
|
entity
|
||||||
.HasOne(bc => bc.Book)
|
.HasOne(bc => bc.Book)
|
||||||
@ -9,12 +9,6 @@ namespace DataLayer.Configurations
|
|||||||
{
|
{
|
||||||
entity.HasKey(c => c.CategoryId);
|
entity.HasKey(c => c.CategoryId);
|
||||||
entity.HasIndex(c => c.AudibleCategoryId);
|
entity.HasIndex(c => c.AudibleCategoryId);
|
||||||
|
|
||||||
entity.Ignore(c => c.CategoryLadders);
|
|
||||||
|
|
||||||
entity
|
|
||||||
.HasMany(e => e._categoryLadders)
|
|
||||||
.WithMany(e => e._categories);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -17,9 +17,6 @@ namespace DataLayer.Configurations
|
|||||||
.Metadata
|
.Metadata
|
||||||
.FindNavigation(nameof(Contributor.BooksLink))
|
.FindNavigation(nameof(Contributor.BooksLink))
|
||||||
.SetPropertyAccessMode(PropertyAccessMode.Field);
|
.SetPropertyAccessMode(PropertyAccessMode.Field);
|
||||||
|
|
||||||
// seeds go here. examples in Dinah.EntityFrameworkCore.Tests\DbContextFactoryExample.cs
|
|
||||||
entity.HasData(Contributor.GetEmpty());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
18
DataLayer/UNTESTED/Configurations/LibraryBookConfig.cs
Normal file
18
DataLayer/UNTESTED/Configurations/LibraryBookConfig.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace DataLayer.Configurations
|
||||||
|
{
|
||||||
|
internal class LibraryBookConfig : IEntityTypeConfiguration<LibraryBook>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<LibraryBook> entity)
|
||||||
|
{
|
||||||
|
entity.HasKey(b => b.BookId);
|
||||||
|
|
||||||
|
entity
|
||||||
|
.HasOne(le => le.Book)
|
||||||
|
.WithOne()
|
||||||
|
.HasForeignKey<LibraryBook>(le => le.BookId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,10 +7,10 @@ namespace DataLayer.Configurations
|
|||||||
{
|
{
|
||||||
public void Configure(EntityTypeBuilder<SeriesBook> entity)
|
public void Configure(EntityTypeBuilder<SeriesBook> entity)
|
||||||
{
|
{
|
||||||
entity.HasKey(sb => new { sb.SeriesId, sb.BookId });
|
entity.HasKey(bc => new { bc.SeriesId, bc.BookId });
|
||||||
|
|
||||||
entity.HasIndex(sb => sb.SeriesId);
|
entity.HasIndex(b => b.SeriesId);
|
||||||
entity.HasIndex(sb => sb.BookId);
|
entity.HasIndex(b => b.BookId);
|
||||||
|
|
||||||
entity
|
entity
|
||||||
.HasOne(sb => sb.Series)
|
.HasOne(sb => sb.Series)
|
||||||
@ -7,8 +7,8 @@ namespace DataLayer.Configurations
|
|||||||
{
|
{
|
||||||
public void Configure(EntityTypeBuilder<Series> entity)
|
public void Configure(EntityTypeBuilder<Series> entity)
|
||||||
{
|
{
|
||||||
entity.HasKey(s => s.SeriesId);
|
entity.HasKey(b => b.SeriesId);
|
||||||
entity.HasIndex(s => s.AudibleSeriesId);
|
entity.HasIndex(b => b.AudibleSeriesId);
|
||||||
|
|
||||||
entity
|
entity
|
||||||
.Metadata
|
.Metadata
|
||||||
251
DataLayer/UNTESTED/EfClasses/Book.cs
Normal file
251
DataLayer/UNTESTED/EfClasses/Book.cs
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Dinah.Core;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace DataLayer
|
||||||
|
{
|
||||||
|
public class AudibleProductId
|
||||||
|
{
|
||||||
|
public string Id { get; }
|
||||||
|
public AudibleProductId(string id)
|
||||||
|
{
|
||||||
|
ArgumentValidator.EnsureNotNullOrWhiteSpace(id, nameof(id));
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public class Book
|
||||||
|
{
|
||||||
|
// implementation detail. set by db only. only used by data layer
|
||||||
|
internal int BookId { get; private set; }
|
||||||
|
|
||||||
|
// immutable
|
||||||
|
public string AudibleProductId { get; private set; }
|
||||||
|
public string Title { get; private set; }
|
||||||
|
public string Description { get; private set; }
|
||||||
|
public int LengthInMinutes { get; private set; }
|
||||||
|
|
||||||
|
// mutable
|
||||||
|
public string PictureId { get; set; }
|
||||||
|
|
||||||
|
// book details
|
||||||
|
public bool IsAbridged { get; private set; }
|
||||||
|
public DateTime? DatePublished { get; private set; }
|
||||||
|
|
||||||
|
// non-null. use "empty pattern"
|
||||||
|
internal int CategoryId { get; private set; }
|
||||||
|
public Category Category { get; private set; }
|
||||||
|
public string[] CategoriesNames
|
||||||
|
=> Category == null ? new string[0]
|
||||||
|
: Category.ParentCategory == null ? new[] { Category.Name }
|
||||||
|
: new[] { Category.ParentCategory.Name, Category.Name };
|
||||||
|
public string[] CategoriesIds
|
||||||
|
=> Category == null ? null
|
||||||
|
: Category.ParentCategory == null ? new[] { Category.AudibleCategoryId }
|
||||||
|
: new[] { Category.ParentCategory.AudibleCategoryId, Category.AudibleCategoryId };
|
||||||
|
|
||||||
|
// is owned, not optional 1:1
|
||||||
|
public UserDefinedItem UserDefinedItem { get; private set; }
|
||||||
|
|
||||||
|
// is owned, not optional 1:1
|
||||||
|
/// <summary>The product's aggregate community rating</summary>
|
||||||
|
public Rating Rating { get; private set; } = new Rating(0, 0, 0);
|
||||||
|
|
||||||
|
// ef-ctor
|
||||||
|
private Book() { }
|
||||||
|
// non-ef ctor
|
||||||
|
/// <param name="audibleProductId">special id class b/c it's too easy to get string order mixed up</param>
|
||||||
|
public Book(
|
||||||
|
AudibleProductId audibleProductId,
|
||||||
|
string title,
|
||||||
|
string description,
|
||||||
|
int lengthInMinutes,
|
||||||
|
IEnumerable<Contributor> authors)
|
||||||
|
{
|
||||||
|
// validate
|
||||||
|
ArgumentValidator.EnsureNotNull(audibleProductId, nameof(audibleProductId));
|
||||||
|
var productId = audibleProductId.Id;
|
||||||
|
ArgumentValidator.EnsureNotNullOrWhiteSpace(productId, nameof(productId));
|
||||||
|
ArgumentValidator.EnsureNotNullOrWhiteSpace(title, nameof(title));
|
||||||
|
|
||||||
|
// non-ef-ctor init.s
|
||||||
|
UserDefinedItem = new UserDefinedItem(this);
|
||||||
|
_contributorsLink = new HashSet<BookContributor>();
|
||||||
|
_seriesLink = new HashSet<SeriesBook>();
|
||||||
|
_supplements = new HashSet<Supplement>();
|
||||||
|
|
||||||
|
// since category/id is never null, nullity means it hasn't been loaded
|
||||||
|
CategoryId = Category.GetEmpty().CategoryId;
|
||||||
|
|
||||||
|
// simple assigns
|
||||||
|
AudibleProductId = productId;
|
||||||
|
Title = title;
|
||||||
|
Description = description;
|
||||||
|
LengthInMinutes = lengthInMinutes;
|
||||||
|
|
||||||
|
// assigns with biz logic
|
||||||
|
ReplaceAuthors(authors);
|
||||||
|
//ReplaceNarrators(narrators);
|
||||||
|
|
||||||
|
// import previously saved tags
|
||||||
|
// do this immediately. any save occurs before reloading tags will overwrite persistent tags with new blank entries; all old persisted tags will be lost
|
||||||
|
// if refactoring, DO NOT use "ProductId" before it's assigned to. to be safe, just use "productId"
|
||||||
|
UserDefinedItem = new UserDefinedItem(this) { Tags = FileManager.TagsPersistence.GetTags(productId) };
|
||||||
|
}
|
||||||
|
|
||||||
|
#region contributors, authors, narrators
|
||||||
|
// use uninitialised backing fields - this means we can detect if the collection was loaded
|
||||||
|
private HashSet<BookContributor> _contributorsLink;
|
||||||
|
// i'd like this to be internal but migration throws this exception when i try:
|
||||||
|
// Value cannot be null.
|
||||||
|
// Parameter name: property
|
||||||
|
public IEnumerable<BookContributor> ContributorsLink
|
||||||
|
=> _contributorsLink?
|
||||||
|
.OrderBy(bc => bc.Order)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
public IEnumerable<Contributor> Authors => getContributions(Role.Author).Select(bc => bc.Contributor).ToList();
|
||||||
|
public string AuthorNames => string.Join(", ", Authors.Select(a => a.Name));
|
||||||
|
|
||||||
|
public IEnumerable<Contributor> Narrators => getContributions(Role.Narrator).Select(bc => bc.Contributor).ToList();
|
||||||
|
public string NarratorNames => string.Join(", ", Narrators.Select(n => n.Name));
|
||||||
|
|
||||||
|
public string Publisher => getContributions(Role.Publisher).SingleOrDefault()?.Contributor.Name;
|
||||||
|
|
||||||
|
public void ReplaceAuthors(IEnumerable<Contributor> authors, DbContext context = null)
|
||||||
|
=> replaceContributors(authors, Role.Author, context);
|
||||||
|
public void ReplaceNarrators(IEnumerable<Contributor> narrators, DbContext context = null)
|
||||||
|
=> replaceContributors(narrators, Role.Narrator, context);
|
||||||
|
public void ReplacePublisher(Contributor publisher, DbContext context = null)
|
||||||
|
=> replaceContributors(new List<Contributor> { publisher }, Role.Publisher, context);
|
||||||
|
private void replaceContributors(IEnumerable<Contributor> newContributors, Role role, DbContext context = null)
|
||||||
|
{
|
||||||
|
ArgumentValidator.EnsureEnumerableNotNullOrEmpty(newContributors, nameof(newContributors));
|
||||||
|
|
||||||
|
// the edge cases of doing local-loaded vs remote-only got weird. just load it
|
||||||
|
if (_contributorsLink == null)
|
||||||
|
{
|
||||||
|
ArgumentValidator.EnsureNotNull(context, nameof(context));
|
||||||
|
if (!context.Entry(this).IsKeySet)
|
||||||
|
throw new InvalidOperationException("Could not add contributors");
|
||||||
|
|
||||||
|
context.Entry(this).Collection(s => s.ContributorsLink).Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
var roleContributions = getContributions(role);
|
||||||
|
var isIdentical = roleContributions.Select(c => c.Contributor).SequenceEqual(newContributors);
|
||||||
|
if (isIdentical)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_contributorsLink.RemoveWhere(bc => bc.Role == role);
|
||||||
|
addNewContributors(newContributors, role);
|
||||||
|
}
|
||||||
|
private void addNewContributors(IEnumerable<Contributor> newContributors, Role role)
|
||||||
|
{
|
||||||
|
byte order = 0;
|
||||||
|
var newContributionsEnum = newContributors.Select(c => new BookContributor(this, c, role, order++));
|
||||||
|
var newContributions = new HashSet<BookContributor>(newContributionsEnum);
|
||||||
|
_contributorsLink.UnionWith(newContributions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BookContributor> getContributions(Role role)
|
||||||
|
=> ContributorsLink
|
||||||
|
.Where(a => a.Role == role)
|
||||||
|
.OrderBy(a => a.Order)
|
||||||
|
.ToList();
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region series
|
||||||
|
private HashSet<SeriesBook> _seriesLink;
|
||||||
|
public IEnumerable<SeriesBook> SeriesLink => _seriesLink?.ToList();
|
||||||
|
public string SeriesNames
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// first: alphabetical by name
|
||||||
|
var withNames = _seriesLink
|
||||||
|
.Where(s => !string.IsNullOrWhiteSpace(s.Series.Name))
|
||||||
|
.Select(s => s.Series.Name)
|
||||||
|
.OrderBy(a => a)
|
||||||
|
.ToList();
|
||||||
|
// then un-named are alpha by series id
|
||||||
|
var nullNames = _seriesLink
|
||||||
|
.Where(s => string.IsNullOrWhiteSpace(s.Series.Name))
|
||||||
|
.Select(s => s.Series.AudibleSeriesId)
|
||||||
|
.OrderBy(a => a)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var all = withNames.Union(nullNames).ToList();
|
||||||
|
return string.Join(", ", all);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpsertSeries(Series series, float? index = null, DbContext context = null)
|
||||||
|
{
|
||||||
|
ArgumentValidator.EnsureNotNull(series, nameof(series));
|
||||||
|
|
||||||
|
// our add() is conditional upon what's already included in the collection.
|
||||||
|
// therefore if not loaded, a trip is required. might as well just load it
|
||||||
|
if (_seriesLink == null)
|
||||||
|
{
|
||||||
|
ArgumentValidator.EnsureNotNull(context, nameof(context));
|
||||||
|
if (!context.Entry(this).IsKeySet)
|
||||||
|
throw new InvalidOperationException("Could not add series");
|
||||||
|
|
||||||
|
context.Entry(this).Collection(s => s.SeriesLink).Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
var singleSeriesBook = _seriesLink.SingleOrDefault(sb => sb.Series == series);
|
||||||
|
if (singleSeriesBook == null)
|
||||||
|
_seriesLink.Add(new SeriesBook(series, this, index));
|
||||||
|
else
|
||||||
|
singleSeriesBook.UpdateIndex(index);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region supplements
|
||||||
|
private HashSet<Supplement> _supplements;
|
||||||
|
public IEnumerable<Supplement> Supplements => _supplements?.ToList();
|
||||||
|
public bool HasPdfs => Supplements.Any();
|
||||||
|
|
||||||
|
public void AddSupplementDownloadUrl(string url)
|
||||||
|
{
|
||||||
|
// supplements are owned by Book, so no need to Load():
|
||||||
|
// OwnsMany: "Can only ever appear on navigation properties of other entity types.
|
||||||
|
// Are automatically loaded, and can only be tracked by a DbContext alongside their owner."
|
||||||
|
|
||||||
|
ArgumentValidator.EnsureNotNullOrWhiteSpace(url, nameof(url));
|
||||||
|
|
||||||
|
if (!_supplements.Any(s => url.EqualsInsensitive(url)))
|
||||||
|
_supplements.Add(new Supplement(this, url));
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public void UpdateProductRating(float overallRating, float performanceRating, float storyRating)
|
||||||
|
=> Rating.Update(overallRating, performanceRating, storyRating);
|
||||||
|
|
||||||
|
public void UpdateBookDetails(bool isAbridged, DateTime? datePublished)
|
||||||
|
{
|
||||||
|
// don't overwrite with default values
|
||||||
|
IsAbridged |= isAbridged;
|
||||||
|
DatePublished = datePublished ?? DatePublished;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateCategory(Category category, DbContext context = null)
|
||||||
|
{
|
||||||
|
// since category is never null, nullity means it hasn't been loaded
|
||||||
|
if (Category != null || CategoryId == Category.GetEmpty().CategoryId)
|
||||||
|
{
|
||||||
|
Category = category;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context == null)
|
||||||
|
throw new Exception("need context");
|
||||||
|
|
||||||
|
context.Entry(this).Reference(s => s.Category).Load();
|
||||||
|
Category = category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
namespace DataLayer
|
namespace DataLayer
|
||||||
{
|
{
|
||||||
public enum Role { Author = 1, Narrator = 2, Publisher = 3 }
|
|
||||||
|
|
||||||
public class BookContributor
|
public class BookContributor
|
||||||
{
|
{
|
||||||
internal int BookId { get; private set; }
|
internal int BookId { get; private set; }
|
||||||
@ -25,7 +23,5 @@ namespace DataLayer
|
|||||||
Role = role;
|
Role = role;
|
||||||
Order = order;
|
Order = order;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public override string ToString() => $"{Book} {Contributor} {Role} {Order}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
52
DataLayer/UNTESTED/EfClasses/Category.cs
Normal file
52
DataLayer/UNTESTED/EfClasses/Category.cs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Dinah.Core;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace DataLayer
|
||||||
|
{
|
||||||
|
public class AudibleCategoryId
|
||||||
|
{
|
||||||
|
public string Id { get; }
|
||||||
|
public AudibleCategoryId(string id)
|
||||||
|
{
|
||||||
|
ArgumentValidator.EnsureNotNullOrWhiteSpace(id, nameof(id));
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public class Category
|
||||||
|
{
|
||||||
|
// Empty is a special case. use private ctor w/o validation
|
||||||
|
public static Category GetEmpty() => new Category { CategoryId = -1, AudibleCategoryId = "", Name = "", ParentCategory = null };
|
||||||
|
public bool IsEmpty() => string.IsNullOrWhiteSpace(AudibleCategoryId) || string.IsNullOrWhiteSpace(Name) || ParentCategory == null;
|
||||||
|
|
||||||
|
internal int CategoryId { get; private set; }
|
||||||
|
public string AudibleCategoryId { get; private set; }
|
||||||
|
|
||||||
|
public string Name { get; private set; }
|
||||||
|
public Category ParentCategory { get; private set; }
|
||||||
|
|
||||||
|
private Category() { }
|
||||||
|
/// <summary>special id class b/c it's too easy to get string order mixed up</summary>
|
||||||
|
public Category(AudibleCategoryId audibleSeriesId, string name, Category parentCategory = null)
|
||||||
|
{
|
||||||
|
ArgumentValidator.EnsureNotNull(audibleSeriesId, nameof(audibleSeriesId));
|
||||||
|
var id = audibleSeriesId.Id;
|
||||||
|
ArgumentValidator.EnsureNotNullOrWhiteSpace(id, nameof(id));
|
||||||
|
ArgumentValidator.EnsureNotNullOrWhiteSpace(name, nameof(name));
|
||||||
|
|
||||||
|
AudibleCategoryId = id;
|
||||||
|
Name = name;
|
||||||
|
|
||||||
|
UpdateParentCategory(parentCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateParentCategory(Category parentCategory)
|
||||||
|
{
|
||||||
|
// don't overwrite with null but not an error
|
||||||
|
if (parentCategory != null)
|
||||||
|
ParentCategory = parentCategory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
82
DataLayer/UNTESTED/EfClasses/Contributor.cs
Normal file
82
DataLayer/UNTESTED/EfClasses/Contributor.cs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Dinah.Core;
|
||||||
|
|
||||||
|
namespace DataLayer
|
||||||
|
{
|
||||||
|
public class Contributor
|
||||||
|
{
|
||||||
|
// contributors search links are just name with url-encoding. space can be + or %20
|
||||||
|
// author search link: /search?searchAuthor=Robert+Bevan
|
||||||
|
// narrator search link: /search?searchNarrator=Robert+Bevan
|
||||||
|
// can also search multiples. concat with comma before url encode
|
||||||
|
|
||||||
|
// id.s
|
||||||
|
// ----
|
||||||
|
// https://www.audible.com/author/Neil-Gaiman/B000AQ01G2 == https://www.audible.com/author/B000AQ01G2
|
||||||
|
// goes to summary page
|
||||||
|
// at bottom "See all titles by Neil Gaiman" goes to https://www.audible.com/search?searchAuthor=Neil+Gaiman
|
||||||
|
// some authors have no id. simply goes to https://www.audible.com/search?searchAuthor=Rufus+Fears
|
||||||
|
// all narrators have no id: https://www.audible.com/search?searchNarrator=Neil+Gaiman
|
||||||
|
|
||||||
|
internal int ContributorId { get; private set; }
|
||||||
|
public string Name { get; private set; }
|
||||||
|
|
||||||
|
private HashSet<BookContributor> _booksLink;
|
||||||
|
public IEnumerable<BookContributor> BooksLink => _booksLink?.ToList();
|
||||||
|
|
||||||
|
private Contributor() { }
|
||||||
|
public Contributor(string name)
|
||||||
|
{
|
||||||
|
ArgumentValidator.EnsureNotNullOrWhiteSpace(name, nameof(name));
|
||||||
|
|
||||||
|
_booksLink = new HashSet<BookContributor>();
|
||||||
|
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string AudibleAuthorId { get; private set; }
|
||||||
|
public void UpdateAudibleAuthorId(string authorId)
|
||||||
|
{
|
||||||
|
// don't overwrite with null or whitespace but not an error
|
||||||
|
if (!string.IsNullOrWhiteSpace(authorId))
|
||||||
|
AudibleAuthorId = authorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region // AudibleAuthorId refactor: separate author-specific info. overkill for a single optional string
|
||||||
|
///// <summary>Most authors in Audible have a unique id</summary>
|
||||||
|
//public AudibleAuthorProperty AudibleAuthorProperty { get; private set; }
|
||||||
|
//public void UpdateAuthorId(string authorId, LibationContext context = null)
|
||||||
|
//{
|
||||||
|
// if (authorId == null)
|
||||||
|
// return;
|
||||||
|
// if (AudibleAuthorProperty != null)
|
||||||
|
// {
|
||||||
|
// AudibleAuthorProperty.UpdateAudibleAuthorId(authorId);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// if (context == null)
|
||||||
|
// throw new ArgumentNullException(nameof(context), "You must provide a context");
|
||||||
|
// if (context.Contributors.Find(ContributorId) == null)
|
||||||
|
// throw new InvalidOperationException("Could not update audible author id.");
|
||||||
|
// var audibleAuthorProperty = new AudibleAuthorProperty();
|
||||||
|
// audibleAuthorProperty.UpdateAudibleAuthorId(authorId);
|
||||||
|
// context.AuthorProperties.Add(audibleAuthorProperty);
|
||||||
|
//}
|
||||||
|
//public class AudibleAuthorProperty
|
||||||
|
//{
|
||||||
|
// public int ContributorId { get; private set; }
|
||||||
|
// public Contributor Contributor { get; set; }
|
||||||
|
|
||||||
|
// public string AudibleAuthorId { get; private set; }
|
||||||
|
|
||||||
|
// public void UpdateAudibleAuthorId(string authorId)
|
||||||
|
// {
|
||||||
|
// if (!string.IsNullOrWhiteSpace(authorId))
|
||||||
|
// AudibleAuthorId = authorId;
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//// ...and create EF table config
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
21
DataLayer/UNTESTED/EfClasses/LibraryBook.cs
Normal file
21
DataLayer/UNTESTED/EfClasses/LibraryBook.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using Dinah.Core;
|
||||||
|
|
||||||
|
namespace DataLayer
|
||||||
|
{
|
||||||
|
public class LibraryBook
|
||||||
|
{
|
||||||
|
internal int BookId { get; private set; }
|
||||||
|
public Book Book { get; private set; }
|
||||||
|
|
||||||
|
public DateTime DateAdded { get; private set; }
|
||||||
|
|
||||||
|
private LibraryBook() { }
|
||||||
|
public LibraryBook(Book book, DateTime dateAdded)
|
||||||
|
{
|
||||||
|
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||||
|
Book = book;
|
||||||
|
DateAdded = dateAdded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,14 +5,14 @@ using Dinah.Core;
|
|||||||
namespace DataLayer
|
namespace DataLayer
|
||||||
{
|
{
|
||||||
/// <summary>Parameterless ctor and setters should be used by EF only. Everything else should treat it as immutable</summary>
|
/// <summary>Parameterless ctor and setters should be used by EF only. Everything else should treat it as immutable</summary>
|
||||||
public class Rating : ValueObject_Static<Rating>, IComparable<Rating>, IComparable
|
public class Rating : ValueObject_Static<Rating>
|
||||||
{
|
{
|
||||||
public float OverallRating { get; private set; }
|
public float OverallRating { get; private set; }
|
||||||
public float PerformanceRating { get; private set; }
|
public float PerformanceRating { get; private set; }
|
||||||
public float StoryRating { get; private set; }
|
public float StoryRating { get; private set; }
|
||||||
|
|
||||||
private Rating() { }
|
private Rating() { }
|
||||||
public Rating(float overallRating, float performanceRating, float storyRating)
|
internal Rating(float overallRating, float performanceRating, float storyRating)
|
||||||
{
|
{
|
||||||
OverallRating = overallRating;
|
OverallRating = overallRating;
|
||||||
PerformanceRating = performanceRating;
|
PerformanceRating = performanceRating;
|
||||||
@ -38,16 +38,39 @@ namespace DataLayer
|
|||||||
yield return StoryRating;
|
yield return StoryRating;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() => $"Overall={OverallRating} Perf={PerformanceRating} Story={StoryRating}";
|
public float FirstScore
|
||||||
|
=> OverallRating > 0 ? OverallRating
|
||||||
|
: PerformanceRating > 0 ? PerformanceRating
|
||||||
|
: StoryRating;
|
||||||
|
|
||||||
public int CompareTo(Rating other)
|
/// <summary>character: ★</summary>
|
||||||
|
const char STAR = '\u2605';
|
||||||
|
/// <summary>character: ½</summary>
|
||||||
|
const char HALF = '\u00BD';
|
||||||
|
string getStars(float score)
|
||||||
{
|
{
|
||||||
var compare = OverallRating.CompareTo(other.OverallRating);
|
var fullStars = (int)Math.Floor(score);
|
||||||
if (compare != 0) return compare;
|
|
||||||
compare = PerformanceRating.CompareTo(other.PerformanceRating);
|
var starString = "".PadLeft(fullStars, STAR);
|
||||||
if (compare != 0) return compare;
|
|
||||||
return StoryRating.CompareTo(other.StoryRating);
|
if (score - fullStars == 0.5f)
|
||||||
|
starString += HALF;
|
||||||
|
|
||||||
|
return starString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToStarString()
|
||||||
|
{
|
||||||
|
var items = new List<string>();
|
||||||
|
|
||||||
|
if (OverallRating > 0)
|
||||||
|
items.Add($"Overall: {getStars(OverallRating)}");
|
||||||
|
if (PerformanceRating > 0)
|
||||||
|
items.Add($"Perform: {getStars(PerformanceRating)}");
|
||||||
|
if (StoryRating > 0)
|
||||||
|
items.Add($"Story: {getStars(StoryRating)}");
|
||||||
|
|
||||||
|
return string.Join("\r\n", items);
|
||||||
}
|
}
|
||||||
public int CompareTo(object obj) => obj is Rating second ? CompareTo(second) : -1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
4
DataLayer/UNTESTED/EfClasses/Role.cs
Normal file
4
DataLayer/UNTESTED/EfClasses/Role.cs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
namespace DataLayer
|
||||||
|
{
|
||||||
|
public enum Role { Author = 1, Narrator = 2, Publisher = 3 }
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace DataLayer
|
namespace DataLayer
|
||||||
{
|
{
|
||||||
@ -47,6 +48,23 @@ namespace DataLayer
|
|||||||
Name = name;
|
Name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() => Name;
|
public void AddBook(Book book, float? index = null, DbContext context = null)
|
||||||
}
|
{
|
||||||
|
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||||
|
|
||||||
|
// our add() is conditional upon what's already included in the collection.
|
||||||
|
// therefore if not loaded, a trip is required. might as well just load it
|
||||||
|
if (_booksLink == null)
|
||||||
|
{
|
||||||
|
ArgumentValidator.EnsureNotNull(context, nameof(context));
|
||||||
|
if (!context.Entry(this).IsKeySet)
|
||||||
|
throw new InvalidOperationException("Could not add series");
|
||||||
|
|
||||||
|
context.Entry(this).Collection(s => s.BooksLink).Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_booksLink.SingleOrDefault(sb => sb.Book == book) == null)
|
||||||
|
_booksLink.Add(new SeriesBook(this, book, index));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
38
DataLayer/UNTESTED/EfClasses/SeriesBook.cs
Normal file
38
DataLayer/UNTESTED/EfClasses/SeriesBook.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
using Dinah.Core;
|
||||||
|
|
||||||
|
namespace DataLayer
|
||||||
|
{
|
||||||
|
public class SeriesBook
|
||||||
|
{
|
||||||
|
internal int SeriesId { get; private set; }
|
||||||
|
internal int BookId { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <para>"index" not "order". This is both for sequence and display</para>
|
||||||
|
/// <para>Float allows for in-between books. eg: 2.5</para>
|
||||||
|
/// <para>To show 2 editions as the same book in a series, give them the same index</para>
|
||||||
|
/// <para>null IS NOT the same as 0. Some series call a book "book 0"</para>
|
||||||
|
/// </summary>
|
||||||
|
public float? Index { get; private set; }
|
||||||
|
|
||||||
|
public Series Series { get; private set; }
|
||||||
|
public Book Book { get; private set; }
|
||||||
|
|
||||||
|
private SeriesBook() { }
|
||||||
|
internal SeriesBook(Series series, Book book, float? index = null)
|
||||||
|
{
|
||||||
|
ArgumentValidator.EnsureNotNull(series, nameof(series));
|
||||||
|
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||||
|
|
||||||
|
Series = series;
|
||||||
|
Book = book;
|
||||||
|
Index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateIndex(float? index)
|
||||||
|
{
|
||||||
|
if (index.HasValue)
|
||||||
|
Index = index.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,7 +20,5 @@ namespace DataLayer
|
|||||||
Book = book;
|
Book = book;
|
||||||
Url = url;
|
Url = url;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public override string ToString() => $"{Book} {Url.Substring(Url.Length - 4)}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
77
DataLayer/UNTESTED/EfClasses/UserDefinedItem.cs
Normal file
77
DataLayer/UNTESTED/EfClasses/UserDefinedItem.cs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Dinah.Core;
|
||||||
|
|
||||||
|
namespace DataLayer
|
||||||
|
{
|
||||||
|
public class UserDefinedItem
|
||||||
|
{
|
||||||
|
internal int BookId { get; private set; }
|
||||||
|
public Book Book { get; private set; }
|
||||||
|
|
||||||
|
private UserDefinedItem() { }
|
||||||
|
internal UserDefinedItem(Book book)
|
||||||
|
{
|
||||||
|
ArgumentValidator.EnsureNotNull(book, nameof(book));
|
||||||
|
Book = book;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _tags = "";
|
||||||
|
public string Tags
|
||||||
|
{
|
||||||
|
get => _tags;
|
||||||
|
set => _tags = sanitize(value);
|
||||||
|
}
|
||||||
|
#region sanitize tags: space delimited. Inline/denormalized. Lower case. Alpha numeric and hyphen
|
||||||
|
// only legal chars are letters numbers underscores and separating whitespace
|
||||||
|
//
|
||||||
|
// technically, the only char.s which aren't easily supported are \ [ ]
|
||||||
|
// however, whitelisting is far safer than blacklisting (eg: new lines, non-printable character)
|
||||||
|
// it's easy to expand whitelist as needed
|
||||||
|
// for lucene, ToLower() isn't needed because search is case-inspecific. for here, it prevents duplicates
|
||||||
|
//
|
||||||
|
// there are also other allowed but misleading characters. eg: the ^ operator defines a 'boost' score
|
||||||
|
// full list of characters which must be escaped:
|
||||||
|
// + - && || ! ( ) { } [ ] ^ " ~ * ? : \
|
||||||
|
static Regex regex = new Regex(@"[^\w\d\s_]", RegexOptions.Compiled);
|
||||||
|
private static string sanitize(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
return "";
|
||||||
|
|
||||||
|
var str = input
|
||||||
|
.Trim()
|
||||||
|
.ToLowerInvariant()
|
||||||
|
// assume a hyphen is supposed to be an underscore
|
||||||
|
.Replace("-", "_");
|
||||||
|
|
||||||
|
var unique = regex
|
||||||
|
// turn illegal characters into a space. this will also take care of turning new lines into spaces
|
||||||
|
.Replace(str, " ")
|
||||||
|
// split and remove excess spaces
|
||||||
|
.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
// de-dup
|
||||||
|
.Distinct()
|
||||||
|
// this will prevent order from being relevant
|
||||||
|
.OrderBy(a => a);
|
||||||
|
|
||||||
|
// currently, the string is the canonical set. if we later make the collection into the canonical set:
|
||||||
|
// var tags = new Hashset<string>(list); // de-dup, order doesn't matter but can seem random due to hashing algo
|
||||||
|
// var isEqual = tagsNew.SetEquals(tagsOld);
|
||||||
|
|
||||||
|
return string.Join(" ", unique);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> TagsEnumerated => Tags == "" ? new string[0] : Tags.Split(null as char[], StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
// owned: not an optional one-to-one
|
||||||
|
/// <summary>The user's individual book rating</summary>
|
||||||
|
public Rating Rating { get; private set; } = new Rating(0, 0, 0);
|
||||||
|
|
||||||
|
public void UpdateRating(float overallRating, float performanceRating, float storyRating)
|
||||||
|
=> Rating.Update(overallRating, performanceRating, storyRating);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,15 +1,16 @@
|
|||||||
using DataLayer.Configurations;
|
using DataLayer.Configurations;
|
||||||
|
using Dinah.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace DataLayer
|
namespace DataLayer
|
||||||
{
|
{
|
||||||
public class LibationContext : DbContext
|
public class LibationContext : InterceptableDbContext
|
||||||
{
|
{
|
||||||
// IMPORTANT: USING DbSet<>
|
// IMPORTANT: USING DbSet<>
|
||||||
// ========================
|
// ========================
|
||||||
// these run against the db. linq queries against these MUST be translatable to sql. primatives only. no POCOs or this error occurs:
|
// these run against the db. linq queries against these MUST be translatable to sql. primatives only. no POCOs or this error occurs:
|
||||||
// Unable to create a constant value of type 'DataLayer.Contributor'. Only primitive types or enumeration types are supported in this context.
|
// Unable to create a constant value of type 'DataLayer.Contributor'. Only primitive types or enumeration types are supported in this context.
|
||||||
// to use full object-linq, load and use Local. HOWEVER, Local is only hashed/indexed on PK. All other searches are very slow
|
// to use full object-linq, load and use local
|
||||||
// load full table:
|
// load full table:
|
||||||
// List<Contributor> contributors = ...;
|
// List<Contributor> contributors = ...;
|
||||||
// Contributors.Load();
|
// Contributors.Load();
|
||||||
@ -18,40 +19,49 @@ namespace DataLayer
|
|||||||
// // overwrite collection
|
// // overwrite collection
|
||||||
// Entry(product).Collection(x => x.Narrators).Load();
|
// Entry(product).Collection(x => x.Narrators).Load();
|
||||||
// product.Narrators = narrators;
|
// product.Narrators = narrators;
|
||||||
public DbSet<LibraryBook> LibraryBooks { get; private set; }
|
public DbSet<LibraryBook> Library { get; private set; }
|
||||||
public DbSet<Book> Books { get; private set; }
|
public DbSet<Book> Books { get; private set; }
|
||||||
public DbSet<Contributor> Contributors { get; private set; }
|
public DbSet<Contributor> Contributors { get; private set; }
|
||||||
public DbSet<Series> Series { get; private set; }
|
public DbSet<Series> Series { get; private set; }
|
||||||
public DbSet<Category> Categories { get; private set; }
|
public DbSet<Category> Categories { get; private set; }
|
||||||
public DbSet<CategoryLadder> CategoryLadders { get; private set; }
|
|
||||||
|
|
||||||
public static LibationContext Create(string connectionString)
|
public static LibationContext Create()
|
||||||
{
|
{
|
||||||
var factory = new LibationContextFactory();
|
var factory = new LibationContextFactory();
|
||||||
var context = factory.Create(connectionString);
|
var context = factory.Create();
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
// see DesignTimeDbContextFactoryBase for info about ctors and connection strings/OnConfiguring()
|
// see DesignTimeDbContextFactoryBase for info about ctors and connection strings/OnConfiguring()
|
||||||
internal LibationContext(DbContextOptions options) : base(options) { }
|
internal LibationContext(DbContextOptions options) : base(options) { }
|
||||||
|
|
||||||
|
// called on each instantiation
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
|
{
|
||||||
|
AddInterceptor(new TagPersistenceInterceptor());
|
||||||
|
|
||||||
|
base.OnConfiguring(optionsBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
// typically only called once per execution; NOT once per instantiation
|
// typically only called once per execution; NOT once per instantiation
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.ApplyConfiguration(new BookConfig());
|
modelBuilder.ApplyConfiguration(new BookConfig());
|
||||||
modelBuilder.ApplyConfiguration(new ContributorConfig());
|
modelBuilder.ApplyConfiguration(new ContributorConfig());
|
||||||
modelBuilder.ApplyConfiguration(new BookContributorConfig());
|
modelBuilder.ApplyConfiguration(new BookContributorConfig());
|
||||||
modelBuilder.ApplyConfiguration(new LibraryBookConfig());
|
modelBuilder.ApplyConfiguration(new LibraryBookConfig());
|
||||||
modelBuilder.ApplyConfiguration(new SeriesConfig());
|
modelBuilder.ApplyConfiguration(new SeriesConfig());
|
||||||
modelBuilder.ApplyConfiguration(new SeriesBookConfig());
|
modelBuilder.ApplyConfiguration(new SeriesBookConfig());
|
||||||
modelBuilder.ApplyConfiguration(new CategoryConfig());
|
modelBuilder.ApplyConfiguration(new CategoryConfig());
|
||||||
modelBuilder.ApplyConfiguration(new CategoryLadderConfig());
|
|
||||||
modelBuilder.ApplyConfiguration(new BookCategoryConfig());
|
|
||||||
|
|
||||||
// views are now supported via "keyless entity types" (instead of "entity types" or the prev "query types"):
|
// seeds go here. examples in scratch pad
|
||||||
// https://docs.microsoft.com/en-us/ef/core/modeling/keyless-entity-types
|
modelBuilder
|
||||||
}
|
.Entity<Category>()
|
||||||
}
|
.HasData(Category.GetEmpty());
|
||||||
|
|
||||||
|
// views are now supported via "query types" (instead of "entity types"): https://docs.microsoft.com/en-us/ef/core/modeling/query-types
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,14 +1,11 @@
|
|||||||
using Dinah.EntityFrameworkCore;
|
using Dinah.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
|
||||||
|
|
||||||
namespace DataLayer
|
namespace DataLayer
|
||||||
{
|
{
|
||||||
public class LibationContextFactory : DesignTimeDbContextFactoryBase<LibationContext>
|
public class LibationContextFactory : DesignTimeDbContextFactoryBase<LibationContext>
|
||||||
{
|
{
|
||||||
protected override LibationContext CreateNewInstance(DbContextOptions<LibationContext> options) => new LibationContext(options);
|
protected override LibationContext CreateNewInstance(DbContextOptions<LibationContext> options) => new LibationContext(options);
|
||||||
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString)
|
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString) => optionsBuilder.UseSqlServer(connectionString);
|
||||||
=> optionsBuilder.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning))
|
|
||||||
.UseSqlite(connectionString, ob => ob.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
38
DataLayer/UNTESTED/QueryObjects/BookQueries.cs
Normal file
38
DataLayer/UNTESTED/QueryObjects/BookQueries.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace DataLayer
|
||||||
|
{
|
||||||
|
public static class BookQueries
|
||||||
|
{
|
||||||
|
public static Book GetBook_Flat_NoTracking(string productId)
|
||||||
|
{
|
||||||
|
using var context = LibationContext.Create();
|
||||||
|
return context
|
||||||
|
.Books
|
||||||
|
.AsNoTracking()
|
||||||
|
.GetBook(productId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Book GetBook(this IQueryable<Book> books, string productId)
|
||||||
|
=> books
|
||||||
|
.GetBooks()
|
||||||
|
.SingleOrDefault(b => b.AudibleProductId == productId);
|
||||||
|
|
||||||
|
/// <summary>This is still IQueryable. YOU MUST CALL ToList() YOURSELF</summary>
|
||||||
|
public static IQueryable<Book> GetBooks(this IQueryable<Book> books, Expression<Func<Book, bool>> predicate)
|
||||||
|
=> books
|
||||||
|
.GetBooks()
|
||||||
|
.Where(predicate);
|
||||||
|
|
||||||
|
public static IQueryable<Book> GetBooks(this IQueryable<Book> books)
|
||||||
|
=> books
|
||||||
|
// owned items are always loaded. eg: book.UserDefinedItem, book.Supplements
|
||||||
|
.Include(b => b.SeriesLink).ThenInclude(sb => sb.Series)
|
||||||
|
.Include(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
|
||||||
|
.Include(b => b.Category).ThenInclude(c => c.ParentCategory);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
DataLayer/UNTESTED/QueryObjects/GenericPaging.cs
Normal file
19
DataLayer/UNTESTED/QueryObjects/GenericPaging.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace DataLayer
|
||||||
|
{
|
||||||
|
public static class GenericPaging
|
||||||
|
{
|
||||||
|
public static IQueryable<T> Page<T>(this IQueryable<T> query, int pageNumZeroStart, int pageSize)
|
||||||
|
{
|
||||||
|
if (pageSize < 1)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(pageSize), "pageSize must be at least 1");
|
||||||
|
|
||||||
|
if (pageNumZeroStart > 0)
|
||||||
|
query = query.Skip(pageNumZeroStart * pageSize);
|
||||||
|
|
||||||
|
return query.Take(pageSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
DataLayer/UNTESTED/QueryObjects/LibraryQueries.cs
Normal file
41
DataLayer/UNTESTED/QueryObjects/LibraryQueries.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace DataLayer
|
||||||
|
{
|
||||||
|
public static class LibraryQueries
|
||||||
|
{
|
||||||
|
public static List<LibraryBook> GetLibrary_Flat_NoTracking()
|
||||||
|
{
|
||||||
|
using var context = LibationContext.Create();
|
||||||
|
return context
|
||||||
|
.Library
|
||||||
|
//.AsNoTracking()
|
||||||
|
.GetLibrary()
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LibraryBook GetLibraryBook_Flat_NoTracking(string productId)
|
||||||
|
{
|
||||||
|
using var context = LibationContext.Create();
|
||||||
|
return context
|
||||||
|
.Library
|
||||||
|
//.AsNoTracking()
|
||||||
|
.GetLibraryBook(productId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>This is still IQueryable. YOU MUST CALL ToList() YOURSELF</summary>
|
||||||
|
public static IQueryable<LibraryBook> GetLibrary(this IQueryable<LibraryBook> library)
|
||||||
|
=> library
|
||||||
|
// owned items are always loaded. eg: book.UserDefinedItem, book.Supplements
|
||||||
|
.Include(le => le.Book).ThenInclude(b => b.SeriesLink).ThenInclude(sb => sb.Series)
|
||||||
|
.Include(le => le.Book).ThenInclude(b => b.ContributorsLink).ThenInclude(c => c.Contributor)
|
||||||
|
.Include(le => le.Book).ThenInclude(b => b.Category).ThenInclude(c => c.ParentCategory);
|
||||||
|
|
||||||
|
public static LibraryBook GetLibraryBook(this IQueryable<LibraryBook> library, string productId)
|
||||||
|
=> library
|
||||||
|
.GetLibrary()
|
||||||
|
.SingleOrDefault(le => le.Book.AudibleProductId == productId);
|
||||||
|
}
|
||||||
|
}
|
||||||
60
DataLayer/UNTESTED/TagPersistenceInterceptor.cs
Normal file
60
DataLayer/UNTESTED/TagPersistenceInterceptor.cs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Dinah.Core.Collections.Generic;
|
||||||
|
using Dinah.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace DataLayer
|
||||||
|
{
|
||||||
|
internal class TagPersistenceInterceptor : IDbInterceptor
|
||||||
|
{
|
||||||
|
public void Executing(DbContext context)
|
||||||
|
{
|
||||||
|
doWork__EFCore(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Executed(DbContext context) { }
|
||||||
|
|
||||||
|
static void doWork__EFCore(DbContext context)
|
||||||
|
{
|
||||||
|
// persist tags:
|
||||||
|
var modifiedEntities = context.ChangeTracker.Entries().Where(p => p.State.In(EntityState.Modified, EntityState.Added)).ToList();
|
||||||
|
var tagSets = modifiedEntities.Select(e => e.Entity as UserDefinedItem).Where(a => a != null).ToList();
|
||||||
|
foreach (var t in tagSets)
|
||||||
|
FileManager.TagsPersistence.Save(t.Book.AudibleProductId, t.Tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region // notes: working with proxies, esp EF 6
|
||||||
|
// EF 6: entities are proxied with lazy loading when collections are virtual
|
||||||
|
// EF Core: lazy loading is supported in 2.1 (there is a version of lazy loading with proxy-wrapping and a proxy-less version with DI) but not on by default and are not supported here
|
||||||
|
|
||||||
|
//static void doWork_EF6(DbContext context)
|
||||||
|
//{
|
||||||
|
// var modifiedEntities = context.ChangeTracker.Entries().Where(p => p.State == EntityState.Modified).ToList();
|
||||||
|
// var unproxiedEntities = modifiedEntities.Select(me => UnProxy(context, me.Entity)).ToList();
|
||||||
|
|
||||||
|
// // persist tags
|
||||||
|
// var tagSets = unproxiedEntities.Select(ue => ue as UserDefinedItem).Where(a => a != null).ToList();
|
||||||
|
// foreach (var t in tagSets)
|
||||||
|
// FileManager.TagsPersistence.Save(t.ProductId, t.TagsRaw);
|
||||||
|
//}
|
||||||
|
|
||||||
|
//// https://stackoverflow.com/a/25774651
|
||||||
|
//private static T UnProxy<T>(DbContext context, T proxyObject) where T : class
|
||||||
|
//{
|
||||||
|
// // alternative: https://docs.microsoft.com/en-us/ef/ef6/fundamentals/proxies
|
||||||
|
// var proxyCreationEnabled = context.Configuration.ProxyCreationEnabled;
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// context.Configuration.ProxyCreationEnabled = false;
|
||||||
|
// return context.Entry(proxyObject).CurrentValues.ToObject() as T;
|
||||||
|
// }
|
||||||
|
// finally
|
||||||
|
// {
|
||||||
|
// context.Configuration.ProxyCreationEnabled = proxyCreationEnabled;
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
122
DataLayer/UNTESTED/_scratch pad/ScratchPad.cs
Normal file
122
DataLayer/UNTESTED/_scratch pad/ScratchPad.cs
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
using System;
|
||||||
|
using Dinah.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace _scratch_pad
|
||||||
|
{
|
||||||
|
////// to use this as a console, open properties and change from class library => console
|
||||||
|
//// DON'T FORGET TO REVERT IT
|
||||||
|
//public class Program
|
||||||
|
//{
|
||||||
|
// public static void Main(string[] args)
|
||||||
|
// {
|
||||||
|
// var user = new Student() { Name = "Dinah Cheshire" };
|
||||||
|
// var udi = new UserDef { UserDefId = 1, TagsRaw = "my,tags" };
|
||||||
|
|
||||||
|
// using var context = new MyTestContextDesignTimeDbContextFactory().Create();
|
||||||
|
// context.Add(user);
|
||||||
|
// //context.Add(udi);
|
||||||
|
// context.Update(udi);
|
||||||
|
// context.SaveChanges();
|
||||||
|
|
||||||
|
// Console.WriteLine($"Student was saved in the database with id: {user.Id}");
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
public class MyTestContextDesignTimeDbContextFactory : DesignTimeDbContextFactoryBase<MyTestContext>
|
||||||
|
{
|
||||||
|
protected override MyTestContext CreateNewInstance(DbContextOptions<MyTestContext> options) => new MyTestContext(options);
|
||||||
|
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString) => optionsBuilder.UseSqlite(connectionString);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MyTestContext : DbContext
|
||||||
|
{
|
||||||
|
// see DesignTimeDbContextFactoryBase for info about ctors and connection strings/OnConfiguring()
|
||||||
|
public MyTestContext(DbContextOptions<MyTestContext> options) : base(options) { }
|
||||||
|
|
||||||
|
#region classes for OnModelCreating() seed example
|
||||||
|
class Blog
|
||||||
|
{
|
||||||
|
public int BlogId { get; set; }
|
||||||
|
public string Url { get; set; }
|
||||||
|
public System.Collections.Generic.ICollection<Post> Posts { get; set; }
|
||||||
|
}
|
||||||
|
class Post
|
||||||
|
{
|
||||||
|
public int PostId { get; set; }
|
||||||
|
public string Content { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public int BlogId { get; set; }
|
||||||
|
public Blog Blog { get; set; }
|
||||||
|
public Name AuthorName { get; set; }
|
||||||
|
}
|
||||||
|
class Name
|
||||||
|
{
|
||||||
|
public string First { get; set; }
|
||||||
|
public string Last { get; set; }
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
// config
|
||||||
|
modelBuilder.Entity<Blog>(entity => entity.Property(e => e.Url).IsRequired());
|
||||||
|
modelBuilder.Entity<Order>().OwnsOne(p => p.OrderDetails, cb =>
|
||||||
|
{
|
||||||
|
cb.OwnsOne(c => c.BillingAddress);
|
||||||
|
cb.OwnsOne(c => c.ShippingAddress);
|
||||||
|
});
|
||||||
|
modelBuilder.Entity<Post>(entity =>
|
||||||
|
entity
|
||||||
|
.HasOne(d => d.Blog)
|
||||||
|
.WithMany(p => p.Posts)
|
||||||
|
.HasForeignKey("BlogId"));
|
||||||
|
|
||||||
|
// BlogSeed
|
||||||
|
modelBuilder.Entity<Blog>().HasData(new Blog { BlogId = 1, Url = "http://sample.com" });
|
||||||
|
|
||||||
|
// PostSeed
|
||||||
|
modelBuilder.Entity<Post>().HasData(new Post() { BlogId = 1, PostId = 1, Title = "First post", Content = "Test 1" });
|
||||||
|
|
||||||
|
// AnonymousPostSeed
|
||||||
|
modelBuilder.Entity<Post>().HasData(new { BlogId = 1, PostId = 2, Title = "Second post", Content = "Test 2" });
|
||||||
|
|
||||||
|
// OwnedTypeSeed
|
||||||
|
modelBuilder.Entity<Post>().OwnsOne(p => p.AuthorName).HasData(
|
||||||
|
new { PostId = 1, First = "Andriy", Last = "Svyryd" },
|
||||||
|
new { PostId = 2, First = "Diego", Last = "Vega" });
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbSet<Student> Students { get; set; }
|
||||||
|
public DbSet<UserDef> UserDefs { get; set; }
|
||||||
|
public DbSet<Order> Orders { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Student
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
public class UserDef
|
||||||
|
{
|
||||||
|
public int UserDefId { get; set; }
|
||||||
|
public string TagsRaw { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Order
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public OrderDetails OrderDetails { get; set; }
|
||||||
|
}
|
||||||
|
public class OrderDetails
|
||||||
|
{
|
||||||
|
public StreetAddress BillingAddress { get; set; }
|
||||||
|
public StreetAddress ShippingAddress { get; set; }
|
||||||
|
}
|
||||||
|
public class StreetAddress
|
||||||
|
{
|
||||||
|
public string Street { get; set; }
|
||||||
|
public string City { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
46
DataLayer/_HowTo- EF Core.txt
Normal file
46
DataLayer/_HowTo- EF Core.txt
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
HOW TO CREATE: EF CORE PROJECT
|
||||||
|
==============================
|
||||||
|
example is for sqlite but the same works with MsSql
|
||||||
|
|
||||||
|
|
||||||
|
nuget
|
||||||
|
Microsoft.EntityFrameworkCore.Tools (needed for using Package Manager Console)
|
||||||
|
Microsoft.EntityFrameworkCore.Sqlite
|
||||||
|
|
||||||
|
MIGRATIONS require standard, not core
|
||||||
|
using standard instead of core. edit 3 things in csproj
|
||||||
|
1of3: pluralize xml TargetFramework tag to TargetFrameworks
|
||||||
|
2of2: TargetFrameworks from: netstandard2.1
|
||||||
|
to: netcoreapp3.0;netstandard2.1
|
||||||
|
3of3: add
|
||||||
|
<PropertyGroup>
|
||||||
|
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
run. error
|
||||||
|
SQLite Error 1: 'no such table: Blogs'.
|
||||||
|
|
||||||
|
set project "Set as StartUp Project"
|
||||||
|
|
||||||
|
Tools >> Nuget Package Manager >> Package Manager Console
|
||||||
|
default project: Examples\SQLite_NETCore2_0
|
||||||
|
|
||||||
|
PM> add-migration InitialCreate
|
||||||
|
PM> Update-Database
|
||||||
|
|
||||||
|
if add-migration xyz throws and error, don't take the error msg at face value. try again with add-migration xyz -verbose
|
||||||
|
|
||||||
|
new sqlite .db file created: Copy always/Copy if newer
|
||||||
|
or copy .db file to destination
|
||||||
|
|
||||||
|
relative:
|
||||||
|
optionsBuilder.UseSqlite("Data Source=blogging.db");
|
||||||
|
absolute (use fwd slashes):
|
||||||
|
optionsBuilder.UseSqlite("Data Source=C:/foo/bar/blogging.db");
|
||||||
|
|
||||||
|
|
||||||
|
REFERENCE ARTICLES
|
||||||
|
------------------
|
||||||
|
https://docs.microsoft.com/en-us/ef/core/get-started/netcore/new-db-sqlite
|
||||||
|
https://carlos.mendible.com/2016/07/11/step-by-step-dotnet-core-and-entity-framework-core/
|
||||||
|
https://www.benday.com/2017/12/19/ef-core-2-0-migrations-without-hard-coded-connection-strings/
|
||||||
8
DataLayer/appsettings.json
Normal file
8
DataLayer/appsettings.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"LibationContext": "Server=(LocalDb)\\MSSQLLocalDB;Database=DataLayer.LibationContext;Integrated Security=true;",
|
||||||
|
|
||||||
|
"// on windows sqlite paths accept windows and/or unix slashes": "",
|
||||||
|
"MyTestContext": "Data Source=%DESKTOP%/sample.db"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"LibationFiles": "/config-internal"
|
|
||||||
}
|
|
||||||
@ -1,174 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
error() {
|
|
||||||
log "ERROR" "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
warn() {
|
|
||||||
log "WARNING" "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
info() {
|
|
||||||
log "info" "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
debug() {
|
|
||||||
if [ "${LOG_LEVEL}" = "debug" ]; then
|
|
||||||
log "debug" "$1"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
log() {
|
|
||||||
LEVEL=$1
|
|
||||||
MESSAGE=$2
|
|
||||||
printf "$(date '+%F %T') %s: %s\n" "${LEVEL}" "${MESSAGE}"
|
|
||||||
}
|
|
||||||
|
|
||||||
init_config_file() {
|
|
||||||
FILE=$1
|
|
||||||
FULLPATH=${LIBATION_CONFIG_DIR}/${FILE}
|
|
||||||
if [ -f ${FULLPATH} ]; then
|
|
||||||
info "loading ${FILE}"
|
|
||||||
cp ${FULLPATH} ${LIBATION_CONFIG_INTERNAL}/
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
warn "${FULLPATH} not found, creating empty file"
|
|
||||||
echo "{}" > ${LIBATION_CONFIG_INTERNAL}/${FILE}
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
update_settings() {
|
|
||||||
FILE=$1
|
|
||||||
KEY=$2
|
|
||||||
VALUE=$3
|
|
||||||
info "setting ${KEY} to ${VALUE}"
|
|
||||||
echo $(jq --arg k "${KEY}" --arg v "${VALUE}" '.[$k] = $v' ${LIBATION_CONFIG_INTERNAL}/${FILE}) > ${LIBATION_CONFIG_INTERNAL}/${FILE}.tmp
|
|
||||||
mv ${LIBATION_CONFIG_INTERNAL}/${FILE}.tmp ${LIBATION_CONFIG_INTERNAL}/${FILE}
|
|
||||||
}
|
|
||||||
|
|
||||||
is_mounted() {
|
|
||||||
DIR=$1
|
|
||||||
if grep -qs "${DIR} " /proc/mounts;
|
|
||||||
then
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
create_db() {
|
|
||||||
DBFILE=$1
|
|
||||||
if [ -f "${DBFILE}" ]; then
|
|
||||||
warn "prexisting database found when creating"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
if ! touch "${DBFILE}"; then
|
|
||||||
error "unable to create database, check permissions on host"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_db() {
|
|
||||||
DBPATH=$1
|
|
||||||
dbpattern="*.db"
|
|
||||||
|
|
||||||
debug "using database directory ${DBPATH}"
|
|
||||||
|
|
||||||
# Figure out the right databse file
|
|
||||||
if [[ -z "${LIBATION_DB_FILE}" ]];
|
|
||||||
then
|
|
||||||
dbCount=$(find "${DBPATH}" -maxdepth 1 -type f -name "${dbpattern}" | wc -l)
|
|
||||||
if [ "${dbCount}" -gt 1 ];
|
|
||||||
then
|
|
||||||
error "too many database files found, set LIBATION_DB_FILE to the filename you wish to use"
|
|
||||||
exit 1
|
|
||||||
elif [ "${dbCount}" -eq 1 ];
|
|
||||||
then
|
|
||||||
files=( ${DBPATH}/${dbpattern} )
|
|
||||||
FILE=${files[0]}
|
|
||||||
else
|
|
||||||
FILE="${DBPATH}/LibationContext.db"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
FILE="${DBPATH}/${LIBATION_DB_FILE}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
debug "planning to use database ${FILE}"
|
|
||||||
|
|
||||||
if [ -f "${FILE}" ]; then
|
|
||||||
info "database found at ${FILE}"
|
|
||||||
elif [ ${LIBATION_CREATE_DB} = "true" ];
|
|
||||||
then
|
|
||||||
warn "database not found, creating one at ${FILE}"
|
|
||||||
create_db ${FILE}
|
|
||||||
else
|
|
||||||
error "database not found and creation is disabled"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
ln -s "${FILE}" "${LIBATION_CONFIG_INTERNAL}/LibationContext.db"
|
|
||||||
}
|
|
||||||
|
|
||||||
run() {
|
|
||||||
info "scanning accounts"
|
|
||||||
/libation/LibationCli scan
|
|
||||||
info "liberating books"
|
|
||||||
/libation/LibationCli liberate
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
info "initializing libation"
|
|
||||||
init_config_file AccountsSettings.json
|
|
||||||
init_config_file Settings.json
|
|
||||||
|
|
||||||
info "loading settings"
|
|
||||||
update_settings Settings.json Books "${LIBATION_BOOKS_DIR:-/data}"
|
|
||||||
update_settings Settings.json InProgress /tmp
|
|
||||||
|
|
||||||
info "loading database"
|
|
||||||
# If user provides a separate database mount, use that
|
|
||||||
if is_mounted "${LIBATION_DB_DIR}";
|
|
||||||
then
|
|
||||||
DB_LOCATION=${LIBATION_DB_DIR}
|
|
||||||
# Otherwise, use the config directory
|
|
||||||
else
|
|
||||||
DB_LOCATION=${LIBATION_CONFIG_DIR}
|
|
||||||
fi
|
|
||||||
setup_db ${DB_LOCATION}
|
|
||||||
|
|
||||||
# Try to warn if books dir wasn't mounted in
|
|
||||||
if ! is_mounted "${LIBATION_BOOKS_DIR}";
|
|
||||||
then
|
|
||||||
warn "${LIBATION_BOOKS_DIR} does not appear to be mounted, books will not be saved"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Let the user know what the run type will be
|
|
||||||
if [[ -z "${SLEEP_TIME}" ]]; then
|
|
||||||
SLEEP_TIME=-1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${SLEEP_TIME}" == -1 ]; then
|
|
||||||
info "running once"
|
|
||||||
else
|
|
||||||
info "running every ${SLEEP_TIME}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# loop
|
|
||||||
while true
|
|
||||||
do
|
|
||||||
run
|
|
||||||
|
|
||||||
# Liberate only once if SLEEP_TIME was set to -1
|
|
||||||
if [ "${SLEEP_TIME}" == -1 ]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
|
|
||||||
sleep "${SLEEP_TIME}"
|
|
||||||
done
|
|
||||||
|
|
||||||
info "exiting"
|
|
||||||
}
|
|
||||||
|
|
||||||
main
|
|
||||||
39
Dockerfile
39
Dockerfile
@ -1,39 +0,0 @@
|
|||||||
# Dockerfile
|
|
||||||
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
|
||||||
ARG TARGETARCH
|
|
||||||
|
|
||||||
COPY Source /Source
|
|
||||||
RUN dotnet publish \
|
|
||||||
/Source/LibationCli/LibationCli.csproj \
|
|
||||||
--arch ${TARGETARCH} \
|
|
||||||
--configuration Release \
|
|
||||||
--output /Source/bin/Publish/Linux-chardonnay \
|
|
||||||
-p:PublishProfile=/Source/LibationCli/Properties/PublishProfiles/LinuxProfile.pubxml
|
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/runtime:9.0
|
|
||||||
ARG USER_UID=1001
|
|
||||||
ARG USER_GID=1001
|
|
||||||
|
|
||||||
# Set the character set that will be used for folder and filenames when liberating
|
|
||||||
ENV LANG=C.UTF-8
|
|
||||||
ENV LC_ALL=C.UTF-8
|
|
||||||
|
|
||||||
ENV SLEEP_TIME=-1
|
|
||||||
ENV LIBATION_CONFIG_INTERNAL=/config-internal
|
|
||||||
ENV LIBATION_CONFIG_DIR=/config
|
|
||||||
ENV LIBATION_DB_DIR=/db
|
|
||||||
ENV LIBATION_DB_FILE=
|
|
||||||
ENV LIBATION_CREATE_DB=true
|
|
||||||
ENV LIBATION_BOOKS_DIR=/data
|
|
||||||
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get -y upgrade && \
|
|
||||||
apt-get install -y jq && \
|
|
||||||
mkdir -m777 ${LIBATION_CONFIG_INTERNAL} ${LIBATION_BOOKS_DIR}
|
|
||||||
|
|
||||||
COPY --from=build /Source/bin/Publish/Linux-chardonnay /libation
|
|
||||||
COPY Docker/* /libation
|
|
||||||
|
|
||||||
USER ${USER_UID}:${USER_GID}
|
|
||||||
|
|
||||||
CMD ["/libation/liberate.sh"]
|
|
||||||
@ -1,112 +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**.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Advanced: Table of Contents
|
|
||||||
|
|
||||||
- [Files and folders](#files-and-folders)
|
|
||||||
- [Settings](#settings)
|
|
||||||
- [Custom File Naming](NamingTemplates.md)
|
|
||||||
- [Command Line Interface](#command-line-interface)
|
|
||||||
- [Custom Theme Colors](#custom-theme-colors) (Chardonnay Only)
|
|
||||||
- [Audio Formats (Dolby Atmos, Widevine, Spacial Audio)](AudioFileFormats.md)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Files and folders
|
|
||||||
|
|
||||||
To make upgrades and reinstalls easier, Libation separates all of its responsibilities to a few different folders. If you don't want to mess with this stuff: ignore it. Read on if you like a little more control over your files.
|
|
||||||
|
|
||||||
* In Libation's initial folder are the files that make up the program. Since nothing else is here, just copy new files here to upgrade the program. Delete this folder to delete Libation.
|
|
||||||
|
|
||||||
* In a separate folder, Libation keeps track of all of the files it creates like settings and downloaded images. After an upgrade, Libation might think that's its being run for the first time. Just click ADVANCED SETUP and point to this folder. Libation will reload your library and settings.
|
|
||||||
|
|
||||||
* The last important folder is the "books location." This is where Libation looks for your downloaded and decrypted books. This is how it knows which books still need to be downloaded. The Audible id must be somewhere in the book's file or folder name for Libation to detect your downloaded book.
|
|
||||||
|
|
||||||
### Settings
|
|
||||||
|
|
||||||
* Allow Libation to fix up audiobook metadata. After decrypting a title, Libation attempts to fix details like chapters and cover art. Some power users and/or control freaks prefer to manage this themselves. By unchecking this setting, Libation will only decrypt the book and will leave metadata as-is, warts and all.
|
|
||||||
|
|
||||||
In addition to the options that are enabled if you allow Libation to "fix up" the audiobook, it does the following:
|
|
||||||
|
|
||||||
* Adds the `TCOM` (`@wrt` in M4B files) metadata tag for the narrators.
|
|
||||||
* Sets the `©gen` metadata tag for the genres.
|
|
||||||
* Unescapes the copyright symbol (replace `©` with `©`)
|
|
||||||
* Replaces the recording copyright `(P)` string with `℗`
|
|
||||||
* Replaces the chapter markers embedded in the aax file with the chapter markers retrieved from Audible's API.
|
|
||||||
* Sets the embedded cover art image with the 500x500 px cover art retrieved from Audible
|
|
||||||
|
|
||||||
### Command Line Interface
|
|
||||||
|
|
||||||
Libationcli.exe allows limited access to Libation's functionalities as a CLI.
|
|
||||||
|
|
||||||
Warnings about relying solely on on the CLI:
|
|
||||||
* CLI will not perform any upgrades.
|
|
||||||
* It will show that there is an upgrade, but that will likely scroll by too fast to notice.
|
|
||||||
* It will not perform all post-upgrade migrations. Some migrations are only be possible by launching GUI.
|
|
||||||
|
|
||||||
```
|
|
||||||
help
|
|
||||||
libationcli --help
|
|
||||||
|
|
||||||
verb-specific help
|
|
||||||
libationcli scan --help
|
|
||||||
|
|
||||||
scan all libraries
|
|
||||||
libationcli scan
|
|
||||||
scan only libraries for specific accounts
|
|
||||||
libationcli scan nickname1 nickname2
|
|
||||||
|
|
||||||
convert all m4b files to mp3
|
|
||||||
libationcli convert
|
|
||||||
|
|
||||||
liberate all books and pdfs
|
|
||||||
libationcli liberate
|
|
||||||
liberate pdfs only
|
|
||||||
libationcli liberate --pdf
|
|
||||||
libationcli liberate -p
|
|
||||||
|
|
||||||
export library to file
|
|
||||||
libationcli export --path "C:\foo\bar\my.json" --json
|
|
||||||
libationcli export -p "C:\foo\bar\my.json" -j
|
|
||||||
libationcli export -p "C:\foo\bar\my.csv" --csv
|
|
||||||
libationcli export -p "C:\foo\bar\my.csv" -c
|
|
||||||
libationcli export -p "C:\foo\bar\my.xlsx" --xlsx
|
|
||||||
libationcli export -p "C:\foo\bar\my.xlsx" -x
|
|
||||||
|
|
||||||
Set download statuses throughout library based on whether each book's audio file can be found.
|
|
||||||
Must include at least one flag: --downloaded , --not-downloaded.
|
|
||||||
Downloaded: If the audio file can be found, set download status to 'Downloaded'.
|
|
||||||
Not Downloaded: If the audio file cannot be found, set download status to 'Not Downloaded'
|
|
||||||
UI: Visible Books \> Set 'Downloaded' status automatically. Visible books. Prompts before saving changes
|
|
||||||
CLI: Full library. No prompt
|
|
||||||
|
|
||||||
libationcli set-status -d
|
|
||||||
libationcli set-status -n
|
|
||||||
libationcli set-status -d -n
|
|
||||||
```
|
|
||||||
### Custom Theme Colors
|
|
||||||
|
|
||||||
In Libation Chardonnay (not Classic), you may adjust the app colors using the built-in theme editor. Open the Settings window (from the menu bar: Settings > Settings). On the "Important" settings tab, click "Edit Theme Colors".
|
|
||||||
|
|
||||||
#### Theme Editor Window
|
|
||||||
|
|
||||||
The theme editor has a list of style names and their currently assigned colors. To change a style color, click on the color swatch in the left-hand column to open the color editor for that style. Observe the color changes in real-time on the built-in preview panel on the right-hand side of the theme editor.
|
|
||||||
|
|
||||||
You may import or export themes using the buttons at the bottom-left of the theme editor.
|
|
||||||
"Cancel" or closing the window will revert any changes you've made in the theme editor.
|
|
||||||
"Reset" will reset any changes you've made in the theme editor.
|
|
||||||
"Defaults" will restore the application default colors for the active theme ("Light" or "Dark")
|
|
||||||
"Save" will save the theme colors to the ChardonnayTheme.json file and close the editor.
|
|
||||||
|
|
||||||
Note: you may only edit the currently applied theme ("Light" or "Dark").
|
|
||||||
|
|
||||||
#### Video Walkthrough
|
|
||||||
The below video demonstrates using the theme editor to make changes to the Dark theme color pallet.
|
|
||||||
|
|
||||||
[](https://github.com/user-attachments/assets/05c0cb7f-578f-4465-9691-77d694111349)
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
# Audio Formats Produced by Libation
|
|
||||||
|
|
||||||
Libation will download audio in a number of different audio formats, depending on the settings you choose within Libation and the per-title availability of audio formats from Audible. The Libation settings which affect the format downloaded by Libation are shown in the Settings menu screenshot below.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Audiobook file extensions are either `.m4b` or `.mp3`. Libation uses the `.m4b` file extension for all non-MP3 files, regardless of the audio codec contained therein. Some media players don't recognize the `.m4b` file extension and may require the extension be changed to `.m4a` or `.mp4`.
|
|
||||||
- Most (but not all) podcasts are delivered by Audible as native MP3 files. None of the following audio formats and settings discussions pertain to those podcasts because MP3s have no DRM, and those episodes are copied directly to their output folders.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Settings Summary
|
|
||||||
### Audio quality to request from Audible
|
|
||||||
Audiobooks can be requested from Audible as "Normal" quality or "High" quality, matching the settings in the Audible mobile apps. This setting affects the audio bitrate and, sometimes, the number of audio channels. This setting has no effect on the _audio codec_.
|
|
||||||
|
|
||||||
### Use Widevine DRM
|
|
||||||
When this setting is disabled, all audiobooks will be downloaded using Audible's in-house DRM (AAX(C)) in the [AAC-LC](#aac-lc) format.
|
|
||||||
When this setting is enabled, Libation will request audio files protected by Google's Widevine Digital Rights Managements scheme, and two additional settings will be unlocked: [Request xHE-AAC Codec](#request-xhe-aac-codec) and [Request Spatial Audio](#request-spatial-audio) (explained further below).
|
|
||||||
|
|
||||||
If you don't enable either of those additional options, then enabling 'Use Widevine DRM' will have no pratcical effect in nearly all circumstances. Audiobooks will be downloaded in the same [AAC-LC](#aac-lc) format with the same bitrate and the same number of audio channels. On rare occasions, enabling 'Use Widevine DRM' without the other two options will result in audio files with a different bitrate.
|
|
||||||
|
|
||||||
### Request xHE-AAC Codec
|
|
||||||
Enable this setting to request audiobooks in the [xHE-AAC](#xhe-aac) format. This codec is generally better quality than the [AAC-LC](#aac-lc) codec at the same bitrate, but it isn't as commonly supported by media players, so you may have some difficulty playing these audiobooks. The highest bitrate version of some audiobooks is only available as [xHE-AAC](#xhe-aac).
|
|
||||||
|
|
||||||
### Request Spatial Audio
|
|
||||||
Enable this setting to request audiobooks in a "spatial" ([Dolby Atmos](#dolby-atmos)) audio format. If an audiobook is not available in a spatial format, it will instead be downloaded in the [xHE-AAC codec](#xhe-aac).
|
|
||||||
|
|
||||||
### Spatial audio codec
|
|
||||||
Choose whether spatial audiobooks are downloaded in the [E-AC-3](#e-ac-3) or [AC-4](#ac-4) format.
|
|
||||||
|
|
||||||
### Download my books in the original audio format (Lossless)
|
|
||||||
If selected, Audiobooks will be downloaded and saved in the format delivered by audible (which depends on the settings explained above). Libation will not change the audio.
|
|
||||||
|
|
||||||
### Download my books as .MP3 files (transcode if necessary).
|
|
||||||
If selected, Libation will decode [AAC-LC](#aac-lc), [xHE-AAC](#xhe-aac), and [E-AC-3](#e-ac-3) audiobooks and re-encode them as MP3s using the MP3 encoder settings ([read about LAME MP3 encoder settings](https://lame.sourceforge.io/lame_ui_example.php)). Note that Libation cannot convert [AC-4](#ac-4) audio to MP3.
|
|
||||||
|
|
||||||
# Audio Formats
|
|
||||||
|
|
||||||
## Traditional Mono and Stereo Formats
|
|
||||||
|
|
||||||
### AAC-LC
|
|
||||||
#### _Full Name_
|
|
||||||
Advanced Audio Coding - Low Complexity
|
|
||||||
#### _Description_
|
|
||||||
This is the base profile for AAC audio and has existed since AAC's initial release in 1997. It enjoys wide support on nearly every conceivable platform capable of playing digital audio, as ubiquitous as MP3.
|
|
||||||
If Widevine support is not enabled, or if the book is not available in the more high-definition formats, Libation will download audiobooks in this format.
|
|
||||||
|
|
||||||
### MP3
|
|
||||||
#### _Full Name_
|
|
||||||
MPEG-1 Audio Layer III or MPEG-2 Audio Layer III
|
|
||||||
#### _Description_
|
|
||||||
An older (released in 1991) but still nearly universally supported audio codec. Its audio quality is generally worse than AAC-LC at similar bitrates. Audible delivers some podcasts in MP3 format, but no audiobooks are natively availble as MP3. Libation supports converting Audiobooks delivered in other audio formats to MP3. Note that the MP3 format supports a maximum of two audio channels, so multichannel E-AC-3 audio will be downsampled to stereo or mono (depending on the Libation's settings). [AC-4](#ac-4) cannot be converted to MP3.
|
|
||||||
|
|
||||||
### xHE-AAC
|
|
||||||
#### _Full Name_
|
|
||||||
Extended High-Efficiency Advanced Audio Coding
|
|
||||||
#### _Description_
|
|
||||||
This is a proprietary codec created by the [Fraunhofer Institute for Integrated Circuits IIS](https://www.iis.fraunhofer.de/en/ff/amm/broadcast-streaming/xheaac.html). It combines features of the HE-AAC v2 and the baseline USAC (Unified Speech and Audio Coding) profiles with the parts of the MPEG-D DRC Loudness Control Profile or Dynamic Range Control Profile. Therefore, USAC and xHE-AAC are not synonymous and should not be used interchangeably. A player capable of decoding USAC will not necessarily be able to decode xHE-AAC.
|
|
||||||
|
|
||||||
xHE-AAC boasts significantly higher quality audio at low bitrates. Though it has existed since at least 2016, playback support is still quite limited. FFmpeg has recently added partial decoder support for the USAC profiles, but it is insufficient to decode the xHE-AAC audio files acquired from Audible (due to FFmpeg's lack of support for MPEG Surround for Mono to Stereo Upmixing; ISO 23003-3:2012 §7.11)
|
|
||||||
|
|
||||||
Note that the xHE-AAC files authored by Audible have some USAC conformance errors including:
|
|
||||||
- Number of samples per frame not matching the UsacConfig coreCoderFrameLength value.
|
|
||||||
- Disagreement between stts and UsacFrame usacIndependencyFlag value.
|
|
||||||
- Stts indicating a frame is an immediate play-out frame, but USAC AudioPreRoll is absent.
|
|
||||||
|
|
||||||
## Dolby Atmos
|
|
||||||
Atmos is a surround sound technology that expands on existing surround sound systems by adding height channels as well as free-moving sound objects. Audible delivers Dolby Atmos in two formats: E-AC-3 and AC-4.
|
|
||||||
|
|
||||||
Your device's ability to play audio from these formats does not necessarily mean that the audio you are hearing is Atmos (spatial). For instance, downloading the AC-4 codec for Windows ([links in the [Supported media Players](#supported-media-players) section) will enable you to play AC-4 audiobooks, but you'll still need to download [Dolby Access](https://apps.microsoft.com/detail/9n0866fs04w8?hl=en-US&gl=US) and pay $15 to enable _Dolby Atmos For Headphones_. Please refer to [this comment](https://github.com/rmcrackan/Libation/pull/1331#discussion_r2268660524) for additional context.
|
|
||||||
|
|
||||||
### E-AC-3
|
|
||||||
#### _Full Name_
|
|
||||||
Dolby Digital Plus (a.k.a Enhanced AC-3, DDP, DD+, and EC-3)
|
|
||||||
#### _Description_
|
|
||||||
A proprietary digital audio compression scheme developed by Dolby Digital for the transport and storage of multichannel audio. This format can be extended to add support for Atmos, making the codec _Dolby Digital Plus Atmos_. _Dolby Digital Plus Atmos_ is backwards compatible with Dolby Digital Plus, so any media player capable of playing Dolby Digital Plus can play _Dolby Digital Plus Atmos_. Audible spatial audiobooks downloaded in the E-AC-3 format are _Dolby Digital Plus Atmos_. If they are played by a media player that supports Atmos, they will play as Atmos audio. If they are played by a media player that does not support Atmos, they will be played as traditional 5.1 surround audio.
|
|
||||||
|
|
||||||
### AC-4
|
|
||||||
#### _Full Name_
|
|
||||||
Dolby AC-4
|
|
||||||
#### _Description_
|
|
||||||
A proprietary audio compression technology developed by Dolby Digital for the transport and storage of audio channels and/or audio objects. Audible spatial audiobooks downloaded in the AC-4 format are 2-channel AC-4 Immersive Stereo (AC4-IMS) audio, intended for playback in headphones or earbuds (though apparently [not supported on Apple devices](https://github.com/rmcrackan/Libation/issues/996#issuecomment-3169574514)).
|
|
||||||
|
|
||||||
# Supported Media Players
|
|
||||||
Below is an incomplete matrix of codec support across various media players and platforms.
|
|
||||||
| Player | [AAC-LC](#aac-lc) | [xHE-AAC](#xhe-aac) | [E-AC-3](#e-ac-3) | [AC-4](#ac-4) |
|
|
||||||
| :--- | :---: | :---: | :---: | :---: |
|
|
||||||
|Windows Native Support|Yes|Yes<sup>1</sup>|Yes<sup>2,3</sup>|Yes<sup>4</sup>|
|
|
||||||
|macOS Native Support|Yes|Yes|Yes<sup>3</sup>| |
|
|
||||||
|Android Native Support<sup>5</sup>|Yes|Yes| | |
|
|
||||||
|FFmpeg (all platforms)|Yes|Yes<sup>6</sup>|Yes<sup>3</sup>||
|
|
||||||
|[VLC](https://www.videolan.org/vlc/) (Windows)|Yes| |Yes<sup>3</sup> | |
|
|
||||||
|[foobar2000](https://www.foobar2000.org/components) (Windows and Mac)|Yes|Yes<sup>7</sup> | | |
|
|
||||||
|[PotPlayer](https://potplayer.daum.net/) (Windows)|Yes|Yes|Yes<sup>3</sup>| |
|
|
||||||
|[Samsung Media Player](https://play.google.com/store/apps/details?id=com.sec.android.app.music)<sup>8</sup> (Samsung devices) |Yes|Yes|Yes|Yes|
|
|
||||||
|
|
||||||
1. Windows 11 22H2 and later
|
|
||||||
2. On Windows [prior to Windows 11, version 24H2](https://support.microsoft.com/en-us/windows/codecs-in-media-player-d5c2cdcd-83a2-4805-abb0-c6888138e456). You can still get the codec by running the following command from a Windows PowerShell console: `winget install --id 9nvjqjbdkn97`
|
|
||||||
3. As mentioned in the [Dolby Atmos](#dolby-atmos) section, just because a media player can play a file does not mean it's rendering Atmos. _Dolby Digital Plus Atmos_ is backwards compatible with _Dolby Digital Plus_, so media players which only support _Dolby Digital Plus_ will play E-AC-3 audio files as regular 5.1 surround without rendering the Atmos spatial qualities. Additional software or hardware support may be required for Dolby Atmos playback.
|
|
||||||
4. You can download the AC-4 codec for Windows from 3rd party sites like [Major Geeks](https://www.majorgeeks.com/files/details/dolby_ac_3ac_4_installer.html) and [Free-Codecs](https://www.free-codecs.com/dolby-ac-4-decoder_download.htm). Once you install the codec bundle from one of those sources, the Windows store app will keep it updated. Read more about the process [in this comment](https://github.com/rmcrackan/Libation/pull/1331#discussion_r2268660524).
|
|
||||||
5. All Android devices will support AAC-LC and xHE-AAC. Some manufactures (such as Samsung) will include Dolby codecs for playing E-AC-3 and AC-4 audio.
|
|
||||||
6. requires FFmpeg to be [built with fdk-aac](https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_aac). You will almost certainly not find pre-build binaries in the wild due to licensing restrictions.
|
|
||||||
7. Requires the [fdk-aac plugin](https://www.foobar2000.org/components/view/foo_pd_aac) (Windows only)
|
|
||||||
8. Requires audio file extensions to be `.m4a` or `.mp4`. Libation sets the file extensions to `.m4b`, so you must manually change it to `.m4a` by renaming the audio file.
|
|
||||||
|
|
||||||
@ -1,76 +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**.
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> ## Breaking Changes
|
|
||||||
> * The docker image now runs as user 1001 and group 1001. Make sure that the permissions on your volumes allow user 1001 to read and write to them or see the User section below for other options, or if you're not sure.
|
|
||||||
> * `SLEEP_TIME` is now set to `-1` by default. This means the image will run once and exit. If you were relying on the previous default, you'll need to explicitly set the `SLEEP_TIME` environment variable to `30m` to replicate the previous behavior.
|
|
||||||
> * The docker image now ignores the values in `Settings.json` for `Books` and `InProgress`. You can now change the folder that books are saved to by using the `LIBATION_BOOKS_DIR` environment variable.
|
|
||||||
|
|
||||||
# Disclaimer
|
|
||||||
The docker image is provided as-is. We hope it can be useful to you but it is not officially supported.
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
Configuration in Libation is handled by two files, `AccountsSettings.json` and `Settings.json`. These files can usually be found in the Libation folder in your user's home directory. The easiest way to configure these is to run the desktop version of Libation and then copy them into a folder, such as `/opt/libation/config`, that you'll volume mount into the image. `Settings.json` is technically optional, and, if not provided, Libation will run using the default settings. Additionally, the `Books` and `InProgress` settings in `Settings.json` will be ignored and the image will instead substitute it's own values.
|
|
||||||
|
|
||||||
### Running
|
|
||||||
Once the configuration files are copied, the docker image can be run with the following command.
|
|
||||||
```
|
|
||||||
sudo docker run -d \
|
|
||||||
-v /opt/libation/config:/config \
|
|
||||||
-v /opt/libation/books:/data \
|
|
||||||
--name libation \
|
|
||||||
--restart=always \
|
|
||||||
rmcrackan/libation:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
By default the container will scan for new books once and download any new ones. This is configurable by passing in a value for the `SLEEP_TIME` environment variable. For example, if you pass in `10m` it will keep running, scan for new books, and download them every 10 minutes.
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo docker run -d \
|
|
||||||
-v /opt/libation/config:/config \
|
|
||||||
-v /opt/libation/books:/data \
|
|
||||||
-e SLEEP_TIME='10m' \
|
|
||||||
--name libation \
|
|
||||||
--restart=always \
|
|
||||||
rmcrackan/libation:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
| Env Var | Default | Description |
|
|
||||||
| -------- | ------- | ----------- |
|
|
||||||
| SLEEP_TIME | -1 | Length of time to sleep before doing another scan/download. Set to -1 to run one. |
|
|
||||||
| LIBATION_BOOKS_DIR | /data | Folder where books will be saved |
|
|
||||||
| LIBATION_CONFIG_DIR | /config | Folder to read configuration from. |
|
|
||||||
| LIBATION_DB_DIR | /db | Optional folder to load database from. If not mounted, will load database from `LIBATION_CONFIG_DIR`. |
|
|
||||||
| LIBATION_DB_FILE | | Name of database file to load. By default it will look for all `.db` files and load one if there is only one present. |
|
|
||||||
| LIBATION_CREATE_DB | true | Whether or not the image should create a database file if none are found. |
|
|
||||||
|
|
||||||
### User
|
|
||||||
This docker image runs as user `1001`. In order for the image to function properly, user `1001` must be able to read and write the volumes that are mounted in. If they are not, you will see errors, including [sqlite error](#1060), [Microsoft.Data.Sqlite.SqliteException](#1110), [unable to open database file](#1113), [Microsoft.EntityFrameworkCore.DbUpdateException](#1049)
|
|
||||||
|
|
||||||
If you're not sure what your user number is, check the output of the `id` command. Docker should normally run with the number of the user who configured and ran it.
|
|
||||||
|
|
||||||
If you want to change the user the image runs as, you can specify `-u <uid>:<gid>`. For example, to run it as user `2000` and group `3000`, you could do the following:
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo docker run -d \
|
|
||||||
-u 2000:3000 \
|
|
||||||
-v /opt/libation/config:/config \
|
|
||||||
-v /opt/libation/books:/data \
|
|
||||||
--name libation \
|
|
||||||
--restart=always \
|
|
||||||
rmcrackan/libation:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
If the user it's running as is correct, and it still cannot write, be sure to check whether the files and/or folders might be owned by the wrong user. You can use the `chown` command to change the owner of the file to the correct user and group number, for example: `chown -R 1001:1001 /mnt/audiobooks /mnt/libation-config`
|
|
||||||
|
|
||||||
### Advanced Database Options
|
|
||||||
The docker image supports an optional database mount location defined by `LIBATION_DB_DIR`. This allows the database to be mounted as read/write, while allowing the rest of the configuration files to be mounted as read only. This is specifically useful if running in Kubernetes where you can use Configmaps and Secrets to define the configuration. If the `LIBATION_DB_DIR` is mounted, it will be used, otherwise it will look for the database in `LIBATION_CONFIG_DIR`. If it does not find the database in the expected location, it will attempt to make an empty database there.
|
|
||||||
|
|
||||||
### Getting help
|
|
||||||
As mentioned above: docker is not officially supported. I'm adding this at the bottom of the page for anyone serious enough to have read this far. If you've tried everything above and would still like help, you can open an [issue](https://github.com/rmcrackan/Libation/issues). Please include `[docker]` in the title. There are also some docker folks who have offered occasional assistance who you can tag within your issue: `@ducamagnifico` , `@wtanksleyjr` , `@CLHatch`.
|
|
||||||
|
|
||||||
**Reminder** that these are just friendly users who are sometimes around. They're *not* our customer support.
|
|
||||||
@ -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,155 +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**.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Getting started: Table of Contents
|
|
||||||
|
|
||||||
- [Download Libation](#download-libation-1)
|
|
||||||
- [Installation](#installation)
|
|
||||||
- [Create Accounts](#create-accounts)
|
|
||||||
- [Import your library](#import-your-library)
|
|
||||||
- [Download your books -- DRM-free!](#download-your-books----drm-free)
|
|
||||||
- [Download PDF attachments](#download-pdf-attachments)
|
|
||||||
- [Details of downloaded files](#details-of-downloaded-files)
|
|
||||||
- [Export your library](#export-your-library)
|
|
||||||
- [I still need help](#i-still-need-help)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### [Download Libation](https://github.com/rmcrackan/Libation/releases)
|
|
||||||
|
|
||||||
##### Which version? Chardonnay vs Classic
|
|
||||||
|
|
||||||
Nearly 100% of the difference is look and feel -- it's a matter of preference.
|
|
||||||
|
|
||||||
Chardonnay has an updated look and will work and look the same on Windows, Mac, and Linux.
|
|
||||||
Classic is Windows only. It has an older look because it's built with older, duller, and more mature technology. This tech has built into it better support for things like accessibility for screen readers.
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
* Windows
|
|
||||||
|
|
||||||
Extract the zip file to a folder and then run `Libation.exe` from inside of that folder. Do not put it in Program Files. The inability to edit files from there causes problems with configuration and updating.
|
|
||||||
|
|
||||||
* [Linux](InstallOnLinux.md)
|
|
||||||
* [MacOS](InstallOnMac.md)
|
|
||||||
|
|
||||||
### Create Accounts
|
|
||||||
|
|
||||||
Create your account(s):
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
New locale options include many more regions including old audible accounts which pre-date the amazon acquisition
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Import your library
|
|
||||||
|
|
||||||
Be default, Libation will periodically scan the accounts you added above with a checkbox next to them. Nothing for you to do. You can also scan manually.
|
|
||||||
|
|
||||||
Select Import > Scan Library:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Or if you have multiple accounts, you'll get to choose whether to scan all accounts or just the ones you select:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
If this is a new installation, or you're scanning an account you haven't scanned before, you'll be prompted to enter your password for the Audible account.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Enter the password and click Submit. Audible will prompt you with a CAPTCHA image.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Enter the CAPTCHA answer characters and click Submit. If all has gone well, Libation will start scanning the account.
|
|
||||||
|
|
||||||
In rare instances, the Captcha image/response will fail in an endless loop. If this happens, delete the problem account, and then click Save. Re-add the account and click Save again. Now try to scan the account again. This time, instead of typing your password, click the link that says "Or click here". This will open the Audible External Login dialog shown below.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
You can either copy the URL shown and paste it into your browser or launch the browser directly by clicking Launch in Browser. Audible will display its standard login page. Login, including answering the CAPTCHA on the next page. In some cases, you might have to approve the login from the email account associated with that login, but once the login is successful, you'll see an error message.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
This actually means you've successfully logged in. Copy the entire URL shown in your browser and return to Libation. Paste that URL into the text box at the bottom of the Audible External Login window and click Submit.
|
|
||||||
|
|
||||||
You'll see this window while it's scanning:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Success! We see how many new titles are imported:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Download your books -- DRM-free!
|
|
||||||
|
|
||||||
Automatically download some or all of your audible books. This shows you how much of your library is not yet downloaded and decrypted:
|
|
||||||
|
|
||||||
The stoplights will tell you a title's status:
|
|
||||||
|
|
||||||
* Green: downloaded and decrypted
|
|
||||||
* Yellow: downloaded but still encrypted with DRM
|
|
||||||
* Red: not downloaded
|
|
||||||
* PDF icon without arrow: downloaded
|
|
||||||
* PDF with arrow: not downloaded
|
|
||||||
|
|
||||||
Or hover over the button to see the status.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Select Liberate > Begin Book Backups
|
|
||||||
|
|
||||||
You can also click on the stop light to download only that title and its PDF
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
First the original book with DRM is downloaded
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Then it's decrypted so you can use it on any device you choose. The very first time you decrypt a book, this step will take a while. Every other book will go much faster. The first time, Libation has to figure out the special decryption key which allows your personal books to be unlocked.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
And voila! If you have multiple books not yet liberated, Libation will automatically move on to the next.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
The Audible id must be somewhere in the book's file or folder name for Libation to detect your downloaded book.
|
|
||||||
|
|
||||||
### Download PDF attachments
|
|
||||||
|
|
||||||
For books which include PDF downloads, Libation can download these for you as well and will attempt to store them with the book. "Book backup" will already download an available PDF. This additional option is useful when Audible adds a PDF to your book after you've already backed it up.
|
|
||||||
|
|
||||||
Select Liberate > Begin PDF Backups
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
The downloads work just like with books, only with no additional decryption needed.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Details of downloaded files
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
When you set up Libation, you'll specify a Books directory. Libation looks inside that directory and all subdirectories to look for files or folders with each library book's audible id. This way, organization is completely up to you. When you download + decrypt a book, you get several files
|
|
||||||
|
|
||||||
* .m4b: your audiobook in m4b format. This is the most pure version of your audiobook and retains the highest quality. Now that it's decrypted, you can play it on any audio player and put it on any device. If you'd like, you can also use 3rd party tools to turn it into an mp3. The freedom to do what you want with your files was the original inspiration for Libation.
|
|
||||||
* .cue: this is a file which logs where chapter breaks occur. Many tools are able to use this if you want to split your book into files along chapter lines.
|
|
||||||
|
|
||||||
### Export your library
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Export your library to Excel, CSV, or JSON
|
|
||||||
|
|
||||||
### I still need help
|
|
||||||
|
|
||||||
[You can open an issue here](https://github.com/rmcrackan/Libation/issues) for bug reports, feature requests, or specialized help.
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user