mirror of
				https://github.com/janeczku/calibre-web
				synced 2025-10-31 07:13:02 +00:00 
			
		
		
		
	Merge branch 'master' into Develop
# Conflicts: # cps/book_formats.py # cps/helper.py # cps/web.py
This commit is contained in:
		| @@ -35,6 +35,7 @@ from babel import Locale as LC | |||||||
| from babel import negotiate_locale | from babel import negotiate_locale | ||||||
| import os | import os | ||||||
| import ub | import ub | ||||||
|  | import sys | ||||||
| from ub import Config, Settings | from ub import Config, Settings | ||||||
| try: | try: | ||||||
|     import cPickle |     import cPickle | ||||||
| @@ -72,8 +73,14 @@ config = Config() | |||||||
|  |  | ||||||
| import db | import db | ||||||
|  |  | ||||||
| with open(os.path.join(config.get_main_dir, 'cps/translations/iso639.pickle'), 'rb') as f: | try: | ||||||
|    language_table = cPickle.load(f) |     with open(os.path.join(config.get_main_dir, 'cps/translations/iso639.pickle'), 'rb') as f: | ||||||
|  |         language_table = cPickle.load(f) | ||||||
|  | except cPickle.UnpicklingError as error: | ||||||
|  |     # app.logger.error("Can't read file cps/translations/iso639.pickle: %s", error) | ||||||
|  |     print("Can't read file cps/translations/iso639.pickle: %s" % error) | ||||||
|  |     sys.exit(1) | ||||||
|  |  | ||||||
|  |  | ||||||
| searched_ids = {} | searched_ids = {} | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								cps/db.py
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								cps/db.py
									
									
									
									
									
								
							| @@ -27,6 +27,7 @@ import ast | |||||||
| from cps import config | from cps import config | ||||||
| import ub | import ub | ||||||
| import sys | import sys | ||||||
|  | import unidecode | ||||||
|  |  | ||||||
| session = None | session = None | ||||||
| cc_exceptions = ['datetime', 'comments', 'float', 'composite', 'series'] | cc_exceptions = ['datetime', 'comments', 'float', 'composite', 'series'] | ||||||
| @@ -46,7 +47,7 @@ def title_sort(title): | |||||||
|  |  | ||||||
|  |  | ||||||
| def lcase(s): | def lcase(s): | ||||||
|     return s.lower() |     return unidecode.unidecode(s.lower()) | ||||||
|  |  | ||||||
|  |  | ||||||
| def ucase(s): | def ucase(s): | ||||||
| @@ -112,6 +113,8 @@ class Identifiers(Base): | |||||||
|             return u"Google Books" |             return u"Google Books" | ||||||
|         elif self.type == "kobo": |         elif self.type == "kobo": | ||||||
|             return u"Kobo" |             return u"Kobo" | ||||||
|  |         if self.type == "lubimyczytac": | ||||||
|  |             return u"Lubimyczytac" | ||||||
|         else: |         else: | ||||||
|             return self.type |             return self.type | ||||||
|  |  | ||||||
| @@ -130,6 +133,8 @@ class Identifiers(Base): | |||||||
|             return u"https://books.google.com/books?id={0}".format(self.val) |             return u"https://books.google.com/books?id={0}".format(self.val) | ||||||
|         elif self.type == "kobo": |         elif self.type == "kobo": | ||||||
|             return u"https://www.kobo.com/ebook/{0}".format(self.val) |             return u"https://www.kobo.com/ebook/{0}".format(self.val) | ||||||
|  |         elif self.type == "lubimyczytac": | ||||||
|  |             return u" http://lubimyczytac.pl/ksiazka/{0}".format(self.val) | ||||||
|         elif self.type == "url": |         elif self.type == "url": | ||||||
|             return u"{0}".format(self.val) |             return u"{0}".format(self.val) | ||||||
|         else: |         else: | ||||||
| @@ -355,8 +360,8 @@ def setup_db(): | |||||||
|     ub.session.commit() |     ub.session.commit() | ||||||
|     config.loadSettings() |     config.loadSettings() | ||||||
|     conn.connection.create_function('title_sort', 1, title_sort) |     conn.connection.create_function('title_sort', 1, title_sort) | ||||||
|     conn.connection.create_function('lower', 1, lcase) |     # conn.connection.create_function('lower', 1, lcase) | ||||||
|     conn.connection.create_function('upper', 1, ucase) |     # conn.connection.create_function('upper', 1, ucase) | ||||||
|  |  | ||||||
|     if not cc_classes: |     if not cc_classes: | ||||||
|         cc = conn.execute("SELECT id, datatype FROM custom_columns") |         cc = conn.execute("SELECT id, datatype FROM custom_columns") | ||||||
|   | |||||||
| @@ -364,7 +364,8 @@ def upload_single_file(request, book, book_id): | |||||||
|             global_WorkerThread.add_upload(current_user.nickname, |             global_WorkerThread.add_upload(current_user.nickname, | ||||||
|                 "<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + uploadText + "</a>") |                 "<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + uploadText + "</a>") | ||||||
|  |  | ||||||
| def upload_cover(request, book): |  | ||||||
|  | def upload_single_file(request, book, book_id): | ||||||
|     if 'btn-upload-cover' in request.files: |     if 'btn-upload-cover' in request.files: | ||||||
|         requested_file = request.files['btn-upload-cover'] |         requested_file = request.files['btn-upload-cover'] | ||||||
|         # check for empty request |         # check for empty request | ||||||
| @@ -380,17 +381,38 @@ def upload_cover(request, book): | |||||||
|                 except OSError: |                 except OSError: | ||||||
|                     flash(_(u"Failed to create path for cover %(path)s (Permission denied).", cover=filepath), |                     flash(_(u"Failed to create path for cover %(path)s (Permission denied).", cover=filepath), | ||||||
|                           category="error") |                           category="error") | ||||||
|                     return redirect(url_for('web.show_book', book_id=book.id)) |                     return redirect(url_for('show_book', book_id=book.id)) | ||||||
|             try: |             try: | ||||||
|                 requested_file.save(saved_filename) |                 requested_file.save(saved_filename) | ||||||
|                 # im=Image.open(saved_filename) |                 # im=Image.open(saved_filename) | ||||||
|                 book.has_cover = 1 |                 book.has_cover = 1 | ||||||
|             except IOError: |  | ||||||
|                 flash(_(u"Cover-file is not a valid image file" % saved_filename), category="error") |  | ||||||
|                 return redirect(url_for('web.show_book', book_id=book.id)) |  | ||||||
|             except OSError: |             except OSError: | ||||||
|                 flash(_(u"Failed to store cover-file %(cover)s.", cover=saved_filename), category="error") |                 flash(_(u"Failed to store cover-file %(cover)s.", cover=saved_filename), category="error") | ||||||
|                 return redirect(url_for('web.show_book', book_id=book.id)) |                 return redirect(url_for('web.show_book', book_id=book.id)) | ||||||
|  |             except IOError: | ||||||
|  |                 flash(_(u"Cover-file is not a valid image file" % saved_filename), category="error") | ||||||
|  |                 return redirect(url_for('web.show_book', book_id=book.id)) | ||||||
|  |             if helper.save_cover(requested_file, book.path) is True: | ||||||
|  |                 return True | ||||||
|  |             else: | ||||||
|  |                 # ToDo Message not always coorect | ||||||
|  |                 flash(_(u"Cover is not a supported imageformat (jpg/png/webp), can't save"), category="error") | ||||||
|  |                 return False | ||||||
|  |     return None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def upload_cover(request, book): | ||||||
|  |     if 'btn-upload-cover' in request.files: | ||||||
|  |         requested_file = request.files['btn-upload-cover'] | ||||||
|  |         # check for empty request | ||||||
|  |         if requested_file.filename != '': | ||||||
|  |             if helper.save_cover(requested_file, book.path) is True: | ||||||
|  |                 return True | ||||||
|  |             else: | ||||||
|  |                 # ToDo Message not always coorect | ||||||
|  |                 flash(_(u"Cover is not a supported imageformat (jpg/png/webp), can't save"), category="error") | ||||||
|  |                 return False | ||||||
|  |     return None | ||||||
|  |  | ||||||
| @editbook.route("/admin/book/<int:book_id>", methods=['GET', 'POST']) | @editbook.route("/admin/book/<int:book_id>", methods=['GET', 'POST']) | ||||||
| @login_required_if_no_ano | @login_required_if_no_ano | ||||||
| @@ -411,7 +433,8 @@ def edit_book(book_id): | |||||||
|         return redirect(url_for("web.index")) |         return redirect(url_for("web.index")) | ||||||
|  |  | ||||||
|     upload_single_file(request, book, book_id) |     upload_single_file(request, book, book_id) | ||||||
|     upload_cover(request, book) |     if upload_cover(request, book) is True: | ||||||
|  |         book.has_cover = 1 | ||||||
|     try: |     try: | ||||||
|         to_save = request.form.to_dict() |         to_save = request.form.to_dict() | ||||||
|         # Update book |         # Update book | ||||||
| @@ -457,7 +480,7 @@ def edit_book(book_id): | |||||||
|  |  | ||||||
|         if not error: |         if not error: | ||||||
|             if to_save["cover_url"]: |             if to_save["cover_url"]: | ||||||
|                 if helper.save_cover(to_save["cover_url"], book.path) is True: |                 if helper.save_cover_from_url(to_save["cover_url"], book.path) is True: | ||||||
|                     book.has_cover = 1 |                     book.has_cover = 1 | ||||||
|                 else: |                 else: | ||||||
|                     flash(_(u"Cover is not a jpg file, can't save"), category="error") |                     flash(_(u"Cover is not a jpg file, can't save"), category="error") | ||||||
|   | |||||||
							
								
								
									
										119
									
								
								cps/helper.py
									
									
									
									
									
								
							
							
						
						
									
										119
									
								
								cps/helper.py
									
									
									
									
									
								
							| @@ -23,6 +23,7 @@ from cps import config, global_WorkerThread, get_locale, db, mimetypes | |||||||
| from flask import current_app as app | from flask import current_app as app | ||||||
| from tempfile import gettempdir | from tempfile import gettempdir | ||||||
| import sys | import sys | ||||||
|  | import io | ||||||
| import os | import os | ||||||
| import re | import re | ||||||
| import unicodedata | import unicodedata | ||||||
| @@ -72,6 +73,12 @@ try: | |||||||
| except ImportError: | except ImportError: | ||||||
|     pass  # We're not using Python 3 |     pass  # We're not using Python 3 | ||||||
|  |  | ||||||
|  | try: | ||||||
|  |     from PIL import Image | ||||||
|  |     use_PIL = True | ||||||
|  | except ImportError: | ||||||
|  |     use_PIL = False | ||||||
|  |  | ||||||
|  |  | ||||||
| def update_download(book_id, user_id): | def update_download(book_id, user_id): | ||||||
|     check = ub.session.query(ub.Downloads).filter(ub.Downloads.user_id == user_id).filter(ub.Downloads.book_id == |     check = ub.session.query(ub.Downloads).filter(ub.Downloads.user_id == user_id).filter(ub.Downloads.book_id == | ||||||
| @@ -459,29 +466,73 @@ def get_book_cover(cover_path): | |||||||
|         return send_from_directory(os.path.join(config.config_calibre_dir, cover_path), "cover.jpg") |         return send_from_directory(os.path.join(config.config_calibre_dir, cover_path), "cover.jpg") | ||||||
|  |  | ||||||
|  |  | ||||||
| # saves book cover to gdrive or locally | # saves book cover from url | ||||||
| def save_cover(url, book_path): | def save_cover_from_url(url, book_path): | ||||||
|     img = requests.get(url) |     img = requests.get(url) | ||||||
|     if img.headers.get('content-type') != 'image/jpeg': |     return save_cover(img, book_path) | ||||||
|         app.logger.error("Cover is no jpg file, can't save") |  | ||||||
|         return False |  | ||||||
|  |  | ||||||
|     if config.config_use_google_drive: |  | ||||||
|         tmpDir = gettempdir() | def save_cover_from_filestorage(filepath, saved_filename, img): | ||||||
|         f = open(os.path.join(tmpDir, "uploaded_cover.jpg"), "wb") |     if hasattr(img, '_content'): | ||||||
|         f.write(img.content) |         f = open(os.path.join(filepath, saved_filename), "wb") | ||||||
|  |         f.write(img._content) | ||||||
|         f.close() |         f.close() | ||||||
|         gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'), os.path.join(tmpDir, f.name)) |     else: | ||||||
|         app.logger.info("Cover is saved on Google Drive") |         # check if file path exists, otherwise create it, copy file to calibre path and delete temp file | ||||||
|         return True |         if not os.path.exists(filepath): | ||||||
|  |             try: | ||||||
|     f = open(os.path.join(config.config_calibre_dir, book_path, "cover.jpg"), "wb") |                 os.makedirs(filepath) | ||||||
|     f.write(img.content) |             except OSError: | ||||||
|     f.close() |                 app.logger.error(u"Failed to create path for cover") | ||||||
|     app.logger.info("Cover is saved") |                 return False | ||||||
|  |         try: | ||||||
|  |             img.save(os.path.join(filepath, saved_filename)) | ||||||
|  |         except OSError: | ||||||
|  |             app.logger.error(u"Failed to store cover-file") | ||||||
|  |             return False | ||||||
|  |         except IOError: | ||||||
|  |             app.logger.error(u"Cover-file is not a valid image file") | ||||||
|  |             return False | ||||||
|     return True |     return True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # saves book cover to gdrive or locally | ||||||
|  | def save_cover(img, book_path): | ||||||
|  |     content_type = img.headers.get('content-type') | ||||||
|  |  | ||||||
|  |     if use_PIL: | ||||||
|  |         if content_type not in ('image/jpeg', 'image/png', 'image/webp'): | ||||||
|  |             app.logger.error("Only jpg/jpeg/png/webp files are supported as coverfile") | ||||||
|  |             return False | ||||||
|  |         # convert to jpg because calibre only supports jpg | ||||||
|  |         if content_type in ('image/png', 'image/webp'): | ||||||
|  |             if hasattr(img,'stream'): | ||||||
|  |                 imgc = Image.open(img.stream) | ||||||
|  |             else: | ||||||
|  |                 imgc = Image.open(io.BytesIO(img.content)) | ||||||
|  |             im = imgc.convert('RGB') | ||||||
|  |             tmp_bytesio = io.BytesIO() | ||||||
|  |             im.save(tmp_bytesio, format='JPEG') | ||||||
|  |             img._content = tmp_bytesio.getvalue() | ||||||
|  |     else: | ||||||
|  |         if content_type not in ('image/jpeg'): | ||||||
|  |             app.logger.error("Only jpg/jpeg files are supported as coverfile") | ||||||
|  |             return False | ||||||
|  |  | ||||||
|  |     if ub.config.config_use_google_drive: | ||||||
|  |         tmpDir = gettempdir() | ||||||
|  |         if save_cover_from_filestorage(tmpDir, "uploaded_cover.jpg", img) is True: | ||||||
|  |             gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'), | ||||||
|  |                                         os.path.join(tmpDir, "uploaded_cover.jpg")) | ||||||
|  |             app.logger.info("Cover is saved on Google Drive") | ||||||
|  |             return True | ||||||
|  |         else: | ||||||
|  |             return False | ||||||
|  |     else: | ||||||
|  |         return save_cover_from_filestorage(os.path.join(config.config_calibre_dir, book_path), "cover.jpg", img) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def do_download_file(book, book_format, data, headers): | def do_download_file(book, book_format, data, headers): | ||||||
|     if config.config_use_google_drive: |     if config.config_use_google_drive: | ||||||
|         startTime = time.time() |         startTime = time.time() | ||||||
| @@ -504,7 +555,6 @@ def do_download_file(book, book_format, data, headers): | |||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def check_unrar(unrarLocation): | def check_unrar(unrarLocation): | ||||||
|     error = False |     error = False | ||||||
|     if os.path.exists(unrarLocation): |     if os.path.exists(unrarLocation): | ||||||
| @@ -652,27 +702,22 @@ def fill_indexpage(page, database, db_filter, order, *join): | |||||||
|  |  | ||||||
| # read search results from calibre-database and return it (function is used for feed and simple search | # read search results from calibre-database and return it (function is used for feed and simple search | ||||||
| def get_search_results(term): | def get_search_results(term): | ||||||
|     q = list() |     def get_search_results(term): | ||||||
|     authorterms = re.split("[, ]+", term) |         db.session.connection().connection.connection.create_function("lower", 1, db.lcase) | ||||||
|     for authorterm in authorterms: |         q = list() | ||||||
|         q.append(db.Books.authors.any(db.or_(db.Authors.name.ilike("%" + authorterm + "%"), |         authorterms = re.split("[, ]+", term) | ||||||
|                                              db.Authors.name.ilike("%" + unidecode.unidecode(authorterm) + "%")))) |         for authorterm in authorterms: | ||||||
|     db.session.connection().connection.connection.create_function("lower", 1, db.lcase) |             q.append(db.Books.authors.any(db.func.lower(db.Authors.name).ilike("%" + authorterm + "%"))) | ||||||
|     db.Books.authors.any(db.or_(db.Authors.name.ilike("%" + term + "%"), |  | ||||||
|                                 db.Authors.name.ilike("%" + unidecode.unidecode(term) + "%"))) |  | ||||||
|  |  | ||||||
|     return db.session.query(db.Books).filter(common_filters()).filter( |         db.Books.authors.any(db.func.lower(db.Authors.name).ilike("%" + term + "%")) | ||||||
|         db.or_(db.Books.tags.any(db.Tags.name.ilike("%" + term + "%")), |  | ||||||
|                db.Books.series.any(db.Series.name.ilike("%" + term + "%")), |  | ||||||
|                db.Books.authors.any(and_(*q)), |  | ||||||
|                db.Books.publishers.any(db.Publishers.name.ilike("%" + term + "%")), |  | ||||||
|                db.Books.title.ilike("%" + term + "%"), |  | ||||||
|                db.Books.tags.any(db.Tags.name.ilike("%" + unidecode.unidecode(term) + "%")), |  | ||||||
|                db.Books.series.any(db.Series.name.ilike("%" + unidecode.unidecode(term) + "%")), |  | ||||||
|                db.Books.publishers.any(db.Publishers.name.ilike("%" + unidecode.unidecode(term) + "%")), |  | ||||||
|                db.Books.title.ilike("%" + unidecode.unidecode(term) + "%") |  | ||||||
|                )).all() |  | ||||||
|  |  | ||||||
|  |         return db.session.query(db.Books).filter(common_filters()).filter( | ||||||
|  |             db.or_(db.Books.tags.any(db.func.lower(db.Tags.name).ilike("%" + term + "%")), | ||||||
|  |                    db.Books.series.any(db.func.lower(db.Series.name).ilike("%" + term + "%")), | ||||||
|  |                    db.Books.authors.any(and_(*q)), | ||||||
|  |                    db.Books.publishers.any(db.func.lower(db.Publishers.name).ilike("%" + term + "%")), | ||||||
|  |                    db.func.lower(db.Books.title).ilike("%" + term + "%") | ||||||
|  |                    )).all() | ||||||
|  |  | ||||||
| def get_unique_other_books(library_books, author_books): | def get_unique_other_books(library_books, author_books): | ||||||
|     # Get all identifiers (ISBN, Goodreads, etc) and filter author's books by that list so we show fewer duplicates |     # Get all identifiers (ISBN, Goodreads, etc) and filter author's books by that list so we show fewer duplicates | ||||||
|   | |||||||
| @@ -1,6 +1,21 @@ | |||||||
| #!/usr/bin/env python | #!/usr/bin/env python | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
|  |  | ||||||
|  | #   This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | ||||||
|  | #     Copyright (C) 2019 pwr | ||||||
|  | # | ||||||
|  | #   This program is free software: you can redistribute it and/or modify | ||||||
|  | #   it under the terms of the GNU General Public License as published by | ||||||
|  | #   the Free Software Foundation, either version 3 of the License, or | ||||||
|  | #   (at your option) any later version. | ||||||
|  | # | ||||||
|  | #   This program is distributed in the hope that it will be useful, | ||||||
|  | #   but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | #   GNU General Public License for more details. | ||||||
|  | # | ||||||
|  | #   You should have received a copy of the GNU General Public License | ||||||
|  | #   along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||||
|  |  | ||||||
| try: | try: | ||||||
|     from iso639 import languages, __version__ |     from iso639 import languages, __version__ | ||||||
|   | |||||||
| @@ -312,14 +312,14 @@ def feed_get_cover(book_id): | |||||||
|     return helper.get_book_cover(book.path) |     return helper.get_book_cover(book.path) | ||||||
|  |  | ||||||
| @opds.route("/opds/readbooks/") | @opds.route("/opds/readbooks/") | ||||||
| @login_required_if_no_ano | @requires_basic_auth_if_no_ano | ||||||
| def feed_read_books(): | def feed_read_books(): | ||||||
|     off = request.args.get("offset") or 0 |     off = request.args.get("offset") or 0 | ||||||
|     return render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True) |     return render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True) | ||||||
|  |  | ||||||
|  |  | ||||||
| @opds.route("/opds/unreadbooks/") | @opds.route("/opds/unreadbooks/") | ||||||
| @login_required_if_no_ano | @requires_basic_auth_if_no_ano | ||||||
| def feed_unread_books(): | def feed_unread_books(): | ||||||
|     off = request.args.get("offset") or 0 |     off = request.args.get("offset") or 0 | ||||||
|     return render_read_books(int(off) / (int(config.config_books_per_page)) + 1, False, True) |     return render_read_books(int(off) / (int(config.config_books_per_page)) + 1, False, True) | ||||||
|   | |||||||
| @@ -1,21 +1,41 @@ | |||||||
| #!/usr/bin/env python | #!/usr/bin/env python | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
|  |  | ||||||
| #  This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) | #  Flask License | ||||||
| #    Copyright (C) 2018 cervinko, janeczku, OzzieIsaacs |  | ||||||
| # | # | ||||||
| #  This program is free software: you can redistribute it and/or modify | #  Copyright © 2010 by the Pallets team, cervinko, janeczku, OzzieIsaacs | ||||||
| #  it under the terms of the GNU General Public License as published by |  | ||||||
| #  the Free Software Foundation, either version 3 of the License, or |  | ||||||
| #  (at your option) any later version. |  | ||||||
| # | # | ||||||
| #  This program is distributed in the hope that it will be useful, | #  Some rights reserved. | ||||||
| #  but WITHOUT ANY WARRANTY; without even the implied warranty of |  | ||||||
| #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the |  | ||||||
| #  GNU General Public License for more details. |  | ||||||
| # | # | ||||||
| #  You should have received a copy of the GNU General Public License | #  Redistribution and use in source and binary forms of the software as | ||||||
| #  along with this program. If not, see <http://www.gnu.org/licenses/>. | #  well as documentation, with or without modification, are permitted | ||||||
|  | #  provided that the following conditions are met: | ||||||
|  | # | ||||||
|  | #  * Redistributions of source code must retain the above copyright notice, | ||||||
|  | #  this list of conditions and the following disclaimer. | ||||||
|  | # | ||||||
|  | #  * Redistributions in binary form must reproduce the above copyright | ||||||
|  | #    notice, this list of conditions and the following disclaimer in the | ||||||
|  | #    documentation and/or other materials provided with the distribution. | ||||||
|  | # | ||||||
|  | #  * Neither the name of the copyright holder nor the names of its | ||||||
|  | #    contributors may be used to endorse or promote products derived from | ||||||
|  | #    this software without specific prior written permission. | ||||||
|  | # | ||||||
|  | #  THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND | ||||||
|  | #  CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, | ||||||
|  | #  BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND | ||||||
|  | #  FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE | ||||||
|  | #  COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, | ||||||
|  | #  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT | ||||||
|  | #  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF | ||||||
|  | #  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||||||
|  | #  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||||
|  | #  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF | ||||||
|  | #  THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF | ||||||
|  | #  SUCH DAMAGE. | ||||||
|  | # | ||||||
|  | # Inspired by http://flask.pocoo.org/snippets/35/ | ||||||
|  |  | ||||||
|  |  | ||||||
| class ReverseProxied(object): | class ReverseProxied(object): | ||||||
|   | |||||||
| @@ -25,15 +25,12 @@ var ggResults = []; | |||||||
|  |  | ||||||
| $(function () { | $(function () { | ||||||
|     var msg = i18nMsg; |     var msg = i18nMsg; | ||||||
|     var douban = "https://api.douban.com"; |     /*var douban = "https://api.douban.com"; | ||||||
|     var dbSearch = "/v2/book/search"; |     var dbSearch = "/v2/book/search";*/ | ||||||
|     // var dbGetInfo = "/v2/book/"; |     var dbDone = true; | ||||||
|     // var db_get_info_by_isbn = "/v2/book/isbn/ "; |  | ||||||
|     var dbDone = false; |  | ||||||
|  |  | ||||||
|     var google = "https://www.googleapis.com/"; |     var google = "https://www.googleapis.com/"; | ||||||
|     var ggSearch = "/books/v1/volumes"; |     var ggSearch = "/books/v1/volumes"; | ||||||
|     // var gg_get_info = "/books/v1/volumes/"; |  | ||||||
|     var ggDone = false; |     var ggDone = false; | ||||||
|  |  | ||||||
|     var showFlag = 0; |     var showFlag = 0; | ||||||
| @@ -96,7 +93,7 @@ $(function () { | |||||||
|             }); |             }); | ||||||
|             ggDone = false; |             ggDone = false; | ||||||
|         } |         } | ||||||
|         if (dbDone && dbResults.length > 0) { |         /*if (dbDone && dbResults.length > 0) { | ||||||
|             dbResults.forEach(function(result) { |             dbResults.forEach(function(result) { | ||||||
|                 var book = { |                 var book = { | ||||||
|                     id: result.id, |                     id: result.id, | ||||||
| @@ -130,7 +127,7 @@ $(function () { | |||||||
|                 $("#book-list").append($book); |                 $("#book-list").append($book); | ||||||
|             }); |             }); | ||||||
|             dbDone = false; |             dbDone = false; | ||||||
|         } |         }*/ | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function ggSearchBook (title) { |     function ggSearchBook (title) { | ||||||
| @@ -150,7 +147,7 @@ $(function () { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function dbSearchBook (title) { |     /*function dbSearchBook (title) { | ||||||
|         $.ajax({ |         $.ajax({ | ||||||
|             url: douban + dbSearch + "?q=" + title + "&fields=all&count=10", |             url: douban + dbSearch + "?q=" + title + "&fields=all&count=10", | ||||||
|             type: "GET", |             type: "GET", | ||||||
| @@ -160,7 +157,7 @@ $(function () { | |||||||
|                 dbResults = data.books; |                 dbResults = data.books; | ||||||
|             }, |             }, | ||||||
|             error: function error() { |             error: function error() { | ||||||
|                 $("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>"); |                 $("#meta-info").html("<p class=\"text-danger\">" + msg.search_error + "!</p>"+ $("#meta-info")[0].innerHTML) | ||||||
|             }, |             }, | ||||||
|             complete: function complete() { |             complete: function complete() { | ||||||
|                 dbDone = true; |                 dbDone = true; | ||||||
| @@ -168,14 +165,13 @@ $(function () { | |||||||
|                 $("#show-douban").trigger("change"); |                 $("#show-douban").trigger("change"); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     } |     }*/ | ||||||
|  |  | ||||||
|     function doSearch (keyword) { |     function doSearch (keyword) { | ||||||
|         showFlag = 0; |         showFlag = 0; | ||||||
|         $("#meta-info").text(msg.loading); |         $("#meta-info").text(msg.loading); | ||||||
|         // var keyword = $("#keyword").val(); |  | ||||||
|         if (keyword) { |         if (keyword) { | ||||||
|             dbSearchBook(keyword); |             // dbSearchBook(keyword); | ||||||
|             ggSearchBook(keyword); |             ggSearchBook(keyword); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -90,7 +90,7 @@ | |||||||
|     <div class="form-group" aria-label="Upload cover from local drive"> |     <div class="form-group" aria-label="Upload cover from local drive"> | ||||||
|         <label class="btn btn-primary btn-file" for="btn-upload-cover">{{ _('Upload Cover from local drive') }}</label> |         <label class="btn btn-primary btn-file" for="btn-upload-cover">{{ _('Upload Cover from local drive') }}</label> | ||||||
|         <div class="upload-cover-input-text" id="upload-cover"></div> |         <div class="upload-cover-input-text" id="upload-cover"></div> | ||||||
|         <input id="btn-upload-cover" name="btn-upload-cover" type="file"> |         <input id="btn-upload-cover" name="btn-upload-cover" type="file" accept=".jpg, .jpeg, .png, .webp"> | ||||||
|     </div> |     </div> | ||||||
|     <div class="form-group"> |     <div class="form-group"> | ||||||
|       <label for="pubdate">{{_('Publishing date')}}</label> |       <label for="pubdate">{{_('Publishing date')}}</label> | ||||||
| @@ -223,8 +223,8 @@ | |||||||
|       </div> |       </div> | ||||||
|       <div class="modal-body"> |       <div class="modal-body"> | ||||||
|         <div class="text-center padded-bottom"> |         <div class="text-center padded-bottom"> | ||||||
|           <input type="checkbox" id="show-douban" class="pill" data-control="douban" checked> |           <!--input type="checkbox" id="show-douban" class="pill" data-control="douban" checked> | ||||||
|           <label for="show-douban">Douban <span class="glyphicon glyphicon-ok"></span></label> |           <label for="show-douban">Douban <span class="glyphicon glyphicon-ok"></span></label--> | ||||||
|  |  | ||||||
|           <input type="checkbox" id="show-google" class="pill" data-control="google" checked> |           <input type="checkbox" id="show-google" class="pill" data-control="google" checked> | ||||||
|           <label for="show-google">Google <span class="glyphicon glyphicon-ok"></span></label> |           <label for="show-google">Google <span class="glyphicon glyphicon-ok"></span></label> | ||||||
|   | |||||||
| @@ -243,7 +243,7 @@ class Updater(threading.Thread): | |||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def _stable_version_info(self): |     def _stable_version_info(self): | ||||||
|         return {'version': '0.6.1'} # Current version |         return {'version': '0.6.2'} # Current version | ||||||
|  |  | ||||||
|     def _nightly_available_updates(self, request_method): |     def _nightly_available_updates(self, request_method): | ||||||
|         tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone) |         tz = datetime.timedelta(seconds=time.timezone if (time.localtime().tm_isdst == 0) else time.altzone) | ||||||
|   | |||||||
| @@ -25,12 +25,12 @@ import os | |||||||
| from flask_babel import gettext as _ | from flask_babel import gettext as _ | ||||||
| import comic | import comic | ||||||
| from cps import app | from cps import app | ||||||
|  |  | ||||||
| try: | try: | ||||||
|     from lxml.etree import LXML_VERSION as lxmlversion |     from lxml.etree import LXML_VERSION as lxmlversion | ||||||
| except ImportError: | except ImportError: | ||||||
|     lxmlversion = None |     lxmlversion = None | ||||||
|  |  | ||||||
|  |  | ||||||
| try: | try: | ||||||
|     from wand.image import Image |     from wand.image import Image | ||||||
|     from wand import version as ImageVersion |     from wand import version as ImageVersion | ||||||
| @@ -39,6 +39,7 @@ try: | |||||||
| except (ImportError, RuntimeError) as e: | except (ImportError, RuntimeError) as e: | ||||||
|     app.logger.warning('cannot import Image, generating pdf covers for pdf uploads will not work: %s', e) |     app.logger.warning('cannot import Image, generating pdf covers for pdf uploads will not work: %s', e) | ||||||
|     use_generic_pdf_cover = True |     use_generic_pdf_cover = True | ||||||
|  |  | ||||||
| try: | try: | ||||||
|     from PyPDF2 import PdfFileReader |     from PyPDF2 import PdfFileReader | ||||||
|     from PyPDF2 import __version__ as PyPdfVersion |     from PyPDF2 import __version__ as PyPdfVersion | ||||||
| @@ -61,6 +62,14 @@ except ImportError as e: | |||||||
|     app.logger.warning('cannot import fb2, extracting fb2 metadata will not work: %s', e) |     app.logger.warning('cannot import fb2, extracting fb2 metadata will not work: %s', e) | ||||||
|     use_fb2_meta = False |     use_fb2_meta = False | ||||||
|  |  | ||||||
|  | try: | ||||||
|  |     from PIL import Image | ||||||
|  |     from PIL import __version__ as PILversion | ||||||
|  |     use_PIL = True | ||||||
|  | except ImportError: | ||||||
|  |     use_PIL = False | ||||||
|  |  | ||||||
|  |  | ||||||
| __author__ = 'lemmsh' | __author__ = 'lemmsh' | ||||||
|  |  | ||||||
| BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, series_id, languages') | BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, series_id, languages') | ||||||
| @@ -138,6 +147,48 @@ def pdf_preview(tmp_file_path, tmp_dir): | |||||||
|     if use_generic_pdf_cover: |     if use_generic_pdf_cover: | ||||||
|         return None |         return None | ||||||
|     else: |     else: | ||||||
|  |         if use_PIL: | ||||||
|  |             try: | ||||||
|  |                 input1 = PdfFileReader(open(tmp_file_path, 'rb'), strict=False) | ||||||
|  |                 page0 = input1.getPage(0) | ||||||
|  |                 xObject = page0['/Resources']['/XObject'].getObject() | ||||||
|  |  | ||||||
|  |                 for obj in xObject: | ||||||
|  |                     if xObject[obj]['/Subtype'] == '/Image': | ||||||
|  |                         size = (xObject[obj]['/Width'], xObject[obj]['/Height']) | ||||||
|  |                         data = xObject[obj]._data # xObject[obj].getData() | ||||||
|  |                         if xObject[obj]['/ColorSpace'] == '/DeviceRGB': | ||||||
|  |                             mode = "RGB" | ||||||
|  |                         else: | ||||||
|  |                             mode = "P" | ||||||
|  |                         if '/Filter' in xObject[obj]: | ||||||
|  |                             if xObject[obj]['/Filter'] == '/FlateDecode': | ||||||
|  |                                 img = Image.frombytes(mode, size, data) | ||||||
|  |                                 cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.png" | ||||||
|  |                                 img.save(filename=os.path.join(tmp_dir, cover_file_name)) | ||||||
|  |                                 return cover_file_name | ||||||
|  |                                 # img.save(obj[1:] + ".png") | ||||||
|  |                             elif xObject[obj]['/Filter'] == '/DCTDecode': | ||||||
|  |                                 cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg" | ||||||
|  |                                 img = open(cover_file_name, "wb") | ||||||
|  |                                 img.write(data) | ||||||
|  |                                 img.close() | ||||||
|  |                                 return cover_file_name | ||||||
|  |                             elif xObject[obj]['/Filter'] == '/JPXDecode': | ||||||
|  |                                 cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jp2" | ||||||
|  |                                 img = open(cover_file_name, "wb") | ||||||
|  |                                 img.write(data) | ||||||
|  |                                 img.close() | ||||||
|  |                                 return cover_file_name | ||||||
|  |                         else: | ||||||
|  |                             img = Image.frombytes(mode, size, data) | ||||||
|  |                             cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.png" | ||||||
|  |                             img.save(filename=os.path.join(tmp_dir, cover_file_name)) | ||||||
|  |                             return cover_file_name | ||||||
|  |                             # img.save(obj[1:] + ".png") | ||||||
|  |             except Exception as ex: | ||||||
|  |                 print(ex) | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg" |             cover_file_name = os.path.splitext(tmp_file_path)[0] + ".cover.jpg" | ||||||
|             with Image(filename=tmp_file_path + "[0]", resolution=150) as img: |             with Image(filename=tmp_file_path + "[0]", resolution=150) as img: | ||||||
| @@ -145,12 +196,13 @@ def pdf_preview(tmp_file_path, tmp_dir): | |||||||
|                 img.save(filename=os.path.join(tmp_dir, cover_file_name)) |                 img.save(filename=os.path.join(tmp_dir, cover_file_name)) | ||||||
|             return cover_file_name |             return cover_file_name | ||||||
|         except PolicyError as ex: |         except PolicyError as ex: | ||||||
|             logger.warning('Pdf extraction forbidden by Imagemagick policy: %s', ex) |             app.logger.warning('Pdf extraction forbidden by Imagemagick policy: %s', ex) | ||||||
|             return None |             return None | ||||||
|         except Exception as ex: |         except Exception as ex: | ||||||
|             logger.warning('Cannot extract cover image, using default: %s', ex) |             app.logger.warning('Cannot extract cover image, using default: %s', ex) | ||||||
|             return None |             return None | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_versions(): | def get_versions(): | ||||||
|     if not use_generic_pdf_cover: |     if not use_generic_pdf_cover: | ||||||
|         IVersion = ImageVersion.MAGICK_VERSION |         IVersion = ImageVersion.MAGICK_VERSION | ||||||
| @@ -166,7 +218,15 @@ def get_versions(): | |||||||
|         XVersion = 'v'+'.'.join(map(str, lxmlversion)) |         XVersion = 'v'+'.'.join(map(str, lxmlversion)) | ||||||
|     else: |     else: | ||||||
|         XVersion = _(u'not installed') |         XVersion = _(u'not installed') | ||||||
|     return {'Image Magick': IVersion, 'PyPdf': PVersion, 'lxml':XVersion, 'Wand Version': WVersion} |     if use_PIL: | ||||||
|  |         PILVersion = 'v' + PILversion | ||||||
|  |     else: | ||||||
|  |         PILVersion = _(u'not installed') | ||||||
|  |     return {'Image Magick': IVersion, | ||||||
|  |             'PyPdf': PVersion, | ||||||
|  |             'lxml':XVersion, | ||||||
|  |             'Wand': WVersion, | ||||||
|  |             'Pillow': PILVersion} | ||||||
|  |  | ||||||
|  |  | ||||||
| def upload(uploadfile): | def upload(uploadfile): | ||||||
|   | |||||||
							
								
								
									
										33
									
								
								cps/web.py
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								cps/web.py
									
									
									
									
									
								
							| @@ -41,18 +41,11 @@ from sqlalchemy.sql.expression import text, func, true, false, not_ | |||||||
| import json | import json | ||||||
| import datetime | import datetime | ||||||
| import isoLanguages | import isoLanguages | ||||||
| from pytz import __version__ as pytzVersion |  | ||||||
| from uuid import uuid4 |  | ||||||
| import os.path | import os.path | ||||||
| import sys |  | ||||||
| import re |  | ||||||
| import db |  | ||||||
| from shutil import move, copyfile |  | ||||||
| import gdriveutils | import gdriveutils | ||||||
| from redirect import redirect_back | from redirect import redirect_back | ||||||
| from cps import lm, babel, ub, config, get_locale, language_table, app, db | from cps import lm, babel, ub, config, get_locale, language_table, app, db | ||||||
| from pagination import Pagination | from pagination import Pagination | ||||||
| import unidecode |  | ||||||
|  |  | ||||||
|  |  | ||||||
| feature_support = dict() | feature_support = dict() | ||||||
| @@ -374,7 +367,8 @@ def get_comic_book(book_id, book_format, page): | |||||||
| # ################################### Typeahead ################################################################## | # ################################### Typeahead ################################################################## | ||||||
|  |  | ||||||
| def get_typeahead(database, query, replace=('','')): | def get_typeahead(database, query, replace=('','')): | ||||||
|     entries = db.session.query(database).filter(database.name.ilike("%" + query + "%")).all() |     db.session.connection().connection.connection.create_function("lower", 1, db.lcase) | ||||||
|  |     entries = db.session.query(database).filter(db.func.lower(database.name).ilike("%" + query + "%")).all() | ||||||
|     json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries]) |     json_dumps = json.dumps([dict(name=r.name.replace(*replace)) for r in entries]) | ||||||
|     return json_dumps |     return json_dumps | ||||||
|  |  | ||||||
| @@ -428,12 +422,13 @@ def get_matching_tags(): | |||||||
|     tag_dict = {'tags': []} |     tag_dict = {'tags': []} | ||||||
|     if request.method == "GET": |     if request.method == "GET": | ||||||
|         q = db.session.query(db.Books) |         q = db.session.query(db.Books) | ||||||
|  |         db.session.connection().connection.connection.create_function("lower", 1, db.lcase) | ||||||
|         author_input = request.args.get('author_name') |         author_input = request.args.get('author_name') | ||||||
|         title_input = request.args.get('book_title') |         title_input = request.args.get('book_title') | ||||||
|         include_tag_inputs = request.args.getlist('include_tag') |         include_tag_inputs = request.args.getlist('include_tag') | ||||||
|         exclude_tag_inputs = request.args.getlist('exclude_tag') |         exclude_tag_inputs = request.args.getlist('exclude_tag') | ||||||
|         q = q.filter(db.Books.authors.any(db.Authors.name.ilike("%" + author_input + "%")), |         q = q.filter(db.Books.authors.any(db.func.lower(db.Authors.name).ilike("%" + author_input + "%")), | ||||||
|                      db.Books.title.ilike("%" + title_input + "%")) |                      db.func.lower(db.Books.title).ilike("%" + title_input + "%")) | ||||||
|         if len(include_tag_inputs) > 0: |         if len(include_tag_inputs) > 0: | ||||||
|             for tag in include_tag_inputs: |             for tag in include_tag_inputs: | ||||||
|                 q = q.filter(db.Books.tags.any(db.Tags.id == tag)) |                 q = q.filter(db.Books.tags.any(db.Tags.id == tag)) | ||||||
| @@ -874,20 +869,15 @@ def advanced_search(): | |||||||
|         searchterm = " + ".join(filter(None, searchterm)) |         searchterm = " + ".join(filter(None, searchterm)) | ||||||
|         q = q.filter() |         q = q.filter() | ||||||
|         if author_name: |         if author_name: | ||||||
|             q = q.filter(db.Books.authors.any(db.or_(db.Authors.name.ilike("%" + author_name + "%"), |             q = q.filter(db.Books.authors.any(db.func.lower(db.Authors.name).ilike("%" + author_name + "%"))) | ||||||
|                                                      db.Authors.name.ilike("%" + unidecode.unidecode(author_name) |  | ||||||
|                                                                            + "%")))) |  | ||||||
|         if book_title: |         if book_title: | ||||||
|             q = q.filter(db.or_(db.Books.title.ilike("%" + book_title + "%"), |             q = q.filter(db.func.lower(db.Books.title).ilike("%" + book_title + "%")) | ||||||
|                                 db.Books.title.ilike("%" + unidecode.unidecode(book_title) + "%"))) |  | ||||||
|         if pub_start: |         if pub_start: | ||||||
|             q = q.filter(db.Books.pubdate >= pub_start) |             q = q.filter(db.Books.pubdate >= pub_start) | ||||||
|         if pub_end: |         if pub_end: | ||||||
|             q = q.filter(db.Books.pubdate <= pub_end) |             q = q.filter(db.Books.pubdate <= pub_end) | ||||||
|         if publisher: |         if publisher: | ||||||
|             q = q.filter(db.Books.publishers.any(db.or_(db.Publishers.name.ilike("%" + publisher + "%"), |             q = q.filter(db.Books.publishers.any(db.func.lower(db.Publishers.name).ilike("%" + publisher + "%"))) | ||||||
|                                                         db.Publishers.name.ilike("%" + unidecode.unidecode(publisher) |  | ||||||
|                                                                                  + "%"),))) |  | ||||||
|         for tag in include_tag_inputs: |         for tag in include_tag_inputs: | ||||||
|             q = q.filter(db.Books.tags.any(db.Tags.id == tag)) |             q = q.filter(db.Books.tags.any(db.Tags.id == tag)) | ||||||
|         for tag in exclude_tag_inputs: |         for tag in exclude_tag_inputs: | ||||||
| @@ -910,9 +900,7 @@ def advanced_search(): | |||||||
|             rating_low = int(rating_low) * 2 |             rating_low = int(rating_low) * 2 | ||||||
|             q = q.filter(db.Books.ratings.any(db.Ratings.rating >= rating_low)) |             q = q.filter(db.Books.ratings.any(db.Ratings.rating >= rating_low)) | ||||||
|         if description: |         if description: | ||||||
|             q = q.filter(db.Books.comments.any(db.or_(db.Comments.text.ilike("%" + description + "%"), |             q = q.filter(db.Books.comments.any(db.func.lower(db.Comments.text).ilike("%" + description + "%"))) | ||||||
|                                                       db.Comments.text.ilike("%" + unidecode.unidecode(description) |  | ||||||
|                                                                              + "%")))) |  | ||||||
|  |  | ||||||
|         # search custom culumns |         # search custom culumns | ||||||
|         for c in cc: |         for c in cc: | ||||||
| @@ -927,8 +915,7 @@ def advanced_search(): | |||||||
|                         db.cc_classes[c.id].value == custom_query)) |                         db.cc_classes[c.id].value == custom_query)) | ||||||
|                 else: |                 else: | ||||||
|                     q = q.filter(getattr(db.Books, 'custom_column_'+str(c.id)).any( |                     q = q.filter(getattr(db.Books, 'custom_column_'+str(c.id)).any( | ||||||
|                         db.or_(db.cc_classes[c.id].value.ilike("%" + custom_query + "%"), |                         db.func.lower(db.cc_classes[c.id].value).ilike("%" + custom_query + "%"))) | ||||||
|                                db.cc_classes[c.id].value.ilike("%" + unidecode.unidecode(custom_query) + "%")))) |  | ||||||
|         q = q.all() |         q = q.all() | ||||||
|         ids = list() |         ids = list() | ||||||
|         for element in q: |         for element in q: | ||||||
|   | |||||||
| @@ -13,3 +13,4 @@ SQLAlchemy>=1.1.0 | |||||||
| tornado>=4.1 | tornado>=4.1 | ||||||
| Wand>=0.4.4 | Wand>=0.4.4 | ||||||
| unidecode>=0.04.19 | unidecode>=0.04.19 | ||||||
|  | Pillow>=5.4.0 | ||||||
		Reference in New Issue
	
	Block a user
	 Ozzieisaacs
					Ozzieisaacs