def import_CSV(search_dir=None): """ Find a csv file in the search_dir and process all the books in it, adding authors to the database if not found and marking the books as "Wanted" """ try: if not search_dir: msg = "Alternate Directory not configured" logger.warn(msg) return msg elif not os.path.isdir(search_dir): msg = "Alternate Directory [%s] not found" % search_dir logger.warn(msg) return msg csvFile = csv_file(search_dir) headers = None content = {} if not csvFile: msg = "No CSV file found in %s" % search_dir logger.warn(msg) return msg else: logger.debug(u'Reading file %s' % csvFile) reader = csv.reader(open(csvFile)) for row in reader: if reader.line_num == 1: # If we are on the first line, create the headers list from the first row headers = row else: # Otherwise, the key in the content dictionary is the first item in the # row and we can create the sub-dictionary by using the zip() function. # we include the key in the dictionary as our exported csv files use # bookid as the key content[row[0]] = dict(zip(headers, row)) # We can now get to the content by using the resulting dictionary, so to see # the list of lines, we can do: print content.keys() to get a list of keys # To see the list of fields available for each book: print headers if 'Author' not in headers or 'Title' not in headers: msg = 'Invalid CSV file found %s' % csvFile logger.warn(msg) return msg myDB = database.DBConnection() bookcount = 0 authcount = 0 skipcount = 0 logger.debug(u"CSV: Found %s book%s in csv file" % (len(content.keys()), plural(len(content.keys())))) for item in content.keys(): authorname = content[item]['Author'] if isinstance(authorname, str) and hasattr(authorname, "decode"): authorname = authorname.decode(lazylibrarian.SYS_ENCODING) authorname = formatAuthorName(authorname) title = content[item]['Title'] if isinstance(title, str) and hasattr(title, "decode"): title = title.decode(lazylibrarian.SYS_ENCODING) authmatch = myDB.match('SELECT * FROM authors where AuthorName=?', (authorname,)) if authmatch: logger.debug(u"CSV: Author %s found in database" % authorname) else: logger.debug(u"CSV: Author %s not found" % authorname) newauthor, authorid, new = addAuthorNameToDB(author=authorname, addbooks=lazylibrarian.CONFIG['NEWAUTHOR_BOOKS']) if len(newauthor) and newauthor != authorname: logger.debug("Preferred authorname changed from [%s] to [%s]" % (authorname, newauthor)) authorname = newauthor if new: authcount += 1 bookmatch = finditem(content[item], authorname, headers) result = '' if bookmatch: authorname = bookmatch['AuthorName'] bookname = bookmatch['BookName'] bookid = bookmatch['BookID'] bookstatus = bookmatch['Status'] if bookstatus in ['Open', 'Wanted', 'Have']: logger.info(u'Found book %s by %s, already marked as "%s"' % (bookname, authorname, bookstatus)) else: # skipped/ignored logger.info(u'Found book %s by %s, marking as "Wanted"' % (bookname, authorname)) controlValueDict = {"BookID": bookid} newValueDict = {"Status": "Wanted"} myDB.upsert("books", newValueDict, controlValueDict) bookcount += 1 else: searchterm = "%s <ll> %s" % (title, authorname) results = search_for(unaccented(searchterm)) if results: result = results[0] if result['author_fuzz'] > lazylibrarian.CONFIG['MATCH_RATIO'] \ and result['book_fuzz'] > lazylibrarian.CONFIG['MATCH_RATIO']: logger.info("Found (%s%% %s%%) %s: %s" % (result['author_fuzz'], result['book_fuzz'], result['authorname'], result['bookname'])) import_book(result['bookid']) bookcount += 1 bookmatch = True if not bookmatch: msg = "Skipping book %s by %s" % (title, authorname) if not result: msg += ', No results returned' logger.warn(msg) else: msg += ', No match found' logger.warn(msg) msg = "Closest match (%s%% %s%%) %s: %s" % (result['author_fuzz'], result['book_fuzz'], result['authorname'], result['bookname']) logger.warn(msg) skipcount += 1 msg = "Added %i new author%s, marked %i book%s as 'Wanted', %i book%s not found" % \ (authcount, plural(authcount), bookcount, plural(bookcount), skipcount, plural(skipcount)) logger.info(msg) return msg except Exception: msg = 'Unhandled exception in importCSV: %s' % traceback.format_exc() logger.error(msg) return msg
def search_rss_book(books=None, library=None): """ books is a list of new books to add, or None for backlog search library is "eBook" or "AudioBook" or None to search all book types """ try: threadname = threading.currentThread().name if "Thread-" in threadname: if books is None: threading.currentThread().name = "SEARCHALLRSS" else: threading.currentThread().name = "SEARCHRSS" if not (lazylibrarian.USE_RSS()): logger.warn('RSS search is disabled') scheduleJob(action='Stop', target='search_rss_book') return myDB = database.DBConnection() resultlist, wishproviders = IterateOverWishLists() new_books = 0 if not wishproviders: logger.debug('No wishlists are set') else: # for each item in resultlist, add to database if necessary, and mark as wanted logger.debug('Processing %s item%s in wishlists' % (len(resultlist), plural(len(resultlist)))) for book in resultlist: # we get rss_author, rss_title, maybe rss_isbn, rss_bookid (goodreads bookid) # we can just use bookid if goodreads, or try isbn and name matching on author/title if googlebooks # not sure if anyone would use a goodreads wishlist if not using goodreads interface... if lazylibrarian.CONFIG['BOOK_API'] == "GoodReads" and book['rss_bookid']: bookmatch = myDB.match('select Status,BookName from books where bookid=?', (book['rss_bookid'],)) if bookmatch: bookstatus = bookmatch['Status'] bookname = bookmatch['BookName'] if bookstatus in ['Open', 'Wanted', 'Have']: logger.info(u'Found book %s, already marked as "%s"' % (bookname, bookstatus)) else: # skipped/ignored logger.info(u'Found book %s, marking as "Wanted"' % bookname) controlValueDict = {"BookID": book['rss_bookid']} newValueDict = {"Status": "Wanted"} myDB.upsert("books", newValueDict, controlValueDict) new_books += 1 else: import_book(book['rss_bookid']) new_books += 1 else: item = {} headers = [] item['Title'] = book['rss_title'] if book['rss_bookid']: item['BookID'] = book['rss_bookid'] headers.append('BookID') if book['rss_isbn']: item['ISBN'] = book['rss_isbn'] headers.append('ISBN') bookmatch = finditem(item, book['rss_author'], headers) if bookmatch: # it's already in the database authorname = bookmatch['AuthorName'] bookname = bookmatch['BookName'] bookid = bookmatch['BookID'] bookstatus = bookmatch['Status'] if bookstatus in ['Open', 'Wanted', 'Have']: logger.info( u'Found book %s by %s, already marked as "%s"' % (bookname, authorname, bookstatus)) else: # skipped/ignored logger.info(u'Found book %s by %s, marking as "Wanted"' % (bookname, authorname)) controlValueDict = {"BookID": bookid} newValueDict = {"Status": "Wanted"} myDB.upsert("books", newValueDict, controlValueDict) new_books += 1 else: # not in database yet results = '' if book['rss_isbn']: results = search_for(book['rss_isbn']) if results: result = results[0] # type: dict if result['isbn_fuzz'] > check_int(lazylibrarian.CONFIG['MATCH_RATIO'], 90): logger.info("Found (%s%%) %s: %s" % (result['isbn_fuzz'], result['authorname'], result['bookname'])) import_book(result['bookid']) new_books += 1 bookmatch = True if not results: searchterm = "%s <ll> %s" % (item['Title'], formatAuthorName(book['rss_author'])) results = search_for(unaccented(searchterm)) if results: result = results[0] # type: dict if result['author_fuzz'] > check_int(lazylibrarian.CONFIG['MATCH_RATIO'], 90) \ and result['book_fuzz'] > check_int(lazylibrarian.CONFIG['MATCH_RATIO'], 90): logger.info("Found (%s%% %s%%) %s: %s" % (result['author_fuzz'], result['book_fuzz'], result['authorname'], result['bookname'])) import_book(result['bookid']) new_books += 1 bookmatch = True if not bookmatch: msg = "Skipping book %s by %s" % (item['Title'], book['rss_author']) if not results: msg += ', No results returned' logger.warn(msg) else: msg += ', No match found' logger.warn(msg) msg = "Closest match (%s%% %s%%) %s: %s" % (result['author_fuzz'], result['book_fuzz'], result['authorname'], result['bookname']) logger.warn(msg) if new_books: logger.info("Wishlist marked %s book%s as Wanted" % (new_books, plural(new_books))) searchbooks = [] if books is None: # We are performing a backlog search cmd = 'SELECT BookID, AuthorName, Bookname, BookSub, BookAdded, books.Status, AudioStatus ' cmd += 'from books,authors WHERE (books.Status="Wanted" OR AudioStatus="Wanted") ' cmd += 'and books.AuthorID = authors.AuthorID order by BookAdded desc' results = myDB.select(cmd) for terms in results: searchbooks.append(terms) else: # The user has added a new book for book in books: cmd = 'SELECT BookID, AuthorName, BookName, BookSub, books.Status, AudioStatus ' cmd += 'from books,authors WHERE BookID=? AND books.AuthorID = authors.AuthorID' results = myDB.select(cmd, (book['bookid'],)) for terms in results: searchbooks.append(terms) if len(searchbooks) == 0: logger.debug("SearchRSS - No books to search for") return resultlist, nproviders = IterateOverRSSSites() if not nproviders and not wishproviders: logger.warn('No rss providers are available') return # No point in continuing logger.info('RSS Searching for %i book%s' % (len(searchbooks), plural(len(searchbooks)))) searchlist = [] for searchbook in searchbooks: # searchterm is only used for display purposes searchterm = searchbook['AuthorName'] + ' ' + searchbook['BookName'] if searchbook['BookSub']: searchterm = searchterm + ': ' + searchbook['BookSub'] if library is None or library == 'eBook': if searchbook['Status'] == "Wanted": searchlist.append( {"bookid": searchbook['BookID'], "bookName": searchbook['BookName'], "bookSub": searchbook['BookSub'], "authorName": searchbook['AuthorName'], "library": "eBook", "searchterm": searchterm}) if library is None or library == 'AudioBook': if searchbook['AudioStatus'] == "Wanted": searchlist.append( {"bookid": searchbook['BookID'], "bookName": searchbook['BookName'], "bookSub": searchbook['BookSub'], "authorName": searchbook['AuthorName'], "library": "AudioBook", "searchterm": searchterm}) rss_count = 0 for book in searchlist: if book['library'] == 'AudioBook': searchtype = 'audio' else: searchtype = 'book' found = processResultList(resultlist, book, searchtype, 'rss') # if you can't find the book, try title without any "(extended details, series etc)" if not found and '(' in book['bookName']: # anything to shorten? searchtype = 'short' + searchtype found = processResultList(resultlist, book, searchtype, 'rss') if not found: logger.info("NZB Searches for %s %s returned no results." % (book['library'], book['searchterm'])) if found > True: rss_count += 1 logger.info("RSS Search for Wanted items complete, found %s book%s" % (rss_count, plural(rss_count))) except Exception: logger.error('Unhandled exception in search_rss_book: %s' % traceback.format_exc()) finally: threading.currentThread().name = "WEBSERVER"
def search_wishlist(): try: threadname = threading.currentThread().name if "Thread-" in threadname: threading.currentThread().name = "SEARCHWISHLIST" myDB = database.DBConnection() resultlist, wishproviders = IterateOverWishLists() new_books = 0 if not wishproviders: logger.debug('No wishlists are set') scheduleJob(action='Stop', target='search_wishlist') return # No point in continuing # for each item in resultlist, add to database if necessary, and mark as wanted logger.debug('Processing %s item%s in wishlists' % (len(resultlist), plural(len(resultlist)))) for book in resultlist: # we get rss_author, rss_title, maybe rss_isbn, rss_bookid (goodreads bookid) # we can just use bookid if goodreads, or try isbn and name matching on author/title if not # eg NYTimes wishlist if 'E' in book['types']: ebook_status = "Wanted" else: ebook_status = "Skipped" if 'A' in book['types']: audio_status = "Wanted" else: audio_status = "Skipped" if lazylibrarian.CONFIG['BOOK_API'] == "GoodReads" and book['rss_bookid']: cmd = 'select books.Status as Status,AudioStatus,authors.Status as AuthorStatus,' cmd += 'AuthorName,BookName,Requester,AudioRequester from books,authors ' cmd += 'where books.AuthorID = authors.AuthorID and bookid=?' bookmatch = myDB.match(cmd, (book['rss_bookid'],)) if bookmatch: cmd = 'SELECT SeriesName,Status from series,member ' cmd += 'where series.SeriesID=member.SeriesID and member.BookID=?' series = myDB.select(cmd, (book['rss_bookid'],)) reject_series = None for ser in series: if ser['Status'] in ['Paused', 'Ignored']: reject_series = {"Name": ser['SeriesName'], "Status": ser['Status']} break bookname = bookmatch['BookName'] if bookmatch['Status'] in ['Open', 'Wanted', 'Have']: logger.info('Found book %s, already marked %s' % (bookname, bookmatch['Status'])) if bookmatch["Requester"]: # Already on a wishlist if book["dispname"] not in bookmatch["Requester"]: newValueDict = {"Requester": bookmatch["Requester"] + book["dispname"] + ' '} controlValueDict = {"BookID": book['rss_bookid']} myDB.upsert("books", newValueDict, controlValueDict) else: newValueDict = {"Requester": book["dispname"] + ' '} controlValueDict = {"BookID": book['rss_bookid']} myDB.upsert("books", newValueDict, controlValueDict) elif bookmatch['AuthorStatus'] in ['Paused', 'Ignored']: logger.info('Found book %s, but author is %s' % (bookname, bookmatch['AuthorStatus'])) elif reject_series: logger.info('Found book %s, but series "%s" is %s' % (bookname, reject_series['Name'], reject_series['Status'])) elif ebook_status == "Wanted": # skipped/ignored logger.info('Found book %s, marking as "Wanted"' % bookname) controlValueDict = {"BookID": book['rss_bookid']} newValueDict = {"Status": "Wanted"} myDB.upsert("books", newValueDict, controlValueDict) new_books += 1 if bookmatch["Requester"]: # Already on a wishlist if book["dispname"] not in bookmatch["Requester"]: newValueDict = {"Requester": bookmatch["Requester"] + book["dispname"] + ' '} controlValueDict = {"BookID": book['rss_bookid']} myDB.upsert("books", newValueDict, controlValueDict) else: newValueDict = {"Requester": book["dispname"] + ' '} controlValueDict = {"BookID": book['rss_bookid']} myDB.upsert("books", newValueDict, controlValueDict) if bookmatch['AudioStatus'] in ['Open', 'Wanted', 'Have']: logger.info('Found audiobook %s, already marked %s' % (bookname, bookmatch['AudioStatus'])) if bookmatch["AudioRequester"]: # Already on a wishlist if book["dispname"] not in bookmatch["AudioRequester"]: newValueDict = {"AudioRequester": bookmatch["AudioRequester"] + book["dispname"] + ' '} controlValueDict = {"BookID": book['rss_bookid']} myDB.upsert("books", newValueDict, controlValueDict) else: newValueDict = {"AudioRequester": book["dispname"] + ' '} controlValueDict = {"BookID": book['rss_bookid']} myDB.upsert("books", newValueDict, controlValueDict) elif bookmatch['AuthorStatus'] in ['Paused', 'Ignored']: logger.info('Found book %s, but author is %s' % (bookname, bookmatch['AuthorStatus'])) elif reject_series: logger.info('Found book %s, but series "%s" is %s' % (bookname, reject_series['Name'], reject_series['Status'])) elif audio_status == "Wanted": # skipped/ignored logger.info('Found audiobook %s, marking as "Wanted"' % bookname) controlValueDict = {"BookID": book['rss_bookid']} newValueDict = {"AudioStatus": "Wanted"} myDB.upsert("books", newValueDict, controlValueDict) new_books += 1 if bookmatch["AudioRequester"]: # Already on a wishlist if book["dispname"] not in bookmatch["AudioRequester"]: newValueDict = {"AudioRequester": bookmatch["AudioRequester"] + book["dispname"] + ' '} controlValueDict = {"BookID": book['rss_bookid']} myDB.upsert("books", newValueDict, controlValueDict) else: newValueDict = {"AudioRequester": book["dispname"] + ' '} controlValueDict = {"BookID": book['rss_bookid']} myDB.upsert("books", newValueDict, controlValueDict) else: import_book(book['rss_bookid'], ebook_status, audio_status) new_books += 1 newValueDict = {"Requester": book["dispname"] + ' '} controlValueDict = {"BookID": book['rss_bookid']} myDB.upsert("books", newValueDict, controlValueDict) newValueDict = {"AudioRequester": book["dispname"] + ' '} controlValueDict = {"BookID": book['rss_bookid']} myDB.upsert("books", newValueDict, controlValueDict) else: item = {} results = None item['Title'] = book['rss_title'] if book['rss_bookid']: item['BookID'] = book['rss_bookid'] if book['rss_isbn']: item['ISBN'] = book['rss_isbn'] bookmatch = finditem(item, book['rss_author']) if bookmatch: # it's already in the database authorname = bookmatch['AuthorName'] bookname = bookmatch['BookName'] bookid = bookmatch['BookID'] auth_res = myDB.match('SELECT Status from authors WHERE authorname=?', (authorname,)) if auth_res: auth_status = auth_res['Status'] else: auth_status = 'Unknown' cmd = 'SELECT SeriesName,Status from series,member ' cmd += 'where series.SeriesID=member.SeriesID and member.BookID=?' series = myDB.select(cmd, (book['rss_bookid'],)) reject_series = None for ser in series: if ser['Status'] in ['Paused', 'Ignored']: reject_series = {"Name": ser['SeriesName'], "Status": ser['Status']} break if bookmatch['Status'] in ['Open', 'Wanted', 'Have']: logger.info( 'Found book %s by %s, already marked as "%s"' % (bookname, authorname, bookmatch['Status'])) if bookmatch["Requester"]: # Already on a wishlist if book["dispname"] not in bookmatch["Requester"]: newValueDict = {"Requester": bookmatch["Requester"] + book["dispname"] + ' '} controlValueDict = {"BookID": bookid} myDB.upsert("books", newValueDict, controlValueDict) else: newValueDict = {"Requester": book["dispname"] + ' '} controlValueDict = {"BookID": bookid} myDB.upsert("books", newValueDict, controlValueDict) elif auth_status in ['Paused', 'Ignored']: logger.info('Found book %s, but author is "%s"' % (bookname, auth_status)) elif reject_series: logger.info('Found book %s, but series "%s" is %s' % (bookname, reject_series['Name'], reject_series['Status'])) elif ebook_status == 'Wanted': # skipped/ignored logger.info('Found book %s by %s, marking as "Wanted"' % (bookname, authorname)) controlValueDict = {"BookID": bookid} newValueDict = {"Status": "Wanted"} myDB.upsert("books", newValueDict, controlValueDict) new_books += 1 if bookmatch["Requester"]: # Already on a wishlist if book["dispname"] not in bookmatch["Requester"]: newValueDict = {"Requester": bookmatch["Requester"] + book["dispname"] + ' '} controlValueDict = {"BookID": bookid} myDB.upsert("books", newValueDict, controlValueDict) else: newValueDict = {"Requester": book["dispname"] + ' '} controlValueDict = {"BookID": bookid} myDB.upsert("books", newValueDict, controlValueDict) if bookmatch['AudioStatus'] in ['Open', 'Wanted', 'Have']: logger.info( 'Found audiobook %s by %s, already marked as "%s"' % (bookname, authorname, bookmatch['AudioStatus'])) if bookmatch["AudioRequester"]: # Already on a wishlist if book["dispname"] not in bookmatch["AudioRequester"]: newValueDict = {"AudioRequester": bookmatch["AudioRequester"] + book["dispname"] + ' '} controlValueDict = {"BookID": bookid} myDB.upsert("books", newValueDict, controlValueDict) else: newValueDict = {"AudioRequester": book["dispname"] + ' '} controlValueDict = {"BookID": bookid} myDB.upsert("books", newValueDict, controlValueDict) elif auth_status in ['Paused', 'Ignored']: logger.info('Found book %s, but author is "%s"' % (bookname, auth_status)) elif reject_series: logger.info('Found book %s, but series "%s" is %s' % (bookname, reject_series['Name'], reject_series['Status'])) elif audio_status == 'Wanted': # skipped/ignored logger.info('Found audiobook %s by %s, marking as "Wanted"' % (bookname, authorname)) controlValueDict = {"BookID": bookid} newValueDict = {"AudioStatus": "Wanted"} myDB.upsert("books", newValueDict, controlValueDict) new_books += 1 if bookmatch["AudioRequester"]: # Already on a wishlist if book["dispname"] not in bookmatch["AudioRequester"]: newValueDict = {"AudioRequester": bookmatch["AudioRequester"] + book["dispname"] + ' '} controlValueDict = {"BookID": bookid} myDB.upsert("books", newValueDict, controlValueDict) else: newValueDict = {"AudioRequester": book["dispname"] + ' '} controlValueDict = {"BookID": bookid} myDB.upsert("books", newValueDict, controlValueDict) else: # not in database yet if book['rss_isbn']: results = search_for(book['rss_isbn']) if results: result = results[0] # type: dict if result['isbn_fuzz'] > check_int(lazylibrarian.CONFIG['MATCH_RATIO'], 90): logger.info("Found (%s%%) %s: %s" % (result['isbn_fuzz'], result['authorname'], result['bookname'])) import_book(result['bookid'], ebook_status, audio_status) new_books += 1 newValueDict = {"Requester": book["dispname"] + ' '} controlValueDict = {"BookID": result['bookid']} myDB.upsert("books", newValueDict, controlValueDict) newValueDict = {"AudioRequester": book["dispname"] + ' '} myDB.upsert("books", newValueDict, controlValueDict) bookmatch = True if not results: searchterm = "%s <ll> %s" % (item['Title'], formatAuthorName(book['rss_author'])) results = search_for(unaccented(searchterm)) if results: result = results[0] # type: dict if result['author_fuzz'] > check_int(lazylibrarian.CONFIG['MATCH_RATIO'], 90) \ and result['book_fuzz'] > check_int(lazylibrarian.CONFIG['MATCH_RATIO'], 90): logger.info("Found (%s%% %s%%) %s: %s" % (result['author_fuzz'], result['book_fuzz'], result['authorname'], result['bookname'])) import_book(result['bookid'], ebook_status, audio_status) new_books += 1 newValueDict = {"Requester": book["dispname"] + ' '} controlValueDict = {"BookID": result['bookid']} myDB.upsert("books", newValueDict, controlValueDict) newValueDict = {"AudioRequester": book["dispname"] + ' '} myDB.upsert("books", newValueDict, controlValueDict) bookmatch = True if not bookmatch: msg = "Skipping book %s by %s" % (item['Title'], book['rss_author']) if not results: msg += ', No results returned' logger.warn(msg) else: msg += ', No match found' logger.warn(msg) result = results[0] # type: dict msg = "Closest match (%s%% %s%%) %s: %s" % (result['author_fuzz'], result['book_fuzz'], result['authorname'], result['bookname']) logger.warn(msg) if new_books: logger.info("Wishlist marked %s book%s as Wanted" % (new_books, plural(new_books))) except Exception: logger.error('Unhandled exception in search_wishlist: %s' % traceback.format_exc()) finally: threading.currentThread().name = "WEBSERVER"
def syncCalibreList(col_read=None, col_toread=None, userid=None): """ Get the lazylibrarian bookid for each read/toread calibre book so we can map our id to theirs, and sync current/supplied user's read/toread or supplied read/toread columns to calibre database. Return message giving totals """ myDB = database.DBConnection() if not userid: cookie = cherrypy.request.cookie if cookie and 'll_uid' in cookie.keys(): userid = cookie['ll_uid'].value if userid: res = myDB.match( 'SELECT UserName,ToRead,HaveRead,CalibreRead,CalibreToRead,Perms from users where UserID=?', (userid, )) if res: username = res['UserName'] if not col_read: col_read = res['CalibreRead'] if not col_toread: col_toread = res['CalibreToRead'] toreadlist = getList(res['ToRead']) readlist = getList(res['HaveRead']) # suppress duplicates (just in case) toreadlist = list(set(toreadlist)) readlist = list(set(readlist)) else: return "Error: Unable to get user column settings for %s" % userid if not userid: return "Error: Unable to find current userid" if not col_read and not col_toread: return "User %s has no calibre columns set" % username # check user columns exist in calibre and create if not res = calibredb('custom_columns') columns = res[0].split('\n') custom_columns = [] for column in columns: if column: custom_columns.append(column.split(' (')[0]) if col_read not in custom_columns: added = calibredb('add_custom_column', [col_read, col_read, 'bool']) if "column created" not in added[0]: return added if col_toread not in custom_columns: added = calibredb('add_custom_column', [col_toread, col_toread, 'bool']) if "column created" not in added[0]: return added nomatch = 0 readcol = '' toreadcol = '' map_ctol = {} map_ltoc = {} if col_read: readcol = '*' + col_read if col_toread: toreadcol = '*' + col_toread calibre_list = calibreList(col_read, col_toread) if not isinstance(calibre_list, list): # got an error message from calibredb return '"%s"' % calibre_list for item in calibre_list: if toreadcol and toreadcol in item or readcol and readcol in item: authorname, authorid, added = addAuthorNameToDB(item['authors'], refresh=False, addbooks=False) if authorname: if authorname != item['authors']: logger.debug( "Changed authorname for [%s] from [%s] to [%s]" % (item['title'], item['authors'], authorname)) item['authors'] = authorname bookid = find_book_in_db(authorname, item['title']) if not bookid: searchterm = "%s <ll> %s" % (item['title'], authorname) results = search_for(unaccented(searchterm)) if results: result = results[0] if result['author_fuzz'] > lazylibrarian.CONFIG['MATCH_RATIO'] \ and result['book_fuzz'] > lazylibrarian.CONFIG['MATCH_RATIO']: logger.debug( "Found (%s%% %s%%) %s: %s" % (result['author_fuzz'], result['book_fuzz'], result['authorname'], result['bookname'])) bookid = result['bookid'] import_book(bookid) if bookid: # NOTE: calibre bookid is always an integer, lazylibrarian bookid is a string # (goodreads could be used as an int, but googlebooks can't as it's alphanumeric) # so convert all dict items to strings for ease of matching. map_ctol[str(item['id'])] = str(bookid) map_ltoc[str(bookid)] = str(item['id']) else: logger.warn( 'Calibre Book [%s] by [%s] is not in lazylibrarian database' % (item['title'], authorname)) nomatch += 1 else: logger.warn( 'Calibre Author [%s] not matched in lazylibrarian database' % (item['authors'])) nomatch += 1 # Now check current users lazylibrarian read/toread against the calibre library, warn about missing ones # which might be books calibre doesn't have, or might be minor differences in author or title for idlist in [("Read", readlist), ("To_Read", toreadlist)]: booklist = idlist[1] for bookid in booklist: cmd = "SELECT AuthorID,BookName from books where BookID=?" book = myDB.match(cmd, (bookid, )) if not book: logger.error('Error finding bookid %s' % bookid) else: cmd = "SELECT AuthorName from authors where AuthorID=?" author = myDB.match(cmd, (book['AuthorID'], )) if not author: logger.error('Error finding authorid %s' % book['AuthorID']) else: match = False for item in calibre_list: if item['authors'] == author['AuthorName'] and item[ 'title'] == book['BookName']: logger.debug("Exact match for %s [%s]" % (idlist[0], book['BookName'])) map_ctol[str(item['id'])] = str(bookid) map_ltoc[str(bookid)] = str(item['id']) match = True break if not match: high = 0 highname = '' highid = '' for item in calibre_list: if item['authors'] == author['AuthorName']: n = fuzz.token_sort_ratio( item['title'], book['BookName']) if n > high: high = n highname = item['title'] highid = item['id'] if high > 95: logger.debug( "Found ratio match %s%% [%s] for %s [%s]" % (high, highname, idlist[0], book['BookName'])) map_ctol[str(highid)] = str(bookid) map_ltoc[str(bookid)] = str(highid) match = True if not match: logger.warn( "No match for %s %s by %s in calibre database, closest match %s%% [%s]" % (idlist[0], book['BookName'], author['AuthorName'], high, highname)) nomatch += 1 logger.debug("BookID mapping complete, %s match %s, nomatch %s" % (username, len(map_ctol), nomatch)) # now sync the lists if userid: last_read = [] last_toread = [] calibre_read = [] calibre_toread = [] cmd = 'select SyncList from sync where UserID=? and Label=?' res = myDB.match(cmd, (userid, col_read)) if res: last_read = getList(res['SyncList']) res = myDB.match(cmd, (userid, col_toread)) if res: last_toread = getList(res['SyncList']) for item in calibre_list: if toreadcol and toreadcol in item and item[ toreadcol]: # only if True if str(item['id']) in map_ctol: calibre_toread.append(map_ctol[str(item['id'])]) else: logger.warn( "Calibre to_read book %s:%s has no lazylibrarian bookid" % (item['authors'], item['title'])) if readcol and readcol in item and item[readcol]: # only if True if str(item['id']) in map_ctol: calibre_read.append(map_ctol[str(item['id'])]) else: logger.warn( "Calibre read book %s:%s has no lazylibrarian bookid" % (item['authors'], item['title'])) logger.debug("Found %s calibre read, %s calibre toread" % (len(calibre_read), len(calibre_toread))) logger.debug("Found %s lazylib read, %s lazylib toread" % (len(readlist), len(toreadlist))) added_to_ll_toread = list(set(toreadlist) - set(last_toread)) removed_from_ll_toread = list(set(last_toread) - set(toreadlist)) added_to_ll_read = list(set(readlist) - set(last_read)) removed_from_ll_read = list(set(last_read) - set(readlist)) logger.debug("lazylibrarian changes to copy to calibre: %s %s %s %s" % (len(added_to_ll_toread), len(removed_from_ll_toread), len(added_to_ll_read), len(removed_from_ll_read))) added_to_calibre_toread = list(set(calibre_toread) - set(last_toread)) removed_from_calibre_toread = list( set(last_toread) - set(calibre_toread)) added_to_calibre_read = list(set(calibre_read) - set(last_read)) removed_from_calibre_read = list(set(last_read) - set(calibre_read)) logger.debug( "calibre changes to copy to lazylibrarian: %s %s %s %s" % (len(added_to_calibre_toread), len(removed_from_calibre_toread), len(added_to_calibre_read), len(removed_from_calibre_read))) calibre_changes = 0 for item in added_to_calibre_read: if item not in readlist: readlist.append(item) logger.debug("Lazylibrarian marked %s as read" % item) calibre_changes += 1 for item in added_to_calibre_toread: if item not in toreadlist: toreadlist.append(item) logger.debug("Lazylibrarian marked %s as to_read" % item) calibre_changes += 1 for item in removed_from_calibre_read: if item in readlist: readlist.remove(item) logger.debug("Lazylibrarian removed %s from read" % item) calibre_changes += 1 for item in removed_from_calibre_toread: if item in toreadlist: toreadlist.remove(item) logger.debug("Lazylibrarian removed %s from to_read" % item) calibre_changes += 1 if calibre_changes: myDB.action('UPDATE users SET ToRead=?,HaveRead=? WHERE UserID=?', (', '.join(toreadlist), ', '.join(readlist), userid)) ll_changes = 0 for item in added_to_ll_toread: if item in map_ltoc: res, err, rc = calibredb('set_custom', [col_toread, map_ltoc[item], 'true'], []) if rc: msg = "calibredb set_custom error: " if err: logger.error(msg + err) elif res: logger.error(msg + res) else: logger.error(msg + str(rc)) else: ll_changes += 1 else: logger.warn("Unable to set calibre %s true for %s" % (col_toread, item)) for item in removed_from_ll_toread: if item in map_ltoc: res, err, rc = calibredb('set_custom', [col_toread, map_ltoc[item], ''], []) if rc: msg = "calibredb set_custom error: " if err: logger.error(msg + err) elif res: logger.error(msg + res) else: logger.error(msg + str(rc)) else: ll_changes += 1 else: logger.warn("Unable to clear calibre %s for %s" % (col_toread, item)) for item in added_to_ll_read: if item in map_ltoc: res, err, rc = calibredb('set_custom', [col_read, map_ltoc[item], 'true'], []) if rc: msg = "calibredb set_custom error: " if err: logger.error(msg + err) elif res: logger.error(msg + res) else: logger.error(msg + str(rc)) else: ll_changes += 1 else: logger.warn("Unable to set calibre %s true for %s" % (col_read, item)) for item in removed_from_ll_read: if item in map_ltoc: res, err, rc = calibredb('set_custom', [col_read, map_ltoc[item], ''], []) if rc: msg = "calibredb set_custom error: " if err: logger.error(msg + err) elif res: logger.error(msg + res) else: logger.error(msg + str(rc)) else: ll_changes += 1 else: logger.warn("Unable to clear calibre %s for %s" % (col_read, item)) # store current sync list as comparison for next sync controlValueDict = {"UserID": userid, "Label": col_read} newValueDict = { "Date": str(time.time()), "Synclist": ', '.join(readlist) } myDB.upsert("sync", newValueDict, controlValueDict) controlValueDict = {"UserID": userid, "Label": col_toread} newValueDict = { "Date": str(time.time()), "Synclist": ', '.join(toreadlist) } myDB.upsert("sync", newValueDict, controlValueDict) msg = "%s sync updated: %s calibre, %s lazylibrarian" % ( username, ll_changes, calibre_changes) return msg
def import_CSV(search_dir=None, library='eBook'): """ Find a csv file in the search_dir and process all the books in it, adding authors to the database if not found and marking the books as "Wanted" Optionally delete the file on successful completion """ # noinspection PyBroadException try: if not search_dir: msg = "Alternate Directory not configured" logger.warn(msg) return msg elif not os.path.isdir(search_dir): msg = "Alternate Directory [%s] not found" % search_dir logger.warn(msg) return msg csvFile = csv_file(search_dir, library=library) headers = None myDB = database.DBConnection() bookcount = 0 authcount = 0 skipcount = 0 total = 0 existing = 0 if not csvFile: msg = "No %s CSV file found in %s" % (library, search_dir) logger.warn(msg) return msg else: logger.debug('Reading file %s' % csvFile) csvreader = reader(open(csvFile, 'rU')) for row in csvreader: if csvreader.line_num == 1: # If we are on the first line, create the headers list from the first row headers = row if 'Author' not in headers or 'Title' not in headers: msg = 'Invalid CSV file found %s' % csvFile logger.warn(msg) return msg else: total += 1 item = dict(list(zip(headers, row))) authorname = formatAuthorName(item['Author']) title = makeUnicode(item['Title']) authmatch = myDB.match('SELECT * FROM authors where AuthorName=?', (authorname,)) if authmatch: logger.debug("CSV: Author %s found in database" % authorname) else: logger.debug("CSV: Author %s not found" % authorname) newauthor, authorid, new = addAuthorNameToDB(author=authorname, addbooks=lazylibrarian.CONFIG['NEWAUTHOR_BOOKS']) if len(newauthor) and newauthor != authorname: logger.debug("Preferred authorname changed from [%s] to [%s]" % (authorname, newauthor)) authorname = newauthor if new: authcount += 1 bookmatch = finditem(item, authorname, library=library) result = '' imported = '' if bookmatch: authorname = bookmatch['AuthorName'] bookname = bookmatch['BookName'] bookid = bookmatch['BookID'] if library == 'eBook': bookstatus = bookmatch['Status'] else: bookstatus = bookmatch['AudioStatus'] if bookstatus in ['Open', 'Wanted', 'Have']: existing += 1 logger.info('Found %s %s by %s, already marked as "%s"' % (library, bookname, authorname, bookstatus)) else: # skipped/ignored logger.info('Found %s %s by %s, marking as "Wanted"' % (library, bookname, authorname)) controlValueDict = {"BookID": bookid} if library == 'eBook': newValueDict = {"Status": "Wanted"} else: newValueDict = {"AudioStatus": "Wanted"} myDB.upsert("books", newValueDict, controlValueDict) bookcount += 1 else: searchterm = "%s <ll> %s" % (title, authorname) results = search_for(unaccented(searchterm)) if results: result = results[0] if result['author_fuzz'] >= lazylibrarian.CONFIG['MATCH_RATIO'] \ and result['book_fuzz'] >= lazylibrarian.CONFIG['MATCH_RATIO']: bookmatch = True if not bookmatch: # no match on full searchterm, try splitting out subtitle newtitle, _ = split_title(authorname, title) if newtitle != title: title = newtitle searchterm = "%s <ll> %s" % (title, authorname) results = search_for(unaccented(searchterm)) if results: result = results[0] if result['author_fuzz'] >= lazylibrarian.CONFIG['MATCH_RATIO'] \ and result['book_fuzz'] >= lazylibrarian.CONFIG['MATCH_RATIO']: bookmatch = True if bookmatch: logger.info("Found (%s%% %s%%) %s: %s for %s: %s" % (result['author_fuzz'], result['book_fuzz'], result['authorname'], result['bookname'], authorname, title)) if library == 'eBook': import_book(result['bookid'], ebook="Wanted", wait=True) else: import_book(result['bookid'], audio="Wanted", wait=True) imported = myDB.match('select * from books where BookID=?', (result['bookid'],)) if imported: bookcount += 1 else: bookmatch = False if not bookmatch: msg = "Skipping book %s by %s" % (title, authorname) if not result: msg += ', No results found' logger.warn(msg) elif not imported: msg += ', Failed to import %s' % result['bookid'] logger.warn(msg) else: msg += ', No match found' logger.warn(msg) msg = "Closest match (%s%% %s%%) %s: %s" % (result['author_fuzz'], result['book_fuzz'], result['authorname'], result['bookname']) logger.warn(msg) skipcount += 1 msg = "Found %i %s%s in csv file, %i already existing or wanted" % (total, library, plural(total), existing) logger.info(msg) msg = "Added %i new author%s, marked %i %s%s as 'Wanted', %i %s%s not found" % \ (authcount, plural(authcount), bookcount, library, plural(bookcount), skipcount, plural(skipcount), library) logger.info(msg) if lazylibrarian.CONFIG['DELETE_CSV']: if skipcount == 0: logger.info("Deleting %s on successful completion" % csvFile) try: os.remove(csvFile) except OSError as why: logger.warn('Unable to delete %s: %s' % (csvFile, why.strerror)) else: logger.warn("Not deleting %s as not all books found" % csvFile) if os.path.isdir(csvFile + '.fail'): try: shutil.rmtree(csvFile + '.fail') except Exception as why: logger.warn("Unable to remove %s, %s %s" % (csvFile + '.fail', type(why).__name__, str(why))) try: _ = safe_move(csvFile, csvFile + '.fail') except Exception as e: logger.error("Unable to rename %s, %s %s" % (csvFile, type(e).__name__, str(e))) if not os.access(csvFile, os.R_OK): logger.error("%s is not readable" % csvFile) if not os.access(csvFile, os.W_OK): logger.error("%s is not writeable" % csvFile) parent = os.path.dirname(csvFile) try: with open(os.path.join(parent, 'll_temp'), 'w') as f: f.write('test') os.remove(os.path.join(parent, 'll_temp')) except Exception as why: logger.error("Directory %s is not writeable: %s" % (parent, why)) return msg except Exception: msg = 'Unhandled exception in importCSV: %s' % traceback.format_exc() logger.error(msg) return msg
def search_rss_book(books=None, reset=False): try: threadname = threading.currentThread().name if "Thread-" in threadname: if books is None: threading.currentThread().name = "SEARCHALLRSS" else: threading.currentThread().name = "SEARCHRSS" if not (lazylibrarian.USE_RSS()): logger.warn('RSS search is disabled') scheduleJob(action='Stop', target='search_rss_book') return if not internet(): logger.warn('Search RSS Book: No internet connection') return myDB = database.DBConnection() resultlist, wishproviders = IterateOverGoodReads() if not wishproviders: logger.debug('No rss wishlists are set') else: # for each item in resultlist, add to database if necessary, and mark as wanted for book in resultlist: # we get rss_author, rss_title, rss_isbn, rss_bookid (goodreads bookid) # we can just use bookid if goodreads, or try isbn and name matching on author/title if googlebooks # not sure if anyone would use a goodreads wishlist if not using goodreads interface... logger.debug('Processing %s item%s in wishlists' % (len(resultlist), plural(len(resultlist)))) if book['rss_bookid'] and lazylibrarian.CONFIG[ 'BOOK_API'] == "GoodReads": bookmatch = myDB.match( 'select Status,BookName from books where bookid="%s"' % book['rss_bookid']) if bookmatch: bookstatus = bookmatch['Status'] bookname = bookmatch['BookName'] if bookstatus in ['Open', 'Wanted', 'Have']: logger.info( u'Found book %s, already marked as "%s"' % (bookname, bookstatus)) else: # skipped/ignored logger.info(u'Found book %s, marking as "Wanted"' % bookname) controlValueDict = {"BookID": bookid} newValueDict = {"Status": "Wanted"} myDB.upsert("books", newValueDict, controlValueDict) else: import_book(book['rss_bookid']) else: item = {} headers = [] item['Title'] = book['rss_title'] if book['rss_bookid']: item['BookID'] = book['rss_bookid'] headers.append('BookID') if book['rss_isbn']: item['ISBN'] = book['rss_isbn'] headers.append('ISBN') bookmatch = finditem(item, book['rss_author'], headers) if bookmatch: # it's already in the database authorname = bookmatch['AuthorName'] bookname = bookmatch['BookName'] bookid = bookmatch['BookID'] bookstatus = bookmatch['Status'] if bookstatus in ['Open', 'Wanted', 'Have']: logger.info( u'Found book %s by %s, already marked as "%s"' % (bookname, authorname, bookstatus)) else: # skipped/ignored logger.info( u'Found book %s by %s, marking as "Wanted"' % (bookname, authorname)) controlValueDict = {"BookID": bookid} newValueDict = {"Status": "Wanted"} myDB.upsert("books", newValueDict, controlValueDict) else: # not in database yet results = '' if book['rss_isbn']: results = search_for(book['rss_isbn']) if results: result = results[0] if result['isbn_fuzz'] > lazylibrarian.CONFIG[ 'MATCH_RATIO']: logger.info( "Found (%s%%) %s: %s" % (result['isbn_fuzz'], result['authorname'], result['bookname'])) import_book(result['bookid']) bookmatch = True if not results: searchterm = "%s <ll> %s" % ( item['Title'], formatAuthorName(book['rss_author'])) results = search_for(unaccented(searchterm)) if results: result = results[0] if result['author_fuzz'] > lazylibrarian.CONFIG['MATCH_RATIO'] \ and result['book_fuzz'] > lazylibrarian.CONFIG['MATCH_RATIO']: logger.info( "Found (%s%% %s%%) %s: %s" % (result['author_fuzz'], result['book_fuzz'], result['authorname'], result['bookname'])) import_book(result['bookid']) bookmatch = True if not bookmatch: msg = "Skipping book %s by %s" % (item['Title'], book['rss_author']) # noinspection PyUnboundLocalVariable if not results: msg += ', No results returned' logger.warn(msg) else: msg += ', No match found' logger.warn(msg) msg = "Closest match (%s%% %s%%) %s: %s" % ( result['author_fuzz'], result['book_fuzz'], result['authorname'], result['bookname']) logger.warn(msg) if books is None: # We are performing a backlog search cmd = 'SELECT BookID, AuthorName, Bookname, BookSub, BookAdded from books,authors ' cmd += 'WHERE books.AuthorID = authors.AuthorID and books.Status="Wanted" order by BookAdded desc' searchbooks = myDB.select(cmd) else: # The user has added a new book searchbooks = [] for book in books: cmd = 'SELECT BookID, AuthorName, BookName, BookSub from books,authors ' cmd += 'WHERE books.AuthorID = authors.AuthorID and BookID="%s" ' % book[ 'bookid'] cmd += 'AND books.Status="Wanted"' searchbook = myDB.select(cmd) for terms in searchbook: searchbooks.append(terms) if len(searchbooks) == 0: return resultlist, nproviders = IterateOverRSSSites() if not nproviders: if not wishproviders: logger.warn('No rss providers are set, check config') return # No point in continuing logger.info('RSS Searching for %i book%s' % (len(searchbooks), plural(len(searchbooks)))) rss_count = 0 for book in searchbooks: authorname, bookname = get_searchterm(book, "book") found = processResultList(resultlist, authorname, bookname, book, 'book') # if you can't find the book, try title without any "(extended details, series etc)" if not found and '(' in bookname: # anything to shorten? authorname, bookname = get_searchterm(book, "shortbook") found = processResultList(resultlist, authorname, bookname, book, 'shortbook') if not found: logger.debug( "Searches returned no results. Adding book %s - %s to queue." % (authorname, bookname)) if found > True: rss_count += 1 logger.info("RSS Search for Wanted items complete, found %s book%s" % (rss_count, plural(rss_count))) if reset: scheduleJob(action='Restart', target='search_rss_book') except Exception: logger.error('Unhandled exception in search_rss_book: %s' % traceback.format_exc())
def search_wishlist(): try: threadname = threading.currentThread().name if "Thread-" in threadname: threading.currentThread().name = "SEARCHWISHLIST" myDB = database.DBConnection() resultlist, wishproviders = IterateOverWishLists() new_books = 0 if not wishproviders: logger.debug('No wishlists are set') scheduleJob(action='Stop', target='search_wishlist') return # No point in continuing # for each item in resultlist, add to database if necessary, and mark as wanted logger.debug('Processing %s item%s in wishlists' % (len(resultlist), plural(len(resultlist)))) for book in resultlist: # we get rss_author, rss_title, maybe rss_isbn, rss_bookid (goodreads bookid) # we can just use bookid if goodreads, or try isbn and name matching on author/title if not # eg NYTimes wishlist if 'E' in book['types']: ebook_status = "Wanted" else: ebook_status = "Skipped" if 'A' in book['types']: audio_status = "Wanted" else: audio_status = "Skipped" if lazylibrarian.CONFIG['BOOK_API'] == "GoodReads" and book[ 'rss_bookid']: cmd = 'select Status,AudioStatus,BookName,Requester,AudioRequester from books where bookid=?' bookmatch = myDB.match(cmd, (book['rss_bookid'], )) if bookmatch: bookname = bookmatch['BookName'] if bookmatch['Status'] in ['Open', 'Wanted', 'Have']: logger.info('Found book %s, already marked as "%s"' % (bookname, bookmatch['Status'])) if bookmatch["Requester"]: # Already on a wishlist if book["dispname"] not in bookmatch["Requester"]: newValueDict = { "Requester": bookmatch["Requester"] + book["dispname"] + ' ' } controlValueDict = { "BookID": book['rss_bookid'] } myDB.upsert("books", newValueDict, controlValueDict) else: newValueDict = { "Requester": book["dispname"] + ' ' } controlValueDict = {"BookID": book['rss_bookid']} myDB.upsert("books", newValueDict, controlValueDict) elif ebook_status == "Wanted": # skipped/ignored logger.info('Found book %s, marking as "Wanted"' % bookname) controlValueDict = {"BookID": book['rss_bookid']} newValueDict = {"Status": "Wanted"} myDB.upsert("books", newValueDict, controlValueDict) new_books += 1 if bookmatch["Requester"]: # Already on a wishlist if book["dispname"] not in bookmatch["Requester"]: newValueDict = { "Requester": bookmatch["Requester"] + book["dispname"] + ' ' } controlValueDict = { "BookID": book['rss_bookid'] } myDB.upsert("books", newValueDict, controlValueDict) else: newValueDict = { "Requester": book["dispname"] + ' ' } controlValueDict = {"BookID": book['rss_bookid']} myDB.upsert("books", newValueDict, controlValueDict) if bookmatch['AudioStatus'] in ['Open', 'Wanted', 'Have']: logger.info( 'Found audiobook %s, already marked as "%s"' % (bookname, bookmatch['AudioStatus'])) if bookmatch[ "AudioRequester"]: # Already on a wishlist if book["dispname"] not in bookmatch[ "AudioRequester"]: newValueDict = { "AudioRequester": bookmatch["AudioRequester"] + book["dispname"] + ' ' } controlValueDict = { "BookID": book['rss_bookid'] } myDB.upsert("books", newValueDict, controlValueDict) else: newValueDict = { "AudioRequester": book["dispname"] + ' ' } controlValueDict = {"BookID": book['rss_bookid']} myDB.upsert("books", newValueDict, controlValueDict) elif audio_status == "Wanted": # skipped/ignored logger.info('Found audiobook %s, marking as "Wanted"' % bookname) controlValueDict = {"BookID": book['rss_bookid']} newValueDict = {"AudioStatus": "Wanted"} myDB.upsert("books", newValueDict, controlValueDict) new_books += 1 if bookmatch[ "AudioRequester"]: # Already on a wishlist if book["dispname"] not in bookmatch[ "AudioRequester"]: newValueDict = { "AudioRequester": bookmatch["AudioRequester"] + book["dispname"] + ' ' } controlValueDict = { "BookID": book['rss_bookid'] } myDB.upsert("books", newValueDict, controlValueDict) else: newValueDict = { "AudioRequester": book["dispname"] + ' ' } controlValueDict = {"BookID": book['rss_bookid']} myDB.upsert("books", newValueDict, controlValueDict) else: import_book(book['rss_bookid'], ebook_status, audio_status) new_books += 1 newValueDict = {"Requester": book["dispname"] + ' '} controlValueDict = {"BookID": book['rss_bookid']} myDB.upsert("books", newValueDict, controlValueDict) newValueDict = {"AudioRequester": book["dispname"] + ' '} controlValueDict = {"BookID": book['rss_bookid']} myDB.upsert("books", newValueDict, controlValueDict) else: item = {} results = None item['Title'] = book['rss_title'] if book['rss_bookid']: item['BookID'] = book['rss_bookid'] if book['rss_isbn']: item['ISBN'] = book['rss_isbn'] bookmatch = finditem(item, book['rss_author']) if bookmatch: # it's already in the database authorname = bookmatch['AuthorName'] bookname = bookmatch['BookName'] bookid = bookmatch['BookID'] if bookmatch['Status'] in ['Open', 'Wanted', 'Have']: logger.info( 'Found book %s by %s, already marked as "%s"' % (bookname, authorname, bookmatch['Status'])) if bookmatch["Requester"]: # Already on a wishlist if book["dispname"] not in bookmatch["Requester"]: newValueDict = { "Requester": bookmatch["Requester"] + book["dispname"] + ' ' } controlValueDict = {"BookID": bookid} myDB.upsert("books", newValueDict, controlValueDict) else: newValueDict = { "Requester": book["dispname"] + ' ' } controlValueDict = {"BookID": bookid} myDB.upsert("books", newValueDict, controlValueDict) elif ebook_status == 'Wanted': # skipped/ignored logger.info( 'Found book %s by %s, marking as "Wanted"' % (bookname, authorname)) controlValueDict = {"BookID": bookid} newValueDict = {"Status": "Wanted"} myDB.upsert("books", newValueDict, controlValueDict) new_books += 1 if bookmatch["Requester"]: # Already on a wishlist if book["dispname"] not in bookmatch["Requester"]: newValueDict = { "Requester": bookmatch["Requester"] + book["dispname"] + ' ' } controlValueDict = {"BookID": bookid} myDB.upsert("books", newValueDict, controlValueDict) else: newValueDict = { "Requester": book["dispname"] + ' ' } controlValueDict = {"BookID": bookid} myDB.upsert("books", newValueDict, controlValueDict) if bookmatch['AudioStatus'] in ['Open', 'Wanted', 'Have']: logger.info( 'Found audiobook %s by %s, already marked as "%s"' % (bookname, authorname, bookmatch['AudioStatus'])) if bookmatch[ "AudioRequester"]: # Already on a wishlist if book["dispname"] not in bookmatch[ "AudioRequester"]: newValueDict = { "AudioRequester": bookmatch["AudioRequester"] + book["dispname"] + ' ' } controlValueDict = {"BookID": bookid} myDB.upsert("books", newValueDict, controlValueDict) else: newValueDict = { "AudioRequester": book["dispname"] + ' ' } controlValueDict = {"BookID": bookid} myDB.upsert("books", newValueDict, controlValueDict) elif audio_status == 'Wanted': # skipped/ignored logger.info( 'Found audiobook %s by %s, marking as "Wanted"' % (bookname, authorname)) controlValueDict = {"BookID": bookid} newValueDict = {"AudioStatus": "Wanted"} myDB.upsert("books", newValueDict, controlValueDict) new_books += 1 if bookmatch[ "AudioRequester"]: # Already on a wishlist if book["dispname"] not in bookmatch[ "AudioRequester"]: newValueDict = { "AudioRequester": bookmatch["AudioRequester"] + book["dispname"] + ' ' } controlValueDict = {"BookID": bookid} myDB.upsert("books", newValueDict, controlValueDict) else: newValueDict = { "AudioRequester": book["dispname"] + ' ' } controlValueDict = {"BookID": bookid} myDB.upsert("books", newValueDict, controlValueDict) else: # not in database yet if book['rss_isbn']: results = search_for(book['rss_isbn']) if results: result = results[0] # type: dict if result['isbn_fuzz'] > check_int( lazylibrarian.CONFIG['MATCH_RATIO'], 90): logger.info( "Found (%s%%) %s: %s" % (result['isbn_fuzz'], result['authorname'], result['bookname'])) import_book(result['bookid'], ebook_status, audio_status) new_books += 1 newValueDict = { "Requester": book["dispname"] + ' ' } controlValueDict = {"BookID": result['bookid']} myDB.upsert("books", newValueDict, controlValueDict) newValueDict = { "AudioRequester": book["dispname"] + ' ' } myDB.upsert("books", newValueDict, controlValueDict) bookmatch = True if not results: searchterm = "%s <ll> %s" % (item['Title'], formatAuthorName( book['rss_author'])) results = search_for(unaccented(searchterm)) if results: result = results[0] # type: dict if result['author_fuzz'] > check_int(lazylibrarian.CONFIG['MATCH_RATIO'], 90) \ and result['book_fuzz'] > check_int(lazylibrarian.CONFIG['MATCH_RATIO'], 90): logger.info( "Found (%s%% %s%%) %s: %s" % (result['author_fuzz'], result['book_fuzz'], result['authorname'], result['bookname'])) import_book(result['bookid'], ebook_status, audio_status) new_books += 1 newValueDict = { "Requester": book["dispname"] + ' ' } controlValueDict = {"BookID": result['bookid']} myDB.upsert("books", newValueDict, controlValueDict) newValueDict = { "AudioRequester": book["dispname"] + ' ' } myDB.upsert("books", newValueDict, controlValueDict) bookmatch = True if not bookmatch: msg = "Skipping book %s by %s" % (item['Title'], book['rss_author']) if not results: msg += ', No results returned' logger.warn(msg) else: msg += ', No match found' logger.warn(msg) result = results[0] # type: dict msg = "Closest match (%s%% %s%%) %s: %s" % ( result['author_fuzz'], result['book_fuzz'], result['authorname'], result['bookname']) logger.warn(msg) if new_books: logger.info("Wishlist marked %s book%s as Wanted" % (new_books, plural(new_books))) except Exception: logger.error('Unhandled exception in search_wishlist: %s' % traceback.format_exc()) finally: threading.currentThread().name = "WEBSERVER"
def import_CSV(search_dir=None): """ Find a csv file in the search_dir and process all the books in it, adding authors to the database if not found and marking the books as "Wanted" Optionally delete the file on successful completion """ # noinspection PyBroadException try: if not search_dir: msg = "Alternate Directory not configured" logger.warn(msg) return msg elif not os.path.isdir(search_dir): msg = "Alternate Directory [%s] not found" % search_dir logger.warn(msg) return msg csvFile = csv_file(search_dir) headers = None myDB = database.DBConnection() bookcount = 0 authcount = 0 skipcount = 0 total = 0 existing = 0 if not csvFile: msg = "No CSV file found in %s" % search_dir logger.warn(msg) return msg else: logger.debug('Reading file %s' % csvFile) csvreader = reader(open(csvFile, 'rU')) for row in csvreader: if csvreader.line_num == 1: # If we are on the first line, create the headers list from the first row headers = row if 'Author' not in headers or 'Title' not in headers: msg = 'Invalid CSV file found %s' % csvFile logger.warn(msg) return msg else: total += 1 item = dict(list(zip(headers, row))) authorname = formatAuthorName(item['Author']) title = makeUnicode(item['Title']) authmatch = myDB.match( 'SELECT * FROM authors where AuthorName=?', (authorname, )) if authmatch: logger.debug("CSV: Author %s found in database" % authorname) else: logger.debug("CSV: Author %s not found" % authorname) newauthor, authorid, new = addAuthorNameToDB( author=authorname, addbooks=lazylibrarian.CONFIG['NEWAUTHOR_BOOKS']) if len(newauthor) and newauthor != authorname: logger.debug( "Preferred authorname changed from [%s] to [%s]" % (authorname, newauthor)) authorname = newauthor if new: authcount += 1 bookmatch = finditem(item, authorname) result = '' imported = '' if bookmatch: authorname = bookmatch['AuthorName'] bookname = bookmatch['BookName'] bookid = bookmatch['BookID'] bookstatus = bookmatch['Status'] if bookstatus in ['Open', 'Wanted', 'Have']: existing += 1 logger.info( 'Found book %s by %s, already marked as "%s"' % (bookname, authorname, bookstatus)) else: # skipped/ignored logger.info( 'Found book %s by %s, marking as "Wanted"' % (bookname, authorname)) controlValueDict = {"BookID": bookid} newValueDict = {"Status": "Wanted"} myDB.upsert("books", newValueDict, controlValueDict) bookcount += 1 else: searchterm = "%s <ll> %s" % (title, authorname) results = search_for(unaccented(searchterm)) if results: result = results[0] if result['author_fuzz'] >= lazylibrarian.CONFIG['MATCH_RATIO'] \ and result['book_fuzz'] >= lazylibrarian.CONFIG['MATCH_RATIO']: bookmatch = True if not bookmatch: # no match on full searchterm, try splitting out subtitle newtitle, _ = split_title(authorname, title) if newtitle != title: title = newtitle searchterm = "%s <ll> %s" % (title, authorname) results = search_for(unaccented(searchterm)) if results: result = results[0] if result['author_fuzz'] >= lazylibrarian.CONFIG['MATCH_RATIO'] \ and result['book_fuzz'] >= lazylibrarian.CONFIG['MATCH_RATIO']: bookmatch = True if bookmatch: logger.info( "Found (%s%% %s%%) %s: %s for %s: %s" % (result['author_fuzz'], result['book_fuzz'], result['authorname'], result['bookname'], authorname, title)) import_book(result['bookid'], wait=True) imported = myDB.match( 'select * from books where BookID=?', (result['bookid'], )) if imported: bookcount += 1 else: bookmatch = False if not bookmatch: msg = "Skipping book %s by %s" % (title, authorname) if not result: msg += ', No results found' logger.warn(msg) elif not imported: msg += ', Failed to import %s' % result['bookid'] logger.warn(msg) else: msg += ', No match found' logger.warn(msg) msg = "Closest match (%s%% %s%%) %s: %s" % ( result['author_fuzz'], result['book_fuzz'], result['authorname'], result['bookname']) logger.warn(msg) skipcount += 1 msg = "Found %i book%s in csv file, %i already existing or wanted" % ( total, plural(total), existing) logger.info(msg) msg = "Added %i new author%s, marked %i book%s as 'Wanted', %i book%s not found" % \ (authcount, plural(authcount), bookcount, plural(bookcount), skipcount, plural(skipcount)) logger.info(msg) if lazylibrarian.CONFIG['DELETE_CSV']: if skipcount == 0: logger.info("Deleting %s on successful completion" % csvFile) try: os.remove(csvFile) except OSError as why: logger.warn('Unable to delete %s: %s' % (csvFile, why.strerror)) else: logger.warn("Not deleting %s as not all books found" % csvFile) return msg except Exception: msg = 'Unhandled exception in importCSV: %s' % traceback.format_exc() logger.error(msg) return msg
def syncCalibreList(col_read=None, col_toread=None, userid=None): """ Get the lazylibrarian bookid for each read/toread calibre book so we can map our id to theirs, and sync current/supplied user's read/toread or supplied read/toread columns to calibre database. Return message giving totals """ myDB = database.DBConnection() username = '' readlist = [] toreadlist = [] if not userid: cookie = cherrypy.request.cookie if cookie and 'll_uid' in list(cookie.keys()): userid = cookie['ll_uid'].value if userid: res = myDB.match('SELECT UserName,ToRead,HaveRead,CalibreRead,CalibreToRead,Perms from users where UserID=?', (userid,)) if res: username = res['UserName'] if not col_read: col_read = res['CalibreRead'] if not col_toread: col_toread = res['CalibreToRead'] toreadlist = getList(res['ToRead']) readlist = getList(res['HaveRead']) # suppress duplicates (just in case) toreadlist = list(set(toreadlist)) readlist = list(set(readlist)) else: return "Error: Unable to get user column settings for %s" % userid if not userid: return "Error: Unable to find current userid" if not col_read and not col_toread: return "User %s has no calibre columns set" % username # check user columns exist in calibre and create if not res = calibredb('custom_columns') columns = res[0].split('\n') custom_columns = [] for column in columns: if column: custom_columns.append(column.split(' (')[0]) if col_read not in custom_columns: added = calibredb('add_custom_column', [col_read, col_read, 'bool']) if "column created" not in added[0]: return added if col_toread not in custom_columns: added = calibredb('add_custom_column', [col_toread, col_toread, 'bool']) if "column created" not in added[0]: return added nomatch = 0 readcol = '' toreadcol = '' map_ctol = {} map_ltoc = {} if col_read: readcol = '*' + col_read if col_toread: toreadcol = '*' + col_toread calibre_list = calibreList(col_read, col_toread) if not isinstance(calibre_list, list): # got an error message from calibredb return '"%s"' % calibre_list for item in calibre_list: if toreadcol and toreadcol in item or readcol and readcol in item: authorname, authorid, added = addAuthorNameToDB(item['authors'], refresh=False, addbooks=False) if authorname: if authorname != item['authors']: logger.debug("Changed authorname for [%s] from [%s] to [%s]" % (item['title'], item['authors'], authorname)) item['authors'] = authorname bookid, mtype = find_book_in_db(authorname, item['title'], ignored=False, library='eBook') if bookid and mtype == "Ignored": logger.warn("Book %s by %s is marked Ignored in database, importing anyway" % (item['title'], authorname)) if not bookid: searchterm = "%s <ll> %s" % (item['title'], authorname) results = search_for(unaccented(searchterm)) if results: result = results[0] if result['author_fuzz'] > lazylibrarian.CONFIG['MATCH_RATIO'] \ and result['book_fuzz'] > lazylibrarian.CONFIG['MATCH_RATIO']: logger.debug("Found (%s%% %s%%) %s: %s" % (result['author_fuzz'], result['book_fuzz'], result['authorname'], result['bookname'])) bookid = result['bookid'] import_book(bookid) if bookid: # NOTE: calibre bookid is always an integer, lazylibrarian bookid is a string # (goodreads could be used as an int, but googlebooks can't as it's alphanumeric) # so convert all dict items to strings for ease of matching. map_ctol[str(item['id'])] = str(bookid) map_ltoc[str(bookid)] = str(item['id']) else: logger.warn('Calibre Book [%s] by [%s] is not in lazylibrarian database' % (item['title'], authorname)) nomatch += 1 else: logger.warn('Calibre Author [%s] not matched in lazylibrarian database' % (item['authors'])) nomatch += 1 # Now check current users lazylibrarian read/toread against the calibre library, warn about missing ones # which might be books calibre doesn't have, or might be minor differences in author or title for idlist in [("Read", readlist), ("To_Read", toreadlist)]: booklist = idlist[1] for bookid in booklist: cmd = "SELECT AuthorID,BookName from books where BookID=?" book = myDB.match(cmd, (bookid,)) if not book: logger.error('Error finding bookid %s' % bookid) else: cmd = "SELECT AuthorName from authors where AuthorID=?" author = myDB.match(cmd, (book['AuthorID'],)) if not author: logger.error('Error finding authorid %s' % book['AuthorID']) else: match = False high = 0 highname = '' for item in calibre_list: if item['authors'] == author['AuthorName'] and item['title'] == book['BookName']: logger.debug("Exact match for %s [%s]" % (idlist[0], book['BookName'])) map_ctol[str(item['id'])] = str(bookid) map_ltoc[str(bookid)] = str(item['id']) match = True break if not match: highid = '' for item in calibre_list: if item['authors'] == author['AuthorName']: n = fuzz.token_sort_ratio(item['title'], book['BookName']) if n > high: high = n highname = item['title'] highid = item['id'] if high > 95: logger.debug("Found ratio match %s%% [%s] for %s [%s]" % (high, highname, idlist[0], book['BookName'])) map_ctol[str(highid)] = str(bookid) map_ltoc[str(bookid)] = str(highid) match = True if not match: logger.warn("No match for %s %s by %s in calibre database, closest match %s%% [%s]" % (idlist[0], book['BookName'], author['AuthorName'], high, highname)) nomatch += 1 logger.debug("BookID mapping complete, %s match %s, nomatch %s" % (username, len(map_ctol), nomatch)) # now sync the lists if not userid: msg = "No userid found" else: last_read = [] last_toread = [] calibre_read = [] calibre_toread = [] cmd = 'select SyncList from sync where UserID=? and Label=?' res = myDB.match(cmd, (userid, col_read)) if res: last_read = getList(res['SyncList']) res = myDB.match(cmd, (userid, col_toread)) if res: last_toread = getList(res['SyncList']) for item in calibre_list: if toreadcol and toreadcol in item and item[toreadcol]: # only if True if str(item['id']) in map_ctol: calibre_toread.append(map_ctol[str(item['id'])]) else: logger.warn("Calibre to_read book %s:%s has no lazylibrarian bookid" % (item['authors'], item['title'])) if readcol and readcol in item and item[readcol]: # only if True if str(item['id']) in map_ctol: calibre_read.append(map_ctol[str(item['id'])]) else: logger.warn("Calibre read book %s:%s has no lazylibrarian bookid" % (item['authors'], item['title'])) logger.debug("Found %s calibre read, %s calibre toread" % (len(calibre_read), len(calibre_toread))) logger.debug("Found %s lazylib read, %s lazylib toread" % (len(readlist), len(toreadlist))) added_to_ll_toread = list(set(toreadlist) - set(last_toread)) removed_from_ll_toread = list(set(last_toread) - set(toreadlist)) added_to_ll_read = list(set(readlist) - set(last_read)) removed_from_ll_read = list(set(last_read) - set(readlist)) logger.debug("lazylibrarian changes to copy to calibre: %s %s %s %s" % (len(added_to_ll_toread), len(removed_from_ll_toread), len(added_to_ll_read), len(removed_from_ll_read))) added_to_calibre_toread = list(set(calibre_toread) - set(last_toread)) removed_from_calibre_toread = list(set(last_toread) - set(calibre_toread)) added_to_calibre_read = list(set(calibre_read) - set(last_read)) removed_from_calibre_read = list(set(last_read) - set(calibre_read)) logger.debug("calibre changes to copy to lazylibrarian: %s %s %s %s" % (len(added_to_calibre_toread), len(removed_from_calibre_toread), len(added_to_calibre_read), len(removed_from_calibre_read))) calibre_changes = 0 for item in added_to_calibre_read: if item not in readlist: readlist.append(item) logger.debug("Lazylibrarian marked %s as read" % item) calibre_changes += 1 for item in added_to_calibre_toread: if item not in toreadlist: toreadlist.append(item) logger.debug("Lazylibrarian marked %s as to_read" % item) calibre_changes += 1 for item in removed_from_calibre_read: if item in readlist: readlist.remove(item) logger.debug("Lazylibrarian removed %s from read" % item) calibre_changes += 1 for item in removed_from_calibre_toread: if item in toreadlist: toreadlist.remove(item) logger.debug("Lazylibrarian removed %s from to_read" % item) calibre_changes += 1 if calibre_changes: myDB.action('UPDATE users SET ToRead=?,HaveRead=? WHERE UserID=?', (', '.join(toreadlist), ', '.join(readlist), userid)) ll_changes = 0 for item in added_to_ll_toread: if item in map_ltoc: res, err, rc = calibredb('set_custom', [col_toread, map_ltoc[item], 'true'], []) if rc: msg = "calibredb set_custom error: " if err: logger.error(msg + err) elif res: logger.error(msg + res) else: logger.error(msg + str(rc)) else: ll_changes += 1 else: logger.warn("Unable to set calibre %s true for %s" % (col_toread, item)) for item in removed_from_ll_toread: if item in map_ltoc: res, err, rc = calibredb('set_custom', [col_toread, map_ltoc[item], ''], []) if rc: msg = "calibredb set_custom error: " if err: logger.error(msg + err) elif res: logger.error(msg + res) else: logger.error(msg + str(rc)) else: ll_changes += 1 else: logger.warn("Unable to clear calibre %s for %s" % (col_toread, item)) for item in added_to_ll_read: if item in map_ltoc: res, err, rc = calibredb('set_custom', [col_read, map_ltoc[item], 'true'], []) if rc: msg = "calibredb set_custom error: " if err: logger.error(msg + err) elif res: logger.error(msg + res) else: logger.error(msg + str(rc)) else: ll_changes += 1 else: logger.warn("Unable to set calibre %s true for %s" % (col_read, item)) for item in removed_from_ll_read: if item in map_ltoc: res, err, rc = calibredb('set_custom', [col_read, map_ltoc[item], ''], []) if rc: msg = "calibredb set_custom error: " if err: logger.error(msg + err) elif res: logger.error(msg + res) else: logger.error(msg + str(rc)) else: ll_changes += 1 else: logger.warn("Unable to clear calibre %s for %s" % (col_read, item)) # store current sync list as comparison for next sync controlValueDict = {"UserID": userid, "Label": col_read} newValueDict = {"Date": str(time.time()), "Synclist": ', '.join(readlist)} myDB.upsert("sync", newValueDict, controlValueDict) controlValueDict = {"UserID": userid, "Label": col_toread} newValueDict = {"Date": str(time.time()), "Synclist": ', '.join(toreadlist)} myDB.upsert("sync", newValueDict, controlValueDict) msg = "%s sync updated: %s calibre, %s lazylibrarian" % (username, ll_changes, calibre_changes) return msg
def search_wishlist(): if not (lazylibrarian.USE_RSS()): logger.warn('RSS search is disabled') scheduleJob(action='Stop', target='search_wishlist') return try: threadname = threading.currentThread().name if "Thread-" in threadname: threading.currentThread().name = "SEARCHWISHLIST" myDB = database.DBConnection() resultlist, wishproviders = IterateOverWishLists() new_books = 0 if not wishproviders: logger.debug('No wishlists are set') scheduleJob(action='Stop', target='search_wishlist') return # No point in continuing # for each item in resultlist, add to database if necessary, and mark as wanted logger.debug('Processing %s item%s in wishlists' % (len(resultlist), plural(len(resultlist)))) for book in resultlist: # we get rss_author, rss_title, maybe rss_isbn, rss_bookid (goodreads bookid) # we can just use bookid if goodreads, or try isbn and name matching on author/title if googlebooks # not sure if anyone would use a goodreads wishlist if not using goodreads interface... if lazylibrarian.CONFIG['BOOK_API'] == "GoodReads" and book['rss_bookid']: bookmatch = myDB.match('select Status,BookName from books where bookid=?', (book['rss_bookid'],)) if bookmatch: bookstatus = bookmatch['Status'] bookname = bookmatch['BookName'] if bookstatus in ['Open', 'Wanted', 'Have']: logger.info('Found book %s, already marked as "%s"' % (bookname, bookstatus)) else: # skipped/ignored logger.info('Found book %s, marking as "Wanted"' % bookname) controlValueDict = {"BookID": book['rss_bookid']} newValueDict = {"Status": "Wanted"} myDB.upsert("books", newValueDict, controlValueDict) new_books += 1 else: import_book(book['rss_bookid']) new_books += 1 else: item = {} results = None item['Title'] = book['rss_title'] if book['rss_bookid']: item['BookID'] = book['rss_bookid'] if book['rss_isbn']: item['ISBN'] = book['rss_isbn'] bookmatch = finditem(item, book['rss_author']) if bookmatch: # it's already in the database authorname = bookmatch['AuthorName'] bookname = bookmatch['BookName'] bookid = bookmatch['BookID'] bookstatus = bookmatch['Status'] if bookstatus in ['Open', 'Wanted', 'Have']: logger.info( 'Found book %s by %s, already marked as "%s"' % (bookname, authorname, bookstatus)) else: # skipped/ignored logger.info('Found book %s by %s, marking as "Wanted"' % (bookname, authorname)) controlValueDict = {"BookID": bookid} newValueDict = {"Status": "Wanted"} myDB.upsert("books", newValueDict, controlValueDict) new_books += 1 else: # not in database yet if book['rss_isbn']: results = search_for(book['rss_isbn']) if results: result = results[0] # type: dict if result['isbn_fuzz'] > check_int(lazylibrarian.CONFIG['MATCH_RATIO'], 90): logger.info("Found (%s%%) %s: %s" % (result['isbn_fuzz'], result['authorname'], result['bookname'])) import_book(result['bookid']) new_books += 1 bookmatch = True if not results: searchterm = "%s <ll> %s" % (item['Title'], formatAuthorName(book['rss_author'])) results = search_for(unaccented(searchterm)) if results: result = results[0] # type: dict if result['author_fuzz'] > check_int(lazylibrarian.CONFIG['MATCH_RATIO'], 90) \ and result['book_fuzz'] > check_int(lazylibrarian.CONFIG['MATCH_RATIO'], 90): logger.info("Found (%s%% %s%%) %s: %s" % (result['author_fuzz'], result['book_fuzz'], result['authorname'], result['bookname'])) import_book(result['bookid']) new_books += 1 bookmatch = True if not bookmatch: msg = "Skipping book %s by %s" % (item['Title'], book['rss_author']) if not results: msg += ', No results returned' logger.warn(msg) else: msg += ', No match found' logger.warn(msg) result = results[0] # type: dict msg = "Closest match (%s%% %s%%) %s: %s" % (result['author_fuzz'], result['book_fuzz'], result['authorname'], result['bookname']) logger.warn(msg) if new_books: logger.info("Wishlist marked %s book%s as Wanted" % (new_books, plural(new_books))) except Exception: logger.error('Unhandled exception in search_wishlist: %s' % traceback.format_exc()) finally: threading.currentThread().name = "WEBSERVER"