Compare commits

...

17 commits

Author SHA1 Message Date
Kai Norman Clasen
b3adb9a331
fix: plugin decrypt rebuild-chapters without -c (#205) 2024-05-02 21:49:03 +02:00
mkb79
1a1f25bc2d Refactor error messages in cmd_decrypt.py
The changes aim to enhance the syntax and readability of the error messages in cmd_decrypt.py. The `raise` method syntax was corrected for better readability and adjustments were made to option usage error messages to improve clarity for the user.
2024-04-16 20:41:09 +02:00
mkb79
6165e95f40 Optimize error handling in cmd_download.py
The current change replaces generic `click.Abort()` statements with `click.BadOptionUsage()` to provide clearer error messages. These updates occur when there are conflicts in options chosen (e.g. choosing both `--all` and `--asin` or `--title`), no download option is chosen, or if both *ignore-podcasts* and *resolve-podcasts* options are selected together. It is now highlighted when the start date is after the end date in the parameters as well.
2024-04-16 20:34:17 +02:00
mkb79
009a7e69ec fix: Raise click.Abort() in audible_cli.decorators
In the audible_cli decorators file, the click.Abort() function previously was not raising an exception in case an error occurred. This has been modified to correctly raise the exception using the "raise" keyword, thereby handling exceptions accurately.
2024-04-16 20:33:44 +02:00
Isaac Lyons
efcad39b8e
refactor: fix typo from 'podcats' to 'podcasts' (#141)
* fix typo from 'podcats' to 'podcasts'

* missed on first check

* Deprecate resolve_podcats method

The resolve_podcats method is marked as deprecated, and a warning message is added to inform users. It is recommended to use the resolve_podcasts method. This commit helps to phase out the resolve_podcats method, aiming to eliminate any spelling errors in method naming.

* Fix typo in variable name

A typo in the variable name 'resolve_podcats' was corrected to 'resolve_podcasts'. This ensures that the conditional check operates as designed, without causing errors due to referencing a non-existent variable.

* Fix typos and improve readability in CHANGELOG.md

The commit fixes several typos including changing `resolve_podcats` to `resolve_podcasts` and fixing spelling of 'titles'. In addition, it formalizes the formatting of terms and phrases by using backticks around them for more readability. Furthermore, a missing comma has also been added to enhance sentence clarity.

---------

Co-authored-by: mkb79 <mkb79@hackitall.de>
2024-04-16 20:10:17 +02:00
mkb79
705d6f0959
fix: error Invalid cross-device (#204)
* refactor: resume file location match target file

* update CHANGELOG.md
2024-04-16 19:39:53 +02:00
dependabot[bot]
3a6db75e0d
build(deps): Update httpx requirement from <0.26.0,>=0.23.3 to >=0.23.3,<0.28.0 (#185)
* Update httpx requirement from <0.26.0,>=0.23.3 to >=0.23.3,<0.28.0

Updates the requirements on [httpx](https://github.com/encode/httpx) to permit the latest version.
- [Release notes](https://github.com/encode/httpx/releases)
- [Changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/httpx/compare/0.23.3...0.27.0)

---
updated-dependencies:
- dependency-name: httpx
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update httpx version range in CHANGELOG

The CHANGELOG.md has been updated to reflect the changes in the httpx version range. This update includes a specification that the version should be >=0.23.3 and <0.28.0.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: mkb79 <mkb79@hackitall.de>
2024-04-16 18:45:34 +02:00
vwkd
22e388dfaa
Add -c, --remove-intro-outro option to decrypt (#171) 2024-04-08 23:31:34 +02:00
Paul
0a9b2f9c7d
fix: decrypt command filename escape (#202) 2024-04-08 22:58:07 +02:00
mkb79
8dc8739f66
Rework flat chapter option (#201)
* Capitalize chapter type in get_content_metadata function

The get_content_metadata function in audible_cli's models.py file has been updated to capitalize the chapter type parameter. This ensures any case variations in the parameter will pass the assert condition, improving the handling of chapter types and method consistency.

* Update chapter type handling in cmd_download.py

Updated the cmd_download.py file to add a "config" choice to the chapter type and move the declaration of chapter_type to a more appropriate location. Also, the logging message has been updated to include the selected chapter type. This enhances traceability and ensures chapter type is handled consistently.

Flat chapters can be enabled by default in the config file. In the APP or profile section must be a setting `chapter_type = "flat"`.

* Update README with new `chapter_type` option

Updated README.md to include a new `chapter_type` field in both the APP and profile sections. This new field allows users to specify a chapter type for the `download` command with the `--chapter-type` option. If not provided, it defaults to "tree". This change aims to increase customizability for users.

* Update CHANGELOG to include new config file option

The CHANGELOG has been updated to reflect the addition of the ability to set a default chapter type in the config file. This allows the user to specify whether chapters should be downloaded as `flat` or `tree` type without having to state it each time a download command is given.

* Update audible-cli version

Version number has been updated from "0.3.2b2" to "0.3.2b3" in the _version.py file. This indicates a new build of the code that may include minor enhancements or bug fixes.
2024-04-01 13:20:06 +02:00
mkb79
7f01949413 Update audible-cli version
The version of the audible-cli package has been updated from "0.3.2b1" to "0.3.2b2". This version upgrade reflects changes made to the functionality or features of the package.
2024-03-31 11:57:09 +02:00
mkb79
48946ab8b8
feat: Add option to download flat chapters (#200)
* Adjust get_content_metadata method in audible_cli model

Updated the get_content_metadata method to accept a new parameter 'chapter_type'. This adjustment also enables passing additional request parameters via **request_kwargs, allowing for more flexible HTTP requests.

* Add chapter type parameter to download chapters function

The download_chapters function has been updated to include 'chapter_type' as a parameter to support different types of chapters. It also modifies the get_content_metadata method to handle the new argument. This makes API interaction more adaptive by facilitating more varied request parameters.

* Update CHANGELOG.md
2024-03-31 11:55:23 +02:00
mkb79
24c57ec73e Update audible-cli version
The version of audible-cli has been updated from 0.3.1 to 0.3.2b1. This update may involve bugs fixes, improvements, or new features in the audible-cli package.
2024-03-24 23:01:53 +01:00
mkb79
513b97a3cc
refactor: downloader head request (#196)
* Replace HEAD request with faster GET request in downloader

Switched from a HEAD to a GET request without loading the body in the get_head_response method. This change improves the program's speed, as the HEAD request was taking considerably longer than a GET request to the same URI.

* Update CHANGELOG.md
2024-03-24 22:58:59 +01:00
mkb79
45bd5820ad
refactor: rework skip podcast options of download command (#195)
* Improve podcast ignore feature in downloader

Added conditional code in the cmd_download.py file to ignore any items that are parent podcasts if the ignore_podcasts flag is set. This allows users to choose whether or not they want to download podcasts.

* Prevent mixing resolve and ignore podcasts options

Added a check in cmd_download.py to prevent combining both "resolve_podcasts" and "ignore_podcasts" options. A flag was added that aborts the process if both options are activated at once. Additionally, enhanced the feature to ignore parent podcasts during download, if "ignore_podcasts" flag is set.

* Update CHANGELOG.md
2024-03-24 22:53:41 +01:00
mkb79
707a4b9192
Release v0.3.1 2024-03-19 23:25:15 +01:00
mkb79
d6ce4041d8
fix: Fix TypeError on some Python versions (#191) 2024-03-19 23:21:24 +01:00
11 changed files with 246 additions and 71 deletions

View file

@ -6,7 +6,36 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
-
### Bugfix
- Fixing `[Errno 18] Invalid cross-device link` when downloading files using the `--output-dir` option. This error is fixed by creating the resume file on the same location as the target file.
### 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
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
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
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
request.
- Update httpx version range to >=0.23.3 and <0.28.0.
- fix typo from `resolve_podcats` to `resolve_podcasts`
- `models.Library.resolve_podcats` is now deprecated and will be removed in a future version
## [0.3.1] - 2024-03-19
### Bugfix
- fix a `TypeError` on some Python versions when calling `importlib.metadata.entry_points` with group argument
## [0.3.0] - 2024-03-19
@ -91,7 +120,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- by default a licenserequest (voucher) will not include chapter information by default
- moved licenserequest part from `models.LibraryItem.get_aaxc_url` to its own `models.LibraryItem.get_license` function
- allow book tiltes with hyphens (#96)
- allow book titles with hyphens (#96)
- if there is no title fallback to an empty string (#98)
- reduce `response_groups` for the download command to speed up fetching the library (#109)
@ -99,7 +128,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `Extreme` quality is not supported by the Audible API anymore (#107)
- download command continued execution after error (#104)
- Currently paths with dots will break the decryption (#97)
- Currently, paths with dots will break the decryption (#97)
- `models.Library.from_api_full_sync` called `models.Library.from_api` with incorrect keyword arguments
### Misc
@ -162,7 +191,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added
- the `--version` option now checks if an update for `audible-cli` is available
- build macOS releases in onedir mode
- build macOS releases in `onedir` mode
### Bugfix

View file

@ -154,7 +154,11 @@ 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
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
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
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.
#### Profile section
@ -162,6 +166,7 @@ The APP section supports the following options:
- auth_file: The auth file for this profile
- country_code: The marketplace for this profile
- filename_mode: See APP section above. Will override the option in APP section.
- chapter_type: See APP section above. Will override the option in APP section.
## Getting started

View file

@ -19,7 +19,6 @@ import typing as t
from enum import Enum
from functools import reduce
from glob import glob
from shlex import quote
from shutil import which
import click
@ -64,7 +63,7 @@ def _get_input_files(
and '*' not in filename
and not SupportedFiles.is_supported_file(filename)
):
raise(click.BadParameter("{filename}: file not found or supported."))
raise click.BadParameter("{filename}: file not found or supported.")
expanded_filter = filter(
lambda x: SupportedFiles.is_supported_file(x), expanded
@ -131,7 +130,7 @@ class ApiChapterInfo:
def count_chapters(self):
return len(self.get_chapters())
def get_chapters(self, separate_intro_outro=False):
def get_chapters(self, separate_intro_outro=False, remove_intro_outro=False):
def extract_chapters(initial, current):
if "chapters" in current:
return initial + [current] + current["chapters"]
@ -148,6 +147,8 @@ class ApiChapterInfo:
if separate_intro_outro:
return self._separate_intro_outro(chapters)
elif remove_intro_outro:
return self._remove_intro_outro(chapters)
return chapters
@ -199,6 +200,24 @@ class ApiChapterInfo:
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()
first = chapters[0]
first["length_ms"] -= intro_dur_ms
for chapter in chapters[1:]:
chapter["start_offset_ms"] -= intro_dur_ms
chapter["start_offset_sec"] -= round(chapter["start_offset_ms"] / 1000)
last = chapters[-1]
last["length_ms"] -= outro_dur_ms
return chapters
class FFMeta:
SECTION = re.compile(r"\[(?P<header>[^]]+)\]")
@ -272,7 +291,8 @@ class FFMeta:
self,
chapter_info: ApiChapterInfo,
force_rebuild_chapters: bool = False,
separate_intro_outro: bool = False
separate_intro_outro: bool = False,
remove_intro_outro: bool = False
) -> None:
if not chapter_info.is_accurate():
echo("Metadata from API is not accurate. Skip.")
@ -286,7 +306,7 @@ class FFMeta:
echo(f"Found {chapter_info.count_chapters()} chapters to prepare.")
api_chapters = chapter_info.get_chapters(separate_intro_outro)
api_chapters = chapter_info.get_chapters(separate_intro_outro, remove_intro_outro)
num_chap = 0
new_chapters = {}
@ -301,6 +321,20 @@ class FFMeta:
"title": chapter["title"],
}
self._ffmeta_parsed["CHAPTER"] = new_chapters
def get_start_end_without_intro_outro(
self,
chapter_info: ApiChapterInfo,
):
intro_dur_ms = chapter_info.get_intro_duration_ms()
outro_dur_ms = chapter_info.get_outro_duration_ms()
total_runtime_ms = chapter_info.get_runtime_length_ms()
start_new = intro_dur_ms
duration_new = total_runtime_ms - intro_dur_ms - outro_dur_ms
return start_new, duration_new
def _get_voucher_filename(file: pathlib.Path) -> pathlib.Path:
@ -329,7 +363,8 @@ class FfmpegFileDecrypter:
rebuild_chapters: bool,
force_rebuild_chapters: bool,
skip_rebuild_chapters: bool,
separate_intro_outro: bool
separate_intro_outro: bool,
remove_intro_outro: bool
) -> None:
file_type = SupportedFiles(file.suffix)
@ -354,6 +389,7 @@ class FfmpegFileDecrypter:
self._force_rebuild_chapters = force_rebuild_chapters
self._skip_rebuild_chapters = skip_rebuild_chapters
self._separate_intro_outro = separate_intro_outro
self._remove_intro_outro = remove_intro_outro
self._api_chapter: t.Optional[ApiChapterInfo] = None
self._ffmeta: t.Optional[FFMeta] = None
self._is_rebuilded: bool = False
@ -385,20 +421,20 @@ class FfmpegFileDecrypter:
key, iv = self._credentials
credentials_cmd = [
"-audible_key",
quote(key),
key,
"-audible_iv",
quote(iv),
iv,
]
else:
credentials_cmd = [
"-activation_bytes",
quote(self._credentials),
self._credentials,
]
base_cmd.extend(credentials_cmd)
extract_cmd = [
"-i",
quote(str(self._source)),
str(self._source),
"-f",
"ffmetadata",
str(metafile),
@ -413,7 +449,7 @@ class FfmpegFileDecrypter:
def rebuild_chapters(self) -> None:
if not self._is_rebuilded:
self.ffmeta.update_chapters_from_chapter_info(
self.api_chapter, self._force_rebuild_chapters, self._separate_intro_outro
self.api_chapter, self._force_rebuild_chapters, self._separate_intro_outro, self._remove_intro_outro
)
self._is_rebuilded = True
@ -440,22 +476,16 @@ class FfmpegFileDecrypter:
key, iv = self._credentials
credentials_cmd = [
"-audible_key",
quote(key),
key,
"-audible_iv",
quote(iv),
iv,
]
else:
credentials_cmd = [
"-activation_bytes",
quote(self._credentials),
self._credentials,
]
base_cmd.extend(credentials_cmd)
base_cmd.extend(
[
"-i",
quote(str(self._source)),
]
)
if self._rebuild_chapters:
metafile = _get_ffmeta_file(self._source, self._tempdir)
@ -468,22 +498,51 @@ class FfmpegFileDecrypter:
else:
raise
else:
base_cmd.extend(
[
"-i",
quote(str(metafile)),
"-map_metadata",
"0",
"-map_chapters",
"1",
]
)
if self._remove_intro_outro:
start_new, duration_new = self.ffmeta.get_start_end_without_intro_outro(self.api_chapter)
base_cmd.extend(
[
"-ss",
f"{start_new}ms",
"-t",
f"{duration_new}ms",
"-i",
str(self._source),
"-i",
str(metafile),
"-map_metadata",
"0",
"-map_chapters",
"1",
]
)
else:
base_cmd.extend(
[
"-i",
str(self._source),
"-i",
str(metafile),
"-map_metadata",
"0",
"-map_chapters",
"1",
]
)
else:
base_cmd.extend(
[
"-i",
str(self._source),
]
)
base_cmd.extend(
[
"-c",
"copy",
quote(str(outfile)),
str(outfile),
]
)
@ -544,6 +603,15 @@ class FfmpegFileDecrypter:
"Only use with `--rebuild-chapters`."
),
)
@click.option(
"--remove-intro-outro",
"-c",
is_flag=True,
help=(
"Remove Audible Brand Intro and Outro. "
"Only use with `--rebuild-chapters`."
),
)
@pass_session
def cli(
session,
@ -555,6 +623,7 @@ def cli(
force_rebuild_chapters: bool,
skip_rebuild_chapters: bool,
separate_intro_outro: bool,
remove_intro_outro: bool,
):
"""Decrypt audiobooks downloaded with audible-cli.
@ -568,21 +637,30 @@ def cli(
ctx = click.get_current_context()
ctx.fail("ffmpeg not found")
if (force_rebuild_chapters or skip_rebuild_chapters or separate_intro_outro) and not rebuild_chapters:
if (force_rebuild_chapters or skip_rebuild_chapters or separate_intro_outro or remove_intro_outro) and not rebuild_chapters:
raise click.BadOptionUsage(
"`--force-rebuild-chapters`, `--skip-rebuild-chapters` and `--separate-intro-outro` can "
"only be used together with `--rebuild-chapters`"
"",
"`--force-rebuild-chapters`, `--skip-rebuild-chapters`, `--separate-intro-outro` "
"and `--remove-intro-outro` can only be used together with `--rebuild-chapters`"
)
if force_rebuild_chapters and skip_rebuild_chapters:
raise click.BadOptionUsage(
"",
"`--force-rebuild-chapters` and `--skip-rebuild-chapters` can "
"not be used together"
)
if separate_intro_outro and remove_intro_outro:
raise click.BadOptionUsage(
"",
"`--separate-intro-outro` and `--remove-intro-outro` can not be used together"
)
if all_:
if files:
raise click.BadOptionUsage(
"",
"If using `--all`, no FILES arguments can be used."
)
files = [f"*{suffix}" for suffix in SupportedFiles.get_supported_list()]
@ -599,6 +677,7 @@ def cli(
rebuild_chapters=rebuild_chapters,
force_rebuild_chapters=force_rebuild_chapters,
skip_rebuild_chapters=skip_rebuild_chapters,
separate_intro_outro=separate_intro_outro
separate_intro_outro=separate_intro_outro,
remove_intro_outro=remove_intro_outro
)
decrypter.run()

View file

@ -49,7 +49,7 @@ setup(
"audible>=0.8.2",
"click>=8",
"colorama; platform_system=='Windows'",
"httpx>=0.23.3,<0.26.0",
"httpx>=0.23.3,<0.28.0",
"packaging",
"Pillow",
"tabulate",

View file

@ -1,7 +1,7 @@
__title__ = "audible-cli"
__description__ = "Command line interface (cli) for the audible package."
__url__ = "https://github.com/mkb79/audible-cli"
__version__ = "0.3.0"
__version__ = "0.3.2b3"
__author__ = "mkb79"
__author_email__ = "mkb79@hackitall.de"
__license__ = "AGPL"

View file

@ -17,9 +17,9 @@ from .exceptions import AudibleCliException
from ._logging import click_basic_config
from . import plugins
try:
if sys.version_info >= (3, 10):
from importlib.metadata import entry_points
except ImportError: # Python < 3.10 (backport)
else: # Python < 3.10 (backport)
from importlib_metadata import entry_points

View file

@ -203,7 +203,7 @@ async def download_pdf(
async def download_chapters(
output_dir, base_filename, item, quality, overwrite_existing
output_dir, base_filename, item, quality, overwrite_existing, chapter_type
):
if not output_dir.is_dir():
raise DirectoryDoesNotExists(output_dir)
@ -217,7 +217,7 @@ async def download_chapters(
return True
try:
metadata = await item.get_content_metadata(quality)
metadata = await item.get_content_metadata(quality, chapter_type=chapter_type)
except NotFoundError:
logger.info(
f"No chapters found for {item.full_title}."
@ -226,7 +226,7 @@ async def download_chapters(
metadata = json.dumps(metadata, indent=4)
async with aiofiles.open(file, "w") as f:
await f.write(metadata)
logger.info(f"Chapter file saved to {file}.")
logger.info(f"Chapter file saved in style '{chapter_type.upper()}' to {file}.")
counter.count_chapter()
@ -291,6 +291,7 @@ async def _add_audioparts_to_queue(
get_pdf=None,
get_annotation=None,
get_chapters=None,
chapter_type=None,
get_aax=get_aax,
get_aaxc=get_aaxc,
client=client,
@ -521,6 +522,7 @@ def queue_job(
filename_mode,
item,
cover_sizes,
chapter_type,
quality,
overwrite_existing,
aax_fallback
@ -558,7 +560,8 @@ def queue_job(
"base_filename": base_filename,
"item": item,
"quality": quality,
"overwrite_existing": overwrite_existing
"overwrite_existing": overwrite_existing,
"chapter_type": chapter_type
}
QUEUE.put_nowait((cmd, kwargs))
@ -686,7 +689,13 @@ def display_counter():
@click.option(
"--chapter",
is_flag=True,
help="saves chapter metadata as JSON file"
help="Saves chapter metadata as JSON file."
)
@click.option(
"--chapter-type",
default="config",
type=click.Choice(["Flat", "Tree", "config"], case_sensitive=False),
help="The chapter type."
)
@click.option(
"--annotation",
@ -749,8 +758,10 @@ async def cli(session, api_client, **params):
asins = params.get("asin")
titles = params.get("title")
if get_all and (asins or titles):
logger.error("Do not mix *asin* or *title* option with *all* option.")
click.Abort()
raise click.BadOptionUsage(
"--all",
"`--all` can not be used together with `--asin` or `--title`"
)
# what to download
get_aax = params.get("aax")
@ -771,8 +782,10 @@ async def cli(session, api_client, **params):
if not any(
[get_aax, get_aaxc, get_annotation, get_chapters, get_cover, get_pdf]
):
logger.error("Please select an option what you want download.")
raise click.Abort()
raise click.BadOptionUsage(
"",
"Please select an option what you want download."
)
# additional options
sim_jobs = params.get("jobs")
@ -781,15 +794,22 @@ async def cli(session, api_client, **params):
overwrite_existing = params.get("overwrite")
ignore_errors = params.get("ignore_errors")
no_confirm = params.get("no_confirm")
resolve_podcats = params.get("resolve_podcasts")
resolve_podcasts = params.get("resolve_podcasts")
ignore_podcasts = params.get("ignore_podcasts")
if all([resolve_podcasts, ignore_podcasts]):
raise click.BadOptionUsage(
"",
"Do not mix *ignore-podcasts* with *resolve-podcasts* option."
)
bunch_size = session.params.get("bunch_size")
start_date = session.params.get("start_date")
end_date = session.params.get("end_date")
if all([start_date, end_date]) and start_date > end_date:
logger.error("start date must be before or equal the end date")
raise click.Abort()
raise click.BadOptionUsage(
"",
"start date must be before or equal the end date"
)
if start_date is not None:
logger.info(
@ -800,6 +820,11 @@ async def cli(session, api_client, **params):
f"Selected end date: {end_date.strftime('%Y-%m-%dT%H:%M:%S.%fZ')}"
)
chapter_type = params.get("chapter_type")
if chapter_type == "config":
chapter_type = session.config.get_profile_option(
session.selected_profile, "chapter_type") or "Tree"
filename_mode = params.get("filename_mode")
if filename_mode == "config":
filename_mode = session.config.get_profile_option(
@ -819,8 +844,9 @@ async def cli(session, api_client, **params):
status="Active",
)
if resolve_podcats:
await library.resolve_podcats(start_date=start_date, end_date=end_date)
if resolve_podcasts:
await library.resolve_podcasts(start_date=start_date, end_date=end_date)
[library.data.remove(i) for i in library if i.is_parent_podcast()]
# collect jobs
jobs = []
@ -837,7 +863,7 @@ async def cli(session, api_client, **params):
else:
if not ignore_errors:
logger.error(f"Asin {asin} not found in library.")
click.Abort()
raise click.Abort()
logger.error(
f"Skip asin {asin}: Not found in library"
)
@ -878,7 +904,10 @@ async def cli(session, api_client, **params):
items = [item]
odir = pathlib.Path(output_dir)
if not ignore_podcasts and item.is_parent_podcast():
if item.is_parent_podcast():
if ignore_podcasts:
continue
items.remove(item)
if item._children is None:
await item.get_child_items(
@ -907,6 +936,7 @@ async def cli(session, api_client, **params):
filename_mode=filename_mode,
item=item,
cover_sizes=cover_sizes,
chapter_type=chapter_type,
quality=quality,
overwrite_existing=overwrite_existing,
aax_fallback=aax_fallback

View file

@ -45,7 +45,7 @@ async def _get_library(session, client, resolve_podcasts):
)
if resolve_podcasts:
await library.resolve_podcats(start_date=start_date, end_date=end_date)
await library.resolve_podcasts(start_date=start_date, end_date=end_date)
return library

View file

@ -95,7 +95,7 @@ def version_option(func=None, **kwargs):
response.raise_for_status()
except Exception as e:
logger.error(e)
click.Abort()
raise click.Abort()
content = response.json()

View file

@ -322,18 +322,31 @@ class Downloader:
async def get_head_response(self, force_recreate: bool = False) -> ResponseInfo:
if self._head_request is None or force_recreate:
head_response = await self._client.head(
self._source, headers=self._additional_headers, follow_redirects=True,
)
self._head_request = ResponseInfo(head_response)
# switched from HEAD to GET request without loading the body
# HEAD request to cds.audible.de will responded in 1 - 2 minutes
# a GET request to the same URI will take ~4-6 seconds
async with self._client.stream(
"GET", self._source, headers=self._additional_headers,
follow_redirects=True,
) as head_response:
if head_response.request.url != self._source:
self._source = head_response.request.url
self._head_request = ResponseInfo(head_response)
return self._head_request
async def _determine_resume_file(self, target_file: File) -> File:
head_response = await self.get_head_response()
etag = head_response.etag
resume_name = target_file.path if etag is None else etag.parsed_etag
resume_file = pathlib.Path(resume_name).with_suffix(self.RESUME_SUFFIX)
if etag is None:
resume_name = target_file.path
else:
parsed_etag = etag.parsed_etag
resume_name = target_file.path.with_name(parsed_etag)
resume_file = resume_name.with_suffix(self.RESUME_SUFFIX)
return File(resume_file)
def _determine_tmp_file(self, target_file: File) -> File:

View file

@ -6,6 +6,7 @@ import unicodedata
from datetime import datetime
from math import ceil
from typing import List, Optional, Union
from warnings import warn
import audible
import httpx
@ -391,15 +392,21 @@ class LibraryItem(BaseItem):
return lr
async def get_content_metadata(self, quality: str = "high"):
async def get_content_metadata(
self, quality: str = "high", chapter_type: str = "Tree", **request_kwargs
):
chapter_type = chapter_type.capitalize()
assert quality in ("best", "high", "normal",)
assert chapter_type in ("Flat", "Tree")
url = f"content/{self.asin}/metadata"
params = {
"response_groups": "last_position_heard, content_reference, "
"chapter_info",
"quality": "High" if quality in ("best", "high") else "Normal",
"drm_type": "Adrm"
"drm_type": "Adrm",
"chapter_titles_type": chapter_type,
**request_kwargs
}
metadata = await self._client.get(url, params=params)
@ -597,6 +604,18 @@ class Library(BaseList):
self,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None
):
warn(
"resolve_podcats is deprecated, use resolve_podcasts instead",
DeprecationWarning,
stacklevel=2
)
return self.resolve_podcasts(start_date, end_date)
async def resolve_podcasts(
self,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None
):
podcast_items = await asyncio.gather(
*[i.get_child_items(start_date=start_date, end_date=end_date)
@ -662,7 +681,7 @@ class Catalog(BaseList):
return cls(resp, api_client=api_client)
async def resolve_podcats(self):
async def resolve_podcasts(self):
podcast_items = await asyncio.gather(
*[i.get_child_items() for i in self if i.is_parent_podcast()]
)