123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239 |
- """model.py: This is a QAbstractTableModel that holds a list of Metadata objects created from books in an OPDS feed"""
- __author__ = "Steinar Bang"
- __copyright__ = "Steinar Bang, 2015-2022"
- __credits__ = ["Steinar Bang"]
- __license__ = "GPL v3"
- import datetime
- from PyQt5.Qt import Qt, QAbstractTableModel, QCoreApplication
- from calibre.ebooks.metadata.book.base import Metadata
- from calibre.gui2 import error_dialog
- from calibre.web.feeds import feedparser
- import urllib.parse
- import urllib.request
- import json
- import re
- class OpdsBooksModel(QAbstractTableModel):
- column_headers = [_('Title'), _('Author(s)'), _('Updated')]
- booktableColumnCount = 3
- filterBooksThatAreNewspapers = False
- filterBooksThatAreAlreadyInLibrary = False
- def __init__(self, parent, books = [], db = None):
- QAbstractTableModel.__init__(self, parent)
- self.db = db
- self.books = self.makeMetadataFromParsedOpds(books)
- self.filterBooks()
- def headerData(self, section, orientation, role):
- if role != Qt.DisplayRole:
- return None
- if orientation == Qt.Vertical:
- return section + 1
- if section >= len(self.column_headers):
- return None
- return self.column_headers[section]
- def rowCount(self, parent):
- return len(self.filteredBooks)
- def columnCount(self, parent):
- return self.booktableColumnCount
- def data(self, index, role):
- row, col = index.row(), index.column()
- if row >= len(self.filteredBooks):
- return None
- opdsBook = self.filteredBooks[row]
- if role == Qt.UserRole:
-
- return opdsBook
- if role != Qt.DisplayRole:
- return None
- if col >= self.booktableColumnCount:
- return None
- if col == 0:
- return opdsBook.title
- if col == 1:
- return u' & '.join(opdsBook.author)
- if col == 2:
- if opdsBook.timestamp is not None:
- return opdsBook.timestamp.strftime("%Y-%m-%d %H:%M:%S")
- return opdsBook.timestamp
- return None
- def downloadOpdsRootCatalog(self, gui, opdsUrl, displayDialogOnErrors):
- feed = feedparser.parse(opdsUrl)
- if 'bozo_exception' in feed:
- exception = feed['bozo_exception']
- message = 'Failed opening the OPDS URL ' + opdsUrl + ': '
- reason = ''
- if hasattr(exception, 'reason') :
- reason = str(exception.reason)
- error_dialog(gui, _('Failed opening the OPDS URL'), message, reason, displayDialogOnErrors)
- return (None, {})
- if 'server' in feed.headers:
- self.serverHeader = feed.headers['server']
- else:
- self.serverHeader = "none"
- print("serverHeader: %s" % self.serverHeader)
- print("feed.entries: %s" % feed.entries)
- catalogEntries = {}
- firstTitle = None
- for entry in feed.entries:
- title = entry.get('title', 'No title')
- if firstTitle is None:
- firstTitle = title
- links = entry.get('links', [])
- firstLink = next(iter(links), None)
- if firstLink is not None:
- print("firstLink: %s" % firstLink)
- catalogEntries[title] = firstLink.href
- return (firstTitle, catalogEntries)
- def downloadOpdsCatalog(self, gui, opdsCatalogUrl):
- print("downloading catalog: %s" % opdsCatalogUrl)
- opdsCatalogFeed = feedparser.parse(opdsCatalogUrl)
- self.books = self.makeMetadataFromParsedOpds(opdsCatalogFeed.entries)
- self.filterBooks()
- QCoreApplication.processEvents()
- nextUrl = self.findNextUrl(opdsCatalogFeed.feed)
- while nextUrl is not None:
- nextFeed = feedparser.parse(nextUrl)
- self.books = self.books + self.makeMetadataFromParsedOpds(nextFeed.entries)
- self.filterBooks()
- QCoreApplication.processEvents()
- nextUrl = self.findNextUrl(nextFeed.feed)
- def isCalibreOpdsServer(self):
- return self.serverHeader.startswith('calibre')
- def setFilterBooksThatAreAlreadyInLibrary(self, value):
- if value != self.filterBooksThatAreAlreadyInLibrary:
- self.filterBooksThatAreAlreadyInLibrary = value
- self.filterBooks()
- def setFilterBooksThatAreNewspapers(self, value):
- if value != self.filterBooksThatAreNewspapers:
- self.filterBooksThatAreNewspapers = value
- self.filterBooks()
- def filterBooks(self):
- self.beginResetModel()
- self.filteredBooks = []
- for book in self.books:
- if (not self.isFilteredNews(book)) and (not self.isFilteredAlreadyInLibrary(book)):
- self.filteredBooks.append(book)
- self.endResetModel()
- def isFilteredNews(self, book):
- if self.filterBooksThatAreNewspapers:
- if u'News' in book.tags:
- return True
- return False
- def isFilteredAlreadyInLibrary(self, book):
- if self.filterBooksThatAreAlreadyInLibrary:
- return self.db.has_book(book)
- return False
- def makeMetadataFromParsedOpds(self, books):
- metadatalist = []
- for book in books:
- metadata = self.opdsToMetadata(book)
- metadatalist.append(metadata)
- return metadatalist
- def opdsToMetadata(self, opdsBookStructure):
- authors = opdsBookStructure.author.replace(u'& ', u'&') if 'author' in opdsBookStructure else ''
- metadata = Metadata(opdsBookStructure.title, authors.split(u'&'))
- metadata.uuid = opdsBookStructure.id.replace('urn:uuid:', '', 1) if 'id' in opdsBookStructure else ''
- try:
- rawTimestamp = opdsBookStructure.updated
- except AttributeError:
- rawTimestamp = "1980-01-01T00:00:00+00:00"
- parsableTimestamp = re.sub('((\.[0-9]+)?\+0[0-9]:00|Z)$', '', rawTimestamp)
- metadata.timestamp = datetime.datetime.strptime(parsableTimestamp, '%Y-%m-%dT%H:%M:%S')
- tags = []
- summary = opdsBookStructure.get(u'summary', u'')
- summarylines = summary.splitlines()
- for summaryline in summarylines:
- if summaryline.startswith(u'TAGS: '):
- tagsline = summaryline.replace(u'TAGS: ', u'')
- tagsline = tagsline.replace(u'<br />',u'')
- tagsline = tagsline.replace(u', ', u',')
- tags = tagsline.split(u',')
- metadata.tags = tags
- bookDownloadUrls = []
- links = opdsBookStructure.get('links', [])
- for link in links:
- url = link.get('href', '')
- bookType = link.get('type', '')
-
- if not bookType.startswith('image/'):
- if bookType == 'application/epub+zip':
-
- bookDownloadUrls.insert(0, url)
- else:
-
- bookDownloadUrls.append(url)
- metadata.links = bookDownloadUrls
- return metadata
- def findNextUrl(self, feed):
- for link in feed.links:
- if link.rel == u'next':
- return link.href
- return None
- def downloadMetadataUsingCalibreRestApi(self, opdsUrl):
-
-
-
-
-
-
-
-
-
- parsedOpdsUrl = urllib.parse.urlparse(opdsUrl)
-
-
-
- parsedCalibreRestSearchUrl = urllib.parse.ParseResult(parsedOpdsUrl.scheme, parsedOpdsUrl.netloc, '/ajax/search', '', '', '')
- calibreRestSearchUrl = parsedCalibreRestSearchUrl.geturl()
- calibreRestSearchResponse = urllib.request.urlopen(calibreRestSearchUrl)
- calibreRestSearchJsonResponse = json.load(calibreRestSearchResponse)
- getAllIdsArgument = 'num=' + str(calibreRestSearchJsonResponse['total_num']) + '&offset=0'
- parsedCalibreRestSearchUrl = urllib.parse.ParseResult(parsedOpdsUrl.scheme, parsedOpdsUrl.netloc, '/ajax/search', '', getAllIdsArgument, '').geturl()
- calibreRestSearchResponse = urllib.request.urlopen(parsedCalibreRestSearchUrl)
- calibreRestSearchJsonResponse = json.load(calibreRestSearchResponse)
- bookIds = list(map(str, calibreRestSearchJsonResponse['book_ids']))
-
-
- bookIdsGetArgument = 'ids=' + ','.join(bookIds)
- parsedCalibreRestBooksUrl = urllib.parse.ParseResult(parsedOpdsUrl.scheme, parsedOpdsUrl.netloc, '/ajax/books', '', bookIdsGetArgument, '')
- calibreRestBooksResponse = urllib.request.urlopen(parsedCalibreRestBooksUrl.geturl())
- booksDictionary = json.load(calibreRestBooksResponse)
- self.updateTimestampInMetadata(bookIds, booksDictionary)
- def updateTimestampInMetadata(self, bookIds, booksDictionary):
- bookMetadataById = {}
- for bookId in bookIds:
- bookMetadata = booksDictionary[bookId]
- uuid = bookMetadata['uuid']
- bookMetadataById[uuid] = bookMetadata
- for book in self.books:
- bookMetadata = bookMetadataById[book.uuid]
- rawTimestamp = bookMetadata['timestamp']
- parsableTimestamp = re.sub('(\.[0-9]+)?\+00:00$', '', rawTimestamp)
- timestamp = datetime.datetime.strptime(parsableTimestamp, '%Y-%m-%dT%H:%M:%S')
- book.timestamp = timestamp
- self.filterBooks()
|