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

Book Submissions #114

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
137 changes: 137 additions & 0 deletions pythonbits/bb.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
from . import imdb
from . import musicbrainz as mb
from . import imagehosting
from . import goodreads
from .googlebooks import find_cover, find_categories
from .ffmpeg import FFMpeg
from . import templating as bb
from .submission import (Submission, form_field, finalize, cat_map,
Expand Down Expand Up @@ -150,6 +152,7 @@ def copy(source, target):
'movie': ['hard', 'sym', 'copy', 'move'],
'tv': ['hard', 'sym', 'copy', 'move'],
'music': ['copy', 'move'],
'book': ['copy', 'move'],
}

method_map = {'hard': os.link,
Expand Down Expand Up @@ -930,6 +933,140 @@ def _render_form_description(self):
return self['description']


class BookSubmission(BbSubmission):

_cat_id = 'book'
_form_type = 'E-Books'

def _desc(self):
s = self['summary']
return re.sub('<[^<]+?>', '', s['description'])

@form_field('book_retail', 'checkbox')
def _render_retail(self):
return bool(
input('Is this a retail release? [y/N] ').lower()
== 'y')

@form_field('book_language')
def _render_language(self):
return self['summary']['language']

@form_field('book_publisher')
def _render_publisher(self):
return self['summary']['publisher']

@form_field('book_author')
def _render_author(self):
return self['summary']['authors'][0]['name']

@form_field('book_format')
def _render_format(self):
book_format = {
'EPUB': 'EPUB',
'MOBI': 'MOBI',
'PDF': 'PDF',
'HTML': 'HTML',
'TXT': 'TXT',
'DJVU': 'DJVU',
'CHM': 'CHM',
'CBR': 'CBR',
'CBZ': 'CBZ',
'CB7': 'CB7',
'TXT': 'TXT',
'AZW3': 'AZW3',
}
znedw marked this conversation as resolved.
Show resolved Hide resolved

_, ext = os.path.splitext(self['path'])
return book_format[ext.replace('.', '').upper()]

def _render_summary(self):
gr = goodreads.Goodreads()
return gr.search(self['path'])

@form_field('book_year')
def _render_year(self):
if 'summary' in self.fields:
return self['summary']['publication_year']
else:
while True:
year = input('Please enter year: ')
try:
year = int(year)
except ValueError:
pass
else:
return year

@form_field('book_isbn')
def _render_isbn(self):
if 'summary' in self.fields:
return self['summary'].get('isbn', '')

@form_field('title')
def _render_form_title(self):
if 'summary' in self.fields:
return self['summary'].get('title', '')

@form_field('tags')
def _render_tags(self):
categories = find_categories(self['summary']['isbn'])
authors = self['summary']['authors']
return ",".join(uniq(list(format_tag(a['name']) for a in authors) +
list(format_tag(a) for a in categories)))

def _render_section_information(self):
def gr_author_link(gra):
return bb.link(gra['name'], gra['link'])

book = self['summary']
links = [("Goodreads", book['url'])]

return dedent("""\
[b]Title[/b]: {title} ({links})
[b]ISBN[/b]: {isbn}
[b]Publisher[/b]: {publisher}
[b]Publication Year[/b]: {publication_year}
[b]Rating[/b]: {rating} [size=1]({ratings_count} ratings)[/size]
[b]Author(s)[/b]: {authors}""").format(
links=", ".join(bb.link(*l) for l in links),
title=book['title'],
isbn=book['isbn'],
publisher=book['publisher'],
publication_year=book['publication_year'],
rating=bb.format_rating(float(book['average_rating']),
max=5),
ratings_count=book['ratings_count'],
authors=" | ".join(gr_author_link(a) for a in book['authors'])
)

def _render_section_description(self):
return self._desc()

@form_field('desc')
def _render_description(self):
sections = [("Description", self['section_description']),
("Information", self['section_information'])]

description = "\n".join(bb.section(*s) for s in sections)
description += bb.release

return description

@finalize
@form_field('image')
def _render_cover(self):
# Goodreads usually won't give you a cover image as they don't have the
# the right to distribute them
if 'nophoto' in self['summary']['image_url']:
return find_cover(self['summary']['isbn'])
else:
return self['summary']['image_url']

def _finalize_cover(self):
return imagehosting.upload(self['cover'])


class AudioSubmission(BbSubmission):
default_fields = ("description", "form_tags", "year", "cover",
"title", "format", "bitrate")
Expand Down
40 changes: 40 additions & 0 deletions pythonbits/calibre.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
import subprocess

from .logging import log

COMMAND = "ebook-meta"


class EbookMetaException(Exception):
pass


def get_version():
try:
ebook_meta = subprocess.Popen(
[COMMAND, '--version'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return ebook_meta.communicate()[0].decode('utf8')
except OSError:
raise EbookMetaException(
"Could not find {}, please ensure it is installed (via Calibre)."
.format(COMMAND))


def read_metadata(path):
version = get_version()
log.debug('Found ebook-meta version: %s' % version)
log.info("Trying to read eBook metadata...")

output = subprocess.check_output(
'{} "{}"'.format(COMMAND, path), shell=True)
result = {}
for row in output.decode('utf8').split('\n'):
if ': ' in row:
try:
key, value = row.split(': ')
result[key.strip(' .')] = value.strip()
except ValueError:
pass
return result
111 changes: 111 additions & 0 deletions pythonbits/goodreads.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
from textwrap import dedent

import goodreads_api_client as gr
import pycountry

from .config import config
from .logging import log
from .calibre import read_metadata
from collections import OrderedDict

config.register(
'Goodreads', 'api_key',
dedent("""\
To find your Goodreads API key, login to https://www.goodreads.com/api/keys
Enter the API Key below
API Key"""))


def _extract_authors(authors):
if isinstance(authors['author'], OrderedDict):
return [{
'name': authors['author']['name'],
'link': authors['author']['link']
}]
else:
return [_extract_author(auth)
for auth in authors['author']]


def _extract_author(auth):
return {
'name': auth['name'],
'link': auth['link']
}


def _extract_language(alpha_3):
return pycountry.languages.get(alpha_3=alpha_3).name


def _process_book(books):
keys_wanted = ['id', 'title', 'isbn', 'isbn13', 'description',
'language_code', 'publication_year', 'publisher',
'image_url', 'url', 'authors', 'average_rating', 'work']
book = {k: v for k, v in books if k in keys_wanted}
book['authors'] = _extract_authors(book['authors'])
book['ratings_count'] = int(book['work']['ratings_count']['#text'])
book['language'] = _extract_language(book['language_code'])
return book


class Goodreads(object):
def __init__(self, interactive=True):
self.goodreads = gr.Client(
developer_key=config.get('Goodreads', 'api_key'))

def show_by_isbn(self, isbn):
return _process_book(self.goodreads.Book.show_by_isbn(
isbn).items())

def search(self, path):

book = read_metadata(path)
isbn = ''
try:
isbn = book['Identifiers'].split(':')[1]
except KeyError:
pass

if isbn:
log.debug("Searching Goodreads by ISBN {} for '{}'",
isbn, book['Title'])
return self.show_by_isbn(isbn)
elif book['Title']:
search_term = book['Title']
log.debug(
"Searching Goodreads by Title only for '{}'", search_term)
book_results = self.goodreads.search_book(search_term)
print("Results:")
for i, book in enumerate(book_results['results']['work']):
print('{}: {} by {} ({})'
.format(i, book['best_book']['title'],
book['best_book']['author']['name'],
book['original_publication_year']
.get('#text', '')))

while True:
choice = input('Select number or enter an alternate'
' search term'
' (or an ISBN with isbn: prefix):'
' [0-{}, 0 default] '
.format(
len(book_results['results']['work']) - 1))
try:
choice = int(choice)
except ValueError:
if choice:
return self.show_by_isbn(choice.replace('isbn:', ''))
choice = 0

try:
result = book_results['results']['work'][choice]
except IndexError:
pass
else:
id = result['best_book']['id'].get('#text', '')
log.debug("Selected Goodreads item {}", id)
log.debug("Searching Goodreads by ID {}", id)
return _process_book(self.goodreads.Book.show(
id).items())
61 changes: 61 additions & 0 deletions pythonbits/googlebooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
import requests
import json

from .logging import log

API_URL = 'https://www.googleapis.com/books/v1/'

cache = {}


def find_cover(isbn):
if _get_or_set(key=isbn):
return _extract_cover(cache[isbn])

path = 'volumes?q=isbn:{}'.format(isbn)
resp = requests.get(API_URL+path)
log.debug('Fetching alt cover art from {}'.format(resp.url))
if resp.status_code == 200:
content = json.loads(resp.content)
_get_or_set(key=isbn, value=content)
return _extract_cover(content)
else:
log.warn('Couldn\'t find cover art for ISBN {}'.format(isbn))
return ''


def find_categories(isbn):
if _get_or_set(key=isbn):
return _extract_categories(cache[isbn])

path = 'volumes?q=isbn:{}'.format(isbn)
resp = requests.get(API_URL+path)
log.debug('Fetching categories from {}'.format(resp.url))
if resp.status_code == 200:
content = json.loads(resp.content)
_get_or_set(key=isbn, value=content)
return _extract_categories(content)
else:
log.warn('Couldn\'t find categories for ISBN {}'.format(isbn))
return ''


def _get_or_set(**kwargs):
value = kwargs.get('value', None)
key = kwargs.get('key', None)
if value:
cache[key] = value
return value
elif key in cache:
return cache[key]


def _extract_categories(book):
return (book['items'][0]['volumeInfo']
['categories'] or '')


def _extract_cover(book):
return (book['items'][0]['volumeInfo']
['imageLinks']['thumbnail'] or '')
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ def find_version(*file_paths):
"mutagen~=1.44",
"musicbrainzngs~=0.7",
"terminaltables~=3.1",
"goodreads_api_client~=0.1.0.dev4",
"pycountry~=20.7.3"
],
python_requires=">=3.5,<3.9",
tests_require=['tox', 'pytest', 'flake8', 'pytest-logbook'],
Expand Down