main.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. """main.py: A GUI to download an OPDS feed, filter out parts of the results, and download selected books from the feed into the local library"""
  2. __author__ = "Steinar Bang"
  3. __copyright__ = "Steinar Bang, 2015-2021"
  4. __credits__ = ["Steinar Bang"]
  5. __license__ = "GPL v3"
  6. import sys
  7. import datetime
  8. from PyQt6.QtCore import Qt, QSortFilterProxyModel, QStringListModel
  9. from PyQt6.QtWidgets import QDialog, QGridLayout, QLineEdit, QComboBox, QPushButton, QCheckBox, QMessageBox, QLabel, QAbstractItemView, QTableView, QHeaderView
  10. from calibre_plugins.opds_client.model import OpdsBooksModel
  11. from calibre_plugins.opds_client.config import prefs
  12. from calibre_plugins.opds_client import config
  13. from calibre.ebooks.metadata.book.base import Metadata
  14. class DynamicBook(dict):
  15. pass
  16. class OpdsDialog(QDialog):
  17. def __init__(self, gui, icon, do_user_config):
  18. QDialog.__init__(self, gui)
  19. self.gui = gui
  20. self.do_user_config = do_user_config
  21. self.db = gui.current_db.new_api
  22. # The model for the book list
  23. self.model = OpdsBooksModel(None, self.dummy_books(), self.db)
  24. self.searchproxymodel = QSortFilterProxyModel(self)
  25. self.searchproxymodel.setFilterCaseSensitivity(Qt.CaseInsensitive)
  26. self.searchproxymodel.setFilterKeyColumn(-1)
  27. self.searchproxymodel.setSourceModel(self.model)
  28. self.layout = QGridLayout()
  29. self.setLayout(self.layout)
  30. self.setWindowTitle('OPDS Client')
  31. self.setWindowIcon(icon)
  32. labelColumnWidths = []
  33. self.opdsUrlLabel = QLabel('OPDS URL: ')
  34. self.layout.addWidget(self.opdsUrlLabel, 0, 0)
  35. labelColumnWidths.append(self.layout.itemAtPosition(0, 0).sizeHint().width())
  36. config.convertSingleStringOpdsUrlPreferenceToListOfStringsPreference()
  37. self.opdsUrlEditor = QComboBox(self)
  38. self.opdsUrlEditor.activated.connect(self.opdsUrlEditorActivated)
  39. self.opdsUrlEditor.addItems(prefs['opds_url'])
  40. self.opdsUrlEditor.setEditable(True)
  41. self.opdsUrlEditor.setInsertPolicy(QComboBox.InsertAtTop)
  42. self.layout.addWidget(self.opdsUrlEditor, 0, 1, 1, 3)
  43. self.opdsUrlLabel.setBuddy(self.opdsUrlEditor)
  44. buttonColumnNumber = 7
  45. buttonColumnWidths = []
  46. self.about_button = QPushButton('About', self)
  47. self.about_button.setAutoDefault(False)
  48. self.about_button.clicked.connect(self.about)
  49. self.layout.addWidget(self.about_button, 0, buttonColumnNumber)
  50. buttonColumnWidths.append(self.layout.itemAtPosition(0, buttonColumnNumber).sizeHint().width())
  51. # Initially download the catalogs found in the root catalog of the URL
  52. # selected at startup. Fail quietly on failing to open the URL
  53. catalogsTuple = self.model.downloadOpdsRootCatalog(self.gui, self.opdsUrlEditor.currentText(), False)
  54. print(catalogsTuple)
  55. firstCatalogTitle = catalogsTuple[0]
  56. self.currentOpdsCatalogs = catalogsTuple[1] # A dictionary of title->feedURL
  57. self.opdsCatalogSelectorLabel = QLabel('OPDS Catalog:')
  58. self.layout.addWidget(self.opdsCatalogSelectorLabel, 1, 0)
  59. labelColumnWidths.append(self.layout.itemAtPosition(1, 0).sizeHint().width())
  60. self.opdsCatalogSelector = QComboBox(self)
  61. self.opdsCatalogSelector.setEditable(False)
  62. self.opdsCatalogSelectorModel = QStringListModel(self.currentOpdsCatalogs.keys())
  63. self.opdsCatalogSelector.setModel(self.opdsCatalogSelectorModel)
  64. self.opdsCatalogSelector.setCurrentText(firstCatalogTitle)
  65. self.layout.addWidget(self.opdsCatalogSelector, 1, 1, 1, 3)
  66. self.download_opds_button = QPushButton('Download OPDS', self)
  67. self.download_opds_button.setAutoDefault(False)
  68. self.download_opds_button.clicked.connect(self.download_opds)
  69. self.layout.addWidget(self.download_opds_button, 1, buttonColumnNumber)
  70. buttonColumnWidths.append(self.layout.itemAtPosition(1, buttonColumnNumber).sizeHint().width())
  71. # Search GUI
  72. self.searchEditor = QLineEdit(self)
  73. self.searchEditor.returnPressed.connect(self.searchBookList)
  74. self.layout.addWidget(self.searchEditor, 2, buttonColumnNumber - 2, 1, 2)
  75. self.searchButton = QPushButton('Search', self)
  76. self.searchButton.setAutoDefault(False)
  77. self.searchButton.clicked.connect(self.searchBookList)
  78. self.layout.addWidget(self.searchButton, 2, buttonColumnNumber)
  79. buttonColumnWidths.append(self.layout.itemAtPosition(2, buttonColumnNumber).sizeHint().width())
  80. # The main book list
  81. self.library_view = QTableView(self)
  82. self.library_view.setAlternatingRowColors(True)
  83. self.library_view.setModel(self.searchproxymodel)
  84. self.library_view.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
  85. self.library_view.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
  86. self.library_view.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
  87. self.library_view.setSelectionBehavior(QAbstractItemView.SelectRows)
  88. self.resizeAllLibraryViewLinesToHeaderHeight()
  89. self.library_view.resizeColumnsToContents()
  90. self.layout.addWidget(self.library_view, 3, 0, 3, buttonColumnNumber + 1)
  91. self.hideNewsCheckbox = QCheckBox('Hide Newspapers', self)
  92. self.hideNewsCheckbox.clicked.connect(self.setHideNewspapers)
  93. self.hideNewsCheckbox.setChecked(prefs['hideNewspapers'])
  94. self.layout.addWidget(self.hideNewsCheckbox, 6, 0, 1, 3)
  95. self.hideBooksAlreadyInLibraryCheckbox = QCheckBox('Hide books already in library', self)
  96. self.hideBooksAlreadyInLibraryCheckbox.clicked.connect(self.setHideBooksAlreadyInLibrary)
  97. self.hideBooksAlreadyInLibraryCheckbox.setChecked(prefs['hideBooksAlreadyInLibrary'])
  98. self.layout.addWidget(self.hideBooksAlreadyInLibraryCheckbox, 7, 0, 1, 3)
  99. # Let the checkbox initial state control the filtering
  100. self.model.setFilterBooksThatAreNewspapers(self.hideNewsCheckbox.isChecked())
  101. self.model.setFilterBooksThatAreAlreadyInLibrary(self.hideBooksAlreadyInLibraryCheckbox.isChecked())
  102. self.downloadButton = QPushButton('Download selected books', self)
  103. self.downloadButton.setAutoDefault(False)
  104. self.downloadButton.clicked.connect(self.downloadSelectedBooks)
  105. self.layout.addWidget(self.downloadButton, 6, buttonColumnNumber)
  106. buttonColumnWidths.append(self.layout.itemAtPosition(6, buttonColumnNumber).sizeHint().width())
  107. self.fixTimestampButton = QPushButton('Fix timestamps of selection', self)
  108. self.fixTimestampButton.setAutoDefault(False)
  109. self.fixTimestampButton.clicked.connect(self.fixBookTimestamps)
  110. self.layout.addWidget(self.fixTimestampButton, 7, buttonColumnNumber)
  111. buttonColumnWidths.append(self.layout.itemAtPosition(7, buttonColumnNumber).sizeHint().width())
  112. # Make all columns of the grid layout the same width as the button column
  113. buttonColumnWidth = max(buttonColumnWidths)
  114. for columnNumber in range(0, buttonColumnNumber):
  115. self.layout.setColumnMinimumWidth(columnNumber, buttonColumnWidth)
  116. # Make sure the first column isn't wider than the labels it holds
  117. labelColumnWidth = max(labelColumnWidths)
  118. self.layout.setColumnMinimumWidth(0, labelColumnWidth)
  119. self.resize(self.sizeHint())
  120. def opdsUrlEditorActivated(self, text):
  121. prefs['opds_url'] = config.saveOpdsUrlCombobox(self.opdsUrlEditor)
  122. catalogsTuple = self.model.downloadOpdsRootCatalog(self.gui, self.opdsUrlEditor.currentText(), True)
  123. firstCatalogTitle = catalogsTuple[0]
  124. self.currentOpdsCatalogs = catalogsTuple[1] # A dictionary of title->feedURL
  125. self.opdsCatalogSelectorModel.setStringList(self.currentOpdsCatalogs.keys())
  126. self.opdsCatalogSelector.setCurrentText(firstCatalogTitle)
  127. def setHideNewspapers(self, checked):
  128. prefs['hideNewspapers'] = checked
  129. self.model.setFilterBooksThatAreNewspapers(checked)
  130. self.resizeAllLibraryViewLinesToHeaderHeight()
  131. def setHideBooksAlreadyInLibrary(self, checked):
  132. prefs['hideBooksAlreadyInLibrary'] = checked
  133. self.model.setFilterBooksThatAreAlreadyInLibrary(checked)
  134. self.resizeAllLibraryViewLinesToHeaderHeight()
  135. def searchBookList(self):
  136. searchString = self.searchEditor.text()
  137. print("starting book list search for: %s" % searchString)
  138. self.searchproxymodel.setFilterFixedString(searchString)
  139. def about(self):
  140. text = get_resources('about.txt')
  141. QMessageBox.about(self, 'About the OPDS Client plugin', text.decode('utf-8'))
  142. def download_opds(self):
  143. opdsCatalogUrl = self.currentOpdsCatalogs.get(self.opdsCatalogSelector.currentText(), None)
  144. if opdsCatalogUrl is None:
  145. # Just give up quietly
  146. return
  147. self.model.downloadOpdsCatalog(self.gui, opdsCatalogUrl)
  148. if self.model.isCalibreOpdsServer():
  149. self.model.downloadMetadataUsingCalibreRestApi(self.opdsUrlEditor.currentText())
  150. self.library_view.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
  151. self.library_view.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
  152. self.library_view.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
  153. self.resizeAllLibraryViewLinesToHeaderHeight()
  154. self.resize(self.sizeHint())
  155. def config(self):
  156. self.do_user_config(parent=self)
  157. def downloadSelectedBooks(self):
  158. selectionmodel = self.library_view.selectionModel()
  159. if selectionmodel.hasSelection():
  160. rows = selectionmodel.selectedRows()
  161. for row in reversed(rows):
  162. book = row.data(Qt.UserRole)
  163. self.downloadBook(book)
  164. def downloadBook(self, book):
  165. if len(book.links) > 0:
  166. self.gui.download_ebook(book.links[0])
  167. def fixBookTimestamps(self):
  168. selectionmodel = self.library_view.selectionModel()
  169. if selectionmodel.hasSelection():
  170. rows = selectionmodel.selectedRows()
  171. for row in reversed(rows):
  172. book = row.data(Qt.UserRole)
  173. self.fixBookTimestamp(book)
  174. def fixBookTimestamp(self, book):
  175. bookTimestamp = book.timestamp
  176. identicalBookIds = self.findIdenticalBooksForBooksWithMultipleAuthors(book)
  177. bookIdToValMap = {}
  178. for identicalBookId in identicalBookIds:
  179. bookIdToValMap[identicalBookId] = bookTimestamp
  180. if len(bookIdToValMap) < 1:
  181. print("Failed to set timestamp of book: %s" % book)
  182. self.db.set_field('timestamp', bookIdToValMap)
  183. def findIdenticalBooksForBooksWithMultipleAuthors(self, book):
  184. authorsList = book.authors
  185. if len(authorsList) < 2:
  186. return self.db.find_identical_books(book)
  187. # Try matching the authors one by one
  188. identicalBookIds = set()
  189. for author in authorsList:
  190. singleAuthorBook = Metadata(book.title, [author])
  191. singleAuthorIdenticalBookIds = self.db.find_identical_books(singleAuthorBook)
  192. identicalBookIds = identicalBookIds.union(singleAuthorIdenticalBookIds)
  193. return identicalBookIds
  194. def dummy_books(self):
  195. dummy_author = ' ' * 40
  196. dummy_title = ' ' * 60
  197. books_list = []
  198. for line in range (1, 10):
  199. book = DynamicBook()
  200. book.author = dummy_author
  201. book.title = dummy_title
  202. book.updated = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S+00:00')
  203. book.id = ''
  204. books_list.append(book)
  205. return books_list
  206. def resizeAllLibraryViewLinesToHeaderHeight(self):
  207. rowHeight = self.library_view.horizontalHeader().height()
  208. for rowNumber in range (0, self.library_view.model().rowCount()):
  209. self.library_view.setRowHeight(rowNumber, rowHeight)