fixed csv importing and core dump issue
@ -84,11 +84,14 @@ class AnimeBackend:
|
|||||||
if header:
|
if header:
|
||||||
cursor = self.db.cursor()
|
cursor = self.db.cursor()
|
||||||
for row in reader:
|
for row in reader:
|
||||||
if len(row) < 8:
|
if len(row) == 7:
|
||||||
|
name, year_str, season, status, type_, comment, url = row
|
||||||
|
elif len(row) == 8:
|
||||||
|
_, name, year_str, season, status, type_, comment, url = row
|
||||||
|
else:
|
||||||
continue
|
continue
|
||||||
_, name, year, season, status, type_, comment, url = row
|
|
||||||
try:
|
try:
|
||||||
year = int(year)
|
year = int(year_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
|
99
frontend.py
@ -3,6 +3,7 @@ import os
|
|||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import html
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QMainWindow, QTabWidget, QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem,
|
QMainWindow, QTabWidget, QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem,
|
||||||
@ -10,7 +11,7 @@ from PyQt5.QtWidgets import (
|
|||||||
QComboBox, QTextEdit, QDialogButtonBox, QAction, QFileDialog, QMessageBox,
|
QComboBox, QTextEdit, QDialogButtonBox, QAction, QFileDialog, QMessageBox,
|
||||||
QInputDialog, QApplication, QAbstractItemView
|
QInputDialog, QApplication, QAbstractItemView
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt, QUrl
|
from PyQt5.QtCore import Qt, QUrl, QEvent
|
||||||
from PyQt5.QtGui import QColor, QIcon
|
from PyQt5.QtGui import QColor, QIcon
|
||||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
||||||
from backend import AnimeBackend
|
from backend import AnimeBackend
|
||||||
@ -76,6 +77,30 @@ class AnimeDialog(QDialog):
|
|||||||
'url': self.url_edit.text()
|
'url': self.url_edit.text()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class HoverLabel(QLabel):
|
||||||
|
def __init__(self, main_window, name, url=None):
|
||||||
|
super().__init__()
|
||||||
|
self.main_window = main_window
|
||||||
|
self.url = url
|
||||||
|
self.fetched = False
|
||||||
|
self.image_file = None
|
||||||
|
if url:
|
||||||
|
self.image_file = os.path.join(main_window.image_cache_dir, hashlib.md5(url.encode()).hexdigest() + '.jpg')
|
||||||
|
name_escaped = html.escape(name)
|
||||||
|
self.setText(f'<a href="{url}">{name_escaped}</a>')
|
||||||
|
self.setOpenExternalLinks(True)
|
||||||
|
if os.path.exists(self.image_file):
|
||||||
|
self.setToolTip(f'<img src="{self.image_file}" width="200">')
|
||||||
|
self.fetched = True
|
||||||
|
else:
|
||||||
|
self.setText(html.escape(name))
|
||||||
|
|
||||||
|
def enterEvent(self, event):
|
||||||
|
if self.url and not self.fetched:
|
||||||
|
self.main_window.fetch_poster(self.url, self)
|
||||||
|
self.fetched = True
|
||||||
|
super().enterEvent(event)
|
||||||
|
|
||||||
class AnimeTracker(QMainWindow):
|
class AnimeTracker(QMainWindow):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -113,9 +138,13 @@ class AnimeTracker(QMainWindow):
|
|||||||
|
|
||||||
def load_tabs(self):
|
def load_tabs(self):
|
||||||
self.tab_widget.clear()
|
self.tab_widget.clear()
|
||||||
|
# Pre-2010 tab
|
||||||
pre_entries = self.backend.get_pre_2010_entries()
|
pre_entries = self.backend.get_pre_2010_entries()
|
||||||
pre_tab = QWidget()
|
pre_tab = QWidget()
|
||||||
self.setup_table(pre_tab, pre_entries, is_pre=True)
|
layout = QVBoxLayout(pre_tab)
|
||||||
|
if pre_entries:
|
||||||
|
table = self.create_table_widget(pre_entries, is_pre=True)
|
||||||
|
layout.addWidget(table)
|
||||||
tab_text = "Pre-2010"
|
tab_text = "Pre-2010"
|
||||||
completed = False
|
completed = False
|
||||||
total = len(pre_entries)
|
total = len(pre_entries)
|
||||||
@ -128,25 +157,32 @@ class AnimeTracker(QMainWindow):
|
|||||||
index = self.tab_widget.addTab(pre_tab, tab_text)
|
index = self.tab_widget.addTab(pre_tab, tab_text)
|
||||||
if completed:
|
if completed:
|
||||||
self.tab_widget.tabBar().setTabTextColor(index, QColor('gray'))
|
self.tab_widget.tabBar().setTabTextColor(index, QColor('gray'))
|
||||||
|
# Years >= 2010
|
||||||
years = self.backend.get_years()
|
years = self.backend.get_years()
|
||||||
for year in years:
|
for year in years:
|
||||||
|
year_tab = QWidget()
|
||||||
|
layout = QVBoxLayout(year_tab)
|
||||||
|
total_entries = 0
|
||||||
|
comp_entries = 0
|
||||||
for season in ['winter', 'spring', 'summer', 'fall', '']:
|
for season in ['winter', 'spring', 'summer', 'fall', '']:
|
||||||
entries = self.backend.get_entries_for_season(year, season)
|
entries = self.backend.get_entries_for_season(year, season)
|
||||||
if entries:
|
if entries:
|
||||||
tab = QWidget()
|
|
||||||
self.setup_table(tab, entries, is_pre=False, year=year, season=season)
|
|
||||||
s_name = season.capitalize() if season else 'Other'
|
s_name = season.capitalize() if season else 'Other'
|
||||||
total = len(entries)
|
label = QLabel(s_name)
|
||||||
comp = sum(1 for e in entries if e[4] == 'completed')
|
layout.addWidget(label)
|
||||||
perc = (comp / total * 100)
|
table = self.create_table_widget(entries, is_pre=False)
|
||||||
tab_text = f"{year} - {s_name} ({perc:.0f}%)"
|
layout.addWidget(table)
|
||||||
completed = (comp == total)
|
total_entries += len(entries)
|
||||||
index = self.tab_widget.addTab(tab, tab_text)
|
comp_entries += sum(1 for e in entries if e[4] == 'completed')
|
||||||
if completed:
|
if total_entries > 0:
|
||||||
self.tab_widget.tabBar().setTabTextColor(index, QColor('gray'))
|
perc = (comp_entries / total_entries * 100) if total_entries > 0 else 0
|
||||||
|
tab_text = f"{year} ({perc:.0f}%)"
|
||||||
|
completed = (comp_entries == total_entries)
|
||||||
|
index = self.tab_widget.addTab(year_tab, tab_text)
|
||||||
|
if completed:
|
||||||
|
self.tab_widget.tabBar().setTabTextColor(index, QColor('gray'))
|
||||||
|
|
||||||
def setup_table(self, tab, entries, is_pre, year=None, season=None):
|
def create_table_widget(self, entries, is_pre):
|
||||||
layout = QVBoxLayout(tab)
|
|
||||||
col_count = 6 if is_pre else 5
|
col_count = 6 if is_pre else 5
|
||||||
table = QTableWidget(len(entries), col_count)
|
table = QTableWidget(len(entries), col_count)
|
||||||
headers = ['Year', 'Name', 'Type', 'Status', 'Comment', 'Actions'] if is_pre else ['Name', 'Type', 'Status', 'Comment', 'Actions']
|
headers = ['Year', 'Name', 'Type', 'Status', 'Comment', 'Actions'] if is_pre else ['Name', 'Type', 'Status', 'Comment', 'Actions']
|
||||||
@ -164,13 +200,7 @@ class AnimeTracker(QMainWindow):
|
|||||||
# Name
|
# Name
|
||||||
name = entry[1]
|
name = entry[1]
|
||||||
url = entry[7]
|
url = entry[7]
|
||||||
name_label = QLabel()
|
name_label = HoverLabel(self, name, url)
|
||||||
if url:
|
|
||||||
name_label.setText(f'<a href="{url}">{name}</a>')
|
|
||||||
name_label.setOpenExternalLinks(True)
|
|
||||||
self.fetch_poster(url, name_label)
|
|
||||||
else:
|
|
||||||
name_label.setText(name)
|
|
||||||
table.setCellWidget(row, col, name_label)
|
table.setCellWidget(row, col, name_label)
|
||||||
col += 1
|
col += 1
|
||||||
# Type
|
# Type
|
||||||
@ -208,7 +238,7 @@ class AnimeTracker(QMainWindow):
|
|||||||
widget = table.cellWidget(r, c)
|
widget = table.cellWidget(r, c)
|
||||||
if widget:
|
if widget:
|
||||||
widget.setStyleSheet(f"background-color: {color.name()};")
|
widget.setStyleSheet(f"background-color: {color.name()};")
|
||||||
layout.addWidget(table)
|
return table
|
||||||
|
|
||||||
def create_actions_widget(self, anime_id, status):
|
def create_actions_widget(self, anime_id, status):
|
||||||
widget = QWidget()
|
widget = QWidget()
|
||||||
@ -239,7 +269,7 @@ class AnimeTracker(QMainWindow):
|
|||||||
return widget
|
return widget
|
||||||
|
|
||||||
def fetch_poster(self, url, label):
|
def fetch_poster(self, url, label):
|
||||||
image_file = os.path.join(self.image_cache_dir, hashlib.md5(url.encode()).hexdigest() + '.jpg')
|
image_file = label.image_file
|
||||||
if os.path.exists(image_file):
|
if os.path.exists(image_file):
|
||||||
label.setToolTip(f'<img src="{image_file}" width="200">')
|
label.setToolTip(f'<img src="{image_file}" width="200">')
|
||||||
return
|
return
|
||||||
@ -280,10 +310,9 @@ class AnimeTracker(QMainWindow):
|
|||||||
default_year = 2009
|
default_year = 2009
|
||||||
default_season = ''
|
default_season = ''
|
||||||
else:
|
else:
|
||||||
parts = tab_text.split(' - ')
|
parts = tab_text.split(' (')
|
||||||
default_year = int(parts[0])
|
default_year = int(parts[0])
|
||||||
s_name = parts[1].split(' (')[0].lower()
|
default_season = ''
|
||||||
default_season = '' if s_name == 'other' else s_name
|
|
||||||
dialog = AnimeDialog(self, None, default_year, default_season)
|
dialog = AnimeDialog(self, None, default_year, default_season)
|
||||||
if dialog.exec_() == QDialog.Accepted:
|
if dialog.exec_() == QDialog.Accepted:
|
||||||
data = dialog.get_data()
|
data = dialog.get_data()
|
||||||
@ -321,18 +350,18 @@ class AnimeTracker(QMainWindow):
|
|||||||
return
|
return
|
||||||
tab_text = self.tab_widget.tabText(self.tab_widget.currentIndex())
|
tab_text = self.tab_widget.tabText(self.tab_widget.currentIndex())
|
||||||
is_pre = 'Pre-2010' in tab_text
|
is_pre = 'Pre-2010' in tab_text
|
||||||
table = self.tab_widget.currentWidget().findChild(QTableWidget)
|
current_widget = self.tab_widget.currentWidget()
|
||||||
if not table:
|
tables = current_widget.findChildren(QTableWidget)
|
||||||
return
|
|
||||||
name_col = 1 if is_pre else 0
|
name_col = 1 if is_pre else 0
|
||||||
status_col = 3 if is_pre else 2
|
status_col = 3 if is_pre else 2
|
||||||
unwatched = []
|
unwatched = []
|
||||||
for row in range(table.rowCount()):
|
for table in tables:
|
||||||
status = table.item(row, status_col).text()
|
for row in range(table.rowCount()):
|
||||||
if status == 'unwatched':
|
status = table.item(row, status_col).text()
|
||||||
name_text = table.cellWidget(row, name_col).text()
|
if status == 'unwatched':
|
||||||
clean_name = re.sub(r'<[^>]+>', '', name_text)
|
name_text = table.cellWidget(row, name_col).text()
|
||||||
unwatched.append(clean_name)
|
clean_name = re.sub(r'<[^>]+>', '', name_text)
|
||||||
|
unwatched.append(clean_name)
|
||||||
if unwatched:
|
if unwatched:
|
||||||
random_name = random.choice(unwatched)
|
random_name = random.choice(unwatched)
|
||||||
QMessageBox.information(self, "Random Pick", f"Watch: {random_name}")
|
QMessageBox.information(self, "Random Pick", f"Watch: {random_name}")
|
||||||
|
BIN
images/05349781156d3b2ad4c2cabc838f84f6.jpg
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
images/1cc8866ed0684b73e453fb63b1c082cb.jpg
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
images/206917dcf06ee7adcaf5ab71e042365c.jpg
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
images/38206b1e9e27b8f79cb7ceab145324e1.jpg
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
images/3abf59b31a2928a9f97da85e1f524c13.jpg
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
images/6bd6a17fa7e50aad825ed7cc348820de.jpg
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
images/8bbe33cf1c3c423758c057e47beb6758.jpg
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
images/8fd82d7c17a27a7a3c523c9bfd0ee6ea.jpg
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
images/af0241c5e85ae6c576c4116fa4cbd06b.jpg
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
images/d1431e16e12867d689fa96d78105b058.jpg
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
images/d749363a2fd4303f005e869d55e4001a.jpg
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
images/f299684860c22376fe01f638adb20966.jpg
Normal file
After Width: | Height: | Size: 55 KiB |