From f6767a3d078d1ef07e8f60f28720536e8a181cb1 Mon Sep 17 00:00:00 2001 From: Phil Borman Date: Fri, 23 Jun 2017 00:04:52 +0200 Subject: [PATCH 1/4] Message changes, libgen is DIRECT download, not Torrent --- lazylibrarian/importer.py | 1 + lazylibrarian/postprocess.py | 11 ++++++----- lazylibrarian/resultlist.py | 21 ++++++++++++--------- lazylibrarian/searchbook.py | 14 +++++++++++--- lazylibrarian/torrentparser.py | 2 +- 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/lazylibrarian/importer.py b/lazylibrarian/importer.py index 4eadcc51b..31b67e0ac 100644 --- a/lazylibrarian/importer.py +++ b/lazylibrarian/importer.py @@ -277,6 +277,7 @@ def update_totals(AuthorID): # author totals needs to be updated every time a book is marked differently match = myDB.select('SELECT AuthorID from authors WHERE AuthorID="%s"' % AuthorID) if not match: + logger.debug('Update_totals - authorid [%s] not found' % AuthorID) return cmd = 'SELECT BookName, BookLink, BookDate from books WHERE AuthorID="%s"' % AuthorID cmd += ' AND Status != "Ignored" order by BookDate DESC' diff --git a/lazylibrarian/postprocess.py b/lazylibrarian/postprocess.py index 34a394540..32ed66a3e 100644 --- a/lazylibrarian/postprocess.py +++ b/lazylibrarian/postprocess.py @@ -529,6 +529,7 @@ def processDir(reset=False): logger.debug("Not removing original files as in download root") logger.info('Successfully processed: %s' % global_name) + ppcount += 1 custom_notify_download(book['BookID']) @@ -838,11 +839,6 @@ def processExtras(dest_file=None, global_name=None, bookid=None, book_type="eBoo myDB = database.DBConnection() - # update authors book counts - match = myDB.match('SELECT AuthorID FROM books WHERE BookID="%s"' % bookid) - if match: - update_totals(match['AuthorID']) - controlValueDict = {"BookID": bookid} if book_type == 'AudioBook': newValueDict = {"AudioFile": dest_file, "AudioStatus": "Open", "AudioLibrary": now()} @@ -851,6 +847,11 @@ def processExtras(dest_file=None, global_name=None, bookid=None, book_type="eBoo newValueDict = {"Status": "Open", "BookFile": dest_file, "BookLibrary": now()} myDB.upsert("books", newValueDict, controlValueDict) + # update authors book counts + match = myDB.match('SELECT AuthorID FROM books WHERE BookID="%s"' % bookid) + if match: + update_totals(match['AuthorID']) + if book_type != 'eBook': # only do autoadd/img/opf for ebooks return diff --git a/lazylibrarian/resultlist.py b/lazylibrarian/resultlist.py index 541b3c6ef..d3de621d9 100644 --- a/lazylibrarian/resultlist.py +++ b/lazylibrarian/resultlist.py @@ -97,8 +97,11 @@ def findBestResult(resultlist, book, searchtype, source): resultTitle = re.sub(r"\s\s+", " ", resultTitle) # remove extra whitespace Author_match = fuzz.token_set_ratio(author, resultTitle) Book_match = fuzz.token_set_ratio(title, resultTitle) + stype = source.upper() + if res[prefix + 'prov'] == 'libgen': + stype = "DIR" logger.debug(u"%s author/book Match: %s/%s for %s at %s" % - (source.upper(), Author_match, Book_match, resultTitle, res[prefix + 'prov'])) + (stype, Author_match, Book_match, resultTitle, res[prefix + 'prov'])) rejected = False @@ -147,11 +150,11 @@ def findBestResult(resultlist, book, searchtype, source): newTitle = (author + ' - ' + title + ' LL.(' + book['bookid'] + ')').strip() if source == 'nzb': - mode = res['nzbmode'] + mode = res['nzbmode'] # nzb, torznab elif source == 'tor': - mode = "torrent" - else: # rss returns torrents - mode = "torrent" + mode = res['tor_type'] # torrent, magnet, direct + else: + mode = res['tor_type'] # torrent, magnet, nzb controlValueDict = {"NZBurl": url} newValueDict = { @@ -197,11 +200,11 @@ def findBestResult(resultlist, book, searchtype, source): dlpriority = highest[4] if score < int(lazylibrarian.CONFIG['MATCH_RATIO']): - logger.info(u'Nearest %s match (%s%%): %s using %s search for %s %s' % - (source.upper(), score, resultTitle, searchtype, book['authorName'], book['bookName'])) + logger.info(u'Nearest match (%s%%): %s using %s search for %s %s' % + (score, resultTitle, searchtype, book['authorName'], book['bookName'])) else: - logger.info(u'Best %s match (%s%%): %s using %s search, %s priority %s' % - (source.upper(), score, resultTitle, searchtype, newValueDict['NZBprov'], dlpriority)) + logger.info(u'Best match (%s%%): %s using %s search, %s priority %s' % + (score, resultTitle, searchtype, newValueDict['NZBprov'], dlpriority)) return highest else: logger.debug("No %s found for [%s] using searchtype %s" % (source, book["searchterm"], searchtype)) diff --git a/lazylibrarian/searchbook.py b/lazylibrarian/searchbook.py index 0d68525bd..637844167 100644 --- a/lazylibrarian/searchbook.py +++ b/lazylibrarian/searchbook.py @@ -40,6 +40,7 @@ def search_book(books=None, library=None): library is "eBook" or "AudioBook" or None to search all book types """ # noinspection PyBroadException + print "***",books,library try: threadname = threading.currentThread().name if "Thread-" in threadname: @@ -67,8 +68,12 @@ def search_book(books=None, library=None): cmd += 'from books,authors WHERE BookID="%s" ' % book['bookid'] cmd += 'AND books.AuthorID = authors.AuthorID' results = myDB.select(cmd) - for terms in results: - searchbooks.append(terms) + if results: + for terms in results: + searchbooks.append(terms) + else: + logger.debug("SearchBooks - BookID %s is not in the database" % book['bookid']) + if len(searchbooks) == 0: logger.debug("SearchBooks - No books to search for") @@ -209,8 +214,11 @@ def search_book(books=None, library=None): logger.info("%s Searches for %s %s returned no results." % (mode.upper(), book['library'], book['searchterm'])) else: + smode = mode.upper() + if match[2]['NZBprov'] == 'libgen': + smode = 'DIR' logger.info("Found %s result: %s %s%%, %s priority %s" % - (mode.upper(), searchtype, match[0], match[2]['NZBprov'], match[4])) + (smode, searchtype, match[0], match[2]['NZBprov'], match[4])) matches.append(match) if matches: diff --git a/lazylibrarian/torrentparser.py b/lazylibrarian/torrentparser.py index 7298cfdad..43f824348 100644 --- a/lazylibrarian/torrentparser.py +++ b/lazylibrarian/torrentparser.py @@ -796,7 +796,7 @@ def GEN(book=None): 'tor_title': title, 'tor_url': url, 'tor_size': str(size), - 'tor_type': 'direct', + 'tor_type': 'download', 'priority': lazylibrarian.CONFIG['GEN_DLPRIORITY'] }) logger.debug('Found %s, Size %s' % (title, size)) From 7ed9768ec27e92f2004e692c0e33af66498f41d0 Mon Sep 17 00:00:00 2001 From: Phil Borman Date: Fri, 23 Jun 2017 16:27:12 +0200 Subject: [PATCH 2/4] Ensure params are url encoded --- lazylibrarian/torrentparser.py | 59 ++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/lazylibrarian/torrentparser.py b/lazylibrarian/torrentparser.py index 43f824348..b22a06d6d 100644 --- a/lazylibrarian/torrentparser.py +++ b/lazylibrarian/torrentparser.py @@ -41,7 +41,7 @@ def TPB(book=None): if not str(host)[:4] == "http": host = 'http://' + host - providerurl = url_fix(host + "/s/?q=" + book['searchterm']) + providerurl = url_fix(host + "/s/?") cat = 0 # 601=ebooks, 102=audiobooks, 0=all, no mag category if 'library' in book: @@ -60,14 +60,17 @@ def TPB(book=None): while next_page: params = { + "q": book['searchterm'], "category": cat, "page": page, "orderby": "99" } - searchURL = providerurl + "&%s" % urllib.urlencode(params) + searchURL = providerurl + "?%s" % urllib.urlencode(params) + next_page = False result, success = fetchURL(searchURL) + if not success: # may return 404 if no results, not really an error if '404' in result: @@ -90,7 +93,6 @@ def TPB(book=None): if len(rows) > 1: rows = rows[1:] # first row is headers - for row in rows: td = row.findAll('td') if len(td) > 2: @@ -169,7 +171,7 @@ def KAT(book=None): if not str(host)[:4] == "http": host = 'http://' + host - providerurl = url_fix(host + "/usearch/" + book['searchterm']) + providerurl = url_fix(host + "/usearch/" + urllib.quote(book['searchterm'])) params = { "category": "books", @@ -298,7 +300,6 @@ def WWT(book=None): next_page = True while next_page: - params = { "search": book['searchterm'], "page": page, @@ -323,7 +324,8 @@ def WWT(book=None): soup = BeautifulSoup(result) try: - table = soup.findAll('table')[2] # un-named table + tables = soup.findAll('table') # un-named table + table = tables[2] if table: rows = table.findAll('tr') except IndexError: # no results table in result page @@ -337,8 +339,7 @@ def WWT(book=None): if len(td) > 3: try: title = unaccented(str(td[0]).split('title="')[1].split('"')[0]) - - # kat can return magnet or torrent or both. + # can return magnet or torrent or both. magnet = '' url = '' mode = 'torrent' @@ -377,6 +378,7 @@ def WWT(book=None): seeders = int(td[2].text) except ValueError: seeders = 0 + if not url or not title: logger.debug('Missing url or title') elif minimumseeders < int(seeders): @@ -396,7 +398,6 @@ def WWT(book=None): except Exception as e: logger.error(u"An error occurred in the %s parser: %s" % (provider, str(e))) logger.debug('%s: %s' % (provider, traceback.format_exc())) - page += 1 if 0 < lazylibrarian.CONFIG['MAX_PAGES'] < page: logger.warn('Maximum results page search reached, still more results available') @@ -492,13 +493,14 @@ def ZOO(book=None): if not str(host)[:4] == "http": host = 'http://' + host - providerurl = url_fix(host + "/search?q=" + book['searchterm']) + providerurl = url_fix(host + "/search") params = { + "q": book['searchterm'], "category": "books", "fmt": "rss" } - searchURL = providerurl + "&%s" % urllib.urlencode(params) + searchURL = providerurl + "?%s" % urllib.urlencode(params) data, success = fetchURL(searchURL) if not success: @@ -572,7 +574,11 @@ def LIME(book=None): if not str(host)[:4] == "http": host = 'http://' + host - searchURL = url_fix(host + "/searchrss/other/?q=" + book['searchterm']) + params = { + "q": book['searchterm'] + } + providerurl = url_fix(host + "/searchrss/other") + searchURL = providerurl + "?%s" % urllib.urlencode(params) data, success = fetchURL(searchURL) if not success: @@ -661,16 +667,29 @@ def GEN(book=None): if search[0] == '/': search = search[1:] - pagenum = '' - if page > 1: - pagenum = '&page=%s' % page - if 'index.php' in search: - searchURL = url_fix(host + "/%s?%s&s=%s" % - (search, pagenum, book['searchterm'])) + params = { + "s": book['searchterm'] + } + if page > 1: + params['page'] = page + + providerurl = url_fix(host + "/%s" % search) + searchURL = providerurl + "?%s" % urllib.urlencode(params) else: - searchURL = url_fix(host + "/%s?view=simple&open=0&phrase=0&column=def&res=100%s&req=%s" % - (search, pagenum, book['searchterm'])) + params = { + "view": "simple", + "open": 0, + "phrase": 0, + "column": "def", + "res": 100, + "req": book['searchterm'] + } + if page > 1: + params['page'] = page + + providerurl = url_fix(host + "/%s" % search) + searchURL = providerurl + "?%s" % urllib.urlencode(params) next_page = False result, success = fetchURL(searchURL) From cb33fd93e51d76b6cf9e20d985fcd6f885b90b62 Mon Sep 17 00:00:00 2001 From: Phil Borman Date: Fri, 23 Jun 2017 16:27:44 +0200 Subject: [PATCH 3/4] Code tidying --- lazylibrarian/resultlist.py | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/lazylibrarian/resultlist.py b/lazylibrarian/resultlist.py index d3de621d9..386d6de84 100644 --- a/lazylibrarian/resultlist.py +++ b/lazylibrarian/resultlist.py @@ -116,34 +116,28 @@ def findBestResult(resultlist, book, searchtype, source): logger.debug("Rejecting %s, blacklisted at %s" % (resultTitle, already_failed['NZBprov'])) rejected = True - if not rejected: - if not url.startswith('http') and not url.startswith('magnet'): + if not rejected and not url.startswith('http') and not url.startswith('magnet'): rejected = True logger.debug("Rejecting %s, invalid URL [%s]" % (resultTitle, url)) if not rejected: - author_words = getList(author.lower()) - title_words = getList(title.lower()) - result_words = getList(resultTitle.lower()) for word in reject_list: - if word in result_words and word not in author_words and word not in title_words: + if word in getList(resultTitle.lower()) and word not in getList(author.lower()) \ + and word not in getList(title.lower()): rejected = True logger.debug("Rejecting %s, contains %s" % (resultTitle, word)) break - size_temp = res[prefix + 'size'] # Need to cater for when this is NONE (Issue 35) - size_temp = check_int(size_temp, 1000) + size_temp = check_int(res[prefix + 'size'], 1000) # Need to cater for when this is NONE (Issue 35) size = round(float(size_temp) / 1048576, 2) - if not rejected: - if maxsize and size > maxsize: + if not rejected and maxsize and size > maxsize: rejected = True logger.debug("Rejecting %s, too large" % resultTitle) - if not rejected: - if minsize and size < minsize: - rejected = True - logger.debug("Rejecting %s, too small" % resultTitle) + if not rejected and minsize and size < minsize: + rejected = True + logger.debug("Rejecting %s, too small" % resultTitle) if not rejected: bookid = book['bookid'] @@ -172,8 +166,7 @@ def findBestResult(resultlist, book, searchtype, source): # lose a point for each unwanted word in the title so we get the closest match # but for RSS ignore anything at the end in square braces [keywords, genres etc] if source == 'rss': - temptitle = resultTitle.rsplit('[', 1)[0] - wordlist = getList(temptitle.lower()) + wordlist = getList(resultTitle.rsplit('[', 1)[0].lower()) else: wordlist = getList(resultTitle.lower()) words = [x for x in wordlist if x not in getList(author.lower())] @@ -187,8 +180,7 @@ def findBestResult(resultlist, book, searchtype, source): booktypes = [x for x in wordlist if x in getList(lazylibrarian.CONFIG['AUDIOBOOK_TYPE'])] score -= len(words) # prioritise titles that include the ebook types we want - if len(booktypes): - score += 1 + score += len(booktypes) matches.append([score, resultTitle, newValueDict, controlValueDict, res['priority']]) if matches: From d3e47fc362093026aa646327b8302ce4f208a784 Mon Sep 17 00:00:00 2001 From: Phil Borman Date: Fri, 23 Jun 2017 16:28:01 +0200 Subject: [PATCH 4/4] Renamed search filter to "Filter" --- data/interfaces/bookstrap/audio.html | 1 + data/interfaces/bookstrap/author.html | 1 + data/interfaces/bookstrap/books.html | 1 + data/interfaces/bookstrap/config.html | 2 +- data/interfaces/bookstrap/history.html | 1 + data/interfaces/bookstrap/index.html | 1 + data/interfaces/bookstrap/issues.html | 1 + data/interfaces/bookstrap/logs.html | 2 +- data/interfaces/bookstrap/magazines.html | 1 + data/interfaces/bookstrap/managebooks.html | 1 + data/interfaces/bookstrap/manageissues.html | 1 + data/interfaces/bookstrap/manualsearch.html | 1 + data/interfaces/bookstrap/members.html | 1 + data/interfaces/bookstrap/searchresults.html | 1 + data/interfaces/bookstrap/series.html | 1 + 15 files changed, 15 insertions(+), 2 deletions(-) diff --git a/data/interfaces/bookstrap/audio.html b/data/interfaces/bookstrap/audio.html index 1b1454a7f..e6718301f 100644 --- a/data/interfaces/bookstrap/audio.html +++ b/data/interfaces/bookstrap/audio.html @@ -133,6 +133,7 @@

${title}

return btn;} } ], "oLanguage": { + "sSearch": "Filter: ", "sLengthMenu":"_MENU_ rows per page", "sEmptyTable": "No books found", "sInfo":"Showing _START_ to _END_ of _TOTAL_ rows", diff --git a/data/interfaces/bookstrap/author.html b/data/interfaces/bookstrap/author.html index 4adfc78ea..6541e2410 100644 --- a/data/interfaces/bookstrap/author.html +++ b/data/interfaces/bookstrap/author.html @@ -235,6 +235,7 @@

${author[ return btn;} } ], "oLanguage": { + "sSearch": "Filter: ", "sLengthMenu":"_MENU_ rows per page", "sEmptyTable": "No books found", "sInfo":"Showing _START_ to _END_ of _TOTAL_ rows", diff --git a/data/interfaces/bookstrap/books.html b/data/interfaces/bookstrap/books.html index 29614ac82..4c6f39ecd 100644 --- a/data/interfaces/bookstrap/books.html +++ b/data/interfaces/bookstrap/books.html @@ -134,6 +134,7 @@

${title}

return btn;} } ], "oLanguage": { + "sSearch": "Filter: ", "sLengthMenu":"_MENU_ rows per page", "sEmptyTable": "No books found", "sInfo":"Showing _START_ to _END_ of _TOTAL_ rows", diff --git a/data/interfaces/bookstrap/config.html b/data/interfaces/bookstrap/config.html index 901e2e0c0..c5a828dfc 100644 --- a/data/interfaces/bookstrap/config.html +++ b/data/interfaces/bookstrap/config.html @@ -1089,7 +1089,7 @@

${title}

-
+

diff --git a/data/interfaces/bookstrap/history.html b/data/interfaces/bookstrap/history.html index 9caa3057b..3e0fd3ffd 100644 --- a/data/interfaces/bookstrap/history.html +++ b/data/interfaces/bookstrap/history.html @@ -97,6 +97,7 @@

${title}

return ftype;} }, ], "oLanguage": { + "sSearch": "Filter: ", "sLengthMenu":"Show _MENU_ rows per page", "sEmptyTable": "No history found", "sInfo":"Showing _START_ to _END_ of _TOTAL_ rows", diff --git a/data/interfaces/bookstrap/index.html b/data/interfaces/bookstrap/index.html index c41bf16da..91362985e 100644 --- a/data/interfaces/bookstrap/index.html +++ b/data/interfaces/bookstrap/index.html @@ -105,6 +105,7 @@

${title}

{ type: 'natural', targets: 3 }, { type: 'natural-nohtml', targets: 4 }], "oLanguage": { + "sSearch": "Filter: ", "sLengthMenu":"_MENU_ rows per page", "sEmptyTable": "No authors found", "sInfo":"Showing _START_ to _END_ of _TOTAL_ rows", diff --git a/data/interfaces/bookstrap/issues.html b/data/interfaces/bookstrap/issues.html index 5b8604b16..2f19f06da 100644 --- a/data/interfaces/bookstrap/issues.html +++ b/data/interfaces/bookstrap/issues.html @@ -91,6 +91,7 @@

${title}

"order": [[ 2, 'desc']], "columnDefs": [{ targets: 'no-sort', orderable: false }], "oLanguage": { + "sSearch": "Filter: ", "sLengthMenu":"Show _MENU_ issues per page", "sEmptyTable": "No issues found", "sInfo":"Showing _START_ to _END_ of _TOTAL_ results", diff --git a/data/interfaces/bookstrap/logs.html b/data/interfaces/bookstrap/logs.html index 08bf46b0a..4f0a9b7b0 100644 --- a/data/interfaces/bookstrap/logs.html +++ b/data/interfaces/bookstrap/logs.html @@ -52,7 +52,7 @@

${title}

"stateSave": true, "order": [[ 0, 'desc' ]], "oLanguage": { - "sSearch":"", + "sSearch":"Filter: ", "sLengthMenu":"Show _MENU_ rows per page", "sEmptyTable": "No log information available", "sInfo":"Showing _START_ to _END_ of _TOTAL_ rows", diff --git a/data/interfaces/bookstrap/magazines.html b/data/interfaces/bookstrap/magazines.html index e7d02c3d2..773fc5f41 100644 --- a/data/interfaces/bookstrap/magazines.html +++ b/data/interfaces/bookstrap/magazines.html @@ -138,6 +138,7 @@

${title}

[{ targets: 'no-sort', orderable: false }, { type: 'natural', targets: 3 }], "oLanguage": { + "sSearch": "Filter: ", "sLengthMenu":"_MENU_ rows per page", "sEmptyTable": "No magazines found", "sInfo":"Showing _START_ to _END_ of _TOTAL_ rows", diff --git a/data/interfaces/bookstrap/managebooks.html b/data/interfaces/bookstrap/managebooks.html index 22ac302e8..08f2b7468 100644 --- a/data/interfaces/bookstrap/managebooks.html +++ b/data/interfaces/bookstrap/managebooks.html @@ -91,6 +91,7 @@

${title}

return 'Rating';} } ], "oLanguage": { + "sSearch": "Filter: ", "sLengthMenu":"_MENU_ rows per page", "sEmptyTable": "No books found", "sInfo":"Showing _START_ to _END_ of _TOTAL_ rows", diff --git a/data/interfaces/bookstrap/manageissues.html b/data/interfaces/bookstrap/manageissues.html index eaf492c92..2de8cb5f5 100644 --- a/data/interfaces/bookstrap/manageissues.html +++ b/data/interfaces/bookstrap/manageissues.html @@ -75,6 +75,7 @@

Magazines with status ${whichStatus}

] , "oLanguage": { + "sSearch": "Filter: ", "sLengthMenu":"_MENU_ rows per page", "sEmptyTable": "No matching issues found", "sInfo":"Showing _START_ to _END_ of _TOTAL_ rows", diff --git a/data/interfaces/bookstrap/manualsearch.html b/data/interfaces/bookstrap/manualsearch.html index 898a9e1ed..5c90f086b 100644 --- a/data/interfaces/bookstrap/manualsearch.html +++ b/data/interfaces/bookstrap/manualsearch.html @@ -50,6 +50,7 @@

${title}

"responsive": true, "order": [[ 0, 'desc' ]], "oLanguage": { + "sSearch": "Filter: ", "sLengthMenu":"Show _MENU_ rows per page", "sEmptyTable": "No results found", "sInfo":"Showing _START_ to _END_ of _TOTAL_ rows", diff --git a/data/interfaces/bookstrap/members.html b/data/interfaces/bookstrap/members.html index 4b847c536..05ac12ce6 100644 --- a/data/interfaces/bookstrap/members.html +++ b/data/interfaces/bookstrap/members.html @@ -93,6 +93,7 @@

${series['AuthorName']} : ${title}

"columnDefs": [{ targets: 'no-sort', orderable: false }, { type: 'natural', targets: 4 }], "oLanguage": { + "sSearch": "Filter: ", "sLengthMenu":"_MENU_ rows per page", "sEmptyTable": "No books found", "sInfo":"Showing _START_ to _END_ of _TOTAL_ rows", diff --git a/data/interfaces/bookstrap/searchresults.html b/data/interfaces/bookstrap/searchresults.html index e35a644e8..ece9a66bb 100644 --- a/data/interfaces/bookstrap/searchresults.html +++ b/data/interfaces/bookstrap/searchresults.html @@ -130,6 +130,7 @@

${title}

[{ targets: 'no-sort', orderable: false }] , "oLanguage": { + "sSearch": "Filter: ", "sLengthMenu":"Show _MENU_ books per page", "sEmptyTable": "No books found", "sInfo":"Showing _START_ to _END_ of _TOTAL_ results", diff --git a/data/interfaces/bookstrap/series.html b/data/interfaces/bookstrap/series.html index 9355e0ca9..2a7945fd1 100644 --- a/data/interfaces/bookstrap/series.html +++ b/data/interfaces/bookstrap/series.html @@ -76,6 +76,7 @@

${title}

} ], "oLanguage": { + "sSearch": "Filter: ", "sLengthMenu":"_MENU_ rows per page", "sEmptyTable": "No series found", "sInfo":"Showing _START_ to _END_ of _TOTAL_ rows",