Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix download using spine information from window.bData #71

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/lint-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ "3.7", "3.8", "3.9", "3.10" , "3.11" ]
python-version: [ "3.8", "3.9", "3.10" , "3.11" , "3.12" ]
steps:
- uses: actions/checkout@v3
with:
Expand All @@ -36,7 +36,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade pip setuptools
pip -q install -r requirements.txt
pip -q install -r requirements-dev.txt
- name: Compile all
Expand Down Expand Up @@ -64,7 +64,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
python-version: [ "3.7", "3.8", "3.9", "3.10" , "3.11" ]
python-version: [ "3.8", "3.8", "3.9", "3.10" , "3.11" , "3.12" ]
needs: lint
steps:
- uses: FedericoCarboni/setup-ffmpeg@v2
Expand All @@ -80,7 +80,7 @@ jobs:
- name: Install dependencies
# Installing wheel due to https://github.com/pypa/pip/issues/8559
run: |
python3 -m pip -q install --upgrade pip wheel
python3 -m pip -q install --upgrade pip wheel setuptools
python3 -m pip -q install -r requirements.txt --upgrade
python3 -m pip -q install -r requirements-dev.txt --upgrade
- name: Run tests on ${{ matrix.os }} with python ${{ matrix.python-version }}
Expand Down Expand Up @@ -115,7 +115,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
python-version: "3.12"
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ odmpy also has useful features for audiobooks such as adding of chapters metadat

Works on Linux, macOS, and Windows.

Requires Python >= 3.7.
Requires Python >= 3.8.

![Screenshot](https://user-images.githubusercontent.com/104607/222746903-0089bea5-ba3f-4eef-8e14-b4870a5bbb27.png)

Expand Down
37 changes: 21 additions & 16 deletions odmpy/libby.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ def parse_part_path(title: str, part_path: str) -> ChapterMarker:
return ChapterMarker(
title=title,
part_name=mobj.group("part_name"),
start_second=float(mobj.group("second_stamp"))
if mobj.group("second_stamp")
else 0,
start_second=(
float(mobj.group("second_stamp")) if mobj.group("second_stamp") else 0
),
end_second=0,
)

Expand Down Expand Up @@ -812,16 +812,26 @@ def prepare_loan(self, loan: Dict) -> Tuple[str, Dict]:
meta = self.open_loan(loan_type, card_id, title_id)
download_base: str = meta["urls"]["web"]

# Sets a needed cookie
# Sets a needed cookie and parse the redirect HTML for meta.
web_url = download_base + "?" + meta["message"]
_ = self.make_request(
html = self.make_request(
web_url,
headers={"Accept": "*/*"},
method="HEAD",
method="GET",
authenticated=False,
return_res=True,
)
return download_base, meta
# audio [nav/toc, spine], ebook [nav/toc, spine, manifest]
# both in window.bData
regex = re.compile(r"window\.bData\s*=\s*({.*});")
match = regex.search(html.text)
if not match:
raise ValueError(f"Failed to parse window.bData for book info: {web_url}")
openbook = json.loads(match.group(1))

# set download_base for ebook
openbook["download_base"] = download_base
return download_base, openbook

def process_audiobook(
self, loan: Dict
Expand All @@ -832,24 +842,19 @@ def process_audiobook(
:param loan:
:return:
"""
download_base, meta = self.prepare_loan(loan)
# contains nav/toc and spine
openbook = self.make_request(meta["urls"]["openbook"])
download_base, openbook = self.prepare_loan(loan)
toc = parse_toc(download_base, openbook["nav"]["toc"], openbook["spine"])
return openbook, toc

def process_ebook(self, loan: Dict) -> Tuple[str, Dict, List[Dict]]:
def process_ebook(self, loan: Dict) -> Tuple[str, Dict]:
"""
Returns the data needed to download an ebook directly.

:param loan:
:return:
"""
download_base, meta = self.prepare_loan(loan)
# contains nav/toc and spine, manifest
openbook = self.make_request(meta["urls"]["openbook"])
rosters: List[Dict] = self.make_request(meta["urls"]["rosters"])
return download_base, openbook, rosters
download_base, openbook = self.prepare_loan(loan)
return download_base, openbook

def return_title(self, title_id: str, card_id: str) -> None:
"""
Expand Down
52 changes: 27 additions & 25 deletions odmpy/libby_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,31 +77,33 @@ def process(http_err: requests.HTTPError) -> None:
:return:
"""
# json response
if (
http_err.response.status_code == HTTPStatus.BAD_REQUEST
and http_err.response.headers.get("content-type", "").startswith(
"application/json"
)
):
error = http_err.response.json()
if error.get("result", "") == "upstream_failure":
upstream = error.get("upstream", {})
if upstream:
raise ClientBadRequestError(
msg=f'{upstream.get("userExplanation", "")} [errorcode: {upstream.get("errorCode", "")}]',
http_status=http_err.response.status_code,
error_response=http_err.response.text,
) from http_err

raise ClientBadRequestError(
msg=str(error),
if http_err.response is not None:
if hasattr(http_err.response, "json") and callable(http_err.response.json):
if (
http_err.response.status_code == HTTPStatus.BAD_REQUEST
and http_err.response.headers.get("content-type", "").startswith(
"application/json"
)
):
error = http_err.response.json()
if error.get("result", "") == "upstream_failure":
upstream = error.get("upstream", {})
if upstream:
raise ClientBadRequestError(
msg=f'{upstream.get("userExplanation", "")} [errorcode: {upstream.get("errorCode", "")}]',
http_status=http_err.response.status_code,
error_response=http_err.response.text,
) from http_err

raise ClientBadRequestError(
msg=str(error),
http_status=http_err.response.status_code,
error_response=http_err.response.text,
) from http_err

# final fallback
raise ClientError(
msg=str(http_err),
http_status=http_err.response.status_code,
error_response=http_err.response.text,
) from http_err

# final fallback
raise ClientError(
msg=str(http_err),
http_status=http_err.response.status_code,
error_response=http_err.response.text,
) from http_err
56 changes: 31 additions & 25 deletions odmpy/odm.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,11 +377,10 @@ def extract_loan_file(
format_id = LibbyFormats.EBookOverdrive

openbook: Dict = {}
rosters: List[Dict] = []
# pre-extract openbook first so that we can use it to create the book folder
# with the creator names (needed to place the cover.jpg download)
if format_id in (LibbyFormats.EBookOverdrive, LibbyFormats.MagazineOverDrive):
_, openbook, rosters = libby_client.process_ebook(selected_loan)
_, openbook = libby_client.process_ebook(selected_loan)

cover_path = None
if format_id in (
Expand All @@ -395,9 +394,7 @@ def extract_loan_file(
file_ext = (
"acsm"
if format_id in (LibbyFormats.EBookEPubAdobe, LibbyFormats.EBookPDFAdobe)
else "pdf"
if format_id == LibbyFormats.EBookPDFOpen
else "epub"
else "pdf" if format_id == LibbyFormats.EBookPDFOpen else "epub"
)
book_folder, book_file_name = generate_names(
title=selected_loan["title"],
Expand Down Expand Up @@ -443,7 +440,6 @@ def extract_loan_file(
loan=selected_loan,
cover_path=cover_path,
openbook=openbook,
rosters=rosters,
libby_client=libby_client,
args=args,
logger=logger,
Expand Down Expand Up @@ -1030,18 +1026,26 @@ def run(custom_args: Optional[List[str]] = None, be_quiet: bool = False) -> None
"%s: %-55s %s %-25s \n * %s %s%s",
colored(f"{index:2d}", attrs=["bold"]),
colored(loan["title"], attrs=["bold"]),
"📰"
if args.include_magazines
and libby_client.is_downloadable_magazine_loan(loan)
else "📕"
if args.include_ebooks
and libby_client.is_downloadable_ebook_loan(loan)
else "🎧"
if args.include_ebooks or args.include_magazines
else "",
loan["firstCreatorName"]
if loan.get("firstCreatorName")
else loan.get("edition", ""),
(
"📰"
if args.include_magazines
and libby_client.is_downloadable_magazine_loan(loan)
else (
"📕"
if args.include_ebooks
and libby_client.is_downloadable_ebook_loan(loan)
else (
"🎧"
if args.include_ebooks or args.include_magazines
else ""
)
)
),
(
loan["firstCreatorName"]
if loan.get("firstCreatorName")
else loan.get("edition", "")
),
f"Expires: {colored(f'{expiry_date:%Y-%m-%d}','blue' if libby_client.is_renewable(loan) else None)}",
next(
iter(
Expand All @@ -1052,13 +1056,15 @@ def run(custom_args: Optional[List[str]] = None, be_quiet: bool = False) -> None
]
)
),
""
if not libby_client.is_renewable(loan)
else (
f'\n * {loan.get("availableCopies", 0)} '
f'{ps(loan.get("availableCopies", 0), "copy", "copies")} available'
)
+ (f" (hold placed: {hold_date:%Y-%m-%d})" if hold else ""),
(
""
if not libby_client.is_renewable(loan)
else (
f'\n * {loan.get("availableCopies", 0)} '
f'{ps(loan.get("availableCopies", 0), "copy", "copies")} available'
)
+ (f" (hold placed: {hold_date:%Y-%m-%d})" if hold else "")
),
)
loan_choices: List[str] = []

Expand Down
35 changes: 21 additions & 14 deletions odmpy/processing/audiobook.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,11 @@ def process_audiobook_loan(
part_download_url,
headers={
"User-Agent": USER_AGENT,
"Range": f"bytes={already_downloaded_len}-"
if already_downloaded_len
else None,
"Range": (
f"bytes={already_downloaded_len}-"
if already_downloaded_len
else None
),
},
timeout=args.timeout,
stream=True,
Expand Down Expand Up @@ -245,8 +247,9 @@ def process_audiobook_loan(
)

except HTTPError as he:
logger.error(f"HTTPError: {str(he)}")
logger.debug(he.response.content)
if he.response is not None:
logger.error(f"HTTPError: {str(he)}")
logger.debug(he.response.content)
raise OdmpyRuntimeError("HTTP Error while downloading part file.")

except ConnectionError as ce:
Expand Down Expand Up @@ -477,15 +480,19 @@ def process_audiobook_loan(
create_opf(
media_info,
cover_filename if keep_cover else None,
file_tracks
if not args.merge_output
else [
{
"file": book_filename
if args.merge_format == "mp3"
else book_m4b_filename
}
],
(
file_tracks
if not args.merge_output
else [
{
"file": (
book_filename
if args.merge_format == "mp3"
else book_m4b_filename
)
}
]
),
opf_file_path,
logger,
)
Expand Down
Loading
Loading