Source code for jwlib.pub

"""
(very untested) Wrapper for the jw.org publications API
"""
from __future__ import annotations

from datetime import datetime
from functools import lru_cache
from typing import Optional
from urllib.error import HTTPError

from .common import NotFoundError, _DictWrapper, _get_json

TYPE_MP3 = 'MP3'
TYPE_PDF = 'PDF'
TYPE_EPUB = 'EPUB'
TYPE_JWPUB = 'JWPUB'
TYPE_RTF = 'RTF'


_API_BASE = 'https://b.jw-cdn.org/apis/pub-media/GETPUBMEDIALINKS'

[docs] class Language(_DictWrapper):
[docs] def __init__(self, code: str, data: dict): super().__init__(data) self.__code = code
def __repr__(self): try: return f'<{self.__class__.__name__} {self.code!r}>' except (TypeError, LookupError, ValueError): return super().__repr__() @property def code(self): return self.__code @property def isocode(self) -> str: """ISO 639 language code Raises LookupError if undefined. """ return self._get_string('locale') @property def name(self) -> str: """Display name""" return self._get_string('name', '') @property def rtl(self) -> bool: """Right to left""" return self.data.get('direction') == 'rtl'
[docs] class Marker(_DictWrapper): def __repr__(self): try: return f'<{self.__class__.__name__} {self.start}-{self.duration}>' except (TypeError, LookupError, ValueError): return super().__repr__() @property def duration(self): return _timestamp_to_float(self.data['duration']) @property def start(self): return _timestamp_to_float(self.data['startTime']) @property def verse(self) -> Optional[int]: return self.data.get('verseNumber')
[docs] class MarkerGroup(_DictWrapper): @property def bible_book_chapter(self) -> int: return self.data['bibleBookChapter'] @property def bible_book_number(self) -> int: return self.data['bibleBookNumber'] @property def hash(self) -> str: """TODO what type?""" return self.data['hash'] @property def first_marker(self) -> Marker: """Marker of introduction""" return Marker(self.data['introduction']) # TODO Use of `functools.lru_cache` or `functools.cache` on methods can lead to memory leaks @property @lru_cache(maxsize=None) def markers(self) -> list[Marker]: """Generate Markers for each verse""" return [Marker(m) for m in self.data.get('markers', [])] @property def type(self) -> str: """May be 'publication' or 'bible' TODO what more?""" return self.data['type'] @property def spoken_language(self) -> str: return self.data['mepsLanguageSpoken'] @property def written_language(self) -> str: return self.data['mepsLanguageWritten']
[docs] class File(_DictWrapper):
[docs] def __init__(self, language: str, filetype: str, data: dict): """ :param language: JW language code :param filetype: File type like MP3 """ super().__init__(data) self.language = language self.type = filetype
def __repr__(self): try: return f'<{self.__class__.__name__} {self.url.split("/")[-1]!r}>' except (TypeError, LookupError, ValueError): return super().__repr__() @property def bible_book(self) -> int: """Bible book, 0 = All, 1 = Genesis""" return self.data['booknum'] @property def bit_rate(self) -> float: """TODO what unit?""" return self.data['bitRate'] @property def checksum(self) -> str: return self.data['file']['checksum'] @property def date(self) -> Optional[datetime]: """Modification date""" try: # Example 2019-01-20T10:28:27+00:00 # TODO Naive datetime constructed using `datetime.datetime.strptime()` without %z return datetime.strptime(self.data['file']['modifiedDatetime'][:-6], '%y-%m-%dT%H:%M:%S') except (IndexError, KeyError, TypeError, ValueError): return None @property def doc_id(self) -> str: """May be used to load articles at jw.org TODO is this true?""" return self.data['docid'] @property def duration(self) -> int: """Duration in seconds""" return self.data['duration'] @property def edition_code(self) -> str: """TODO what?""" return self.data['edition'] @property def edition_descr(self) -> str: """TODO what? Example: Regular""" return self.data['editionDescr'] @property def frame_height(self) -> int: return self.data['frameHeight'] @property def frame_rate(self) -> int: return self.data['frameRate'] @property def frame_width(self) -> int: return self.data['frameWidth'] @property def has_track(self) -> bool: return self.data['hasTrack'] @property def image(self) -> Optional[str]: return self.data['trackImage']['url'] or None @property def label(self) -> str: """TODO is this like video quality? Example: 0p""" return self.data['label'] @property def markers(self) -> Optional[MarkerGroup]: """MarkerCollection - holds list of Markers plus some metadata""" if not self.data.get('markers'): return None return MarkerGroup(self.data['markers']) @property def mimetype(self) -> str: return self.data['mimetype'] @property def pub_code(self) -> str: return self.data['pub'] @property def pub_format(self) -> str: """TODO what?""" return self.data['format'] @property def pub_format_descr(self) -> str: """TODO what? Example: Regular""" return self.data['formatDescr'] @property def size(self) -> int: """TODO what unit?""" return self.data['filesize'] @property def specialty_code(self) -> str: """Example: BR2 (braille)""" return self.data['specialty'] @property def specialty_descr(self) -> str: """Example: Braille Grade 2""" return self.data['specialtyDescr'] @property def stream(self) -> str: """TODO What is this? Example: https://jw.org""" return self.data['file']['stream'] @property def subtitled(self) -> bool: return self.data['subtitled'] @property def title(self) -> str: return self.data['title'] @property def track(self) -> int: """Track number (ie chapter number of book when dealing with audio recordings)""" return self.data['track'] @property def url(self) -> str: """Download URL""" return self.data['file']['url']
[docs] class Publication(_DictWrapper): def __repr__(self): try: string = f'<{self.__class__.__name__} code={self.code!r}' except (TypeError, LookupError, ValueError): return super().__repr__() if self.bible_book is not None: string += f' bible_book={self.bible_book!r}' if self.issue is not None: string += f' issue={self.issue!r}' if self.track is not None: string += f' track={self.track!r}' string += '>' return string @property def bible_book(self) -> Optional[int]: """Number of bible book (0 is index page)""" return self.data.get('booknum') or None @property def code(self) -> str: """Publication code""" return self.data['pub'] @property def date(self) -> str: """TODO Formatted date of some kind""" return self.data.get('formattedDate', '') @property def format(self) -> list: """TODO List of some kind of file formats""" return self.data['fileformat'] @property def image(self) -> Optional[str]: return self.data.get('pubImage', {}).get('url') @property def issue(self) -> Optional[str]: """Magazine issue code""" return self.data.get('issue') or None @property @lru_cache(maxsize=None) def files(self) -> list[File]: """List of File info objects for all languages and file types""" return [File(lang, filetype, f) for lang in self.data.get('files', {}) for filetype in self.data['files'][lang] for f in self.data['files'][lang][filetype]] @property @lru_cache(maxsize=None) def languages(self) -> list[Language]: """List of Language info""" return [Language(code, value) for code, value in self.data.get('languages', {}).items()] @property def name(self) -> str: return self.data['pubName'] @property def parent_name(self) -> str: """Display name of parent publication""" return self.data['parentPubName'] @property def speciality(self) -> str: """TODO Braille code etc?""" return self.data['speciality'] @property def track(self) -> Optional[int]: """Track number (chapter of a book etc when dealing with sound recordings)""" return self.data.get('track')
def _timestamp_to_float(string: str) -> float: """Convert HH:MM:SS.nnn to float""" factor = 1 result = 0.0 for part in reversed(string.split(':')): result += float(part) * factor factor *= 60 return result
[docs] def get_publication(pub: str, lang: str, *, issue: Optional[int] = None, bible_book: Optional[int] = None, all_langs=False, filetype: Optional[str] = None ) -> Publication: query = { 'output': 'json', 'fileformat': filetype, 'pub': pub, 'issue': issue, 'booknum': bible_book, 'langwritten': lang, 'txtCMSLang': lang, 'alllangs': all_langs or None # TODO check if this is needed } try: return _get_json(_API_BASE, query) except HTTPError as e: if e.code != 404: raise raise NotFoundError