mirror of
https://github.com/mkb79/audible-cli.git
synced 2025-04-22 21:57:10 -04:00
Compare commits
17 commits
Author | SHA1 | Date | |
---|---|---|---|
|
b3adb9a331 | ||
|
1a1f25bc2d | ||
|
6165e95f40 | ||
|
009a7e69ec | ||
|
efcad39b8e | ||
|
705d6f0959 | ||
|
3a6db75e0d | ||
|
22e388dfaa | ||
|
0a9b2f9c7d | ||
|
8dc8739f66 | ||
|
7f01949413 | ||
|
48946ab8b8 | ||
|
24c57ec73e | ||
|
513b97a3cc | ||
|
45bd5820ad | ||
|
707a4b9192 | ||
|
d6ce4041d8 |
11 changed files with 246 additions and 71 deletions
37
CHANGELOG.md
37
CHANGELOG.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
2
setup.py
2
setup.py
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()]
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue