From 21f3c0a76768bf65c213a19d2d00e7bce93d22e8 Mon Sep 17 00:00:00 2001 From: pewpews Date: Fri, 1 Jan 2016 21:49:32 +0100 Subject: [PATCH] Pewnet refactor; Start on tableview redesign; --- happypanda.ico | Bin 99678 -> 32038 bytes version/app.py | 18 +- version/app_constants.py | 2 + version/database/db.py | 18 +- version/fetch.py | 64 +----- version/gallery.py | 467 ++++++++++++++++++++++++++++---------- version/gallerydb.py | 19 +- version/io_misc.py | 2 +- version/main.py | 2 +- version/misc.py | 4 +- version/pewnet.py | 403 ++++++++++++++++---------------- version/settingsdialog.py | 2 +- 12 files changed, 605 insertions(+), 396 deletions(-) diff --git a/happypanda.ico b/happypanda.ico index d3443f3d93af8eeebb86014a0e02cbb9859e54f4..156f3f93e14a442f3063379b5f5fa67036378a61 100644 GIT binary patch literal 32038 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 ZT6)y-KrIi{^1%OJ9*{n)P5;9b_&<7JeAoa0 literal 99678 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 diff --git a/version/app.py b/version/app.py index b01900d..fca85db 100644 --- a/version/app.py +++ b/version/app.py @@ -230,6 +230,8 @@ def tray_activate(r=None): self.resize(x, y) else: self.resize(app_constants.MAIN_W, app_constants.MAIN_H) + self.setMinimumWidth(600) + self.setMinimumHeight(400) misc.centerWidget(self) self.init_spinners() self.show() @@ -422,14 +424,14 @@ def manga_display(self): self.manga_table_view.sort_model = self.manga_list_view.sort_model self.manga_table_view.setModel(self.manga_table_view.sort_model) self.manga_table_view.sort_model.change_model(self.manga_table_view.gallery_model) - self.manga_table_view.setColumnWidth(app_constants.FAV, 20) - self.manga_table_view.setColumnWidth(app_constants.ARTIST, 200) - self.manga_table_view.setColumnWidth(app_constants.TITLE, 400) - self.manga_table_view.setColumnWidth(app_constants.TAGS, 300) - self.manga_table_view.setColumnWidth(app_constants.TYPE, 60) - self.manga_table_view.setColumnWidth(app_constants.CHAPTERS, 60) - self.manga_table_view.setColumnWidth(app_constants.LANGUAGE, 100) - self.manga_table_view.setColumnWidth(app_constants.LINK, 400) + #self.manga_table_view.setColumnWidth(app_constants.FAV, 20) + #self.manga_table_view.setColumnWidth(app_constants.ARTIST, 200) + #self.manga_table_view.setColumnWidth(app_constants.TITLE, 400) + #self.manga_table_view.setColumnWidth(app_constants.TAGS, 300) + #self.manga_table_view.setColumnWidth(app_constants.TYPE, 60) + #self.manga_table_view.setColumnWidth(app_constants.CHAPTERS, 60) + #self.manga_table_view.setColumnWidth(app_constants.LANGUAGE, 100) + #self.manga_table_view.setColumnWidth(app_constants.LINK, 400) def init_spinners(self): diff --git a/version/app_constants.py b/version/app_constants.py index 46d13bc..aee57fe 100644 --- a/version/app_constants.py +++ b/version/app_constants.py @@ -48,6 +48,8 @@ SIZE_FACTOR = get(10, 'Visual', 'size factor', int) GRIDBOX_H_SIZE = 200 + SIZE_FACTOR GRIDBOX_W_SIZE = GRIDBOX_H_SIZE//1.40 #1.47 +LISTBOX_H_SIZE = 190 +LISTBOX_W_SIZE = 950 GRIDBOX_LBL_H = 50 + SIZE_FACTOR GRIDBOX_H_SIZE += GRIDBOX_LBL_H THUMB_H_SIZE = 190 + SIZE_FACTOR diff --git a/version/database/db.py b/version/database/db.py index 54b3085..c6f6429 100644 --- a/version/database/db.py +++ b/version/database/db.py @@ -325,12 +325,14 @@ def begin(cls): "Useful when modifying for a large amount of data" cls._AUTO_COMMIT = False cls.execute(cls, "BEGIN TRANSACTION") + print("STARTED DB OPTIMIZE") @classmethod def end(cls): "Called to commit and end transaction" cls._AUTO_COMMIT = True cls._DB_CONN.commit() + print("ENDED DB OPTIMIZE") def execute(self, *args): "Same as cursor.execute" @@ -339,9 +341,21 @@ def execute(self, *args): log_d('DB Query: {}'.format(args).encode(errors='ignore')) if self._AUTO_COMMIT: with self._DB_CONN: - return self._DB_CONN.execute(*args) + try: + return self._DB_CONN.execute(*args) + except: + print("with autocommit") + print(args) + print(type(args[1][0])) + raise RuntimeError else: - return self._DB_CONN.execute(*args) + try: + return self._DB_CONN.execute(*args) + except: + print("no autocommit") + print(args) + print(type(args[1][0])) + raise RuntimeError def executemany(self, *args): "Same as cursor.executemany" diff --git a/version/fetch.py b/version/fetch.py index 5c7b901..bb69524 100644 --- a/version/fetch.py +++ b/version/fetch.py @@ -272,64 +272,6 @@ def _return_gallery_metadata(self, gallery): self.GALLERY_EMITTER.emit(gallery, None, False) log_d('Success') - @staticmethod - def apply_metadata(g, data, append=True): - if app_constants.USE_JPN_TITLE: - try: - title = data['title']['jpn'] - except KeyError: - title = data['title']['def'] - else: - title = data['title']['def'] - - if 'Language' in data['tags']: - try: - lang = [x for x in data['tags']['Language'] if not x == 'translated'][0].capitalize() - except IndexError: - lang = "" - else: - lang = "" - - title_artist_dict = utils.title_parser(title) - if not append: - g.title = title_artist_dict['title'] - if title_artist_dict['artist']: - g.artist = title_artist_dict['artist'] - g.language = title_artist_dict['language'].capitalize() - if 'Artist' in data['tags']: - g.artist = data['tags']['Artist'][0].capitalize() - if lang: - g.language = lang - g.type = data['type'] - g.pub_date = data['pub_date'] - g.tags = data['tags'] - else: - if not g.title: - g.title = title_artist_dict['title'] - if not g.artist: - g.artist = title_artist_dict['artist'] - if 'Artist' in data['tags']: - g.artist = data['tags']['Artist'][0].capitalize() - if not g.language: - g.language = title_artist_dict['language'].capitalize() - if lang: - g.language = lang - if not g.type or g.type == 'Other': - g.type = data['type'] - if not g.pub_date: - g.pub_date = data['pub_date'] - if not g.tags: - g.tags = data['tags'] - else: - for ns in data['tags']: - if ns in g.tags: - for tag in data['tags'][ns]: - if not tag in g.tags[ns]: - g.tags[ns].append(tag) - else: - g.tags[ns] = data['tags'][ns] - return g - def fetch_metadata(self, gallery, hen, proc=False): """ Puts gallery in queue for metadata fetching. Applies received galleries and sends @@ -365,10 +307,10 @@ def fetch_metadata(self, gallery, hen, proc=False): log_i('({}/{}) Applying metadata for gallery: {}'.format(x, len(self.galleries_in_queue), g.title.encode(errors='ignore'))) if app_constants.REPLACE_METADATA: - g = Fetch.apply_metadata(g, data, False) + g = hen.apply_metadata(g, data, False) g.link = g.temp_url else: - g = Fetch.apply_metadata(g, data) + g = hen.apply_metadata(g, data) if not g.link: g.link = g.temp_url self._return_gallery_metadata(g) @@ -438,7 +380,7 @@ def auto_web_metadata(self): # dict -> hash:[list of title,url tuples] or None self.AUTO_METADATA_PROGRESS.emit("({}/{}) Finding url for gallery: {}".format(x, len(self.galleries), gallery.title)) - found_url = hen.eh_hash_search(gallery.hash) + found_url = hen.search(gallery.hash) if found_url == 'error': app_constants.GLOBAL_EHEN_LOCK = False self.FINISHED.emit(True) diff --git a/version/gallery.py b/version/gallery.py index 48ed0e7..145af61 100644 --- a/version/gallery.py +++ b/version/gallery.py @@ -534,6 +534,185 @@ def fetchMore(self, index): self.db_emitter.fetch_more() class CustomDelegate(QStyledItemDelegate): + def __init__(self, parent=None): + super().__init__(parent) + + def text_layout(self, text, width, font, font_metrics, alignment=Qt.AlignCenter): + "Lays out wrapped text" + text_option = QTextOption(alignment) + text_option.setUseDesignMetrics(True) + text_option.setWrapMode(QTextOption.WordWrap) + layout = QTextLayout(text, font) + layout.setTextOption(text_option) + leading = font_metrics.leading() + height = 0 + layout.setCacheEnabled(True) + layout.beginLayout() + while True: + line = layout.createLine() + if not line.isValid(): + break + line.setLineWidth(width) + height += leading + line.setPosition(QPointF(0, height)) + height += line.height() + layout.endLayout() + return layout + +class ListDelegate(CustomDelegate): + "A custom delegate for the model/view framework" + + def __init__(self, parent): + super().__init__(parent) + self.dynamic_width = app_constants.LISTBOX_W_SIZE + self.dynamic_height = app_constants.LISTBOX_H_SIZE + self.parent_font_m = parent.fontMetrics() + self.parent_font = parent.font() + self.title_font = QFont() + self.title_font.setPointSize(10) + self.title_font.setBold(True) + self.artist_font = QFont() + self.artist_font.setPointSize(9) + self.artist_font_m = QFontMetrics(self.artist_font) + self.title_font_m = QFontMetrics(self.title_font) + + self.pic_width = 122 + self.pic_height = 172 + self._pic_margin = 10 + + def paint(self, painter, option, index): + assert isinstance(painter, QPainter) + if index.data(Qt.UserRole+1): + c_gallery = index.data(Qt.UserRole+1) + + start_x = x = option.rect.x() + y = option.rect.y() + w = option.rect.width() + h = option.rect.height() + + if app_constants.HIGH_QUALITY_THUMBS: + painter.setRenderHint(QPainter.SmoothPixmapTransform) + painter.setRenderHint(QPainter.Antialiasing) + + # border + painter.setPen(QColor("#A6A6B7")) + painter.drawRect(option.rect) + + # background + painter.setBrush(QBrush(QColor("#464646"))) + painter.drawRect(option.rect) + + # pic + pic_rect = QRect(x+self._pic_margin, y+self._pic_margin, self.pic_width, self.pic_height) + painter.setBrush(QBrush(QColor("white"))) + painter.drawRect(pic_rect) + + # remaining rect with left margin + star_x = x + w - self._pic_margin + x = pic_rect.x() + pic_rect.width() + self._pic_margin*2 + w -= (pic_rect.width() + self._pic_margin) + + # title & artist + title_margin = 40 + title_top_margin = 15 + title_x = x + title_margin + title_y = y + title_top_margin + title_width = w - title_margin + title_layout = self.text_layout(c_gallery.title, title_width-title_margin, self.title_font, self.title_font_m) + painter.setPen(QColor("white")) + title_layout.draw(painter, QPointF(title_x, title_y)) + + artist_layout = self.text_layout(c_gallery.artist, title_width-title_margin, self.artist_font, self.artist_font_m) + painter.setPen(QColor("#A6A6B7")) + title_rect = title_layout.boundingRect() + artist_y = title_y+title_rect.height() + artist_layout.draw(painter, QPointF(title_x, artist_y)) + + # meta info + start_y = y + title_rect.height()+title_top_margin+artist_layout.boundingRect().height() + txt_height = painter.fontMetrics().height() + txt_list = self.gallery_info(c_gallery) + for g_data in txt_list: + painter.drawText(x, start_y, g_data) + start_y += txt_height + 3 + # descr + descr_y = artist_y + artist_layout.boundingRect().height() + descr_x = title_x + (painter.fontMetrics().width(txt_list[6])*1.1) + descr_layout = self.text_layout(c_gallery.info, title_width, painter.font(), painter.fontMetrics(), Qt.AlignLeft) + descr_layout.draw(painter, QPointF(descr_x, descr_y)) + + # tags + tags_y = descr_y + descr_layout.boundingRect().height() + tags_h = painter.fontMetrics().height() * 1.1 + tags_y += tags_h + + for ns in c_gallery.tags: + ns_text = "{}:".format(ns) + painter.drawText(descr_x, tags_y, ns_text) + tag_x = descr_x + painter.fontMetrics().width(ns_text) * 1.2 + tags_txt = self.tags_text(c_gallery.tags[ns]) + tags_layout = self.text_layout(tags_txt, w-(tag_x*1.1 - x), painter.font(), painter.fontMetrics(), Qt.AlignLeft) + tags_layout.draw(painter, QPointF(tag_x, tags_y-tags_h*0.7)) + tags_y += tags_layout.boundingRect().height() + + # fav star + if c_gallery.fav: + star_pix = QPixmap(app_constants.STAR_PATH) + star_x -= star_pix.width() + painter.drawPixmap(star_x, y+5, star_pix) + + else: + return super().paint(painter, option, index) + + def tags_text(self, tag_list): + tag_txt = "" + l = len(tag_list) + for n, tag in enumerate(tag_list, 1): + if n == l: + tag_txt += tag + else: + tag_txt += "{}, ".format(tag) + return tag_txt + + def gallery_info(self, c_gallery): + txt_list = ["Type: {}".format(c_gallery.type), "Chapters: {}".format(c_gallery.chapters.count()), + "Language: {}".format(c_gallery.language), "Pages: {}".format(c_gallery.chapters.pages()), + "Status: {}".format(c_gallery.status), "Added: {}".format(c_gallery.date_added.strftime('%d %b %Y')), + "Published: {}".format(c_gallery.pub_date.strftime('%d %b %Y') if c_gallery.pub_date else "Unknown"), + "Last read: {}".format('{} ago'.format(utils.get_date_age(c_gallery.last_read)) if c_gallery.last_read else "Never!")] + return txt_list + + def sizeHint(self, option, index): + g = index.data(Qt.UserRole+1) + margin = 10 + w = option.rect.width()-(self.pic_width+self._pic_margin*2+ + self.parent_font_m.width("Added: {}".format(g.date_added.strftime('%d %b %Y')))) + w = abs(w) + h = self.text_layout(g.info, w, self.parent_font, self.parent_font_m, Qt.AlignLeft).boundingRect().height() + for ns in g.tags: + tags = g.tags[ns] + txt = self.tags_text(tags) + txt_layout = self.text_layout(txt, w, self.parent_font, self.parent_font_m, Qt.AlignLeft) + h += txt_layout.boundingRect().height() + + h2 = 0 + title_layout = self.text_layout(g.title, w, self.title_font, self.title_font_m) + h2 += title_layout.boundingRect().height() + self.title_font_m.height() + artist_layout = self.text_layout(g.artist, w, self.artist_font, self.artist_font_m) + h2 += artist_layout.boundingRect().height() + self.artist_font_m.height() + h2 += self.parent_font_m.height()*len(self.gallery_info(g)) + print("h:", h, "h2", h2) + if h > app_constants.LISTBOX_H_SIZE: + dynamic_height = h - self.title_font_m.height() + else: + dynamic_height = app_constants.LISTBOX_H_SIZE + + if h2 > app_constants.LISTBOX_H_SIZE > h: + dynamic_height = h2 + self.title_font_m.height() + + return QSize(self.dynamic_width, dynamic_height) + +class GridDelegate(CustomDelegate): "A custom delegate for the model/view framework" POPUP = pyqtSignal() @@ -543,7 +722,7 @@ class CustomDelegate(QStyledItemDelegate): G_NORMAL, G_DOWNLOAD = range(2) def __init__(self, parent=None): - super().__init__() + super().__init__(parent) QPixmapCache.setCacheLimit(app_constants.THUMBNAIL_CACHE_SIZE[0]* app_constants.THUMBNAIL_CACHE_SIZE[1]) self._painted_indexes = {} @@ -843,34 +1022,88 @@ def draw_text_label(lbl_h): else: super().paint(painter, option, index) - def text_layout(self, text, width, font, font_metrics): - "Lays out wrapped text" - text_option = QTextOption(Qt.AlignCenter) - text_option.setUseDesignMetrics(True) - text_option.setWrapMode(QTextOption.WordWrap) - layout = QTextLayout(text, font) - layout.setTextOption(text_option) - leading = font_metrics.leading() - height = 0 - layout.setCacheEnabled(True) - layout.beginLayout() - while True: - line = layout.createLine() - if not line.isValid(): - break - line.setLineWidth(width) - height += leading - line.setPosition(QPointF(0, height)) - height += line.height() - layout.endLayout() - return layout - def sizeHint(self, option, index): return QSize(self.W, self.H) +class MangaTableView(QListView): + """ + """ + STATUS_BAR_MSG = pyqtSignal(str) + SERIES_DIALOG = pyqtSignal() + + def __init__(self, parent=None): + super().__init__(parent) + self.parent_widget = parent + self.setViewMode(self.IconMode) + #self.H = app_constants.GRIDBOX_H_SIZE + #self.W = app_constants.GRIDBOX_W_SIZE + (app_constants.SIZE_FACTOR//5) + self.setResizeMode(self.Adjust) + #self.setIconSize(QSize(app_constants.THUMB_W_SIZE-app_constants.SIZE_FACTOR, + # app_constants.THUMB_H_SIZE-app_constants.SIZE_FACTOR)) + # all items have the same size (perfomance) + #self.setUniformItemSizes(True) + # improve scrolling + self.setVerticalScrollMode(self.ScrollPerPixel) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setLayoutMode(self.SinglePass) + self.setMouseTracking(True) + #self.sort_model = SortFilterModel() + #self.sort_model.setDynamicSortFilter(True) + #self.sort_model.setFilterCaseSensitivity(Qt.CaseInsensitive) + #self.sort_model.setSortLocaleAware(True) + #self.sort_model.setSortCaseSensitivity(Qt.CaseInsensitive) + self.manga_delegate = ListDelegate(parent) + self.setItemDelegate(self.manga_delegate) + self.setSelectionBehavior(self.SelectItems) + self.setSelectionMode(self.ExtendedSelection) + #self.gallery_model = GalleryModel(parent) + #self.gallery_model.db_emitter.DONE.connect(self.sort_model.setup_search) + #self.sort_model.change_model(self.gallery_model) + #self.sort_model.sort(0) + #self.sort_model.ROWCOUNT_CHANGE.connect(self.gallery_model.ROWCOUNT_CHANGE.emit) + #self.setModel(self.sort_model) + #self.SERIES_DIALOG.connect(self.spawn_dialog) + self.doubleClicked.connect(lambda idx: idx.data(Qt.UserRole+1).chapters[0].open()) + self.setViewportMargins(0,0,0,0) + + self.gallery_window = misc.GalleryMetaWindow(parent if parent else self) + self.gallery_window.arrow_size = (10,10,) + self.clicked.connect(lambda idx: self.gallery_window.show_gallery(idx, self)) + + self.current_sort = app_constants.CURRENT_SORT + #self.sort(self.current_sort) + if app_constants.DEBUG: + def debug_print(a): + try: + print(a.data(Qt.UserRole+1)) + except: + print("{}".format(a.data(Qt.UserRole+1)).encode(errors='ignore')) + + self.clicked.connect(debug_print) + + self.k_scroller = QScroller.scroller(self) + + def resizeEvent(self, event): + from PyQt5.QtGui import QResizeEvent + width = event.size().width() + if width >= app_constants.LISTBOX_W_SIZE: + possible = self.width()//app_constants.LISTBOX_W_SIZE + print(possible) + new_width = self.width()//possible-9 # 9 because.. reasons + + self.manga_delegate.dynamic_width = new_width + #self.setGridSize(QSize(new_width, app_constants.LISTBOX_H_SIZE)) + #self.setIconSize(QSize(new_width, app_constants.LISTBOX_H_SIZE)) + else: + self.manga_delegate.dynamic_width = width + #self.setGridSize(QSize(width, app_constants.LISTBOX_H_SIZE)) + + return super().resizeEvent(event) + + class MangaView(QListView): """ - TODO: (zoom-in/zoom-out) mousekeys + Grid View """ STATUS_BAR_MSG = pyqtSignal(str) @@ -898,7 +1131,7 @@ def __init__(self, parent=None): self.sort_model.setFilterCaseSensitivity(Qt.CaseInsensitive) self.sort_model.setSortLocaleAware(True) self.sort_model.setSortCaseSensitivity(Qt.CaseInsensitive) - self.manga_delegate = CustomDelegate(parent) + self.manga_delegate = GridDelegate(parent) self.setItemDelegate(self.manga_delegate) self.setSelectionBehavior(self.SelectItems) self.setSelectionMode(self.ExtendedSelection) @@ -1124,7 +1357,7 @@ def contextMenuEvent(self, event): index = self.indexAt(event.pos()) index = self.sort_model.mapToSource(index) - if index.data(Qt.UserRole+1) and index.data(Qt.UserRole+1).state == CustomDelegate.G_DOWNLOAD: + if index.data(Qt.UserRole+1) and index.data(Qt.UserRole+1).state == GridDelegate.G_DOWNLOAD: event.ignore() return @@ -1133,7 +1366,7 @@ def contextMenuEvent(self, event): select_indexes = [] for idx in s_indexes: if idx.isValid() and idx.column() == 0: - if not idx.data(Qt.UserRole+1).state == CustomDelegate.G_DOWNLOAD: + if not idx.data(Qt.UserRole+1).state == GridDelegate.G_DOWNLOAD: select_indexes.append(self.sort_model.mapToSource(idx)) if len(select_indexes) > 1: selected = True @@ -1214,96 +1447,96 @@ def updateGeometries(self): super().updateGeometries() self.verticalScrollBar().setSingleStep(app_constants.SCROLL_SPEED) -class MangaTableView(QTableView): - STATUS_BAR_MSG = pyqtSignal(str) - SERIES_DIALOG = pyqtSignal() - - def __init__(self, parent=None): - super().__init__(parent) - # options - self.parent_widget = parent - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) - self.setSelectionBehavior(self.SelectRows) - self.setSelectionMode(self.ExtendedSelection) - self.setShowGrid(True) - self.setSortingEnabled(True) - h_header = self.horizontalHeader() - h_header.setSortIndicatorShown(True) - v_header = self.verticalHeader() - v_header.sectionResizeMode(QHeaderView.Fixed) - v_header.setDefaultSectionSize(24) - v_header.hide() - palette = self.palette() - palette.setColor(palette.Highlight, QColor(88, 88, 88, 70)) - palette.setColor(palette.HighlightedText, QColor('black')) - self.setPalette(palette) - self.setIconSize(QSize(0,0)) - self.doubleClicked.connect(lambda idx: idx.data(Qt.UserRole+1).chapters[0].open()) - self.grabGesture(Qt.SwipeGesture) - self.k_scroller = QScroller.scroller(self) - - # display tooltip only for elided text - #def viewportEvent(self, event): - # if event.type() == QEvent.ToolTip: - # h_event = QHelpEvent(event) - # index = self.indexAt(h_event.pos()) - # if index.isValid(): - # size_hint = self.itemDelegate(index).sizeHint(self.viewOptions(), - # index) - # rect = QRect(0, 0, size_hint.width(), size_hint.height()) - # rect_visual = self.visualRect(index) - # if rect.width() <= rect_visual.width(): - # QToolTip.hideText() - # return True - # return super().viewportEvent(event) - - def keyPressEvent(self, event): - if event.key() == Qt.Key_Return: - s_idx = self.selectionModel().selectedRows() - if s_idx: - for idx in s_idx: - self.doubleClicked.emit(idx) - return super().keyPressEvent(event) - - def remove_gallery(self, index_list, local=False): - self.parent_widget.manga_list_view.remove_gallery(index_list, local) - - def contextMenuEvent(self, event): - handled = False - index = self.indexAt(event.pos()) - index = self.sort_model.mapToSource(index) - - if index.data(Qt.UserRole+1) and index.data(Qt.UserRole+1).state == CustomDelegate.G_DOWNLOAD: - event.ignore() - return - - selected = False - s_indexes = self.selectionModel().selectedRows() - select_indexes = [] - for idx in s_indexes: - if idx.isValid(): - if not idx.data(Qt.UserRole+1).state == CustomDelegate.G_DOWNLOAD: - select_indexes.append(self.sort_model.mapToSource(idx)) - if len(select_indexes) > 1: - selected = True - - if index.isValid(): - if selected: - menu = misc.GalleryMenu(self, - index, - self.parent_widget.manga_list_view.gallery_model, - self.parent_widget, select_indexes) - else: - menu = misc.GalleryMenu(self, index, self.gallery_model, - self.parent_widget) - handled = True - - if handled: - menu.exec_(event.globalPos()) - event.accept() - del menu - else: - event.ignore() +#class MangaTableView(QTableView): +# STATUS_BAR_MSG = pyqtSignal(str) +# SERIES_DIALOG = pyqtSignal() + +# def __init__(self, parent=None): +# super().__init__(parent) +# # options +# self.parent_widget = parent +# self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) +# self.setSelectionBehavior(self.SelectRows) +# self.setSelectionMode(self.ExtendedSelection) +# self.setShowGrid(True) +# self.setSortingEnabled(True) +# h_header = self.horizontalHeader() +# h_header.setSortIndicatorShown(True) +# v_header = self.verticalHeader() +# v_header.sectionResizeMode(QHeaderView.Fixed) +# v_header.setDefaultSectionSize(24) +# v_header.hide() +# palette = self.palette() +# palette.setColor(palette.Highlight, QColor(88, 88, 88, 70)) +# palette.setColor(palette.HighlightedText, QColor('black')) +# self.setPalette(palette) +# self.setIconSize(QSize(0,0)) +# self.doubleClicked.connect(lambda idx: idx.data(Qt.UserRole+1).chapters[0].open()) +# self.grabGesture(Qt.SwipeGesture) +# self.k_scroller = QScroller.scroller(self) + +# # display tooltip only for elided text +# #def viewportEvent(self, event): +# # if event.type() == QEvent.ToolTip: +# # h_event = QHelpEvent(event) +# # index = self.indexAt(h_event.pos()) +# # if index.isValid(): +# # size_hint = self.itemDelegate(index).sizeHint(self.viewOptions(), +# # index) +# # rect = QRect(0, 0, size_hint.width(), size_hint.height()) +# # rect_visual = self.visualRect(index) +# # if rect.width() <= rect_visual.width(): +# # QToolTip.hideText() +# # return True +# # return super().viewportEvent(event) + +# def keyPressEvent(self, event): +# if event.key() == Qt.Key_Return: +# s_idx = self.selectionModel().selectedRows() +# if s_idx: +# for idx in s_idx: +# self.doubleClicked.emit(idx) +# return super().keyPressEvent(event) + +# def remove_gallery(self, index_list, local=False): +# self.parent_widget.manga_list_view.remove_gallery(index_list, local) + +# def contextMenuEvent(self, event): +# handled = False +# index = self.indexAt(event.pos()) +# index = self.sort_model.mapToSource(index) + +# if index.data(Qt.UserRole+1) and index.data(Qt.UserRole+1).state == CustomDelegate.G_DOWNLOAD: +# event.ignore() +# return + +# selected = False +# s_indexes = self.selectionModel().selectedRows() +# select_indexes = [] +# for idx in s_indexes: +# if idx.isValid(): +# if not idx.data(Qt.UserRole+1).state == CustomDelegate.G_DOWNLOAD: +# select_indexes.append(self.sort_model.mapToSource(idx)) +# if len(select_indexes) > 1: +# selected = True + +# if index.isValid(): +# if selected: +# menu = misc.GalleryMenu(self, +# index, +# self.parent_widget.manga_list_view.gallery_model, +# self.parent_widget, select_indexes) +# else: +# menu = misc.GalleryMenu(self, index, self.gallery_model, +# self.parent_widget) +# handled = True + +# if handled: +# menu.exec_(event.globalPos()) +# event.accept() +# del menu +# else: +# event.ignore() class CommonView: """ diff --git a/version/gallerydb.py b/version/gallerydb.py index bb76b23..1d4a493 100644 --- a/version/gallerydb.py +++ b/version/gallerydb.py @@ -739,18 +739,15 @@ def del_gallery_mapping(cls, series_id): # We first get all the current tags_mappings_ids related to gallery tag_m_ids = [] c = cls.execute(cls, 'SELECT tags_mappings_id FROM series_tags_map WHERE series_id=?', - (series_id,)) + [int(series_id)]) for tmd in c.fetchall(): - tag_m_ids.append(tmd['tags_mappings_id']) + tag_m_ids.append((tmd['tags_mappings_id'],)) # Then we delete all mappings related to the given series_id - cls.execute(cls, 'DELETE FROM series_tags_map WHERE series_id=?', (series_id,)) - - executing = [] - for tmd_id in tag_m_ids: - executing.append((tmd_id,)) + cls.execute(cls, 'DELETE FROM series_tags_map WHERE series_id=?', [series_id]) - cls.executemany(cls, 'DELETE FROM tags_mappings WHERE tags_mappings_id=?', executing) + print(tag_m_ids) + cls.executemany(cls, 'DELETE FROM tags_mappings WHERE tags_mappings_id=?', tag_m_ids) @classmethod def get_gallery_tags(cls, series_id): @@ -1613,6 +1610,12 @@ def create_chapter(self, number=None): self[next_number] = chp return chp + def pages(self): + p = 0 + for c in self: + p += c.pages + return p + def get_chapter(self, number): return self[number] diff --git a/version/io_misc.py b/version/io_misc.py index 57afbe9..a754b5b 100644 --- a/version/io_misc.py +++ b/version/io_misc.py @@ -213,7 +213,7 @@ def _gallery_to_model(self, gallery_list): if gallery_list: gallery = gallery_list[0] if d_item.item.metadata: - gallery = fetch.Fetch.apply_metadata(gallery, d_item.item.metadata) + gallery = pewnet.EHen.apply_metadata(gallery, d_item.item.metadata) gallery.link = d_item.item.gallery_url gallerydb.add_method_queue( gallerydb.GalleryDB.add_gallery_return, False, gallery) diff --git a/version/main.py b/version/main.py index 51adf3a..be4e1f3 100644 --- a/version/main.py +++ b/version/main.py @@ -91,7 +91,7 @@ def uncaught_exceptions(ex_type, ex, tb): sys.excepthook = uncaught_exceptions if app_constants.FORCE_HIGH_DPI_SUPPORT: - log_i("Enablind high DPI display support") + log_i("Enabling high DPI display support") os.environ.putenv("QT_DEVICE_PIXEL_RATIO", "auto") application = QApplication(sys.argv) diff --git a/version/misc.py b/version/misc.py index 9ca702b..c111ed1 100644 --- a/version/misc.py +++ b/version/misc.py @@ -687,9 +687,7 @@ def apply_gallery(self, gallery): chap_txt = "chapters" if gallery.chapters.count() > 1 else "chapter" self.g_chap_count_lbl.setText('{} {}'.format(gallery.chapters.count(), chap_txt)) self.g_type_lbl.setText(gallery.type) - pages = 0 - for ch in gallery.chapters: - pages += ch.pages + pages = gallery.chapters.pages() self.g_pages_total_lbl.setText('{}'.format(pages)) self.g_status_lbl.setText(gallery.status) self.g_d_added_lbl.setText(gallery.date_added.strftime('%d %b %Y')) diff --git a/version/pewnet.py b/version/pewnet.py index 873c9fb..4d89bf2 100644 --- a/version/pewnet.py +++ b/version/pewnet.py @@ -250,7 +250,7 @@ def commit_metadata(self): d_m = {self.metadata['gmetadata'][0]['gid']:g_id} except KeyError: return - self.metadata = CommenHen.parse_metadata(self.metadata, d_m)[g_id] + self.metadata = EHen.parse_metadata(self.metadata, d_m)[g_id] class DLManager(QObject): "Base class for site-specific download managers" @@ -377,7 +377,7 @@ def __init__(self): cookies = exprops.cookies if not cookies: if exprops.username and exprops.password: - cookies = CommenHen.login(exprops.username, exprops.password) + cookies = EHen.login(exprops.username, exprops.password) else: raise app_constants.NeedLogin @@ -437,7 +437,7 @@ def from_gallery_url(self, g_url): h_item = HenItem(self._browser.session) h_item.gallery_url = g_url - h_item.metadata = CommenHen.parse_metadata(api_metadata, gallery_gid_dict) + h_item.metadata = EHen.parse_metadata(api_metadata, gallery_gid_dict) try: h_item.metadata = h_item.metadata[g_url] except KeyError: @@ -492,7 +492,7 @@ def from_gallery_url(self, g_url): h_item.torrents_found = int(gallery['torrentcount']) h_item.fetch_thumb() if h_item.torrents_found > 0: - g_id_token = CommenHen.parse_url(g_url) + g_id_token = EHen.parse_url(g_url) url_and_file = self._torrent_url_d(g_id_token[0], g_id_token[1]) if url_and_file: h_item.download_url = url_and_file[0] @@ -520,14 +520,6 @@ class CommenHen: LAST_USED = time.time() HEADERS = {'user-agent':"Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0"} - @staticmethod - def hash_search(g_hash): - """ - Searches ex or g.e for a gallery with the hash value - Return list with titles of galleries found. - """ - raise NotImplementedError - def begin_lock(self): log_d('locked') self.LOCK.acquire() @@ -552,11 +544,11 @@ def add_to_queue(self, url, proc=False, parse=True): log_i("Status on queue: {}/25".format(len(self.QUEUE))) if proc: if parse: - return CommenHen.parse_metadata(*self.process_queue()) + return self.parse_metadata(*self.process_queue()) return self.process_queue() if len(self.QUEUE) > 24: if parse: - return CommenHen.parse_metadata(*self.process_queue()) + return self.parse_metadata(*self.process_queue()) return self.process_queue() else: return 0 @@ -580,47 +572,129 @@ def process_queue(self): self.QUEUE.clear() return api_data, galleryid_dict - @staticmethod - def login(user, password): + @classmethod + def login(cls, user, password): + pass + + @classmethod + def check_login(cls, cookies): + pass + + def check_cookie(self, cookie): + cookies = self.COOKIES.keys() + present = [] + for c in cookie: + if c in cookies: + present.append(True) + else: + present.append(False) + if not all(present): + log_i("Updating cookies...") + try: + self.COOKIES.update(cookie) + except requests.cookies.CookieConflictError: + pass + + def handle_error(self, response): + pass + + @classmethod + def parse_metadata(cls, metadata_json, dict_metadata): """ - Logs into g.e-h + :metadata_json <- raw data provided by site + :dict_metadata <- a dict with gallery id's as keys and url as value + + returns a dict with url as key and gallery metadata as value """ - eh_c = {} - exprops = settings.ExProperties() - if CommenHen.COOKIES: - if CommenHen.check_login(CommenHen.COOKIES): - return CommenHen.COOKIES - elif exprops.cookies: - if CommenHen.check_login(exprops.cookies): - CommenHen.COOKIES.update(exprops.cookies) - return CommenHen.COOKIES + pass - p = { - 'CookieDate': '1', - 'b':'d', - 'bt':'1-1', - 'UserName':user, - 'PassWord':password - } + def get_metadata(self, list_of_urls, cookies=None): + """ + Fetches the metadata from the provided list of urls + returns raw api data and a dict with gallery id as key and url as value + """ + pass - eh_c = requests.post('https://forums.e-hentai.org/index.php?act=Login&CODE=01', data=p).cookies.get_dict() - exh_c = requests.get('http://exhentai.org', cookies=eh_c).cookies.get_dict() + @classmethod + def apply_metadata(cls, gallery, data, append=True): + """ + Applies fetched metadata to gallery + """ + pass - eh_c.update(exh_c) + def search(self, search_string, cookies=None): + """ + Searches ehentai for the provided string or list of hashes, + returns a dict with search_string:[list of title,url tuples] of hits found or emtpy dict if no hits are found. + """ + pass - if not CommenHen.check_login(eh_c): - raise app_constants.WrongLogin - exprops.cookies = eh_c - exprops.username = user - exprops.password = password - exprops.save() - CommenHen.COOKIES.update(eh_c) +class EHen(CommenHen): + "Fetches galleries from ehen" + def __init__(self): + self.e_url = "http://g.e-hentai.org/api.php" - return eh_c + @classmethod + def apply_metadata(cls, g, data, append = True): + if app_constants.USE_JPN_TITLE: + try: + title = data['title']['jpn'] + except KeyError: + title = data['title']['def'] + else: + title = data['title']['def'] - @staticmethod - def check_login(cookies): + if 'Language' in data['tags']: + try: + lang = [x for x in data['tags']['Language'] if not x == 'translated'][0].capitalize() + except IndexError: + lang = "" + else: + lang = "" + + title_artist_dict = utils.title_parser(title) + if not append: + g.title = title_artist_dict['title'] + if title_artist_dict['artist']: + g.artist = title_artist_dict['artist'] + g.language = title_artist_dict['language'].capitalize() + if 'Artist' in data['tags']: + g.artist = data['tags']['Artist'][0].capitalize() + if lang: + g.language = lang + g.type = data['type'] + g.pub_date = data['pub_date'] + g.tags = data['tags'] + else: + if not g.title: + g.title = title_artist_dict['title'] + if not g.artist: + g.artist = title_artist_dict['artist'] + if 'Artist' in data['tags']: + g.artist = data['tags']['Artist'][0].capitalize() + if not g.language: + g.language = title_artist_dict['language'].capitalize() + if lang: + g.language = lang + if not g.type or g.type == 'Other': + g.type = data['type'] + if not g.pub_date: + g.pub_date = data['pub_date'] + if not g.tags: + g.tags = data['tags'] + else: + for ns in data['tags']: + if ns in g.tags: + for tag in data['tags'][ns]: + if not tag in g.tags[ns]: + g.tags[ns].append(tag) + else: + g.tags[ns] = data['tags'][ns] + return g + + @classmethod + def check_login(cls, cookies): """ Checks if user is logged in """ @@ -630,21 +704,6 @@ def check_login(cookies): else: return False - def check_cookie(self, cookie): - cookies = self.COOKIES.keys() - present = [] - for c in cookie: - if c in cookies: - present.append(True) - else: - present.append(False) - if not all(present): - log_i("Updating cookies...") - try: - self.COOKIES.update(cookie) - except requests.cookies.CookieConflictError: - pass - def handle_error(self, response): content_type = response.headers['content-type'] text = response.text @@ -662,16 +721,55 @@ def handle_error(self, response): time.sleep(random.randint(10,50)) return True - @staticmethod - def parse_url(url): + @classmethod + def parse_url(cls, url): "Parses url into a list of gallery id and token" gallery_id = int(regex.search('(\d+)(?=\S{4,})', url).group()) gallery_token = regex.search('(?<=\d/)(\S+)(?=/$)', url).group() parsed_url = [gallery_id, gallery_token] return parsed_url - @staticmethod - def parse_metadata(metadata_json, dict_metadata): + def get_metadata(self, list_of_urls, cookies=None): + """ + Fetches the metadata from the provided list of urls + through the official API. + returns raw api data and a dict with gallery id as key and url as value + """ + assert isinstance(list_of_urls, list) + if len(list_of_urls) > 25: + log_e('More than 25 urls are provided. Aborting.') + return None + + payload = {"method": "gdata", + "gidlist": [], + "namespace": 1 + } + dict_metadata = {} + for url in list_of_urls: + parsed_url = EHen.parse_url(url.strip()) + dict_metadata[parsed_url[0]] = url # gallery id + payload['gidlist'].append(parsed_url) + + if payload['gidlist']: + self.begin_lock() + if cookies: + self.check_cookie(cookies) + r = requests.post(self.e_url, json=payload, timeout=30, headers=self.HEADERS, cookies=self.COOKIES) + else: + r = requests.post(self.e_url, json=payload, timeout=30, headers=self.HEADERS) + if not self.handle_error(r): + return 'error' + self.end_lock() + else: return None + try: + r.raise_for_status() + except: + log.exception('Could not fetch metadata: connection error') + return None + return r.json(), dict_metadata + + @classmethod + def parse_metadata(cls, metadata_json, dict_metadata): """ :metadata_json <- raw data provided by E-H API :dict_metadata <- a dict with gallery id's as keys and url as value @@ -720,53 +818,56 @@ def fix_titles(text): return parsed_metadata - def get_metadata(self, list_of_urls, cookies=None): + @classmethod + def login(cls, user, password): """ - Fetches the metadata from the provided list of urls - through the official API. - returns raw api data and a dict with gallery id as key and url as value + Logs into g.e-h """ - assert isinstance(list_of_urls, list) - if len(list_of_urls) > 25: - log_e('More than 25 urls are provided. Aborting.') - return None + log_i("Attempting EH Login") + eh_c = {} + exprops = settings.ExProperties() + if cls.COOKIES: + if cls.check_login(cls.COOKIES): + return cls.COOKIES + elif exprops.cookies: + if cls.check_login(exprops.cookies): + cls.COOKIES.update(exprops.cookies) + return cls.COOKIES - payload = {"method": "gdata", - "gidlist": [], - "namespace": 1 - } - dict_metadata = {} - for url in list_of_urls: - parsed_url = CommenHen.parse_url(url.strip()) - dict_metadata[parsed_url[0]] = url # gallery id - payload['gidlist'].append(parsed_url) + p = { + 'CookieDate': '1', + 'b':'d', + 'bt':'1-1', + 'UserName':user, + 'PassWord':password + } - if payload['gidlist']: - self.begin_lock() - if cookies: - self.check_cookie(cookies) - r = requests.post(self.e_url, json=payload, timeout=30, headers=self.HEADERS, cookies=self.COOKIES) - else: - r = requests.post(self.e_url, json=payload, timeout=30, headers=self.HEADERS) - if not self.handle_error(r): - return 'error' - self.end_lock() - else: return None - try: - r.raise_for_status() - except: - log.exception('Could not fetch metadata: connection error') - return None - return r.json(), dict_metadata + eh_c = requests.post('https://forums.e-hentai.org/index.php?act=Login&CODE=01', data=p).cookies.get_dict() + exh_c = requests.get('http://exhentai.org', cookies=eh_c).cookies.get_dict() + + eh_c.update(exh_c) + + if not cls.check_login(eh_c): + log_w("EH login failed") + raise app_constants.WrongLogin - def eh_hash_search(self, hash_string, cookies=None): + log_i("EH login succes") + exprops.cookies = eh_c + exprops.username = user + exprops.password = password + exprops.save() + cls.COOKIES.update(eh_c) + + return eh_c + + def search(self, search_string, cookies=None): """ Searches ehentai for the provided string or list of hashes, returns a dict with hash:[list of title,url tuples] of hits found or emtpy dict if no hits are found. """ - assert isinstance(hash_string, (str, list)) - if isinstance(hash_string, str): - hash_string = [hash_string] + assert isinstance(search_string, (str, list)) + if isinstance(search_string, str): + search_string = [search_string] def no_hits_found_check(html): "return true if hits are found" @@ -780,7 +881,7 @@ def no_hits_found_check(html): hash_url = app_constants.DEFAULT_EHEN_URL + '?f_shash=' found_galleries = {} log_i('Initiating hash search on ehentai') - for h in hash_string: + for h in search_string: log_d('Hash search: {}'.format(h)) self.begin_lock() if cookies: @@ -817,92 +918,13 @@ def no_hits_found_check(html): continue if found_galleries: - log_i('Found {} out of {} galleries'.format(len(found_galleries), len(hash_string))) + log_i('Found {} out of {} galleries'.format(len(found_galleries), len(search_string))) return found_galleries else: log_w('Could not find any galleries') return {} - def eh_gallery_parser(self, url, cookies=None): - """ - Parses an ehentai page for metadata. - Returns gallery dict with following metadata: - - title - - jap_title - - type - - language - - publication date - - namespace & tags - """ - self.begin_lock() - if cookies: - self.check_cookie(cookies) - r = requests.get(url, headers=self.HEADERS, timeout=30, cookies=self.COOKIES) - else: - r = requests.get(url, headers=self.HEADERS, timeout=30) - self.end_lock() - if not self.handle_error(r): - return {} - html = r.text - if len(html)<5000: - log_w("Length of HTML response is only {} => Failure".format(len(html))) - return {} - - gallery = {} - soup = BeautifulSoup(html) - - #title - div_gd2 = soup.body.find('div', id='gd2') - # normal - title = div_gd2.find('h1', id='gn').text.strip() - # japanese - jap_title = div_gd2.find('h1', id='gj').text.strip() - - gallery['title'] = title - gallery['jap_title'] = jap_title - - # Type - div_gd3 = soup.body.find('div', id='gd3') - gallery['type'] = div_gd3.find('img').get('alt') - - # corrects name - if gallery['type'] == 'artistcg': - gallery['type'] = 'artist cg sets' - elif gallery['type'] == 'imageset': - gallery['type'] = 'image sets' - elif gallery['type'] == 'gamecg': - gallery['type'] = 'game cg sets' - elif gallery['type'] == 'asianporn': - gallery['type'] = 'asian porn' - - # Language - lang_tag = soup.find('td', text='Language:').next_sibling - lang = lang_tag.text.split(' ')[0] - gallery['language'] = lang - - # Publication date - pub_tag = soup.find('td', text='Posted:').next_sibling - pub_date = datetime.strptime(pub_tag.text.split(' ')[0], '%Y-%m-%d').date() - gallery['published'] = pub_date - - # Namespace & Tags - found_tags = {} - def tags_in_ns(tags): - return not tags.has_attr('class') - tag_table = soup.find('div', id='taglist').next_element - namespaces = tag_table.find_all('tr') - for ns in namespaces: - namespace = ns.next_element.text.replace(':', '') - namespace = namespace.capitalize() - found_tags[namespace] = [] - tags = ns.find(tags_in_ns).find_all('div') - for tag in tags: - found_tags[namespace].append(tag.text) - - gallery['tags'] = found_tags - return gallery - -class ExHen(CommenHen): +class ExHen(EHen): "Fetches gallery metadata from exhen" def __init__(self, cookies): self.cookies = cookies @@ -911,14 +933,7 @@ def __init__(self, cookies): def get_metadata(self, list_of_urls): return super().get_metadata(list_of_urls, self.cookies) - def eh_gallery_parser(self, url): - return super().eh_gallery_parser(url, self.cookies) - - def eh_hash_search(self, hash_string): - return super().eh_hash_search(hash_string, self.cookies) + def search(self, hash_string): + return super().search(hash_string, self.cookies) -class EHen(CommenHen): - "Fetches galleries from ehen" - def __init__(self): - self.e_url = "http://g.e-hentai.org/api.php" diff --git a/version/settingsdialog.py b/version/settingsdialog.py index ff9f902..2ed316f 100644 --- a/version/settingsdialog.py +++ b/version/settingsdialog.py @@ -658,7 +658,7 @@ def make_login_forms(layout, cookies, baseHen_class): # ehentai ehentai_group, ehentai_l = groupbox("E-Hentai", QFormLayout, logins_page) logins_layout.addRow(ehentai_group) - ehentai_user, ehentai_pass, ehentai_status = make_login_forms(ehentai_l, settings.ExProperties().cookies, pewnet.CommenHen) + ehentai_user, ehentai_pass, ehentai_status = make_login_forms(ehentai_l, settings.ExProperties().cookies, pewnet.EHen) # Web / Downloader web_downloader, web_downloader_l = new_tab('Downloader', web)