├── requirements.txt ├── MANIFEST.in ├── requirements-dev.txt ├── pylibgen ├── __init__.py ├── exceptions.py ├── constants.py └── pylibgen.py ├── .gitignore ├── .travis.yml ├── tox.ini ├── Makefile ├── tests ├── book_test.py └── library_test.py ├── .pre-commit-config.yaml ├── LICENSE ├── setup.py └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.22.0 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE requirements.txt 2 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pre-commit==1.16.* 2 | pytest==4.* 3 | -------------------------------------------------------------------------------- /pylibgen/__init__.py: -------------------------------------------------------------------------------- 1 | from .pylibgen import Book 2 | from .pylibgen import constants 3 | from .pylibgen import Library 4 | -------------------------------------------------------------------------------- /pylibgen/exceptions.py: -------------------------------------------------------------------------------- 1 | class LibraryException(Exception): 2 | pass 3 | 4 | 5 | class BookException(Exception): 6 | pass 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | build/ 3 | dist/ 4 | sdist/ 5 | *.egg-info/ 6 | *.egg 7 | .cache/ 8 | .pytest_cache/ 9 | .tox/ 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | 4 | branches: 5 | only: 6 | - master 7 | 8 | install: 9 | - pip install tox 10 | 11 | script: 12 | - tox 13 | 14 | matrix: 15 | include: 16 | - python: 3.6 17 | env: TOXENV=py36 18 | - python: 3.7 19 | env: TOXENV=py37 20 | 21 | cache: 22 | directories: 23 | - $HOME/.cache/pip 24 | - $HOME/.cache/pre-commit 25 | 26 | notifications: 27 | email: 28 | on_success: never 29 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.12 3 | envlist = py36, py37 4 | skipsdist = true 5 | 6 | [testenv] 7 | extras = dev 8 | usedevelop = True 9 | # alternatively: 10 | # deps = 11 | # -r requirements-dev.txt 12 | # -e . 13 | # requirements-dev is read by my setup.py, so pip install -e '.[dev]' is equivalent 14 | commands = 15 | pre-commit run --all-files 16 | pytest -v 17 | 18 | [flake8] 19 | filename = *.py 20 | exclude = .git,__pycache__,.tox,__init__.py,setup.py 21 | max-line-length = 90 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: build 3 | 4 | .PHONY: build 5 | build: clean test 6 | python -m pip install --upgrade setuptools wheel 7 | python setup.py sdist bdist_wheel 8 | 9 | .PHONY: test 10 | test: 11 | tox 12 | 13 | .PHONY: publish 14 | publish: build 15 | python -m pip install --upgrade twine 16 | python -m twine upload --sign dist/* 17 | 18 | .PHONY: clean 19 | clean: 20 | rm -rf venv/ build/ dist/ *.egg-info/ 21 | find -type f -iname '*.pyc' -delete 22 | find -type d -iname '__pycache__' -delete 23 | -------------------------------------------------------------------------------- /tests/book_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import requests 3 | 4 | import pylibgen 5 | 6 | 7 | def check_url(url): 8 | r = requests.head(url, allow_redirects=True, timeout=3) 9 | r.raise_for_status() 10 | 11 | 12 | @pytest.mark.parametrize("fh", pylibgen.constants.FILEHOST_URLS) 13 | def test_filehosts(fh): 14 | b = pylibgen.Book(id=1_421_206, md5="1af2c71c1342e850e1e47013b06f9eb9") 15 | try: 16 | check_url(b.get_url(fh)) 17 | except requests.exceptions.ReadTimeout: 18 | pytest.xfail(f"Attempt to reach filehost {fh} timed out.") 19 | except requests.exceptions.SSLError: 20 | pytest.xfail( 21 | f"SSLError occurred with filehost {fh}, but an actual browser" 22 | "might have the appropriate certs.", 23 | ) 24 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.2.3 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: fix-encoding-pragma 8 | args: [--remove] 9 | - id: check-docstring-first 10 | - id: check-merge-conflict 11 | - id: check-yaml 12 | - id: debug-statements 13 | - id: name-tests-test 14 | - id: check-added-large-files 15 | - id: check-byte-order-marker 16 | - id: mixed-line-ending 17 | - id: requirements-txt-fixer 18 | - id: flake8 19 | - repo: https://github.com/pre-commit/mirrors-autopep8 20 | rev: v1.4.4 21 | hooks: 22 | - id: autopep8 23 | - repo: https://github.com/asottile/reorder_python_imports 24 | rev: v1.5.0 25 | hooks: 26 | - id: reorder-python-imports 27 | - repo: https://github.com/asottile/add-trailing-comma 28 | rev: v1.1.0 29 | hooks: 30 | - id: add-trailing-comma 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Joshua Li (github.com/joshuarli) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages 4 | from setuptools import setup 5 | 6 | version = "2.0.2" 7 | url = "https://github.com/JoshuaRLi/pylibgen" 8 | 9 | here = os.path.abspath(os.path.dirname(__file__)) 10 | 11 | with open(os.path.join(here, "README.md")) as f: 12 | long_description = f.read() 13 | 14 | with open(os.path.join(here, "requirements.txt")) as f: 15 | install_requires = f.read() 16 | 17 | with open(os.path.join(here, "requirements-dev.txt")) as f: 18 | dev_requires = f.read() 19 | 20 | setup( 21 | name="pylibgen", 22 | version=version, 23 | description="Python interface to Library Genesis.", 24 | long_description=long_description, 25 | long_description_content_type="text/markdown", 26 | license="MIT", 27 | url=url, 28 | download_url="{}/archive/v{}.tar.gz".format(url, version), 29 | author="Joshua Li", 30 | author_email="josh@jrl.ninja", 31 | maintainer="Joshua Li", 32 | maintainer_email="josh@jrl.ninja", 33 | keywords=[ 34 | "libgen", 35 | "library", 36 | "genesis", 37 | "search", 38 | "download", 39 | "books", 40 | "ebooks", 41 | "textbooks", 42 | ], 43 | packages=find_packages(exclude=("tests",)), 44 | python_requires=">=3.6", 45 | install_requires=install_requires, 46 | extras_require={ 47 | # linting and formatting tools (flake8, black) we let pre-commit pin versions 48 | # build and publishing tools (setuptools, wheel, twine) we always want latest (see makefile) 49 | # only dev dependencies that should be pinned in extras (imo) is testing (pytest) and pre-commit 50 | "dev": dev_requires, 51 | }, 52 | classifiers=[ 53 | "Development Status :: 7 - Inactive", 54 | "Intended Audience :: Developers", 55 | "Topic :: Software Development", 56 | "Natural Language :: English", 57 | "License :: OSI Approved :: MIT License", 58 | "Programming Language :: Python", 59 | "Programming Language :: Python :: 3 :: Only", 60 | "Programming Language :: Python :: 3.6", 61 | "Programming Language :: Python :: 3.7", 62 | ], 63 | ) 64 | -------------------------------------------------------------------------------- /tests/library_test.py: -------------------------------------------------------------------------------- 1 | from types import GeneratorType 2 | 3 | import pytest 4 | 5 | import pylibgen 6 | 7 | test_books = { 8 | # Automate the Boring Stuff with Python: Practical Programming for Total Beginners 9 | ("automate the boring stuff", "title"): { 10 | # id: hash 11 | "1529338": "d826b3e593b12422784f50d59c97a966", 12 | "1421207": "b34564156c3778261ed03167b09f6694", 13 | "1381538": "4e0efdd614737fd66408fd43a9d5ff10", 14 | "1381540": "5a64e12e79af379110a31ea04bb6320c", 15 | "1421208": "c157d6ec28d1a7c4b528f4e6a1ea4c9e", 16 | "1351717": "054255117b2e86251415292ef48320fd", 17 | "1421206": "1af2c71c1342e850e1e47013b06f9eb9", 18 | "2149756": "2699081bc2e3908ece25013109941028", 19 | "2308943": "4dd22932bdaae6b35d947c3fc943e789", 20 | "2308961": "f964817cbd1cbd76e5b6872b789c21fa", 21 | "2308966": "4423eb3d3760978bf3a31c8f3bb87a41", 22 | "2318822": "42b5a4990f66ff4ee5a426a09230d25b", 23 | "2337526": "ac5e8b50ed98f8a0714a454293317594", 24 | }, 25 | # Free software, free society: selected essays of Richard M. Stallman 26 | ("1882114981", "isbn"): { 27 | "112887": "861c055b960e7f36d95164cab34e0e97", 28 | "2340415": "bf0f4e004b6d29086a7d3197e2a624cb", 29 | "310297": "3b46ed45310545cc1f6d7b627f9b640d", 30 | }, 31 | # Same as above, but ISBN 13 instead of 10 32 | ("9781882114986", "isbn"): { 33 | "112887": "861c055b960e7f36d95164cab34e0e97", 34 | "310297": "3b46ed45310545cc1f6d7b627f9b640d", 35 | }, 36 | } 37 | 38 | 39 | @pytest.mark.parametrize("mirror", pylibgen.constants.MIRRORS) 40 | @pytest.mark.parametrize("test_book", list(test_books.items())) 41 | def test_mirror(mirror, test_book): 42 | search_params, md5_of_ids = test_book 43 | library = pylibgen.Library(mirror) 44 | ids = library.search(*search_params) 45 | 46 | assert isinstance(ids, list) 47 | assert set(ids) == set(md5_of_ids.keys()) 48 | 49 | books = library.lookup(ids) 50 | assert isinstance(books, GeneratorType) 51 | for book in books: 52 | assert isinstance(book, pylibgen.Book) 53 | assert md5_of_ids[book.id] == book.md5.lower() 54 | 55 | 56 | @pytest.mark.parametrize("mirror", pylibgen.constants.MIRRORS) 57 | def test_all_book_fields(mirror): 58 | library = pylibgen.Library(mirror) 59 | book = library.lookup("112887", fields=["*"]) 60 | assert set(next(book).__dict__.keys()) == pylibgen.constants.ALL_BOOK_FIELDS 61 | -------------------------------------------------------------------------------- /pylibgen/constants.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | __Mirror = namedtuple("Mirror", ("name", "search", "lookup")) 4 | 5 | MIRRORS = { 6 | "libgen.io": __Mirror( 7 | "libgen.io", 8 | "http://libgen.io/search.php" 9 | "?req={req}" 10 | "&page={page}" 11 | "&res={per_page}" 12 | "&column={mode}" 13 | "&lg_topic={lg_topic}" 14 | "&view={view}" 15 | "&open={open}" 16 | "&phrase={phrase}", 17 | "http://libgen.io/json.php" "?ids={ids}" "&fields={fields}", 18 | ), 19 | # TODO gen.lib.rus.ec support 20 | } 21 | 22 | DEFAULT_MIRROR = "libgen.io" 23 | 24 | # these query parameters for mirror/search.php are pinned. 25 | SEARCH_BASE_PARAMS = { 26 | # database to search in. libgen is also known as Sci-Tech. 27 | "lg_topic": "libgen", 28 | # View results: simple 29 | "view": "simple", 30 | # Download type: Resumed dl with original filename 31 | "open": 0, 32 | # Search with mask (e.g. word*), 0 actually enables this 33 | "phrase": 0, 34 | } 35 | 36 | # modes year, publisher, series, language, extension, tags are possible, 37 | # but way too general and not recommended. 38 | # AFAIK, there isn't a way to combine multiple search modes and respective 39 | # strings to filter down the search. 40 | SEARCH_MODES = ("title", "author", "isbn") 41 | 42 | # strangely, libgen only allows these amounts. 43 | SEARCH_RESULTS_PER_PAGE = (25, 50, 100) 44 | 45 | FILEHOST_URLS = { 46 | "libgen.io": "http://libgen.io/ads.php?md5={md5}", 47 | # currently unresolvable with 8.8.8.8, but works on quad9 and cloudflare 48 | # "ambry.pw": "https://ambry.pw/item/detail/id/{id}", 49 | "library1.org": "http://library1.org/_ads/{md5}", 50 | "b-ok.org": "http://b-ok.org/md5/{md5}", 51 | "bookfi.net": "http://bookfi.net/md5/{md5}", 52 | } 53 | 54 | DEFAULT_FILEHOST = "libgen.io" 55 | 56 | DEFAULT_BOOK_FIELDS = [ 57 | "title", 58 | "author", 59 | "year", 60 | "edition", 61 | "pages", 62 | "identifier", 63 | "extension", 64 | "filesize", 65 | "md5", 66 | "id", 67 | ] 68 | 69 | ALL_BOOK_FIELDS = { 70 | "aich", 71 | "asin", 72 | "author", 73 | "bookmarked", 74 | "btih", 75 | "city", 76 | "cleaned", 77 | "color", 78 | "commentary", 79 | "coverurl", 80 | "crc32", 81 | "ddc", 82 | "descr", 83 | "doi", 84 | "dpi", 85 | "edition", 86 | "extension", 87 | "filesize", 88 | "generic", 89 | "googlebookid", 90 | "id", 91 | "identifierwodash", 92 | "issn", 93 | "language", 94 | "lbc", 95 | "lcc", 96 | "library", 97 | "local", 98 | "locator", 99 | "md5", 100 | "openlibraryid", 101 | "orientation", 102 | "pages", 103 | "paginated", 104 | "periodical", 105 | "publisher", 106 | "scanned", 107 | "searchable", 108 | "series", 109 | "sha1", 110 | "sha256", 111 | "tags", 112 | "timeadded", 113 | "timelastmodified", 114 | "title", 115 | "toc", 116 | "topic", 117 | "torrent", 118 | "tth", 119 | "udc", 120 | "visible", 121 | "volumeinfo", 122 | "year", 123 | } 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pylibgen 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/pylibgen.svg)](https://pypi.org/project/pylibgen/) 4 | [![Travis CI](https://travis-ci.org/JoshuaRLi/pylibgen.svg?branch=master)](https://travis-ci.org/JoshuaRLi/pylibgen) 5 | [![License MIT](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/JoshuaRLi/pylibgen/blob/master/LICENSE) 6 | 7 | Python interface to Library Genesis. Only the LibGen/Sci-Tech database is supported. 8 | 9 | **This project is unmaintained as of the 2.0.2 release.** Feel free to maintain a public fork, or just patch my existing code in any way you see fit, privately. 10 | 11 | 12 | ## Installation 13 | 14 | pylibgen is well-tested on Python 3.6 - 3.7, and can be installed via `pip install pylibgen`. 15 | 16 | 17 | ## Usage 18 | 19 | ```python 20 | >>> from pylibgen import Library 21 | >>> l = Library() 22 | >>> ids = l.search('stallman essays') 23 | >>> ids 24 | ['112887', '310297', '688326', '1594161', '1610379'] 25 | >>> book1, book2, *_ = l.lookup(ids) 26 | >>> book1.__dict__ 27 | {'id': '112887', 'title': 'Free software, free society: selected essays of Richard M. Stallman', 'author': 'Richard M. Stallman, Lawrence Lessig, Joshua Gay, Laurence Lessig', 'year': '2002', 'edition': 'First Printing, First Edition', 'pages': '230', 'identifier': '9781882114986,1882114981', 'extension': 'pdf', 'filesize': '2210323', 'md5': '861C055B960E7F36D95164CAB34E0E97'} 28 | >>> book2.__dict__ 29 | {'id': '310297', 'title': 'Free Software Free Society: Selected Essays of Richard Stallman', 'author': 'Richard Stallman', 'year': '2010', 'edition': '2nd Edition', 'pages': '278', 'identifier': '0983159203,9780983159209', 'extension': 'pdf', 'filesize': '1597349', 'md5': '6C3C2593BBB5D77154D50DFDDC0EA669'} 30 | >>> book1.get_url(filehost='b-ok.org') 31 | 'http://b-ok.org/md5/861C055B960E7F36D95164CAB34E0E97' 32 | ``` 33 | 34 | 35 | ## Support Library Genesis\! 36 | 37 | `Book.get_url(filehost='...')` will return the standard filehost gateway url. 38 | 39 | There is no functionality to bypass any intermediate advertisement pages, and this behavior is intended because Library Genesis is a service worth supporting. 40 | 41 | 42 | ## Development Setup 43 | 44 | You'll need python 3.6, python 3.7, and `tox`. It's recommended to use [`pyenv`](https://github.com/pyenv/pyenv) to install + manage python versions and executable modules. An example: 45 | 46 | $ pyenv install 3.6.8 47 | $ pyenv install 3.7.3 48 | $ pyenv global 3.7.3 3.6.8 # puts both pyenv-managed python3.6 and python3.7 on the PATH for tox 49 | $ python3.7 -m pip install tox # python will be resolved to 3.7.3 by pyenv's shim, this is just being explicit 50 | 51 | To run pre-commit hooks and tests for all supported python versions (make sure you're not in a virtualenv before running this): 52 | 53 | $ tox 54 | 55 | To enter the version-specific virtualenv created above (you'll need this to commit with `pre-commit` hooks): 56 | 57 | $ source .tox/py37/bin/activate 58 | 59 | To only create the tox virtualenvs without running any (test) commands: 60 | 61 | $ tox --notest 62 | 63 | 64 | ## Disclaimer 65 | 66 | Use `pylibgen` responsibly and at your own risk. The author(s) are not responsible or liable for any piracy, copyright infringement, or other offences committed by anyone using this software. Please consider supporting your favorite authors by purchasing their works\! 67 | -------------------------------------------------------------------------------- /pylibgen/pylibgen.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Iterator 3 | from typing import List 4 | from typing import Union 5 | from urllib.parse import quote_plus 6 | 7 | import requests 8 | 9 | from . import constants 10 | from . import exceptions 11 | 12 | 13 | class Book(object): 14 | """ 15 | Models a Library Genesis book. 16 | Class properties are dynamically set during initialization if all fields are valid. 17 | """ 18 | 19 | __MANDATORY_FIELDS = ('id', 'md5') 20 | 21 | def __init__(self, **fields: str): 22 | self.__dict__.update( 23 | {k: v for k, v in fields.items() if k in constants.ALL_BOOK_FIELDS}, 24 | ) 25 | for f in self.__MANDATORY_FIELDS: 26 | if f not in self.__dict__: 27 | raise exceptions.BookException( 28 | "Book is missing mandatory field {}.".format(f), 29 | ) 30 | 31 | def get_url(self, filehost=constants.DEFAULT_FILEHOST) -> str: 32 | url = self.__fmt_filehost_url(filehost, id=self.id, md5=self.md5) 33 | return url 34 | 35 | def __fmt_filehost_url(self, filehost, **kwargs): 36 | try: 37 | return constants.FILEHOST_URLS[filehost].format(**kwargs) 38 | except KeyError: 39 | raise exceptions.BookException( 40 | "filehost {} not supported.\nPlease specify one of: {}".format( 41 | filehost, ", ".join(constants.FILEHOST_URLS), 42 | ), 43 | ) 44 | 45 | 46 | class Library(object): 47 | """Library Genesis search interface wrapper.""" 48 | 49 | def __init__(self, mirror: str = constants.DEFAULT_MIRROR): 50 | if mirror not in constants.MIRRORS: 51 | raise NotImplementedError( 52 | "Search mirror {} not supported.".format(mirror), 53 | ) 54 | self.mirror = constants.MIRRORS[mirror] 55 | 56 | def __repr__(self): 57 | return "".format(self.mirror.name) 58 | 59 | def search( 60 | self, query: str, mode: str = "title", page: int = 1, per_page: int = 25, 61 | ) -> List[str]: 62 | """Searches Library Genesis. 63 | 64 | Notes: 65 | For search type isbn, either ISBN 10 or 13 is accepted. 66 | 67 | Args: 68 | query: Search query. 69 | mode: Search query mode. Can be one of constants.SEARCH_MODES. 70 | page: Result page number. 71 | per_page: Results per page. 72 | 73 | Returns: 74 | List of Library Genesis book IDs that matched the query. 75 | 76 | Raises: 77 | pylibgen.exceptions.LibraryException: unexpected/invalid search query. 78 | """ 79 | if mode not in constants.SEARCH_MODES: 80 | raise exceptions.LibraryException( 81 | "Search mode {} not supported.\nPlease specify one of: {}".format( 82 | mode, ", ".join(constants.SEARCH_MODES), 83 | ), 84 | ) 85 | 86 | if page <= 0: 87 | raise exceptions.LibraryException('page number must be > 0.') 88 | 89 | if per_page not in constants.SEARCH_RESULTS_PER_PAGE: 90 | raise exceptions.LibraryException( 91 | ( 92 | "{} results per page is not supported, sadly.\n" 93 | "Please specify one of: {}" 94 | ).format( 95 | per_page, ", ".join(map(str, constants.SEARCH_RESULTS_PER_PAGE)), 96 | ), 97 | ) 98 | 99 | resp = self.__req( 100 | self.mirror.search, 101 | req=quote_plus(query), 102 | mode=mode, 103 | page=page, 104 | per_page=per_page, 105 | ) 106 | return re.findall(r'(\d+)', resp.text) 107 | 108 | def lookup( 109 | self, 110 | ids: List[Union[str, int]], 111 | fields: List[str] = constants.DEFAULT_BOOK_FIELDS, 112 | ) -> Iterator[Book]: 113 | """Looks up one or more books by id and returns Book objects. 114 | 115 | Notes: 116 | To get book ids, use Library.search(). 117 | The default fields suffice for most use cases, 118 | but there are a lot more like openlibraryid, publisher, etc. 119 | To get all fields, use fields=['*']. 120 | 121 | Args: 122 | ids: Library Genesis book ids. 123 | fields: Library Genesis book properties. 124 | 125 | Returns: 126 | iterator of Book objects corresponding to ids, with the specified 127 | fields filled out and accessible class property. 128 | 129 | Raises: 130 | requests.HTTPError: a 400 is raised if the response is empty. 131 | """ 132 | if isinstance(ids, (str, int)): 133 | ids = [ids] 134 | ids = tuple(map(str, ids)) 135 | 136 | if 'id' not in fields: 137 | fields.append('id') 138 | if '*' in fields: 139 | fields = ['*'] 140 | 141 | resp = self.__req( 142 | self.mirror.lookup, 143 | ids=','.join(ids), 144 | fields=','.join(fields), 145 | ).json() 146 | 147 | if not resp: 148 | # In rare cases, certain queries can result in a [] resp, e.g. 149 | # https://github.com/JoshuaRLi/pylibgen/pull/3 150 | # As of Jan 12 2019 the example in the PR has been resolved, 151 | # but we're going to keep this here just in case. 152 | raise requests.HTTPError(400) 153 | 154 | for book_data, _id in zip(resp, ids): 155 | assert book_data['id'] == _id 156 | yield Book(**book_data) 157 | 158 | def __req(self, endpoint, **kwargs): 159 | r = requests.get(endpoint.format(**constants.SEARCH_BASE_PARAMS, **kwargs)) 160 | r.raise_for_status() 161 | return r 162 | --------------------------------------------------------------------------------