mirror of
				https://github.com/janeczku/calibre-web
				synced 2025-10-30 14:53:01 +00:00 
			
		
		
		
	Add support for displaying author information from Goodreads
Requires the "goodread" module (added to optional-requirements.txt) and an API key Retrieves Goodreads author information and displays their photo and "about" text
This commit is contained in:
		| @@ -53,3 +53,6 @@ span.glyphicon.glyphicon-tags {padding-right: 5px;color: #999;vertical-align: te | ||||
| .spinner2 {margin:0 41%;} | ||||
|  | ||||
| .block-label {display: block;} | ||||
|  | ||||
| .author-bio img {margin: 0 1em 1em 0;} | ||||
| .author-link img {display: inline-block;max-width: 100px;} | ||||
							
								
								
									
										1
									
								
								cps/static/img/goodreads.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cps/static/img/goodreads.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" width="673.826" height="144" viewBox="0 0 673.826 144"><g fill="#5A481C"><path d="M66.66 86.444h-.315c-3.34 14.507-18.19 22.964-32.213 22.964C11.33 109.408 0 91.212 0 70.163c0-22.008 12.146-40.18 35.245-40.18 15.643 0 27.917 10.368 31.1 23.76h.315v-21.85h3.194v79.26C69.854 133.474 57.09 144 35.71 144c-16.576 0-30.78-7.49-31.257-25.843h3.205c.642 16.273 13.08 22.65 27.896 22.65 19.787 0 31.106-9.402 31.106-29.656V86.445zM35.245 33.176c-21.215 0-32.062 17.06-32.062 36.987 0 20.25 10.846 36.062 30.768 36.062 21.065 0 32.558-16.295 32.558-36.062.152-18.825-10.678-36.987-31.263-36.987zM115.787 29.982c23.926 0 36.825 20.58 36.825 42.897 0 22.482-12.898 42.894-36.987 42.894-23.915 0-36.853-20.41-36.853-42.895 0-22.318 12.938-42.898 37.015-42.898zm0 82.598c21.828 0 33.642-18.972 33.642-39.7 0-20.418-11.815-39.704-33.643-39.704-22.176 0-33.81 19.287-33.81 39.703 0 20.728 11.634 39.7 33.81 39.7zM194.57 29.982c23.908 0 36.824 20.58 36.824 42.897 0 22.482-12.916 42.894-36.987 42.894-23.925 0-36.84-20.41-36.84-42.895-.002-22.318 12.914-42.898 37.003-42.898zm0 82.598c21.856 0 33.643-18.972 33.643-39.7 0-20.418-11.786-39.704-33.643-39.704-22.17 0-33.822 19.287-33.822 39.703 0 20.728 11.65 39.7 33.822 39.7zM304.436 0h3.194v113.86h-3.194V90.91h-.326c-4.14 14.337-16.082 24.863-32.837 24.863-21.7 0-34.942-18.027-34.942-42.73 0-22.97 12.3-43.06 34.943-43.06 17.386 0 29.02 10.053 32.837 24.87h.326V0zm-33.163 33.176c-22.493 0-31.736 20.883-31.736 39.866 0 21.04 10.526 39.538 31.736 39.538 21.052 0 33.163-18.32 33.163-39.538 0-25.36-13.236-39.866-33.163-39.866zM323.093 31.58h9.25v19.286h.327c5.103-13.248 16.253-21.052 31.098-20.4v10.042c-18.196-.967-30.62 12.427-30.62 29.492v43.86h-10.054V31.58zM372.38 75.426c.147 14.684 7.806 32.363 27.092 32.363 14.688 0 22.65-8.604 25.832-21.03h10.064c-4.308 18.656-15.16 29.486-35.896 29.486-26.124 0-37.146-20.097-37.146-43.52 0-21.693 11.02-43.543 37.146-43.543 26.483 0 37.032 23.132 36.21 46.243h-63.3zm53.25-8.446c-.462-15.148-9.886-29.363-26.158-29.363-16.42 0-25.495 14.372-27.09 29.363h53.248zM444.297 56.775c.945-19.293 14.508-27.592 33.333-27.592 14.54 0 30.342 4.47 30.342 26.46v43.71c0 3.836 1.923 6.063 5.915 6.063 1.113 0 2.36-.326 3.183-.64v8.445c-2.238.484-3.835.642-6.557.642-10.2 0-11.82-5.735-11.82-14.36h-.28c-7.04 10.683-14.226 16.745-30.05 16.745-15.124 0-27.573-7.467-27.573-24.078 0-23.12 22.48-23.913 44.185-26.46 8.31-.978 12.933-2.09 12.933-11.172 0-13.557-9.728-16.92-21.56-16.92-12.436 0-21.67 5.76-22.03 19.16H444.3zm53.61 12.106h-.314c-1.27 2.397-5.758 3.207-8.457 3.68-17.082 3.024-38.292 2.867-38.292 18.968 0 10.054 8.93 16.262 18.342 16.262 15.317 0 28.89-9.716 28.722-25.82V68.88zM596.488 113.86h-9.232V98.24h-.326c-4.308 10.685-17.386 18.006-29.34 18.006-25.068 0-37.01-20.23-37.01-43.52 0-23.29 11.94-43.543 37.01-43.543 12.27 0 24.223 6.22 28.52 18.016h.348V0h10.03v113.86zm-38.9-6.07c21.356 0 28.868-18.017 28.868-35.063 0-17.083-7.512-35.11-28.868-35.11-19.14 0-26.956 18.027-26.956 35.11 0 17.046 7.817 35.062 26.956 35.062zM660.926 55.645c-.494-12.438-10.043-18.027-21.535-18.027-8.94 0-19.443 3.52-19.443 14.215 0 8.918 10.188 12.112 17.06 13.877l13.395 3.014c11.47 1.76 23.425 8.457 23.425 22.804 0 17.88-17.667 24.72-33.007 24.72-19.14 0-32.208-8.93-33.805-29.016h10.03c.82 13.54 10.864 20.558 24.27 20.558 9.38 0 22.47-4.14 22.47-15.62 0-9.56-8.92-12.745-18.005-14.99l-12.933-2.855c-13.068-3.51-22.976-7.984-22.976-22.008 0-16.745 16.43-23.132 30.95-23.132 16.418 0 29.52 8.625 30.14 26.46h-10.034z"/></g></svg> | ||||
| After Width: | Height: | Size: 3.5 KiB | 
| @@ -2,6 +2,20 @@ var displaytext; | ||||
| var updateTimerID; | ||||
| var updateText; | ||||
|  | ||||
| // Generic control/related handler to show/hide fields based on a checkbox' value | ||||
| // e.g. | ||||
| //  <input type="checkbox" data-control="stuff-to-show"> | ||||
| //  <div data-related="stuff-to-show">...</div> | ||||
| $(document).on("change", "input[type=\"checkbox\"][data-control]", function () { | ||||
|     var $this = $(this); | ||||
|     var name = $this.data("control"); | ||||
|     var showOrHide = $this.prop("checked"); | ||||
|  | ||||
|     $("[data-related=\""+name+"\"]").each(function () { | ||||
|         $(this).toggle(showOrHide); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| $(function() { | ||||
|  | ||||
|     function restartTimer() { | ||||
| @@ -121,6 +135,8 @@ $(function() { | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     $("input[data-control]").trigger("change"); | ||||
|  | ||||
|     $(window).resize(function(event) { | ||||
|         $(".discover .row").isotope("reLayout"); | ||||
|     }); | ||||
|   | ||||
							
								
								
									
										65
									
								
								cps/templates/author.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								cps/templates/author.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| {% extends "layout.html" %} | ||||
| {% block body %} | ||||
| <h2>{{title}}</h2> | ||||
|  | ||||
| {% if author is not none %} | ||||
| <section class="author-bio"> | ||||
|   {%if author['image_url'] is not none %} | ||||
|   <img src="{{author.image_url}}" alt="{{author.name}}" class="author-photo pull-left"> | ||||
|   {% endif %} | ||||
|  | ||||
|   {%if author.about is not none %} | ||||
|   <p>{{author.about|safe}}</p> | ||||
|   {% endif %} | ||||
| </section> | ||||
|  | ||||
| <a href="{{author.link}}" class="author-link" target="_blank"> | ||||
|   <img src="{{ url_for('static', filename='img/goodreads.svg') }}" alt="Goodreads"> | ||||
| </a> | ||||
|  | ||||
| <div class="clearfix"></div> | ||||
| {% endif %} | ||||
|  | ||||
| <div class="discover load-more"> | ||||
|   <div class="row"> | ||||
|     {% if entries[0] %} | ||||
|     {% for entry in entries %} | ||||
|     <div id="books" class="col-sm-3 col-lg-2 col-xs-6 book"> | ||||
|       <div class="cover"> | ||||
|         <a href="{{ url_for('show_book', book_id=entry.id) }}"> | ||||
|           {% if entry.has_cover %} | ||||
|           <img src="{{ url_for('get_cover', cover_path=entry.path.replace('\\','/')) }}" /> | ||||
|           {% else %} | ||||
|           <img src="{{ url_for('static', filename='generic_cover.jpg') }}" /> | ||||
|           {% endif %} | ||||
|         </a> | ||||
|       </div> | ||||
|       <div class="meta"> | ||||
|         <p class="title">{{entry.title|shortentitle}}</p> | ||||
|         <p class="author"> | ||||
|           {% for author in entry.authors %} | ||||
|           <a href="{{url_for('author', book_id=author.id) }}">{{author.name}}</a> | ||||
|           {% if not loop.last %} | ||||
|           & | ||||
|           {% endif %} | ||||
|           {% endfor %} | ||||
|         </p> | ||||
|         {% if entry.ratings.__len__() > 0 %} | ||||
|         <div class="rating"> | ||||
|           {% for number in range((entry.ratings[0].rating/2)|int(2)) %} | ||||
|           <span class="glyphicon glyphicon-star good"></span> | ||||
|           {% if loop.last and loop.index < 5 %} | ||||
|           {% for numer in range(5 - loop.index) %} | ||||
|           <span class="glyphicon glyphicon-star"></span> | ||||
|           {% endfor %} | ||||
|           {% endif %} | ||||
|           {% endfor %} | ||||
|         </div> | ||||
|         {% endif %} | ||||
|       </div> | ||||
|     </div> | ||||
|     {% endfor %} | ||||
|     {% endif %} | ||||
|   </div> | ||||
| </div> | ||||
| {% endblock %} | ||||
| @@ -97,6 +97,24 @@ | ||||
|       <input type="checkbox" id="config_remote_login" name="config_remote_login" {% if content.config_remote_login %}checked{% endif %}> | ||||
|       <label for="config_remote_login">{{_('Enable remote login ("magic link")')}}</label> | ||||
|     </div> | ||||
|     {% if goodreads %} | ||||
|     <div class="form-group"> | ||||
|       <input type="checkbox" id="config_use_goodreads" name="config_use_goodreads" data-control="goodreads-settings" {% if content.config_use_goodreads %}checked{% endif %}> | ||||
|       <label for="config_use_goodreads">{{_('Use')}} Goodreads</label> | ||||
|       <a href="https://www.goodreads.com/api/keys" target="_blank" style="margin-left: 5px">{{_('Obtain an API Key')}}</a> | ||||
|     </div> | ||||
|     <div data-related="goodreads-settings"> | ||||
|       <div class="form-group"> | ||||
|         <label for="config_goodreads_api_key">{{_('Goodreads API Key')}}</label> | ||||
|         <input type="text" class="form-control" id="config_goodreads_api_key" name="config_goodreads_api_key" value="{% if content.config_goodreads_api_key != None %}{{ content.config_goodreads_api_key }}{% endif %}" autocomplete="off"> | ||||
|       </div> | ||||
|       <div class="form-group"> | ||||
|         <label for="config_goodreads_api_secret">{{_('Goodreads API Secret')}}</label> | ||||
|         <input type="text" class="form-control" id="config_goodreads_api_secret" name="config_goodreads_api_secret" value="{% if content.config_goodreads_api_secret != None %}{{ content.config_goodreads_api_secret }}{% endif %}" autocomplete="off"> | ||||
|       </div> | ||||
|     </div> | ||||
|     {% endif %} | ||||
|  | ||||
|     <h2>{{_('Default Settings for new users')}}</h2> | ||||
|     <div class="form-group"> | ||||
|       <input type="checkbox" name="admin_role" id="admin_role" {% if content.role_admin() %}checked{% endif %}> | ||||
|   | ||||
							
								
								
									
										13
									
								
								cps/ub.py
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								cps/ub.py
									
									
									
									
									
								
							| @@ -263,6 +263,9 @@ class Settings(Base): | ||||
|     config_google_drive_watch_changes_response = Column(String) | ||||
|     config_columns_to_ignore = Column(String) | ||||
|     config_remote_login = Column(Boolean) | ||||
|     config_use_goodreads = Column(Boolean) | ||||
|     config_goodreads_api_key = Column(String) | ||||
|     config_goodreads_api_secret = Column(String) | ||||
|  | ||||
|     def __repr__(self): | ||||
|         pass | ||||
| @@ -320,6 +323,9 @@ class Config: | ||||
|         self.db_configured = bool(self.config_calibre_dir is not None and | ||||
|                 (not self.config_use_google_drive or os.path.exists(self.config_calibre_dir + '/metadata.db'))) | ||||
|         self.config_remote_login = data.config_remote_login | ||||
|         self.config_use_goodreads = data.config_use_goodreads | ||||
|         self.config_goodreads_api_key = data.config_goodreads_api_key | ||||
|         self.config_goodreads_api_secret = data.config_goodreads_api_secret | ||||
|  | ||||
|     @property | ||||
|     def get_main_dir(self): | ||||
| @@ -475,6 +481,13 @@ def migrate_Database(): | ||||
|     except exc.OperationalError: | ||||
|         conn = engine.connect() | ||||
|         conn.execute("ALTER TABLE Settings ADD column `config_remote_login` INTEGER DEFAULT 0") | ||||
|     try: | ||||
|         session.query(exists().where(Settings.config_use_goodreads)).scalar() | ||||
|     except exc.OperationalError: | ||||
|         conn = engine.connect() | ||||
|         conn.execute("ALTER TABLE Settings ADD column `config_use_goodreads` INTEGER DEFAULT 0") | ||||
|         conn.execute("ALTER TABLE Settings ADD column `config_goodreads_api_key` String DEFAULT ''") | ||||
|         conn.execute("ALTER TABLE Settings ADD column `config_goodreads_api_secret` String DEFAULT ''") | ||||
|  | ||||
| def clean_database(): | ||||
|     # Remove expired remote login tokens | ||||
|   | ||||
							
								
								
									
										35
									
								
								cps/web.py
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								cps/web.py
									
									
									
									
									
								
							| @@ -7,6 +7,12 @@ try: | ||||
| except ImportError: | ||||
|     gdrive_support = False | ||||
|  | ||||
| try: | ||||
|     from goodreads import client as gr_client | ||||
|     goodreads_support = True | ||||
| except ImportError: | ||||
|     goodreads_support = False | ||||
|  | ||||
| import mimetypes | ||||
| import logging | ||||
| from logging.handlers import RotatingFileHandler | ||||
| @@ -1086,10 +1092,16 @@ def author_list(): | ||||
| def author(book_id, page): | ||||
|     entries, random, pagination = fill_indexpage(page, db.Books, db.Books.authors.any(db.Authors.id == book_id), | ||||
|                                                  db.Books.timestamp.desc()) | ||||
|     name = db.session.query(db.Authors).filter(db.Authors.id == book_id).first().name | ||||
|     if entries: | ||||
|         return render_title_template('index.html', random=random, entries=entries, pagination=pagination, | ||||
|                                      title=_(u"Author: %(name)s", name=name)) | ||||
|         name = db.session.query(db.Authors).filter(db.Authors.id == book_id).first().name | ||||
|  | ||||
|         author_info = None | ||||
|         if goodreads_support and config.config_use_goodreads: | ||||
|             gc = gr_client.GoodreadsClient(config.config_goodreads_api_key, config.config_goodreads_api_secret) | ||||
|             author_info = gc.find_author(author_name=name) | ||||
|  | ||||
|         return render_title_template('author.html', entries=entries, pagination=pagination, | ||||
|                                      title=name, author=author_info) | ||||
|     else: | ||||
|         flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error") | ||||
|         return redirect(url_for("index")) | ||||
| @@ -2289,11 +2301,20 @@ def configuration_helper(origin): | ||||
|             content.config_anonbrowse = 1 | ||||
|         if "config_public_reg" in to_save and to_save["config_public_reg"] == "on": | ||||
|             content.config_public_reg = 1 | ||||
|         content.config_remote_login = ("config_remote_login" in to_save and to_save["config_remote_login"] == "on") | ||||
|  | ||||
|         # Remote login configuration | ||||
|         content.config_remote_login = ("config_remote_login" in to_save and to_save["config_remote_login"] == "on") | ||||
|         if not content.config_remote_login: | ||||
|             ub.session.query(ub.RemoteAuthToken).delete() | ||||
|  | ||||
|         # Goodreads configuration | ||||
|         content.config_use_goodreads = ("config_use_goodreads" in to_save and to_save["config_use_goodreads"] == "on") | ||||
|         if "config_goodreads_api_key" in to_save: | ||||
|             content.config_goodreads_api_key = to_save["config_goodreads_api_key"] | ||||
|         if "config_goodreads_api_secret" in to_save: | ||||
|             content.config_goodreads_api_secret = to_save["config_goodreads_api_secret"] | ||||
|  | ||||
|  | ||||
|         content.config_default_role = 0 | ||||
|         if "admin_role" in to_save: | ||||
|             content.config_default_role = content.config_default_role + ub.ROLE_ADMIN | ||||
| @@ -2324,13 +2345,13 @@ def configuration_helper(origin): | ||||
|         except e: | ||||
|             flash(e, category="error") | ||||
|             return render_title_template("config_edit.html", content=config, origin=origin, gdrive=gdrive_support, | ||||
|                                          title=_(u"Basic Configuration")) | ||||
|                                          goodreads=goodreads_support, title=_(u"Basic Configuration")) | ||||
|         if db_change: | ||||
|             reload(db) | ||||
|             if not db.setup_db(): | ||||
|                 flash(_(u'DB location is not valid, please enter correct path'), category="error") | ||||
|                 return render_title_template("config_edit.html", content=config, origin=origin, gdrive=gdrive_support, | ||||
|                                              title=_(u"Basic Configuration")) | ||||
|                                              goodreads=goodreads_support, title=_(u"Basic Configuration")) | ||||
|         if reboot_required: | ||||
|             # db.engine.dispose() # ToDo verify correct | ||||
|             ub.session.close() | ||||
| @@ -2344,7 +2365,7 @@ def configuration_helper(origin): | ||||
|             success = True | ||||
|     return render_title_template("config_edit.html", origin=origin, success=success, content=config, | ||||
|                                  show_authenticate_google_drive=not is_gdrive_ready(), gdrive=gdrive_support, | ||||
|                                  title=_(u"Basic Configuration")) | ||||
|                                  goodreads=goodreads_support, title=_(u"Basic Configuration")) | ||||
|  | ||||
|  | ||||
| @app.route("/admin/user/new", methods=["GET", "POST"]) | ||||
|   | ||||
| @@ -11,3 +11,4 @@ PyYAML==3.12 | ||||
| rsa==3.4.2 | ||||
| six==1.10.0 | ||||
| uritemplate==3.0.0 | ||||
| goodreads==0.3.2 | ||||
		Reference in New Issue
	
	Block a user
	 Jonathan Rehm
					Jonathan Rehm