mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from custcol trunk
This commit is contained in:
commit
6f504a1009
@ -1,11 +1,10 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
|
__copyright__ = '2009-2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
'''
|
'''
|
||||||
www.instapaper.com
|
www.instapaper.com
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
import urllib
|
||||||
from calibre import strftime
|
from calibre import strftime
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
@ -22,18 +21,15 @@ class Instapaper(BasicNewsRecipe):
|
|||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
remove_javascript = True
|
|
||||||
needs_subscription = True
|
needs_subscription = True
|
||||||
INDEX = u'http://www.instapaper.com'
|
INDEX = u'http://www.instapaper.com'
|
||||||
LOGIN = INDEX + u'/user/login'
|
LOGIN = INDEX + u'/user/login'
|
||||||
|
|
||||||
html2lrf_options = [
|
conversion_options = {
|
||||||
'--comment', description
|
'comment' : description
|
||||||
, '--category', category
|
, 'tags' : category
|
||||||
, '--publisher', publisher
|
, 'publisher' : publisher
|
||||||
]
|
}
|
||||||
|
|
||||||
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\noverride_css=" p {text-indent: 0em; margin-top: 0em; margin-bottom: 0.5em} img {margin-top: 0em; margin-bottom: 0.4em}"'
|
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
(u'Unread articles' , INDEX + u'/u' )
|
(u'Unread articles' , INDEX + u'/u' )
|
||||||
@ -63,7 +59,7 @@ class Instapaper(BasicNewsRecipe):
|
|||||||
description = self.tag_to_string(item.div)
|
description = self.tag_to_string(item.div)
|
||||||
atag = item.a
|
atag = item.a
|
||||||
if atag and atag.has_key('href'):
|
if atag and atag.has_key('href'):
|
||||||
url = self.INDEX + atag['href'] + '/text'
|
url = atag['href']
|
||||||
title = self.tag_to_string(atag)
|
title = self.tag_to_string(atag)
|
||||||
date = strftime(self.timefmt)
|
date = strftime(self.timefmt)
|
||||||
articles.append({
|
articles.append({
|
||||||
@ -75,3 +71,6 @@ class Instapaper(BasicNewsRecipe):
|
|||||||
totalfeeds.append((feedtitle, articles))
|
totalfeeds.append((feedtitle, articles))
|
||||||
return totalfeeds
|
return totalfeeds
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
return self.INDEX + '/text?u=' + urllib.quote(url)
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ class NewYorkReviewOfBooks(BasicNewsRecipe):
|
|||||||
no_javascript = True
|
no_javascript = True
|
||||||
needs_subscription = True
|
needs_subscription = True
|
||||||
|
|
||||||
keep_only_tags = [dict(id='article-body')]
|
keep_only_tags = [dict(id=['article-body','page-title'])]
|
||||||
remove_tags = [dict(attrs={'class':['article-tools', 'article-links',
|
remove_tags = [dict(attrs={'class':['article-tools', 'article-links',
|
||||||
'center advertisement']})]
|
'center advertisement']})]
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ class NewYorkReviewOfBooks(BasicNewsRecipe):
|
|||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
no_javascript = True
|
no_javascript = True
|
||||||
|
|
||||||
keep_only_tags = [dict(id='article-body')]
|
keep_only_tags = [dict(id=['article-body', 'page-title'])]
|
||||||
remove_tags = [dict(attrs={'class':['article-tools', 'article-links',
|
remove_tags = [dict(attrs={'class':['article-tools', 'article-links',
|
||||||
'center advertisement']})]
|
'center advertisement']})]
|
||||||
|
|
||||||
|
@ -380,7 +380,9 @@ class BookList(list):
|
|||||||
3. size (file size of the book)
|
3. size (file size of the book)
|
||||||
4. datetime (a UTC time tuple)
|
4. datetime (a UTC time tuple)
|
||||||
5. path (path on the device to the book)
|
5. path (path on the device to the book)
|
||||||
6. thumbnail (can be None)
|
6. thumbnail (can be None) thumbnail is either a str/bytes object with the
|
||||||
|
image data or it should have an attribute image_path that stores an
|
||||||
|
absolute (platform native) path to the image
|
||||||
7. tags (a list of strings, can be empty).
|
7. tags (a list of strings, can be empty).
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@ -38,8 +38,10 @@ class Book(MetaInformation):
|
|||||||
self.lpath = lpath
|
self.lpath = lpath
|
||||||
self.mime = mime_type_ext(path_to_ext(lpath))
|
self.mime = mime_type_ext(path_to_ext(lpath))
|
||||||
self.size = size # will be set later if None
|
self.size = size # will be set later if None
|
||||||
|
try:
|
||||||
|
self.datetime = time.gmtime(os.path.getctime(self.path))
|
||||||
|
except:
|
||||||
self.datetime = time.gmtime()
|
self.datetime = time.gmtime()
|
||||||
|
|
||||||
if other:
|
if other:
|
||||||
self.smart_update(other)
|
self.smart_update(other)
|
||||||
|
|
||||||
|
@ -276,8 +276,10 @@ class USBMS(CLI, Device):
|
|||||||
# bl = cls.booklist_class()
|
# bl = cls.booklist_class()
|
||||||
js = []
|
js = []
|
||||||
need_sync = False
|
need_sync = False
|
||||||
|
cache_file = cls.normalize_path(os.path.join(prefix, name))
|
||||||
|
if os.access(cache_file, os.R_OK):
|
||||||
try:
|
try:
|
||||||
with open(cls.normalize_path(os.path.join(prefix, name)), 'rb') as f:
|
with open(cache_file, 'rb') as f:
|
||||||
js = json.load(f, encoding='utf-8')
|
js = json.load(f, encoding='utf-8')
|
||||||
for item in js:
|
for item in js:
|
||||||
book = cls.book_class(prefix, item.get('lpath', None))
|
book = cls.book_class(prefix, item.get('lpath', None))
|
||||||
@ -289,6 +291,8 @@ class USBMS(CLI, Device):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
bl = []
|
bl = []
|
||||||
need_sync = True
|
need_sync = True
|
||||||
|
else:
|
||||||
|
need_sync = True
|
||||||
return need_sync
|
return need_sync
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -37,8 +37,13 @@ class EPUBInput(InputFormatPlugin):
|
|||||||
scheme = item.get(xkey)
|
scheme = item.get(xkey)
|
||||||
if (scheme and scheme.lower() == 'uuid') or \
|
if (scheme and scheme.lower() == 'uuid') or \
|
||||||
(item.text and item.text.startswith('urn:uuid:')):
|
(item.text and item.text.startswith('urn:uuid:')):
|
||||||
|
try:
|
||||||
key = str(item.text).rpartition(':')[-1]
|
key = str(item.text).rpartition(':')[-1]
|
||||||
key = list(map(ord, uuid.UUID(key).bytes))
|
key = list(map(ord, uuid.UUID(key).bytes))
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
key = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
root = etree.parse(encfile)
|
root = etree.parse(encfile)
|
||||||
@ -49,7 +54,7 @@ class EPUBInput(InputFormatPlugin):
|
|||||||
cr = em.getparent().xpath('descendant::*[contains(name(), "CipherReference")]')[0]
|
cr = em.getparent().xpath('descendant::*[contains(name(), "CipherReference")]')[0]
|
||||||
uri = cr.get('URI')
|
uri = cr.get('URI')
|
||||||
path = os.path.abspath(os.path.join(os.path.dirname(encfile), '..', *uri.split('/')))
|
path = os.path.abspath(os.path.join(os.path.dirname(encfile), '..', *uri.split('/')))
|
||||||
if os.path.exists(path):
|
if key is not None and os.path.exists(path):
|
||||||
self._encrypted_font_uris.append(uri)
|
self._encrypted_font_uris.append(uri)
|
||||||
self.decrypt_font(key, path)
|
self.decrypt_font(key, path)
|
||||||
return True
|
return True
|
||||||
|
@ -20,7 +20,7 @@ from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
|
|||||||
pixmap_to_data, warning_dialog, \
|
pixmap_to_data, warning_dialog, \
|
||||||
question_dialog
|
question_dialog
|
||||||
from calibre.ebooks.metadata import authors_to_string, authors_to_sort_string
|
from calibre.ebooks.metadata import authors_to_string, authors_to_sort_string
|
||||||
from calibre import preferred_encoding
|
from calibre import preferred_encoding, prints
|
||||||
from calibre.utils.filenames import ascii_filename
|
from calibre.utils.filenames import ascii_filename
|
||||||
from calibre.devices.errors import FreeSpaceError
|
from calibre.devices.errors import FreeSpaceError
|
||||||
from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \
|
from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \
|
||||||
@ -88,7 +88,7 @@ class DeviceManager(Thread):
|
|||||||
self.connected_device = None
|
self.connected_device = None
|
||||||
self.ejected_devices = set([])
|
self.ejected_devices = set([])
|
||||||
self.connected_device_is_folder = False
|
self.connected_device_is_folder = False
|
||||||
self.folder_connection_path = None
|
self.folder_connection_requests = Queue.Queue(0)
|
||||||
|
|
||||||
def report_progress(self, *args):
|
def report_progress(self, *args):
|
||||||
pass
|
pass
|
||||||
@ -175,15 +175,20 @@ class DeviceManager(Thread):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while self.keep_going:
|
while self.keep_going:
|
||||||
if not self.is_device_connected and \
|
folder_path = None
|
||||||
self.folder_connection_path is not None:
|
while True:
|
||||||
f = self.folder_connection_path
|
|
||||||
self.folder_connection_path = None # Make sure we try this folder only once
|
|
||||||
try:
|
try:
|
||||||
dev = FOLDER_DEVICE(f)
|
folder_path = self.folder_connection_requests.get_nowait()
|
||||||
|
except Queue.Empty:
|
||||||
|
break
|
||||||
|
if not folder_path or not os.access(folder_path, os.R_OK):
|
||||||
|
folder_path = None
|
||||||
|
if not self.is_device_connected and folder_path is not None:
|
||||||
|
try:
|
||||||
|
dev = FOLDER_DEVICE(folder_path)
|
||||||
self.do_connect([[dev, None],], is_folder_device=True)
|
self.do_connect([[dev, None],], is_folder_device=True)
|
||||||
except:
|
except:
|
||||||
print 'Unable to open folder as device', f
|
prints('Unable to open folder as device', folder_path)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
else:
|
else:
|
||||||
self.detect_device()
|
self.detect_device()
|
||||||
@ -226,7 +231,7 @@ class DeviceManager(Thread):
|
|||||||
# This will be called on the GUI thread. Because of this, we must store
|
# This will be called on the GUI thread. Because of this, we must store
|
||||||
# information that the scanner thread will use to do the real work.
|
# information that the scanner thread will use to do the real work.
|
||||||
def connect_to_folder(self, path):
|
def connect_to_folder(self, path):
|
||||||
self.folder_connection_path = path
|
self.folder_connection_requests.put(path)
|
||||||
|
|
||||||
# This is called on the GUI thread. No problem here, because it calls the
|
# This is called on the GUI thread. No problem here, because it calls the
|
||||||
# device driver, telling it to tell the scanner when it passes by that the
|
# device driver, telling it to tell the scanner when it passes by that the
|
||||||
|
@ -29,7 +29,7 @@ from calibre.utils.date import dt_factory, qt_to_dt, isoformat, now
|
|||||||
from calibre.utils.pyparsing import ParseException
|
from calibre.utils.pyparsing import ParseException
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
|
|
||||||
|
# Delegates {{{
|
||||||
class RatingDelegate(QStyledItemDelegate):
|
class RatingDelegate(QStyledItemDelegate):
|
||||||
COLOR = QColor("blue")
|
COLOR = QColor("blue")
|
||||||
SIZE = 16
|
SIZE = 16
|
||||||
@ -303,7 +303,9 @@ class CcBoolDelegate(QStyledItemDelegate):
|
|||||||
val = 2 if val is None else 1 if not val else 0
|
val = 2 if val is None else 1 if not val else 0
|
||||||
editor.setCurrentIndex(val)
|
editor.setCurrentIndex(val)
|
||||||
|
|
||||||
class BooksModel(QAbstractTableModel):
|
# }}}
|
||||||
|
|
||||||
|
class BooksModel(QAbstractTableModel): # {{{
|
||||||
|
|
||||||
about_to_be_sorted = pyqtSignal(object, name='aboutToBeSorted')
|
about_to_be_sorted = pyqtSignal(object, name='aboutToBeSorted')
|
||||||
sorting_done = pyqtSignal(object, name='sortingDone')
|
sorting_done = pyqtSignal(object, name='sortingDone')
|
||||||
@ -973,13 +975,13 @@ class BooksModel(QAbstractTableModel):
|
|||||||
self.db.set(row, column, val)
|
self.db.set(row, column, val)
|
||||||
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
|
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
|
||||||
index, index)
|
index, index)
|
||||||
#if column == self.sorted_on[0]:
|
|
||||||
# self.resort()
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def set_search_restriction(self, s):
|
def set_search_restriction(self, s):
|
||||||
self.db.data.set_search_restriction(s)
|
self.db.data.set_search_restriction(s)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class BooksView(TableView):
|
class BooksView(TableView):
|
||||||
TIME_FMT = '%d %b %Y'
|
TIME_FMT = '%d %b %Y'
|
||||||
wrapper = textwrap.TextWrapper(width=20)
|
wrapper = textwrap.TextWrapper(width=20)
|
||||||
@ -1084,6 +1086,11 @@ class BooksView(TableView):
|
|||||||
if not self.restore_column_widths():
|
if not self.restore_column_widths():
|
||||||
self.resizeColumnsToContents()
|
self.resizeColumnsToContents()
|
||||||
|
|
||||||
|
sort_col = self._model.sorted_on[0]
|
||||||
|
if sort_col in cm:
|
||||||
|
idx = cm.index(sort_col)
|
||||||
|
self.horizontalHeader().setSortIndicator(idx, self._model.sorted_on[1])
|
||||||
|
|
||||||
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
|
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
|
||||||
save, open_folder, book_details, delete, similar_menu=None):
|
save, open_folder, book_details, delete, similar_menu=None):
|
||||||
self.setContextMenuPolicy(Qt.DefaultContextMenu)
|
self.setContextMenuPolicy(Qt.DefaultContextMenu)
|
||||||
@ -1418,8 +1425,11 @@ class DeviceBooksModel(BooksModel):
|
|||||||
data = {}
|
data = {}
|
||||||
item = self.db[self.map[current.row()]]
|
item = self.db[self.map[current.row()]]
|
||||||
cdata = item.thumbnail
|
cdata = item.thumbnail
|
||||||
if cdata:
|
if cdata is not None:
|
||||||
img = QImage()
|
img = QImage()
|
||||||
|
if hasattr(cdata, 'image_path'):
|
||||||
|
img.load(cdata.image_path)
|
||||||
|
else:
|
||||||
img.loadFromData(cdata)
|
img.loadFromData(cdata)
|
||||||
if img.isNull():
|
if img.isNull():
|
||||||
img = self.default_image
|
img = self.default_image
|
||||||
|
@ -348,6 +348,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.save_menu.addAction(_('Save to disk in a single directory'))
|
self.save_menu.addAction(_('Save to disk in a single directory'))
|
||||||
self.save_menu.addAction(_('Save only %s format to disk')%
|
self.save_menu.addAction(_('Save only %s format to disk')%
|
||||||
prefs['output_format'].upper())
|
prefs['output_format'].upper())
|
||||||
|
self.save_menu.addAction(
|
||||||
|
_('Save only %s format to disk in a single directory')%
|
||||||
|
prefs['output_format'].upper())
|
||||||
|
|
||||||
self.save_sub_menu = SaveMenu(self)
|
self.save_sub_menu = SaveMenu(self)
|
||||||
self.save_menu.addMenu(self.save_sub_menu)
|
self.save_menu.addMenu(self.save_sub_menu)
|
||||||
self.connect(self.save_sub_menu, SIGNAL('save_fmt(PyQt_PyObject)'),
|
self.connect(self.save_sub_menu, SIGNAL('save_fmt(PyQt_PyObject)'),
|
||||||
@ -376,6 +380,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.save_to_single_dir)
|
self.save_to_single_dir)
|
||||||
QObject.connect(self.save_menu.actions()[2], SIGNAL("triggered(bool)"),
|
QObject.connect(self.save_menu.actions()[2], SIGNAL("triggered(bool)"),
|
||||||
self.save_single_format_to_disk)
|
self.save_single_format_to_disk)
|
||||||
|
QObject.connect(self.save_menu.actions()[3], SIGNAL("triggered(bool)"),
|
||||||
|
self.save_single_fmt_to_single_dir)
|
||||||
QObject.connect(self.action_view, SIGNAL("triggered(bool)"),
|
QObject.connect(self.action_view, SIGNAL("triggered(bool)"),
|
||||||
self.view_book)
|
self.view_book)
|
||||||
QObject.connect(self.view_menu.actions()[0],
|
QObject.connect(self.view_menu.actions()[0],
|
||||||
@ -670,7 +676,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.search.setMaximumWidth(self.width()-150)
|
self.search.setMaximumWidth(self.width()-150)
|
||||||
|
|
||||||
def connect_to_folder(self):
|
def connect_to_folder(self):
|
||||||
dir = choose_dir(self, 'Select Device Folder', 'Select folder to open')
|
dir = choose_dir(self, 'Select Device Folder',
|
||||||
|
_('Select folder to open as device'))
|
||||||
if dir is not None:
|
if dir is not None:
|
||||||
self.device_manager.connect_to_folder(dir)
|
self.device_manager.connect_to_folder(dir)
|
||||||
|
|
||||||
@ -1809,6 +1816,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
def save_to_single_dir(self, checked):
|
def save_to_single_dir(self, checked):
|
||||||
self.save_to_disk(checked, True)
|
self.save_to_disk(checked, True)
|
||||||
|
|
||||||
|
def save_single_fmt_to_single_dir(self, *args):
|
||||||
|
self.save_to_disk(False, single_dir=True,
|
||||||
|
single_format=prefs['output_format'])
|
||||||
|
|
||||||
def save_to_disk(self, checked, single_dir=False, single_format=None):
|
def save_to_disk(self, checked, single_dir=False, single_format=None):
|
||||||
rows = self.current_view().selectionModel().selectedRows()
|
rows = self.current_view().selectionModel().selectedRows()
|
||||||
if not rows or len(rows) == 0:
|
if not rows or len(rows) == 0:
|
||||||
@ -2151,14 +2162,25 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
format = d.format()
|
format = d.format()
|
||||||
self.view_format(row, format)
|
self.view_format(row, format)
|
||||||
|
|
||||||
|
def _view_check(self, num, max_=3):
|
||||||
|
if num <= max_:
|
||||||
|
return True
|
||||||
|
return question_dialog(self, _('Multiple Books Selected'),
|
||||||
|
_('You are attempting to open %d books. Opening too many '
|
||||||
|
'books at once can be slow and have a negative effect on the '
|
||||||
|
'responsiveness of your computer. Once started the process '
|
||||||
|
'cannot be stopped until complete. Do you wish to continue?'
|
||||||
|
) % num)
|
||||||
|
|
||||||
def view_folder(self, *args):
|
def view_folder(self, *args):
|
||||||
rows = self.current_view().selectionModel().selectedRows()
|
rows = self.current_view().selectionModel().selectedRows()
|
||||||
if self.current_view() is self.library_view:
|
|
||||||
if not rows or len(rows) == 0:
|
if not rows or len(rows) == 0:
|
||||||
d = error_dialog(self, _('Cannot open folder'),
|
d = error_dialog(self, _('Cannot open folder'),
|
||||||
_('No book selected'))
|
_('No book selected'))
|
||||||
d.exec_()
|
d.exec_()
|
||||||
return
|
return
|
||||||
|
if not self._view_check(len(rows)):
|
||||||
|
return
|
||||||
for row in rows:
|
for row in rows:
|
||||||
path = self.library_view.model().db.abspath(row.row())
|
path = self.library_view.model().db.abspath(row.row())
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
|
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
|
||||||
@ -2176,13 +2198,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self._launch_viewer()
|
self._launch_viewer()
|
||||||
return
|
return
|
||||||
|
|
||||||
if len(rows) >= 3:
|
if not self._view_check(len(rows)):
|
||||||
if not question_dialog(self, _('Multiple Books Selected'),
|
|
||||||
_('You are attempting to open %d books. Opening too many '
|
|
||||||
'books at once can be slow and have a negative effect on the '
|
|
||||||
'responsiveness of your computer. Once started the process '
|
|
||||||
'cannot be stopped until complete. Do you wish to continue?'
|
|
||||||
)% len(rows)):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.current_view() is self.library_view:
|
if self.current_view() is self.library_view:
|
||||||
@ -2261,6 +2277,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.save_menu.actions()[2].setText(
|
self.save_menu.actions()[2].setText(
|
||||||
_('Save only %s format to disk')%
|
_('Save only %s format to disk')%
|
||||||
prefs['output_format'].upper())
|
prefs['output_format'].upper())
|
||||||
|
self.save_menu.actions()[3].setText(
|
||||||
|
_('Save only %s format to disk in a single directory')%
|
||||||
|
prefs['output_format'].upper())
|
||||||
self.library_view.model().read_config()
|
self.library_view.model().read_config()
|
||||||
self.library_view.model().refresh()
|
self.library_view.model().refresh()
|
||||||
self.library_view.model().research()
|
self.library_view.model().research()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user