diff --git a/happypanda.ico b/happypanda.ico index d3443f3..156f3f9 100644 Binary files a/happypanda.ico and b/happypanda.ico differ 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)