Skip to content

Commit faeb971

Browse files
committed
Feature: eBook uploads
1 parent 15ea346 commit faeb971

File tree

5 files changed

+351
-0
lines changed

5 files changed

+351
-0
lines changed

pythonbits/bb.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
from . import imdb
2525
from . import musicbrainz as mb
2626
from . import imagehosting
27+
from . import goodreads
28+
from .googlebooks import find_cover, find_categories
2729
from .ffmpeg import FFMpeg
2830
from . import templating as bb
2931
from .submission import (Submission, form_field, finalize, cat_map,
@@ -150,6 +152,7 @@ def copy(source, target):
150152
'movie': ['hard', 'sym', 'copy', 'move'],
151153
'tv': ['hard', 'sym', 'copy', 'move'],
152154
'music': ['copy', 'move'],
155+
'book': ['copy', 'move'],
153156
}
154157

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

932935

936+
class BookSubmission(BbSubmission):
937+
938+
_cat_id = 'book'
939+
_form_type = 'E-Books'
940+
941+
def _desc(self):
942+
s = self['summary']
943+
return re.sub('<[^<]+?>', '', s['description'])
944+
945+
@form_field('book_retail', 'checkbox')
946+
def _render_retail(self):
947+
return bool(
948+
input('Is this a retail release? [y/N] ').lower()
949+
== 'y')
950+
951+
@form_field('book_language')
952+
def _render_language(self):
953+
return self['summary']['language']
954+
955+
@form_field('book_publisher')
956+
def _render_publisher(self):
957+
return self['summary']['publisher']
958+
959+
@form_field('book_author')
960+
def _render_author(self):
961+
return self['summary']['authors'][0]['name']
962+
963+
@form_field('book_format')
964+
def _render_format(self):
965+
book_format = {
966+
'EPUB': 'EPUB',
967+
'MOBI': 'MOBI',
968+
'PDF': 'PDF',
969+
'HTML': 'HTML',
970+
'TXT': 'TXT',
971+
'DJVU': 'DJVU',
972+
'CHM': 'CHM',
973+
'CBR': 'CBR',
974+
'CBZ': 'CBZ',
975+
'CB7': 'CB7',
976+
'TXT': 'TXT',
977+
'AZW3': 'AZW3',
978+
}
979+
980+
_, ext = os.path.splitext(self['path'])
981+
return book_format[ext.replace('.', '').upper()]
982+
983+
def _render_summary(self):
984+
gr = goodreads.Goodreads()
985+
return gr.search(self['path'])
986+
987+
@form_field('book_year')
988+
def _render_year(self):
989+
if 'summary' in self.fields:
990+
return self['summary']['publication_year']
991+
else:
992+
while True:
993+
year = input('Please enter year: ')
994+
try:
995+
year = int(year)
996+
except ValueError:
997+
pass
998+
else:
999+
return year
1000+
1001+
@form_field('book_isbn')
1002+
def _render_isbn(self):
1003+
if 'summary' in self.fields:
1004+
return self['summary'].get('isbn', '')
1005+
1006+
@form_field('title')
1007+
def _render_form_title(self):
1008+
if 'summary' in self.fields:
1009+
return self['summary'].get('title', '')
1010+
1011+
@form_field('tags')
1012+
def _render_tags(self):
1013+
categories = find_categories(self['summary']['isbn'])
1014+
authors = self['summary']['authors']
1015+
return ",".join(uniq(list(format_tag(a['name']) for a in authors) +
1016+
list(format_tag(a) for a in categories)))
1017+
1018+
def _render_section_information(self):
1019+
def gr_author_link(gra):
1020+
return bb.link(gra['name'], gra['link'])
1021+
1022+
book = self['summary']
1023+
links = [("Goodreads", book['url'])]
1024+
1025+
return dedent("""\
1026+
[b]Title[/b]: {title} ({links})
1027+
[b]ISBN[/b]: {isbn}
1028+
[b]Publisher[/b]: {publisher}
1029+
[b]Publication Year[/b]: {publication_year}
1030+
[b]Rating[/b]: {rating} [size=1]({ratings_count} ratings)[/size]
1031+
[b]Author(s)[/b]: {authors}""").format(
1032+
links=", ".join(bb.link(*l) for l in links),
1033+
title=book['title'],
1034+
isbn=book['isbn'],
1035+
publisher=book['publisher'],
1036+
publication_year=book['publication_year'],
1037+
rating=bb.format_rating(float(book['average_rating']),
1038+
max=5),
1039+
ratings_count=book['ratings_count'],
1040+
authors=" | ".join(gr_author_link(a) for a in book['authors'])
1041+
)
1042+
1043+
def _render_section_description(self):
1044+
return self._desc()
1045+
1046+
@form_field('desc')
1047+
def _render_description(self):
1048+
sections = [("Description", self['section_description']),
1049+
("Information", self['section_information'])]
1050+
1051+
description = "\n".join(bb.section(*s) for s in sections)
1052+
description += bb.release
1053+
1054+
return description
1055+
1056+
@finalize
1057+
@form_field('image')
1058+
def _render_cover(self):
1059+
# Goodreads usually won't give you a cover image as they don't have the
1060+
# the right to distribute them
1061+
if 'nophoto' in self['summary']['image_url']:
1062+
return find_cover(self['summary']['isbn'])
1063+
else:
1064+
return self['summary']['image_url']
1065+
1066+
def _finalize_cover(self):
1067+
return imagehosting.upload(self['cover'])
1068+
1069+
9331070
class AudioSubmission(BbSubmission):
9341071
default_fields = ("description", "form_tags", "year", "cover",
9351072
"title", "format", "bitrate")

pythonbits/calibre.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# -*- coding: utf-8 -*-
2+
import subprocess
3+
4+
from .logging import log
5+
6+
COMMAND = "ebook-meta"
7+
8+
9+
class EbookMetaException(Exception):
10+
pass
11+
12+
13+
def get_version():
14+
try:
15+
ebook_meta = subprocess.Popen(
16+
[COMMAND, '--version'],
17+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
18+
return ebook_meta.communicate()[0].decode('utf8')
19+
except OSError:
20+
raise EbookMetaException(
21+
"Could not find {}, please ensure it is installed (via Calibre)."
22+
.format(COMMAND))
23+
24+
25+
def read_metadata(path):
26+
version = get_version()
27+
log.debug('Found ebook-meta version: %s' % version)
28+
log.info("Trying to read eBook metadata...")
29+
30+
output = subprocess.check_output(
31+
'{} "{}"'.format(COMMAND, path), shell=True)
32+
result = {}
33+
for row in output.decode('utf8').split('\n'):
34+
if ': ' in row:
35+
try:
36+
key, value = row.split(': ')
37+
result[key.strip(' .')] = value.strip()
38+
except ValueError:
39+
pass
40+
return result

pythonbits/goodreads.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# -*- coding: utf-8 -*-
2+
from textwrap import dedent
3+
4+
import goodreads_api_client as gr
5+
import pycountry
6+
7+
from .config import config
8+
from .logging import log
9+
from .calibre import read_metadata
10+
from collections import OrderedDict
11+
12+
config.register(
13+
'Goodreads', 'api_key',
14+
dedent("""\
15+
To find your Goodreads API key, login to https://www.goodreads.com/api/keys
16+
Enter the API Key below
17+
API Key"""))
18+
19+
20+
def _extract_authors(authors):
21+
if isinstance(authors['author'], OrderedDict):
22+
return [{
23+
'name': authors['author']['name'],
24+
'link': authors['author']['link']
25+
}]
26+
else:
27+
return [_extract_author(auth)
28+
for auth in authors['author']]
29+
30+
31+
def _extract_author(auth):
32+
return {
33+
'name': auth['name'],
34+
'link': auth['link']
35+
}
36+
37+
38+
def _extract_language(alpha_3):
39+
return pycountry.languages.get(alpha_3=alpha_3).name
40+
41+
42+
def _process_book(books):
43+
keys_wanted = ['id', 'title', 'isbn', 'isbn13', 'description',
44+
'language_code', 'publication_year', 'publisher',
45+
'image_url', 'url', 'authors', 'average_rating', 'work']
46+
book = {k: v for k, v in books if k in keys_wanted}
47+
book['authors'] = _extract_authors(book['authors'])
48+
book['ratings_count'] = int(book['work']['ratings_count']['#text'])
49+
book['language'] = _extract_language(book['language_code'])
50+
return book
51+
52+
53+
class Goodreads(object):
54+
def __init__(self, interactive=True):
55+
self.goodreads = gr.Client(
56+
developer_key=config.get('Goodreads', 'api_key'))
57+
58+
def show_by_isbn(self, isbn):
59+
return _process_book(self.goodreads.Book.show_by_isbn(
60+
isbn).items())
61+
62+
def search(self, path):
63+
64+
book = read_metadata(path)
65+
isbn = ''
66+
try:
67+
isbn = book['Identifiers'].split(':')[1]
68+
except KeyError:
69+
pass
70+
71+
if isbn:
72+
log.debug("Searching Goodreads by ISBN {} for '{}'",
73+
isbn, book['Title'])
74+
return self.show_by_isbn(isbn)
75+
elif book['Title']:
76+
search_term = book['Title']
77+
log.debug(
78+
"Searching Goodreads by Title only for '{}'", search_term)
79+
book_results = self.goodreads.search_book(search_term)
80+
print("Results:")
81+
for i, book in enumerate(book_results['results']['work']):
82+
print('{}: {} by {} ({})'
83+
.format(i, book['best_book']['title'],
84+
book['best_book']['author']['name'],
85+
book['original_publication_year']
86+
.get('#text', '')))
87+
88+
while True:
89+
choice = input('Select number or enter an alternate'
90+
' search term'
91+
' (or an ISBN with isbn: prefix):'
92+
' [0-{}, 0 default] '
93+
.format(
94+
len(book_results['results']['work']) - 1))
95+
try:
96+
choice = int(choice)
97+
except ValueError:
98+
if choice:
99+
return self.show_by_isbn(choice.replace('isbn:', ''))
100+
choice = 0
101+
102+
try:
103+
result = book_results['results']['work'][choice]
104+
except IndexError:
105+
pass
106+
else:
107+
id = result['best_book']['id'].get('#text', '')
108+
log.debug("Selected Goodreads item {}", id)
109+
log.debug("Searching Goodreads by ID {}", id)
110+
return _process_book(self.goodreads.Book.show(
111+
id).items())

pythonbits/googlebooks.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# -*- coding: utf-8 -*-
2+
import requests
3+
import json
4+
5+
from .logging import log
6+
7+
API_URL = 'https://www.googleapis.com/books/v1/'
8+
9+
cache = {}
10+
11+
12+
def find_cover(isbn):
13+
if _get_or_set(key=isbn):
14+
return _extract_cover(cache[isbn])
15+
16+
path = 'volumes?q=isbn:{}'.format(isbn)
17+
resp = requests.get(API_URL+path)
18+
log.debug('Fetching alt cover art from {}'.format(resp.url))
19+
if resp.status_code == 200:
20+
content = json.loads(resp.content)
21+
_get_or_set(key=isbn, value=content)
22+
return _extract_cover(content)
23+
else:
24+
log.warn('Couldn\'t find cover art for ISBN {}'.format(isbn))
25+
return ''
26+
27+
28+
def find_categories(isbn):
29+
if _get_or_set(key=isbn):
30+
return _extract_categories(cache[isbn])
31+
32+
path = 'volumes?q=isbn:{}'.format(isbn)
33+
resp = requests.get(API_URL+path)
34+
log.debug('Fetching categories from {}'.format(resp.url))
35+
if resp.status_code == 200:
36+
content = json.loads(resp.content)
37+
_get_or_set(key=isbn, value=content)
38+
return _extract_categories(content)
39+
else:
40+
log.warn('Couldn\'t find categories for ISBN {}'.format(isbn))
41+
return ''
42+
43+
44+
def _get_or_set(**kwargs):
45+
value = kwargs.get('value', None)
46+
key = kwargs.get('key', None)
47+
if value:
48+
cache[key] = value
49+
return value
50+
elif key in cache:
51+
return cache[key]
52+
53+
54+
def _extract_categories(book):
55+
return (book['items'][0]['volumeInfo']
56+
['categories'] or '')
57+
58+
59+
def _extract_cover(book):
60+
return (book['items'][0]['volumeInfo']
61+
['imageLinks']['thumbnail'] or '')

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ def find_version(*file_paths):
4444
"mutagen~=1.44",
4545
"musicbrainzngs~=0.7",
4646
"terminaltables~=3.1",
47+
"goodreads_api_client~=0.1.0.dev4",
48+
"pycountry~=20.7.3"
4749
],
4850
python_requires=">=3.5,<3.9",
4951
tests_require=['tox', 'pytest', 'flake8', 'pytest-logbook'],

0 commit comments

Comments
 (0)