diff --git a/.darglint b/.darglint new file mode 100644 index 0000000..72ccc6c --- /dev/null +++ b/.darglint @@ -0,0 +1,2 @@ +[darglint] +strictness = long diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..9b77ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,40 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "" +labels: "" +assignees: "" +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] + +**Smartphone (please complete the following information):** + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..2bc5d5f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "" +labels: "" +assignees: "" +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5eba1a0..bb95ce6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -47,4 +47,4 @@ updates: dependency-type: development pip-security-updates: applies-to: security-updates - dependency-type: production \ No newline at end of file + dependency-type: production diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 0000000..a07c2c1 --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,72 @@ +--- +# Labels names are important as they are used by Release Drafter to decide +# regarding where to record them in changelog or if to skip them. +# +# The repository labels will be automatically configured using this file and +# the GitHub Action https://github.com/marketplace/actions/github-labeler. +- name: breaking + description: Breaking Changes + color: "#bfd4f2" +- name: bug + description: Something isn't working + color: "#d73a4a" +- name: build + description: Build System and Dependencies + color: "#bfdadc" +- name: ci + description: Continuous Integration + color: "#4a97d6" +- name: dependencies + description: Pull requests that update a dependency file + color: "#0366d6" +- name: documentation + description: Improvements or additions to documentation + color: "#0075ca" +- name: duplicate + description: This issue or pull request already exists + color: "#cfd3d7" +- name: enhancement + description: New feature or request + color: "#a2eeef" +- name: github_actions + description: Pull requests that update Github_actions code + color: "#000000" +- name: good first issue + description: Good for newcomers + color: "#7057ff" +- name: help wanted + description: Extra attention is needed + color: "#008672" +- name: invalid + description: This doesn't seem right + color: "#e4e669" +- name: performance + description: Performance + color: "#016175" +- name: python + description: Pull requests that update Python code + color: "#2b67c6" +- name: question + description: Further information is requested + color: "#d876e3" +- name: refactoring + description: Refactoring + color: "#ef67c4" +- name: removal + description: Removals and Deprecations + color: "#9ae7ea" +- name: style + description: Style + color: "#c120e5" +- name: testing + description: Testing + color: "#b1fc6f" +- name: skip-changelog + description: Skip pull request from release notes + color: "#964B00" +- name: skip-stale + description: Skip issue or pull request to be marked as stale + color: "#964B50" +- name: wontfix + description: This will not be worked on + color: "#ffffff" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8a39807..899ea7f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,7 @@ name: Build on: push: tags: - - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 + - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 env: PYTHON_VERSION: "3.13" @@ -15,16 +15,16 @@ jobs: outputs: release_url: ${{ steps.create-release.outputs.upload_url }} steps: - - name: Create Release - id: create-release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - draft: false - prerelease: false + - name: Create Release + id: create-release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false build: name: Build packages @@ -36,72 +36,72 @@ jobs: - os: ubuntu-latest TARGET: linux CMD_BUILD: > - uv run pyinstaller --clean -F --hidden-import audible_cli -n audible -c pyi_entrypoint.py && - cd dist/ && - zip -r9 audible_linux_ubuntu_latest audible + uv run pyinstaller --clean -F --hidden-import audible_cli -n audible -c pyi_entrypoint.py && + cd dist/ && + zip -r9 audible_linux_ubuntu_latest audible OUT_FILE_NAME: audible_linux_ubuntu_latest.zip - ASSET_MIME: application/zip # application/octet-stream + ASSET_MIME: application/zip # application/octet-stream - os: ubuntu-22.04 TARGET: linux CMD_BUILD: > - uv run pyinstaller --clean -F --hidden-import audible_cli -n audible -c pyi_entrypoint.py && - cd dist/ && - zip -r9 audible_linux_ubuntu_22_04 audible + uv run pyinstaller --clean -F --hidden-import audible_cli -n audible -c pyi_entrypoint.py && + cd dist/ && + zip -r9 audible_linux_ubuntu_22_04 audible OUT_FILE_NAME: audible_linux_ubuntu_22_04.zip - ASSET_MIME: application/zip # application/octet-stream + ASSET_MIME: application/zip # application/octet-stream - os: macos-latest TARGET: macos CMD_BUILD: > - uv run pyinstaller --clean -F --hidden-import audible_cli -n audible -c pyi_entrypoint.py && - cd dist/ && - zip -r9 audible_mac audible + uv run pyinstaller --clean -F --hidden-import audible_cli -n audible -c pyi_entrypoint.py && + cd dist/ && + zip -r9 audible_mac audible OUT_FILE_NAME: audible_mac.zip ASSET_MIME: application/zip - os: macos-latest TARGET: macos CMD_BUILD: > - uv run pyinstaller --clean -D --hidden-import audible_cli -n audible -c pyi_entrypoint.py && - cd dist/ && - zip -r9 audible_mac_dir audible + uv run pyinstaller --clean -D --hidden-import audible_cli -n audible -c pyi_entrypoint.py && + cd dist/ && + zip -r9 audible_mac_dir audible OUT_FILE_NAME: audible_mac_dir.zip - ASSET_MIME: application/zip + ASSET_MIME: application/zip - os: windows-latest TARGET: windows CMD_BUILD: > - uv run pyinstaller --clean -D --hidden-import audible_cli -n audible -c pyi_entrypoint.py && - cd dist/ && - powershell Compress-Archive audible audible_win_dir.zip + uv run pyinstaller --clean -D --hidden-import audible_cli -n audible -c pyi_entrypoint.py && + cd dist/ && + powershell Compress-Archive audible audible_win_dir.zip OUT_FILE_NAME: audible_win_dir.zip ASSET_MIME: application/zip - os: windows-latest TARGET: windows CMD_BUILD: > - uv run pyinstaller --clean -F --hidden-import audible_cli -n audible -c pyi_entrypoint.py && - cd dist/ && - powershell Compress-Archive audible.exe audible_win.zip + uv run pyinstaller --clean -F --hidden-import audible_cli -n audible -c pyi_entrypoint.py && + cd dist/ && + powershell Compress-Archive audible.exe audible_win.zip OUT_FILE_NAME: audible_win.zip ASSET_MIME: application/zip steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v5 - with: - python-version: ${{ env.PYTHON_VERSION }} + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} - - name: Install uv - uses: astral-sh/setup-uv@v6 + - name: Install uv + uses: astral-sh/setup-uv@v6 - - name: Build with pyinstaller for ${{matrix.TARGET}} - run: ${{matrix.CMD_BUILD}} + - name: Build with pyinstaller for ${{matrix.TARGET}} + run: ${{matrix.CMD_BUILD}} - - name: Upload Release Asset - id: upload-release-asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.createrelease.outputs.release_url }} - asset_path: ./dist/${{ matrix.OUT_FILE_NAME}} - asset_name: ${{ matrix.OUT_FILE_NAME}} - asset_content_type: ${{ matrix.ASSET_MIME}} + - name: Upload Release Asset + id: upload-release-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.createrelease.outputs.release_url }} + asset_path: ./dist/${{ matrix.OUT_FILE_NAME}} + asset_name: ${{ matrix.OUT_FILE_NAME}} + asset_content_type: ${{ matrix.ASSET_MIME}} diff --git a/.github/workflows/constraints.txt b/.github/workflows/constraints.txt new file mode 100644 index 0000000..ac89369 --- /dev/null +++ b/.github/workflows/constraints.txt @@ -0,0 +1,5 @@ +pip==25.1.1 +nox==2025.5.1 +nox-uv==0.3.0 +uv==0.7.6 +virtualenv==20.31.2 diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..6b6431f --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,23 @@ +name: Labeler + +on: + push: + branches: + - main + - master + +jobs: + labeler: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Check out the repository + uses: actions/checkout@v4 + + - name: Run Labeler + uses: crazy-max/ghaction-github-labeler@v5.3.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + skip-delete: true diff --git a/.github/workflows/pypi-publish-test.yml b/.github/workflows/pypi-publish-test.yml index 24d25f7..e576450 100644 --- a/.github/workflows/pypi-publish-test.yml +++ b/.github/workflows/pypi-publish-test.yml @@ -10,7 +10,7 @@ jobs: build-n-publish: name: Build and publish Audible-cli to TestPyPI runs-on: ubuntu-latest - + steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..a6c2f03 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,130 @@ +name: Tests + +on: + - push + - pull_request + +env: + PYTHON_VERSION: "3.13" + +jobs: + tests: + name: ${{ matrix.session }} ${{ matrix.python }} / ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - { python: "3.13", os: "ubuntu-latest", session: "pre-commit" } + - { python: "3.13", os: "ubuntu-latest", session: "safety" } + # - { python: "3.13", os: "ubuntu-latest", session: "mypy" } + # - { python: "3.12", os: "ubuntu-latest", session: "mypy" } + # - { python: "3.11", os: "ubuntu-latest", session: "mypy" } + # - { python: "3.10", os: "ubuntu-latest", session: "mypy" } + - { python: "3.13", os: "ubuntu-latest", session: "tests" } + - { python: "3.12", os: "ubuntu-latest", session: "tests" } + - { python: "3.11", os: "ubuntu-latest", session: "tests" } + - { python: "3.10", os: "ubuntu-latest", session: "tests" } + - { python: "3.13", os: "windows-latest", session: "tests" } + - { python: "3.13", os: "macos-latest", session: "tests" } + - { python: "3.13", os: "ubuntu-latest", session: "typeguard" } + - { python: "3.13", os: "ubuntu-latest", session: "xdoctest" } + - { python: "3.13", os: "ubuntu-latest", session: "docs-build" } + + env: + NOXSESSION: ${{ matrix.session }} + FORCE_COLOR: "1" + PRE_COMMIT_COLOR: "always" + + steps: + - name: Check out the repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - name: Install uv + uses: astral-sh/setup-uv@v6 + + - name: Compute pre-commit cache key + if: matrix.session == 'pre-commit' + id: pre-commit-cache + shell: python + run: | + import hashlib + import os + import sys + + python = "py{}.{}".format(*sys.version_info[:2]) + payload = sys.version.encode() + sys.executable.encode() + digest = hashlib.sha256(payload).hexdigest() + result = "${{ runner.os }}-{}-{}-pre-commit".format(python, digest[:8]) + + with open(os.environ['GITHUB_OUTPUT'], 'a') as io: + print(f"result={result}", file=io) + + - name: Restore pre-commit cache + uses: actions/cache@v4 + if: matrix.session == 'pre-commit' + with: + path: ~/.cache/pre-commit + key: ${{ steps.pre-commit-cache.outputs.result }}-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: | + ${{ steps.pre-commit-cache.outputs.result }}- + + - name: Run Nox + run: | + uv run nox --python=${{ matrix.python }} + + - name: Upload coverage data + if: always() && matrix.session == 'tests' + uses: "actions/upload-artifact@v4" + with: + name: coverage-data-${{ matrix.python }}-${{ matrix.os }} + include-hidden-files: true + path: ".coverage.*" + + - name: Upload documentation + if: matrix.session == 'docs-build' + uses: actions/upload-artifact@v4 + with: + name: docs + path: docs/_build + + coverage: + runs-on: ubuntu-latest + needs: tests + steps: + - name: Check out the repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install uv + uses: astral-sh/setup-uv@v6 + + - name: Download coverage data + uses: actions/download-artifact@v4 + with: + pattern: coverage-data-* + merge-multiple: true + + - name: Combine coverage data and display human readable report + run: | + uv run nox --session=coverage + + - name: Create coverage report + run: | + uv run nox --session=coverage -- xml + + - name: Upload coverage report + uses: codecov/codecov-action@v5.4.3 + env: + token: ${{ secrets.CODECOV_TOKEN }} + with: + files: ./coverage.xml,./coverage1.xml,./coverage2.xml diff --git a/.github/workflows/uv-lock-maintenance.yml b/.github/workflows/uv-lock-maintenance.yml index 45494c3..aa44d33 100644 --- a/.github/workflows/uv-lock-maintenance.yml +++ b/.github/workflows/uv-lock-maintenance.yml @@ -35,4 +35,4 @@ jobs: - name: "Run if no changes have been detected" if: steps.auto-commit-action.outputs.changes_detected == 'false' - run: echo "No Changes!" \ No newline at end of file + run: echo "No Changes!" diff --git a/.gitignore b/.gitignore index 3183e33..5e05059 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,10 @@ !/src/audible_cli /src/audible_cli/*.bak library.csv +/.idea/ +/.pytype/ +.DS_Store + # Byte-compiled / optimized / DLL files __pycache__/ @@ -132,7 +136,8 @@ venv.bak/ .dmypy.json dmypy.json +# ruff +.ruff_cache/ + # Pyre type checker .pyre/ - -.idea diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..47243b3 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,56 @@ +repos: + - repo: local + hooks: + - id: ruff-format + name: ruff format + entry: ruff format + language: system + types: [python] + require_serial: true + - id: check-added-large-files + name: Check for added large files + entry: check-added-large-files + language: system + - id: check-toml + name: Check Toml + entry: check-toml + language: system + types: [toml] + - id: check-yaml + name: Check Yaml + entry: check-yaml + language: system + types: [yaml] + - id: darglint + name: darglint + entry: darglint + language: system + types: [python] + stages: [manual] + - id: end-of-file-fixer + name: Fix End of Files + entry: end-of-file-fixer + language: system + types: [text] + stages: [pre-commit, pre-push, manual] + - id: ruff-check + name: ruff check + entry: ruff check + language: system + types: [python] + require_serial: true + - id: trailing-whitespace + name: Trim Trailing Whitespace + entry: trailing-whitespace-fixer + language: system + types: [text] + stages: [pre-commit, pre-push, manual] + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.0.3 + hooks: + - id: prettier + - repo: https://github.com/astral-sh/uv-pre-commit + # uv version. + rev: 0.7.6 + hooks: + - id: uv-lock diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..a269fdf --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,13 @@ +version: 2 +build: + os: ubuntu-24.04 + tools: + python: "3.13" +sphinx: + configuration: docs/source/conf.py +formats: + - pdf +python: + install: + - requirements: docs/requirements.txt + - path: . diff --git a/.sshignore b/.sshignore new file mode 100644 index 0000000..9b21af1 --- /dev/null +++ b/.sshignore @@ -0,0 +1,10 @@ +# This file is used by WorkingCopy App + +/.mypy_cache/** +/.nox/** +/.pytest_cache/** +/.venv/** +/.ruff_cache/** +__pycache__/** +/dist/** +/src/* diff --git a/CHANGELOG.md b/CHANGELOG.md index 61d8b98..f6957e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,20 +19,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added -- The `--chapter-type` option is added to the download command. Chapter can now be - downloaded as `flat` or `tree` type. `tree` is the default. A default chapter type +- The `--chapter-type` option is added to the download command. Chapter can now be + downloaded as `flat` or `tree` type. `tree` is the default. A default chapter type can be set in the config file. ### Changed - Improved podcast ignore feature in download command -- make `--ignore-podcasts` and `--resolve-podcasts` options of download command mutual +- make `--ignore-podcasts` and `--resolve-podcasts` options of download command mutual exclusive -- Switched from a HEAD to a GET request without loading the body in the downloader - class. This change improves the program's speed, as the HEAD request was taking +- Switched from a HEAD to a GET request without loading the body in the downloader + class. This change improves the program's speed, as the HEAD request was taking considerably longer than a GET request on some Audible pages. -- `models.LibraryItem.get_content_metadatata` now accept a `chapter_type` argument. - Additional keyword arguments to this method are now passed through the metadata +- `models.LibraryItem.get_content_metadatata` now accept a `chapter_type` argument. + Additional keyword arguments to this method are now passed through the metadata request. - Update httpx version range to >=0.23.3 and <0.28.0. - fix typo from `resolve_podcats` to `resolve_podcasts` @@ -55,7 +55,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Added a resume feature when downloading aaxc files. - New `downlaoder` module which contains a rework of the Downloader class. - If necessary, large audiobooks are now downloaded in parts. -- Plugin command help page now contains additional information about the source of +- Plugin command help page now contains additional information about the source of the plugin. - Command help text now starts with ´(P)` for plugin commands. @@ -77,8 +77,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed -- Update httpx version range to >=0.23.3 and <0.26.0. - +- Update httpx version range to >=0.23.3 and <0.26.0. + ### Misc - add `freeze_support` to pyinstaller entry script (#78) @@ -95,7 +95,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Allow download multiple cover sizes at once. Each cover size must be provided with the `--cover-size` option - ### Changed - Rework start_date and end_date option @@ -183,7 +182,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - the `library export` and `wishlist export` command will now export to `csv` correctly -- +- ## [0.1.3] - 2022-03-27 @@ -223,7 +222,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - the `--ignore-podcasts` flag to download subcommand; if a podcast contains multiple episodes, the podcast will be ignored - the`models.Library.resolve_podcasts` method to append all podcast episodes to given library. - the `models.LibraryItem.get_child_items` method to get all episodes of a podcast item or parts for a MultiPartBook. -- the`models.BaseItem` now holds a list of `response_groups` in the `_response_groups` attribute. +- the`models.BaseItem` now holds a list of `response_groups` in the `_response_groups` attribute. - the`--format` option to `library export` subcommand - the `models.Catalog` class - the `models.Library.from_api_full_sync` method to fetch the full library @@ -233,9 +232,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - the `--aaxc` flag of the download command now try to check if a voucher file exists before a `licenserequest` is make (issue #60) - the `--aaxc` flag of the download command now downloads mp3/m4a files if the `aaxc` format is not available and the `licenserequest` offers this formats - the `download` subcommand now download podcasts -- *Remove sync code where async code are available. All plugins should take care about this!!!* +- _Remove sync code where async code are available. All plugins should take care about this!!!_ - Bump `audible` to v0.7.0 -- rebuild `models.LibraryItem.get_aax_url` to build the aax download url in another way +- rebuild `models.LibraryItem.get_aax_url` to build the aax download url in another way - `models.BaseItem.full_title` now contains publication name for podcast episodes - `models.LibraryItem` now checks the customer rights when calling `LibraryItem._is_downloadable` - `models.BaseItem` and `models.BaseList` now holds the `api_client` instead the `locale` and `auth` @@ -276,9 +275,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Add timeout option to download command ### Changed + - models.py: If no supported codec is found when downloading aax files, no url is returned now. -- utils.py: Downloading a file with the `Downloader` class now checks the +- utils.py: Downloading a file with the `Downloader` class now checks the response status code, the content type and compares the file size. - models.py: Now all books are fetched if the library is greater than 1000. This works for the download and library command. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..5b70988 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[mkb79@hackitall.de](mailto:mkb79@hackitall.de). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][mozilla coc]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][faq]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[mozilla coc]: https://github.com/mozilla/diversity +[faq]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a5a194f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,111 @@ +# Contributor Guide + +Thank you for your interest in improving this project. +This project is open-source under the [AGPL 3.0 license] and +welcomes contributions in the form of bug reports, feature requests, and pull requests. + +Here is a list of important resources for contributors: + +- [Source Code] +- [Documentation] +- [Issue Tracker] +- [Code of Conduct] + +[agpl 3.0 license]: https://opensource.org/license/agpl-v3 +[source code]: https://github.com/mkb79/audible-cli +[documentation]: https://audible-cli.readthedocs.io/ +[issue tracker]: https://github.com/mkb79/audible-cli/issues + +## How to report a bug + +Report bugs on the [Issue Tracker]. + +When filing an issue, make sure to answer these questions: + +- Which operating system and Python version are you using? +- Which version of this project are you using? +- What did you do? +- What did you expect to see? +- What did you see instead? + +The best way to get your bug fixed is to provide a test case, +and/or steps to reproduce the issue. + +## How to request a feature + +Request features on the [Issue Tracker]. + +## How to set up your development environment + +You need Python 3.10+ and the following tools: + +- [uv] + +Install the package with development requirements: + +```console +$ uv sync +``` + +You can now run an interactive Python session, +or the command-line interface: + +```console +$ uv run python +``` + +[uv]: https://docs.astral.sh/uv/ + +## How to test the project + +Run the full test suite: + +```console +$ uv run nox +``` + +List the available Nox sessions: + +```console +$ uv run nox --list-sessions +``` + +You can also run a specific Nox session. +For example, invoke the unit test suite like this: + +```console +$ uv run nox --session=tests +``` + +Unit tests are located in the _tests_ directory, +and are written using the [pytest] testing framework. + +[pytest]: https://pytest.readthedocs.io/ + +## How to submit changes + +Open a [pull request] to submit changes to this project. + +Your pull request needs to meet the following guidelines for acceptance: + +- The Nox test suite must pass without errors and warnings. +- Include unit tests. This project maintains 100% code coverage. +- If your changes add functionality, update the documentation accordingly. + +Feel free to submit early, though—we can always iterate on this. + +To run linting and code formatting checks before committing your change, you +can install pre-commit as a Git hook by running the following command: + +```console +$ uv run nox --session=pre-commit -- install +``` + +It is recommended to open an issue before starting work on anything. +This will allow a chance to talk it over with the owners and validate your approach. + +[pull request]: https://github.com/mkb79/audible-cli/pulls + + + +[code of conduct]: CODE_OF_CONDUCT.md diff --git a/README.md b/README.md index fec195d..86caf88 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,24 @@ # audible-cli -**audible-cli** is a command line interface for the -[Audible](https://github.com/mkb79/Audible) package. +**audible-cli** is a command line interface for the +[Audible](https://github.com/mkb79/Audible) package. Both are written with Python. ## Requirements -audible-cli needs at least *Python 3.10* and *Audible v0.8.2*. +audible-cli needs at least _Python 3.10_ and _Audible v0.8.2_. It depends on the following packages: -* aiofiles -* audible -* click -* colorama (on Windows machines) -* httpx -* Pillow -* tabulate -* toml -* tqdm +- aiofiles +- audible +- click +- colorama (on Windows machines) +- httpx +- Pillow +- tabulate +- toml +- tqdm ## Installation @@ -51,22 +51,24 @@ uvx --with audible-cli audible ## Standalone executables If you don't want to install `Python` and `audible-cli` on your machine, you can -find standalone exe files below or on the [releases](https://github.com/mkb79/audible-cli/releases) +find standalone exe files below or on the [releases](https://github.com/mkb79/audible-cli/releases) page (including beta releases). At this moment Windows, Linux and macOS are supported. ### Links 1. Linux - - [ubuntu latest onefile](https://github.com/mkb79/audible-cli/releases/latest/download/audible_linux_ubuntu_latest.zip) - - [ubuntu 22.04 onefile](https://github.com/mkb79/audible-cli/releases/latest/download/audible_linux_ubuntu_22_04.zip) + + - [ubuntu latest onefile](https://github.com/mkb79/audible-cli/releases/latest/download/audible_linux_ubuntu_latest.zip) + - [ubuntu 22.04 onefile](https://github.com/mkb79/audible-cli/releases/latest/download/audible_linux_ubuntu_22_04.zip) 2. macOS - - [macOS latest onefile](https://github.com/mkb79/audible-cli/releases/latest/download/audible_mac.zip) - - [macOS latest onedir](https://github.com/mkb79/audible-cli/releases/latest/download/audible_mac_dir.zip) + + - [macOS latest onefile](https://github.com/mkb79/audible-cli/releases/latest/download/audible_mac.zip) + - [macOS latest onedir](https://github.com/mkb79/audible-cli/releases/latest/download/audible_mac_dir.zip) 3. Windows - - [Windows onefile](https://github.com/mkb79/audible-cli/releases/latest/download/audible_win.zip) - - [Windows onedir](https://github.com/mkb79/audible-cli/releases/latest/download/audible_win_dir.zip) + - [Windows onefile](https://github.com/mkb79/audible-cli/releases/latest/download/audible_win.zip) + - [Windows onedir](https://github.com/mkb79/audible-cli/releases/latest/download/audible_win_dir.zip) On every execution, the binary code must be extracted. On Windows machines this can result in a long start time. If you use `audible-cli` often, I would prefer the `directory` package for Windows! @@ -88,58 +90,57 @@ uv run pyinstaller --clean -D --hidden-import audible_cli -n audible -c pyi_entr ### Hints There are some limitations when using plugins. The binary maybe does not contain -all the dependencies from your plugin script. +all the dependencies from your plugin script. ## Tab Completion -Tab completion can be provided for commands, options and choice values. Bash, -Zsh and Fish are supported. More information can be found +Tab completion can be provided for commands, options and choice values. Bash, +Zsh and Fish are supported. More information can be found [here](https://github.com/mkb79/audible-cli/tree/master/utils/code_completion). - ## Basic information ### App dir audible-cli use an app dir where it expects all necessary files. -If the ``AUDIBLE_CONFIG_DIR`` environment variable is set, it uses the value -as config dir. Otherwise, it will use a folder depending on the operating +If the `AUDIBLE_CONFIG_DIR` environment variable is set, it uses the value +as config dir. Otherwise, it will use a folder depending on the operating system. -| OS | Path | -|----------|-------------------------------------------| -| Windows | ``C:\Users\\AppData\Local\audible`` | -| Unix | ``~/.audible`` | -| Mac OS X | ``~/.audible`` | +| OS | Path | +| -------- | --------------------------------------- | +| Windows | `C:\Users\\AppData\Local\audible` | +| Unix | `~/.audible` | +| Mac OS X | `~/.audible` | ### The config file -The config data will be stored in the [toml](https://github.com/toml-lang/toml) -format as ``config.toml``. +The config data will be stored in the [toml](https://github.com/toml-lang/toml) +format as `config.toml`. -It has a main section named ``APP`` and sections for each profile created -named ``profile.`` +It has a main section named `APP` and sections for each profile created +named `profile.` ### profiles -audible-cli make use of profiles. Each profile contains the name of the -corresponding auth file and the country code for the audible marketplace. If -you have audiobooks on multiple marketplaces, you have to create a profile for +audible-cli make use of profiles. Each profile contains the name of the +corresponding auth file and the country code for the audible marketplace. If +you have audiobooks on multiple marketplaces, you have to create a profile for each one with the same auth file. -In the main section of the config file, a primary profile is defined. -This profile is used, if no other is specified. You can call +In the main section of the config file, a primary profile is defined. +This profile is used, if no other is specified. You can call `audible -P PROFILE_NAME`, to select another profile. ### auth files -Like the config file, auth files are stored in the config dir too. If you -protected your auth file with a password call `audible -p PASSWORD`, to +Like the config file, auth files are stored in the config dir too. If you +protected your auth file with a password call `audible -p PASSWORD`, to provide the password. -If the auth file is encrypted, and you don’t provide the password, you will be -asked for it with a „hidden“ input field. +If the auth file is encrypted, and you don’t provide the password, you will be +asked for it with a „hidden“ input field. ### Config options @@ -149,12 +150,13 @@ an option must be entered with a dash. #### APP section The APP section supports the following options: + - primary_profile: The profile to use, if no other is specified -- filename_mode: When using the `download` command, a filename mode can be +- filename_mode: When using the `download` command, a filename mode can be specified here. If not present, "ascii" will be used as default. To override these option, you can provide a mode with the `--filename-mode` option of the download command. -- chapter_type: When using the `download` command, a chapter type can be specified +- chapter_type: When using the `download` command, a chapter type can be specified here. If not present, "tree" will be used as default. To override these option, you can provide a type with the `--chapter-type` option of the download command. @@ -168,54 +170,56 @@ The APP section supports the following options: ## Getting started -Use the `audible-quickstart` or `audible quickstart` command in your shell -to create your first config, profile and auth file. `audible-quickstart` +Use the `audible-quickstart` or `audible quickstart` command in your shell +to create your first config, profile and auth file. `audible-quickstart` runs on the interactive mode, so you have to answer multiple questions to finish. If you have used `audible quickstart` and want to add a second profile, you need to first create a new authfile and then update your config.toml file. So the correct order is: - 1. add a new auth file using your second account using `audible manage auth-file add` - 2. add a new profile to your config and use the second auth file using `audible manage profile add` - +1. add a new auth file using your second account using `audible manage auth-file add` +2. add a new profile to your config and use the second auth file using `audible manage profile add` ## Commands Call `audible -h` to show the help and a list of all available subcommands. You can show the help for each subcommand like so: `audible -h`. If a subcommand has another subcommands, you csn do it the same way. -At this time, there the following buildin subcommands: +At this time, there the following buildin subcommands: - `activation-bytes` - `api` - `download` - `library` - - `export` - - `list` + - `export` + - `list` - `manage` - - `auth-file` - - `add` - - `remove` - - `config` - - `edit` - - `profile` - - `add` - - `list` - - `remove` -- `quickstart` -- `wishlist` - - `export` - - `list` + - `auth-file` - `add` - `remove` + - `config` + - `edit` + - `profile` + - `add` + - `list` + - `remove` +- `quickstart` +- `wishlist` + - `export` + - `list` + - `add` + - `remove` ## Example Usage To download all of your audiobooks in the aaxc format use: + ```shell audible download --all --aaxc ``` -To download all of your audiobooks after the Date 2022-07-21 in aax format use: + +To download all of your audiobooks after the Date 2022-07-21 in aax format use: + ```shell audible download --start-date "2022-07-21" --aax --all ``` @@ -238,22 +242,22 @@ If you use the `download` subcommand with the `--all` flag there will be a huge ### Plugin Folder -If the ``AUDIBLE_PLUGIN_DIR`` environment variable is set, it uses the value -as location for the plugin dir. Otherwise, it will use a the `plugins` subdir +If the `AUDIBLE_PLUGIN_DIR` environment variable is set, it uses the value +as location for the plugin dir. Otherwise, it will use a the `plugins` subdir of the app dir. Read above how Audible-cli searches the app dir. ### Custom Commands You can provide own subcommands and execute them with `audible SUBCOMMAND`. -All plugin commands must be placed in the plugin folder. Every subcommand must -have his own file. Every file have to be named ``cmd_{SUBCOMMAND}.py``. -Each subcommand file must have a function called `cli` as entrypoint. -This function has to be decorated with ``@click.group(name="GROUP_NAME")`` or -``@click.command(name="GROUP_NAME")``. - -Relative imports in the command files doesn't work. So you have to work with -absolute imports. Please take care about this. If you have any issues with -absolute imports please add your plugin path to the `PYTHONPATH` variable or +All plugin commands must be placed in the plugin folder. Every subcommand must +have his own file. Every file have to be named `cmd_{SUBCOMMAND}.py`. +Each subcommand file must have a function called `cli` as entrypoint. +This function has to be decorated with `@click.group(name="GROUP_NAME")` or +`@click.command(name="GROUP_NAME")`. + +Relative imports in the command files doesn't work. So you have to work with +absolute imports. Please take care about this. If you have any issues with +absolute imports please add your plugin path to the `PYTHONPATH` variable or add this lines of code to the beginning of your command script: ```python @@ -262,15 +266,14 @@ import pathlib sys.path.insert(0, str(pathlib.Path(__file__).parent)) ``` -Examples can be found +Examples can be found [here](https://github.com/mkb79/audible-cli/tree/master/plugin_cmds). - ## Own Plugin Packages -If you want to develop a complete plugin package for ``audible-cli`` you can -do this on an easy way. You only need to register your sub-commands or -subgroups to an entry-point in your setup.py that is loaded by the core +If you want to develop a complete plugin package for `audible-cli` you can +do this on an easy way. You only need to register your sub-commands or +subgroups to an entry-point in your setup.py that is loaded by the core package. Example for a setup.py diff --git a/audible.spec b/audible.spec index adc375c..f5d037d 100644 --- a/audible.spec +++ b/audible.spec @@ -24,7 +24,7 @@ exe = EXE(pyz, a.scripts, a.binaries, a.zipfiles, - a.datas, + a.datas, [], name='audible', debug=False, diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..7c98e6c --- /dev/null +++ b/codecov.yml @@ -0,0 +1,9 @@ +comment: false +coverage: + status: + project: + default: + target: "10" + patch: + default: + target: "10" diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..df18f55 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,6 @@ +sphinx==8.2.3 +sphinx-autobuild==2024.9.19 +sphinx-autodoc-typehints==2.4.4 +sphinx-rtd-theme==3.0.2 +sphinxcontrib-httpdomain==1.8.1 +myst_parser==4.0.1 diff --git a/docs/source/conf.py b/docs/source/conf.py index 5c7d322..c3531f4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,6 +12,8 @@ # import os import sys + + sys.path.insert(0, os.path.abspath("../../src")) import audible_cli @@ -19,7 +21,7 @@ # -- Project information ----------------------------------------------------- project = "audible-cli" -copyright = "2020, mkb79" +copyright = "2020, mkb79" # noqa: A001 author = "mkb79" # The full version, including alpha/beta/rc tags @@ -31,15 +33,27 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. + extensions = [ - "recommonmark", + "myst_parser", "sphinx.ext.autodoc", "sphinx.ext.coverage", "sphinx.ext.napoleon", "sphinx_rtd_theme", - "sphinx.ext.autosummary" + "sphinx.ext.viewcode", + "sphinxcontrib.httpdomain", + "sphinx_autodoc_typehints", + "sphinx.ext.autosummary", ] +# Napoleon +napoleon_numpy_docstring = False + +# Autodoc Typehints +set_type_checking_flag = True +typehints_fully_qualified = False +always_document_param_types = True + master_doc = "index" # Add any paths that contain templates here, relative to this directory. diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..c7f2a2c --- /dev/null +++ b/noxfile.py @@ -0,0 +1,247 @@ +"""Nox sessions.""" + +import os +import shlex +import shutil +import sys +from pathlib import Path +from textwrap import dedent + +import nox +from nox_uv import session + + +nox.needs_version = ">= 2023.04.22" +nox.options.default_venv_backend = "uv" +nox.options.error_on_external_run = True +nox.options.sessions = ( + "pre-commit", + "safety", + # "mypy", + "tests", + "typeguard", + "xdoctest", + "docs-build", +) + +PACKAGE = "audible_cli" +PROJECT = nox.project.load_toml("pyproject.toml") +PYTHON_VERSIONS = nox.project.python_versions(PROJECT) +DEFAULT_PYTHON_VERSION = PYTHON_VERSIONS[-1] + +# Group declaration +DEV_GROUP = "dev" +DOCS_GROUP = "docs" +MYPY_GROUP = "mypy" +PRE_COMMIT_GROUP = "pre-commit" +SAFETY_GROUP = "safety" +TESTS_GROUP = "tests" +COVERAGE_GROUP = TESTS_GROUP +TYPEGUARD_GROUP = "typeguard" +XDOCTEST_GROUP = "xdocs" + + +def activate_virtualenv_in_precommit_hooks(s: nox.Session) -> None: + """Activate virtualenv in hooks installed by pre-commit. + + This function patches git hooks installed by pre-commit to activate the + session's virtual environment. This allows pre-commit to locate hooks in + that environment when invoked from git. + + Args: + s: The Session object. + """ + assert s.bin is not None # noqa: S101 + + # Only patch hooks containing a reference to this session's bindir. Support + # quoting rules for Python and bash, but strip the outermost quotes so we + # can detect paths within the bindir, like /python. + bindirs = [ + bindir[1:-1] if bindir[0] in "'\"" else bindir + for bindir in (repr(s.bin), shlex.quote(s.bin)) + ] + + virtualenv = s.env.get("VIRTUAL_ENV") + if virtualenv is None: + return + + headers = { + # pre-commit < 2.16.0 + "python": f"""\ + import os + os.environ["VIRTUAL_ENV"] = {virtualenv!r} + os.environ["PATH"] = os.pathsep.join(( + {s.bin!r}, + os.environ.get("PATH", ""), + )) + """, + # pre-commit >= 2.16.0 + "bash": f"""\ + VIRTUAL_ENV={shlex.quote(virtualenv)} + PATH={shlex.quote(s.bin)}"{os.pathsep}$PATH" + """, + # pre-commit >= 2.17.0 on Windows forces sh shebang + "/bin/sh": f"""\ + VIRTUAL_ENV={shlex.quote(virtualenv)} + PATH={shlex.quote(s.bin)}"{os.pathsep}$PATH" + """, + } + + hookdir = Path(".git") / "hooks" + if not hookdir.is_dir(): + return + + for hook in hookdir.iterdir(): + if hook.name.endswith(".sample") or not hook.is_file(): + continue + + if not hook.read_bytes().startswith(b"#!"): + continue + + text = hook.read_text() + + if not any( + (Path("A") == Path("a") and bindir.lower() in text.lower()) + or bindir in text + for bindir in bindirs + ): + continue + + lines = text.splitlines() + + for executable, header in headers.items(): + if executable in lines[0].lower(): + lines.insert(1, dedent(header)) + hook.write_text("\n".join(lines)) + break + + +@session(name="pre-commit", python=DEFAULT_PYTHON_VERSION, uv_groups=[PRE_COMMIT_GROUP]) +def precommit(s: nox.Session) -> None: + """Lint using pre-commit.""" + default_args = [ + "run", + "--all-files", + "--hook-stage=manual", + "--show-diff-on-failure", + ] + args = s.posargs or default_args + + s.run("pre-commit", *args) + if args and args[0] == "install": + activate_virtualenv_in_precommit_hooks(s) + + +@session(python=DEFAULT_PYTHON_VERSION, uv_groups=[SAFETY_GROUP]) +def safety(s: nox.Session) -> None: + """Scan dependencies for insecure packages.""" + # Use uv to generate requirements.txt + requirement_path = f"{s.virtualenv.location}/requirements.txt" + s.run_always( + "uv", + "export", + "--no-hashes", + "--format", + "requirements-txt", + "-o", + requirement_path, + ) + s.run( + "safety", + "check", + "--full-report", + f"--file={requirement_path}", + ) + + +@session(python=PYTHON_VERSIONS, uv_groups=[MYPY_GROUP]) +def mypy(s: nox.Session) -> None: + """Type-check using mypy.""" + default_args = [ + "src/audible_cli", + "tests", + "docs/source/conf.py", + "plugin_cmds", + "utils", + ] + args = s.posargs or default_args + + s.run("mypy", *args) + if not s.posargs: + s.run("mypy", f"--python-executable={sys.executable}", "noxfile.py") + + +@session(python=PYTHON_VERSIONS, uv_groups=[TESTS_GROUP]) +def tests(s: nox.Session) -> None: + """Run the test suite.""" + try: + s.run( + "coverage", + "run", + "--parallel", + "-m", + "pytest", + *s.posargs, + ) + finally: + if s.interactive: + s.notify("coverage", posargs=[]) + + +@session(python=DEFAULT_PYTHON_VERSION, uv_groups=[COVERAGE_GROUP]) +def coverage(s: nox.Session) -> None: + """Produce the coverage report.""" + default_args = ["report"] + args = s.posargs or default_args + if not s.posargs and any(Path().glob(".coverage.*")): + s.run("coverage", "combine") + + s.run("coverage", *args) + + +@session(python=DEFAULT_PYTHON_VERSION, uv_groups=[TYPEGUARD_GROUP]) +def typeguard(s: nox.Session) -> None: + """Runtime type checking using Typeguard.""" + s.run("pytest", f"--typeguard-packages={PACKAGE}", *s.posargs) + + +@session(python=PYTHON_VERSIONS, uv_groups=[XDOCTEST_GROUP]) +def xdoctest(s: nox.Session) -> None: + """Run examples with xdoctest.""" + if s.posargs: + args = [PACKAGE, *s.posargs] + else: + args = [f"--modname={PACKAGE}", "--command=all"] + if "FORCE_COLOR" in os.environ: + args.append("--colored=1") + + s.run("python", "-m", "xdoctest", *args) + + +@session(name="docs-build", python=DEFAULT_PYTHON_VERSION, uv_groups=[DOCS_GROUP]) +def docs_build(s: nox.Session) -> None: + """Build the documentation.""" + default_args = ["docs/source", "docs/_build"] + args = s.posargs or default_args + + if not s.posargs and "FORCE_COLOR" in os.environ: + args.insert(0, "--color") + + build_dir = Path("docs", "_build") + if build_dir.exists(): + shutil.rmtree(build_dir) + + s.run("sphinx-build", *args) + + +@session(python=DEFAULT_PYTHON_VERSION, uv_groups=[DOCS_GROUP]) +def docs(s: nox.Session) -> None: + """Build and serve the documentation with live reloading on file changes.""" + default_args = ["--open-browser", "docs/source", "docs/_build"] + args = s.posargs or default_args + + build_dir = Path("docs", "_build") + if build_dir.exists(): + shutil.rmtree(build_dir) + + s.run("sphinx-autobuild", *args) diff --git a/plugin_cmds/README.md b/plugin_cmds/README.md index c56154f..1d3f90c 100644 --- a/plugin_cmds/README.md +++ b/plugin_cmds/README.md @@ -13,7 +13,7 @@ This enables you to "replace" build-in commands very easy. ## Location -Audible-cli expected plugin commands in the `plugins` subdir of the app dir. You can provide a custom dir with the ``AUDIBLE_PLUGIN_DIR`` environment variable. +Audible-cli expected plugin commands in the `plugins` subdir of the app dir. You can provide a custom dir with the `AUDIBLE_PLUGIN_DIR` environment variable. ## Commands in this folder @@ -22,20 +22,20 @@ To use commands in these folder simply copy them to the plugin folder. ## Custom Commands You can provide own subcommands and execute them with `audible SUBCOMMAND`. -All plugin commands must be placed in the plugin folder. Every subcommand must -have his own file. Every file have to be named ``cmd_{SUBCOMMAND}.py``. -Each subcommand file must have a function called `cli` as entrypoint. -This function have to be decorated with ``@click.group(name="GROUP_NAME")`` or -``@click.command(name="GROUP_NAME")``. +All plugin commands must be placed in the plugin folder. Every subcommand must +have his own file. Every file have to be named `cmd_{SUBCOMMAND}.py`. +Each subcommand file must have a function called `cli` as entrypoint. +This function have to be decorated with `@click.group(name="GROUP_NAME")` or +`@click.command(name="GROUP_NAME")`. -Relative imports in the command files doesn't work. So you have to work with +Relative imports in the command files doesn't work. So you have to work with absolute imports. Please take care about this. ## Own Plugin Packages -If you want to develop a complete plugin package for ``audible-cli`` you can -do this on an easy way. You only need to register your sub-commands or -sub-groups to an entry-point in your setup.py that is loaded by the core +If you want to develop a complete plugin package for `audible-cli` you can +do this on an easy way. You only need to register your sub-commands or +sub-groups to an entry-point in your setup.py that is loaded by the core package. Example for a setup.py diff --git a/plugin_cmds/cmd_decrypt.py b/plugin_cmds/cmd_decrypt.py index 8a09304..7ea4709 100644 --- a/plugin_cmds/cmd_decrypt.py +++ b/plugin_cmds/cmd_decrypt.py @@ -8,12 +8,11 @@ Needs at least ffmpeg 4.4 """ - import json import operator import pathlib import re -import subprocess # noqa: S404 +import subprocess import tempfile import typing as t from enum import Enum @@ -38,7 +37,7 @@ class SupportedFiles(Enum): @classmethod def get_supported_list(cls): - return list(set(item.value for item in cls)) + return list({item.value for item in cls}) @classmethod def is_supported_suffix(cls, value): @@ -50,9 +49,8 @@ def is_supported_file(cls, value): def _get_input_files( - files: t.Union[t.Tuple[str], t.List[str]], - recursive: bool = True -) -> t.List[pathlib.Path]: + files: tuple[str] | list[str], recursive: bool = True +) -> list[pathlib.Path]: filenames = [] for filename in files: # if the shell does not do filename globbing @@ -60,7 +58,7 @@ def _get_input_files( if ( len(expanded) == 0 - and '*' not in filename + and "*" not in filename and not SupportedFiles.is_supported_file(filename) ): raise click.BadParameter("{filename}: file not found or supported.") @@ -68,13 +66,13 @@ def _get_input_files( expanded_filter = filter( lambda x: SupportedFiles.is_supported_file(x), expanded ) - expanded = list(map(lambda x: pathlib.Path(x).resolve(), expanded_filter)) + expanded = [pathlib.Path(x).resolve() for x in expanded_filter] filenames.extend(expanded) return filenames -def recursive_lookup_dict(key: str, dictionary: t.Dict[str, t.Any]) -> t.Any: +def recursive_lookup_dict(key: str, dictionary: dict[str, t.Any]) -> t.Any: if key in dictionary: return dictionary[key] for value in dictionary.values(): @@ -85,7 +83,7 @@ def recursive_lookup_dict(key: str, dictionary: t.Dict[str, t.Any]) -> t.Any: continue else: return item - + raise KeyError @@ -104,12 +102,12 @@ def get_aaxc_credentials(voucher_file: pathlib.Path): class ApiChapterInfo: - def __init__(self, content_metadata: t.Dict[str, t.Any]) -> None: + def __init__(self, content_metadata: dict[str, t.Any]) -> None: chapter_info = self._parse(content_metadata) self._chapter_info = chapter_info @classmethod - def from_file(cls, file: t.Union[pathlib.Path, str]) -> "ApiChapterInfo": + def from_file(cls, file: pathlib.Path | str) -> "ApiChapterInfo": file = pathlib.Path(file) if not file.exists() or not file.is_file(): raise ChapterError(f"Chapter file {file} not found.") @@ -118,7 +116,7 @@ def from_file(cls, file: t.Union[pathlib.Path, str]) -> "ApiChapterInfo": return cls(content_json) @staticmethod - def _parse(content_metadata: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: + def _parse(content_metadata: dict[str, t.Any]) -> dict[str, t.Any]: if "chapters" in content_metadata: return content_metadata @@ -135,7 +133,7 @@ def extract_chapters(initial, current): if "chapters" in current: return initial + [current] + current["chapters"] else: - return initial + [current] + return [*initial, current] chapters = list( reduce( @@ -167,17 +165,17 @@ def is_accurate(self): def _separate_intro_outro(self, chapters): echo("Separate Audible Brand Intro and Outro to own Chapter.") chapters.sort(key=operator.itemgetter("start_offset_ms")) - + first = chapters[0] intro_dur_ms = self.get_intro_duration_ms() first["start_offset_ms"] = intro_dur_ms first["start_offset_sec"] = round(first["start_offset_ms"] / 1000) first["length_ms"] -= intro_dur_ms - + last = chapters[-1] outro_dur_ms = self.get_outro_duration_ms() last["length_ms"] -= outro_dur_ms - + chapters.append( { "length_ms": intro_dur_ms, @@ -197,13 +195,13 @@ def _separate_intro_outro(self, chapters): } ) chapters.sort(key=operator.itemgetter("start_offset_ms")) - + return chapters def _remove_intro_outro(self, chapters): echo("Delete Audible Brand Intro and Outro.") chapters.sort(key=operator.itemgetter("start_offset_ms")) - + intro_dur_ms = self.get_intro_duration_ms() outro_dur_ms = self.get_outro_duration_ms() @@ -216,14 +214,15 @@ def _remove_intro_outro(self, chapters): last = chapters[-1] last["length_ms"] -= outro_dur_ms - + return chapters + class FFMeta: SECTION = re.compile(r"\[(?P
[^]]+)\]") OPTION = re.compile(r"(?P