Skip to content

Commit c63ac6a

Browse files
committed
Feature: eBook uploads
- Goodreads & Google Books integration working - Description generation working - Upload form untested
1 parent 15ea346 commit c63ac6a

File tree

5 files changed

+292
-0
lines changed

5 files changed

+292
-0
lines changed

pythonbits/bb.py

Lines changed: 130 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
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,133 @@ def _render_form_description(self):
930933
return self['description']
931934

932935

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

pythonbits/calibre.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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 %s, please ensure it is installed (via Calibre)." % COMMAND)
22+
23+
24+
def read_metadata(path):
25+
version = get_version()
26+
log.debug('Found ebook-meta version: %s' % version)
27+
log.info("Trying to read eBook metadata...")
28+
29+
output = subprocess.check_output(
30+
'{} "{}"'.format(COMMAND, path), shell=True)
31+
result = {}
32+
for row in output.decode('utf8').split('\n'):
33+
if ': ' in row:
34+
try:
35+
key, value = row.split(': ')
36+
result[key.strip(' .')] = value.strip()
37+
except:
38+
pass
39+
return result

pythonbits/goodreads.py

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

pythonbits/googlebooks.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
10+
def find_cover(isbn):
11+
path = 'volumes?q=isbn:{}'.format(isbn)
12+
resp = requests.get(API_URL+path)
13+
log.debug('Fetching alt cover art from {}'.format(resp.url))
14+
if resp.status_code == 200:
15+
content = json.loads(resp.content)
16+
return content['items'][0]
17+
['volumeInfo']['imageLinks']['thumbnail'] or ''
18+
else:
19+
log.warn('Couldn\'t find cover art for ISBN {}'.format(isbn))
20+
return ''

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ 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"
4748
],
4849
python_requires=">=3.5,<3.9",
4950
tests_require=['tox', 'pytest', 'flake8', 'pytest-logbook'],

0 commit comments

Comments
 (0)