Skip to content

Commit

Permalink
Added NextorySource
Browse files Browse the repository at this point in the history
  • Loading branch information
lullius committed Jan 31, 2023
1 parent db2f8e6 commit 50ce349
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 11 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ audiobook-dl currently supports downloading from the following sources:
- [Chirp](https://www.chirpbooks.com/)
- [eReolen (Danish Library)](https://ereolen.dk)
- [Librivox](https://librivox.org)
- [Nextory](https://nextory.com)
- [Overdrive (Library service)](https://www.overdrive.com/)
- [Scribd](https://scribd.com)
- [Storytel](https://www.storytel.com/)
Expand Down
2 changes: 2 additions & 0 deletions audiobookdl/sources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .chirp import ChirpSource
from .ereolen import EreolenSource
from .librivox import LibrivoxSource
from .nextory import NextorySource
from .overdrive import OverdriveSource
from .scribd import ScribdSource
from .storytel import StorytelSource
Expand All @@ -31,6 +32,7 @@ def get_source_classes():
ChirpSource,
EreolenSource,
LibrivoxSource,
NextorySource,
OverdriveSource,
ScribdSource,
StorytelSource,
Expand Down
135 changes: 135 additions & 0 deletions audiobookdl/sources/nextory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
from .source import Source
from audiobookdl import AudiobookFile
from typing import Any
import hashlib
import uuid
import platform


def calculate_checksum(username: str, password: str, salt: str) -> str:
return get_checksum(username + salt + password)


def calculate_password_checksum(password: str, salt: str) -> str:
return get_checksum(password + salt)


def get_checksum(s: str) -> str:
return hashlib.md5(s.encode()).digest().hex().zfill(32).upper()


def get_device_id() -> str:
return str(uuid.uuid3(uuid.NAMESPACE_DNS, "audiobook-dl"))


class NextorySource(Source):
match = [
r"https?://(www.)?nextory.+",
]
names = [ "Nextory" ]
_authentication_methods = [
"login",
]

user_data: dict
book_info: dict

def get_salt(self) -> str:
url = "https://api.nextory.se/api/app/catalogue/7.5/salt"
resp = self._session.get(url)
if resp.status_code != 200:
raise RuntimeError("Couldn't get salt from nextory.")
return resp.json()["data"]["salt"]

def _login(self, username: str, password: str):
# Step one
login_url = "https://api.nextory.se/api/app/user/7.5/login"
headers = {
"appid": "200",
"model": "Personal Computer",
"locale": "en_GB",
"deviceid": get_device_id(),
"osinfo": platform.platform(),
"version": "4.34.6",
#"user-agent": "okhttp/4.9.3",
}

self._session.headers = headers
salt = self.get_salt()
files = {
"username": (None, username),
"password": (None, password),
"checksum": (None, calculate_checksum(username, password, salt)),
}
resp = self._session.post(login_url, files=files)
if resp.status_code != 200:
raise PermissionError("Error in NextorySource login step one")

login_info = resp.json()
self._session.headers.update({'token': login_info["data"]["token"]})

# Step two
resp = self._session.get("https://api.nextory.se/api/app/user/7.5/accounts/list")
if resp.status_code != 200:
raise PermissionError("Error in NextorySource login step two")
account_list = resp.json()

# Step three
params = {
"loginkey": account_list["data"]["accounts"][0]["loginkey"],
"checksum": calculate_password_checksum(account_list["data"]["accounts"][0]["loginkey"], salt)
}

resp = self._session.get(login_url, params=params)
if resp.status_code != 200:
raise PermissionError("Error in NextorySource login step three")

account_info = resp.json()
self._session.headers.update({'token': account_info["data"]["token"]})
self._session.headers.update({'canary': account_info["data"]["canary"]})

# Step four
resp = self._session.get("https://api.nextory.se/api/app/library/7.5/active")
if resp.status_code != 200:
raise PermissionError("Error in NextorySource login step four")

active = resp.json()

self.user_data = {
"login_info": login_info,
"account_list": account_list,
"account_info": account_info,
"active": active,
}
self._session.headers.update({'apiver': "7.5"})

def get_title(self) -> str:
return self.book_info["title"]

def get_files(self) -> list[AudiobookFile]:
return [AudiobookFile(url=self.book_info["file"]["url"], headers=self._session.headers, ext="mp3")]

def get_metadata(self) -> dict[str,Any]:
try:
book_info = self._session.get("https://api.nextory.se/api/app/product/7.5/bookinfo",
params={"id": self.book_info["id"]}).json()
metadata = {"authors": [a for a in self.book_info["authors"]],
"narrators": [n for n in book_info["data"]["books"]["narrators"]]}
return metadata
except:
return {}

def get_chapters(self) -> list[tuple[int, str]] | None:
# Nextory has no chapters...?
return None

def get_cover(self) -> bytes | None:
return self.get(self.book_info["imgurl"].replace("{$width}", "640"))

def before(self):
wanted_id = self.url.split("-")[-1].replace("/", "")
for book in self.user_data["active"]["data"]["books"]:
if str(book["id"]) == wanted_id:
self.book_info = book
return
raise PermissionError(f"Book with id {wanted_id} was not found in My Library.")
23 changes: 12 additions & 11 deletions supported_sites.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# Supported Sites

| Site | Cookies | Username/Password | Notes |
|:-----------------|:-------:|:-----------------:|:-----------------------------------|
| audiobooks.com ||| |
| Chapter ||| |
| Chirp ||| |
| eReolen ||| Requires library for login |
| Librivox ||| Authentication not required |
| Overdrive ||| |
| Scribd ||| |
| Storytel ||| Books have to be on your bookshelf |
| YourCloudLibrary ||| |
| Site | Cookies | Username/Password | Notes |
|:-----------------|:-------:|:-----------------:|:---------------------------------------------------------------------------|
| audiobooks.com ||| |
| Chapter ||| |
| Chirp ||| |
| eReolen ||| Requires library for login |
| Librivox ||| Authentication not required |
| Nextory ||| Books must be in "Your Library" <br/>Only single (first) account supported |
| Overdrive ||| |
| Scribd ||| |
| Storytel ||| Books have to be on your bookshelf |
| YourCloudLibrary ||| |
1 change: 1 addition & 0 deletions tests/test_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"https://ereolen.dk/ting/object/870970-basis%3A53978223": "Ereolen",
"https://www.chirpbooks.com/player/11435746": "Chirp",
"https://librivox.org/library-of-the-worlds-best-literature-ancient-and-modern-volume-3-by-various/": "Librivox",
"https://www.nextory.no/bok/somethingsomethingsomething-99999999/": "Nextory",
"https://ofs-d2b6150a9dec641552f953da2637d146.listen.overdrive.com/?d=...": "Overdrive",
"https://www.scribd.com/listen/579426746": "Scribd",
"https://www.storytel.com/no/nn/books/somethingsomething-9999999": "Storytel",
Expand Down

0 comments on commit 50ce349

Please sign in to comment.