Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:X0F:branches:multimedia
trackma
trackma_PR722.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File trackma_PR722.patch of Package trackma
From cff295a16ba8922a465704de85c222ea00254fd9 Mon Sep 17 00:00:00 2001 From: BigBoyBarney <86712892+BigBoyBarney@users.noreply.github.com> Date: Sun, 24 Dec 2023 23:45:38 +0100 Subject: [PATCH 01/16] Implemented (GTK) 'Next episode' column, refactored List Tree code UI: - QoL changes to the right-click context menu of a show, dynamic site name in particular - Added "Next episode" column with custom sorting - Renamed the old "Progress" column (e.g: 7/13) to Watched, and renamed "Percent" column (the progress bar) to "Progress". - Set minimum width for the progress bar to 200 pixels Code: - Refactored a significant part of the list tree (showtreeview.py), making it much easier to read and build upon. - Unified column / row reference constants - The tree model is now set immediately after tree initialisation --- trackma/ui/gtk/mainview.py | 38 ++-- trackma/ui/gtk/showtreeview.py | 405 +++++++++++++++++++++++---------- trackma/utils.py | 57 ++++- 3 files changed, 363 insertions(+), 137 deletions(-) diff --git a/trackma/ui/gtk/mainview.py b/trackma/ui/gtk/mainview.py index 0a7897ba..6ebaa6ac 100644 --- a/trackma/ui/gtk/mainview.py +++ b/trackma/ui/gtk/mainview.py @@ -30,12 +30,11 @@ @Gtk.Template.from_file(os.path.join(gtk_dir, 'data/mainview.ui')) class MainView(Gtk.Box): - __gtype_name__ = 'MainView' __gsignals__ = { 'error': (GObject.SignalFlags.RUN_FIRST, None, - (str, )), + (str,)), 'success': (GObject.SignalFlags.RUN_CLEANUP, None, ()), 'error-fatal': (GObject.SignalFlags.RUN_FIRST, None, @@ -535,7 +534,8 @@ def _on_show_action(self, page, event_type, data): def get_current_status(self): print(self._engine.mediainfo['statuses']) - return self._current_page.status if self._current_page.status is not None else self._engine.mediainfo['statuses'][-1] + return self._current_page.status if self._current_page.status is not None else \ + self._engine.mediainfo['statuses'][-1] def get_selected_show(self): if not self._current_page: @@ -565,7 +565,7 @@ class NotebookPage(Gtk.ScrolledWindow): __gsignals__ = { 'show-selected': (GObject.SignalFlags.RUN_FIRST, None, - (int, )), + (int,)), 'show-action': (GObject.SignalFlags.RUN_FIRST, None, (int, object)), 'column-toggled': (GObject.SignalFlags.RUN_FIRST, None, @@ -580,7 +580,8 @@ def __init__(self, engine, page_num, status, config, _list=None, title=None): self._selected_show = 0 self._list = _list self._title = title - self._title_text = self._engine.mediainfo['statuses_dict'][status] if status in self._engine.mediainfo['statuses_dict'].keys( + self._title_text = self._engine.mediainfo['statuses_dict'][status] if status in self._engine.mediainfo[ + 'statuses_dict'].keys( ) else 'All' self._init_widgets(page_num, status, config) @@ -592,15 +593,18 @@ def _init_widgets(self, page_num, status, config): self._show_tree_view = ShowTreeView( config['colors'], config['visible_columns'], - config['episodebar_style']) - self._show_tree_view.set_model( - Gtk.TreeModelSort( - model=ShowListFilter( - status=self.status, - child_model=self._list - ) - ) + self.status, + self._list, + config['episodebar_style'], ) + # self._show_tree_view.set_model( + # Gtk.TreeModelSort( + # model=ShowListFilter( + # status=self.status, + # child_model=self._list + # ) + # ) + # ) self._title.set_text('%s (%d)' % ( self._title_text, len(self._show_tree_view.props.model) @@ -687,17 +691,17 @@ def _view_context_menu(self, event): show = self._engine.get_show_info(self._selected_show) menu = Gtk.Menu() - mb_play = Gtk.ImageMenuItem('Play Next', + mb_play = Gtk.ImageMenuItem('Play next episode', Gtk.Image.new_from_icon_name( "media-playback-start", Gtk.IconSize.MENU)) mb_play.connect("activate", self._on_mb_activate, ShowEventType.PLAY_NEXT) - mb_info = Gtk.MenuItem("Show details...") + mb_info = Gtk.MenuItem("Show details") mb_info.connect("activate", self._on_mb_activate, ShowEventType.DETAILS) - mb_web = Gtk.MenuItem("Open web site") + mb_web = Gtk.MenuItem("Open on " + self._engine.api_info['name']) mb_web.connect("activate", self._on_mb_activate, ShowEventType.OPEN_WEBSITE) @@ -709,7 +713,7 @@ def _view_context_menu(self, event): mb_copy.connect("activate", self._on_mb_activate, ShowEventType.COPY_TITLE) - mb_alt_title = Gtk.MenuItem("Set alternate title...") + mb_alt_title = Gtk.MenuItem("Set alternate title") mb_alt_title.connect("activate", self._on_mb_activate, ShowEventType.CHANGE_ALTERNATIVE_TITLE) diff --git a/trackma/ui/gtk/showtreeview.py b/trackma/ui/gtk/showtreeview.py index 652fd6dd..258450fe 100644 --- a/trackma/ui/gtk/showtreeview.py +++ b/trackma/ui/gtk/showtreeview.py @@ -19,32 +19,60 @@ from trackma import utils +# Declare named constants for the tree references, so it's more unified and easier to read +# Putting them in their own class also enables whatever we might want to do with it in the future. +class TreeConstants: + SHOW_ID = 0 + TITLE = 1 + MY_PROGRESS = 2 + MY_SCORE = 3 + EPISODES = 4 + SCORE = 5 + TOTAL_EPS = 6 + AIRED_EPS = 7 + AVAILABLE_EPS = 8 + COLOR = 9 + PROGRESS = 10 + START_DATE = 11 + END_DATE = 12 + MY_START_DATE = 13 + MY_FINISH_DATE = 14 + MY_STATUS = 15 + SHOW_STATUS = 16 + NEXT_EPISODE_AIR_TIME_RELATIVE = 17 + + class ShowListStore(Gtk.ListStore): + # Determines the structure of the tree and holds the actual data after it is appended from row[]. + # Entry order must match with row[]. __cols = ( - ('id', int), - ('title', str), - ('stat', int), - ('score', float), - ('stat-text', str), - ('score-text', str), - ('total-eps', int), - ('subvalue', int), - ('avail-eps', GObject.TYPE_PYOBJECT), - ('color', str), - ('stat-pcent', int), - ('start', str), - ('end', str), - ('my-start', str), - ('my-end', str), - ('my-status', str), - ('status', int), + (TreeConstants.SHOW_ID, int), + (TreeConstants.TITLE, str), + (TreeConstants.MY_PROGRESS, int), + (TreeConstants.MY_SCORE, float), + (TreeConstants.EPISODES, str), + (TreeConstants.SCORE, str), + (TreeConstants.TOTAL_EPS, int), + (TreeConstants.AIRED_EPS, int), + (TreeConstants.AVAILABLE_EPS, GObject.TYPE_PYOBJECT), + (TreeConstants.COLOR, str), + (TreeConstants.PROGRESS, int), + (TreeConstants.START_DATE, str), + (TreeConstants.END_DATE, str), + (TreeConstants.MY_START_DATE, str), + (TreeConstants.MY_FINISH_DATE, str), + (TreeConstants.MY_STATUS, str), + (TreeConstants.SHOW_STATUS, int), + (TreeConstants.NEXT_EPISODE_AIR_TIME_RELATIVE, str), ) - def __init__(self, decimals=0, colors=dict()): + def __init__(self, decimals=0, colors=None): super().__init__(*self.__class__.__columns__()) + if colors is None: + colors = dict() self.colors = colors self.decimals = decimals - self.set_sort_column_id(1, Gtk.SortType.ASCENDING) + self.set_sort_column_id(TreeConstants.TITLE, Gtk.SortType.ASCENDING) @staticmethod def format_date(date): @@ -63,7 +91,7 @@ def __columns__(cls): @classmethod def column(cls, key): try: - return cls.__cols.index(next(i for i in cls.__cols if i[0] == key)) + return cls.__cols.index(next(i for i in cls.__cols if i[TreeConstants.SHOW_ID] == key)) except ValueError: return None @@ -104,6 +132,11 @@ def append(self, show, altname=None, eps=None): my_start_date = self.format_date(show['my_start_date']) my_finish_date = self.format_date(show['my_finish_date']) + # Gets the (short) relative airing time of the next episode compared to UTC + next_episode_air_time_relative = utils.calculate_relative_time(show['next_ep_time'], + utc=True, fulltime=False) + + # Corresponds to __cols, but is used locally to store the data before appending. row = [show['id'], title_str, show['my_progress'], @@ -120,13 +153,14 @@ def append(self, show, altname=None, eps=None): my_start_date, my_finish_date, show['my_status'], - show['status'] + show['status'], + next_episode_air_time_relative, ] super().append(row) def update_or_append(self, show): for row in self: - if int(row[0]) == show['id']: + if int(row[TreeConstants.SHOW_ID]) == show['id']: self.update(show, row) return self.append(show) @@ -134,49 +168,49 @@ def update_or_append(self, show): def update(self, show, row=None): if not row: for row in self: - if int(row[0]) == show['id']: + if int(row[TreeConstants.SHOW_ID]) == show['id']: break - if row and int(row[0]) == show['id']: + if row and int(row[TreeConstants.SHOW_ID]) == show['id']: episodes_str = "{} / {}".format(show['my_progress'], show['total'] or '?') - row[2] = show['my_progress'] - row[4] = episodes_str + row[TreeConstants.MY_PROGRESS] = show['my_progress'] + row[TreeConstants.EPISODES] = episodes_str score_str = "%0.*f" % (self.decimals, show['my_score']) - row[3] = show['my_score'] - row[5] = score_str - row[9] = self._get_color(show, row[8]) - row[15] = show['my_status'] + row[TreeConstants.MY_SCORE] = show['my_score'] + row[TreeConstants.SCORE] = score_str + row[TreeConstants.COLOR] = self._get_color(show, row[TreeConstants.AVAILABLE_EPS]) + row[TreeConstants.MY_STATUS] = show['my_status'] return # print("Warning: Show ID not found in ShowView (%d)" % show['id']) def update_title(self, show, altname=None): for row in self: - if int(row[0]) == show['id']: + if int(row[TreeConstants.SHOW_ID]) == show['id']: if altname: title_str = "%s [%s]" % (show['title'], altname) else: title_str = show['title'] - row[1] = title_str + row[TreeConstants.SHOW_ID] = title_str return def remove(self, show=None, show_id=None): for row in self: - if int(row[0]) == (show['id'] if show is not None else show_id): + if int(row[TreeConstants.SHOW_ID]) == (show['id'] if show is not None else show_id): Gtk.ListStore.remove(self, row.iter) return def playing(self, show, is_playing): # Change the color if the show is currently playing for row in self: - if int(row[0]) == show['id']: + if int(row[TreeConstants.SHOW_ID]) == show['id']: if is_playing: - row[9] = self.colors['is_playing'] + row[TreeConstants.COLOR] = self.colors['is_playing'] else: - row[9] = self._get_color(show, row[8]) + row[TreeConstants.COLOR] = self._get_color(show, row[TreeConstants.AVAILABLE_EPS]) return @@ -190,7 +224,7 @@ def __init__(self, status=None, *args, **kwargs): self._status = status def status_filter(self, model, iterator, data): - return self._status is None or model[iterator][15] == self._status + return self._status is None or model[iterator][TreeConstants.MY_STATUS] == self._status def get_value(self, obj, key='id'): try: @@ -207,100 +241,204 @@ class ShowTreeView(Gtk.TreeView): __gsignals__ = {'column-toggled': (GObject.SignalFlags.RUN_LAST, GObject.TYPE_PYOBJECT, (GObject.TYPE_STRING, GObject.TYPE_BOOLEAN))} - def __init__(self, colors, visible_columns, progress_style=1): + def __init__(self, colors, visible_columns, status, _list, progress_style=1): Gtk.TreeView.__init__(self) self.colors = colors self.visible_columns = visible_columns self.progress_style = progress_style + self.status = status + self._list = _list + + self.set_model( + Gtk.TreeModelSort( + model=ShowListFilter( + status=self.status, + child_model=self._list + ) + ) + ) self.set_enable_search(True) - self.set_search_column(1) + self.set_search_column(TreeConstants.TITLE) self.set_property('has-tooltip', True) self.connect('query-tooltip', self.show_tooltip) self.cols = dict() + # Defines the default column order as well self.available_columns = ( - ('Title', 1), - ('Progress', 2), - ('Score', 3), - ('Percent', 10), - ('Start', 11), - ('End', 12), - ('My start', 13), - ('My end', 14), + ('Title', TreeConstants.TITLE), + ('Watched', TreeConstants.MY_PROGRESS), + ('Score', TreeConstants.MY_SCORE), + ('Next episode', TreeConstants.NEXT_EPISODE_AIR_TIME_RELATIVE), + ('Start', TreeConstants.START_DATE), + ('End', TreeConstants.END_DATE), + ('My start', TreeConstants.MY_START_DATE), + ('My end', TreeConstants.MY_FINISH_DATE), + ('Progress', TreeConstants.PROGRESS), ) - for (name, sort) in self.available_columns: - self.cols[name] = Gtk.TreeViewColumn(name) - self.cols[name].set_sort_column_id(sort) + for (name, key) in self.available_columns: + self.cols[name] = Gtk.TreeViewColumn() + self.cols[name].set_sort_column_id(key) + + # Set up the percent / progress bar + if name == 'Progress': + if self.progress_style == 0: + renderer = Gtk.CellRendererProgress() + self.cols[name].pack_start(renderer, False) + self.cols[name].add_attribute(renderer, 'value', TreeConstants.PROGRESS) + else: + renderer = ProgressCellRenderer(self.colors) + self.cols[name].pack_start(renderer, False) + self.cols[name].add_attribute(renderer, 'value', TreeConstants.MY_PROGRESS) + self.cols[name].add_attribute(renderer, 'total', TreeConstants.TOTAL_EPS) + self.cols[name].add_attribute(renderer, 'subvalue', TreeConstants.AIRED_EPS) + self.cols[name].add_attribute(renderer, 'eps', TreeConstants.AVAILABLE_EPS) + else: + renderer = Gtk.CellRendererText() + self.cols[name].pack_start(renderer, False) + + if name not in self.visible_columns: + self.cols[name].set_visible(False) + + # Populate columns + match name: + case 'Title': + self.cols[name].set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) + self.cols[name].set_resizable(True) + self.cols[name].set_expand(True) + self.cols[name].add_attribute(renderer, 'text', TreeConstants.TITLE) + self.cols[name].add_attribute(renderer, 'foreground', TreeConstants.COLOR) + renderer.set_property('ellipsize', Pango.EllipsizeMode.END) + + case 'Watched': + self.cols[name].add_attribute(renderer, 'text', TreeConstants.EPISODES) + + case 'Progress': + self.cols[name].set_min_width(200) + + case 'Score': + self.cols[name].add_attribute(renderer, 'text', TreeConstants.SCORE) + + case 'Start': + self.cols[name].add_attribute(renderer, 'text', TreeConstants.START_DATE) + + case 'End': + self.cols[name].add_attribute(renderer, 'text', TreeConstants.END_DATE) + + case 'My start': + self.cols[name].add_attribute(renderer, 'text', TreeConstants.MY_START_DATE) + + case 'My end': + self.cols[name].add_attribute(renderer, 'text', TreeConstants.MY_FINISH_DATE) + + case 'Next episode': + self.cols[name].add_attribute(renderer, 'text', TreeConstants.NEXT_EPISODE_AIR_TIME_RELATIVE) + self.get_model().set_sort_func(sort_column_id=TreeConstants.NEXT_EPISODE_AIR_TIME_RELATIVE, + sort_func=self._next_episode_sort_func, user_data=self) + + case _: + pass # This is a hack to allow for right-clickable header label = Gtk.Label(name) label.show() self.cols[name].set_widget(label) - - self.append_column(self.cols[name]) - w = self.cols[name].get_widget() while not isinstance(w, Gtk.Button): w = w.get_parent() - w.connect('button-press-event', self._header_button_press) - if name not in self.visible_columns: - self.cols[name].set_visible(False) + self.append_column(self.cols[name]) - # renderer_id = Gtk.CellRendererText() - # self.cols['ID'].pack_start(renderer_id, False, True, 0) - # self.cols['ID'].set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) - # self.cols['ID'].set_expand(False) - # self.cols['ID'].add_attribute(renderer_id, 'text', 0) - - renderer_title = Gtk.CellRendererText() - self.cols['Title'].pack_start(renderer_title, False) - self.cols['Title'].set_resizable(True) - self.cols['Title'].set_sizing(Gtk.TreeViewColumnSizing.FIXED) - self.cols['Title'].set_expand(True) - self.cols['Title'].add_attribute(renderer_title, 'text', 1) - # Using foreground-gdk does not work, possibly due to the timing of it being set - self.cols['Title'].add_attribute(renderer_title, 'foreground', 9) - renderer_title.set_property('ellipsize', Pango.EllipsizeMode.END) - - renderer_progress = Gtk.CellRendererText() - self.cols['Progress'].pack_start(renderer_progress, False) - self.cols['Progress'].add_attribute(renderer_progress, 'text', 4) - self.cols['Progress'].set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) - self.cols['Progress'].set_expand(False) - - if self.progress_style == 0: - renderer_percent = Gtk.CellRendererProgress() - self.cols['Percent'].pack_start(renderer_percent, False) - self.cols['Percent'].add_attribute(renderer_percent, 'value', 10) - else: - renderer_percent = ProgressCellRenderer(self.colors) - self.cols['Percent'].pack_start(renderer_percent, False) - self.cols['Percent'].add_attribute(renderer_percent, 'value', 2) - self.cols['Percent'].add_attribute(renderer_percent, 'total', 6) - self.cols['Percent'].add_attribute(renderer_percent, 'subvalue', 7) - self.cols['Percent'].add_attribute(renderer_percent, 'eps', 8) - renderer_percent.set_fixed_size(100, -1) - - renderer = Gtk.CellRendererText() - self.cols['Score'].pack_start(renderer, False) - self.cols['Score'].add_attribute(renderer, 'text', 5) - renderer = Gtk.CellRendererText() - self.cols['Start'].pack_start(renderer, False) - self.cols['Start'].add_attribute(renderer, 'text', 11) - renderer = Gtk.CellRendererText() - self.cols['End'].pack_start(renderer, False) - self.cols['End'].add_attribute(renderer, 'text', 12) - renderer = Gtk.CellRendererText() - self.cols['My start'].pack_start(renderer, False) - self.cols['My start'].add_attribute(renderer, 'text', 13) - renderer = Gtk.CellRendererText() - self.cols['My end'].pack_start(renderer, False) - self.cols['My end'].add_attribute(renderer, 'text', 14) + # for (name, key) in self.available_columns: + # self.cols[name] = Gtk.TreeViewColumn(name) + # self.cols[name].set_sort_column_id(key) + # self.cols[name].set_resizable(False) + # self.cols[name].set_reorderable(True) + # + # if name == 'Title': + # self.cols[name].set_alignment(0.0) + # self.cols[name].set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) + # else: + # self.cols[name].set_alignment(0.5) + # self.cols[name].set_sizing(Gtk.TreeViewColumnSizing.FIXED) + # + # # This is a hack to allow for right-clickable header + # label = Gtk.Label(name) + # label.show() + # self.cols[name].set_widget(label) + # + # w = self.cols[name].get_widget() + # while not isinstance(w, Gtk.Button): + # w = w.get_parent() + # + # w.connect('button-press-event', self._header_button_press) + # + # if name not in self.visible_columns: + # self.cols[name].set_visible(False) + # + # self.append_column(self.cols[name]) + + # renderer_title = Gtk.CellRendererText() + # self.cols['Title'].pack_start(renderer_title, True) + # self.cols['Title'].set_resizable(True) + # self.cols['Title'].set_expand(True) + # self.cols['Title'].add_attribute(renderer_title, 'text', RowConstants.TITLE) + # # Using foreground-gdk does not work, possibly due to the timing of it being set + # self.cols['Title'].add_attribute(renderer_title, 'foreground', RowConstants.COLOR) + # renderer_title.set_property('ellipsize', Pango.EllipsizeMode.END) + # + # renderer_progress = Gtk.CellRendererText() + # renderer_progress.set_alignment(0.5, 0.5) + # self.cols['Progress'].set_reorderable(True) + # self.cols['Progress'].pack_start(renderer_progress, False) + # self.cols['Progress'].add_attribute(renderer_progress, 'text', RowConstants.EPISODES) + # + # if self.progress_style == 0: + # renderer_percent = Gtk.CellRendererProgress() + # self.cols['Percent'].pack_start(renderer_percent, False) + # self.cols['Percent'].add_attribute(renderer_percent, 'value', RowConstants.PROGRESS) + # else: + # renderer_percent = ProgressCellRenderer(self.colors) + # self.cols['Percent'].pack_start(renderer_percent, False) + # self.cols['Percent'].add_attribute(renderer_percent, 'value', RowConstants.MY_PROGRESS) + # self.cols['Percent'].add_attribute(renderer_percent, 'total', RowConstants.TOTAL_EPS) + # self.cols['Percent'].add_attribute(renderer_percent, 'subvalue', RowConstants.AIRED_EPS) + # self.cols['Percent'].add_attribute(renderer_percent, 'eps', RowConstants.AVAILABLE_EPS) + # renderer_percent.set_fixed_size(100, -1) + # self.cols['Percent'].set_min_width(100) + # + # renderer_score = Gtk.CellRendererText() + # renderer_score.set_alignment(0.5, 0.5) + # self.cols['Score'].pack_end(renderer_score, False) + # self.cols['Score'].add_attribute(renderer_score, 'text', RowConstants.SCORE) + # + # renderer_start = Gtk.CellRendererText() + # renderer_start.set_alignment(0.5, 0.5) + # self.cols['Start'].pack_start(renderer_start, False) + # self.cols['Start'].add_attribute(renderer_start, 'text', RowConstants.START_DATE) + # + # renderer_end = Gtk.CellRendererText() + # renderer_end.set_alignment(0.5, 0.5) + # self.cols['End'].pack_start(renderer_end, False) + # self.cols['End'].add_attribute(renderer_end, 'text', RowConstants.END_DATE) + # + # renderer_my_start = Gtk.CellRendererText() + # renderer_my_start.set_alignment(0.5, 0.5) + # self.cols['My start'].pack_start(renderer_my_start, False) + # self.cols['My start'].add_attribute(renderer_my_start, 'text', RowConstants.MY_START_DATE) + # + # renderer_my_end = Gtk.CellRendererText() + # renderer_my_end.set_alignment(0.5, 0.5) + # self.cols['My end'].pack_start(renderer_my_end, False) + # self.cols['My end'].add_attribute(renderer_my_end, 'text', RowConstants.MY_FINISH_DATE) + # + # renderer_next_episode = Gtk.CellRendererText() + # renderer_next_episode.set_alignment(0.5, 0.5) + # self.cols['Next episode'].pack_start(renderer_next_episode, False) + # self.cols['Next episode'].add_attribute(renderer_next_episode, 'text', RowConstants.NEXT_EPISODE_AIR_TIME) def _header_button_press(self, button, event): if event.button == 3: @@ -320,6 +458,35 @@ def _header_button_press(self, button, event): return False + # Time based sort function for the "Next episode" column. Always sorts "-" and "?" below everything. + @staticmethod + def _next_episode_sort_func(model, iter1, iter2, user_data) -> int: + # Get the values from the "Next episode" column for the two rows + value1 = model.get_value(iter1, TreeConstants.NEXT_EPISODE_AIR_TIME_RELATIVE) + value2 = model.get_value(iter2, TreeConstants.NEXT_EPISODE_AIR_TIME_RELATIVE) + + sort_order = user_data.cols['Next episode'].get_sort_order() + special_cases = ('-', '?') + + if value1 in special_cases: + return 1 if sort_order == Gtk.SortType.ASCENDING else -1 + elif value2 in special_cases: + return -1 if sort_order == Gtk.SortType.ASCENDING else 1 + + # Parse the time intervals, convert everything to minutes and sort accordingly + days1, hours1, minutes1 = utils.parse_time_interval(value1) + days2, hours2, minutes2 = utils.parse_time_interval(value2) + + total_minutes1 = days1 * 24 * 60 + hours1 * 60 + minutes1 + total_minutes2 = days2 * 24 * 60 + hours2 * 60 + minutes2 + + if total_minutes1 < total_minutes2: + return -1 + elif total_minutes1 == total_minutes2: + return 0 + else: + return 1 + @property def filter(self): return self.props.model.props.model @@ -331,26 +498,25 @@ def show_tooltip(self, view, x, y, kbd, tip): return False _, col, _, _ = view.get_path_at_pos(tx, ty) - if col != self.cols['Percent']: + if col != self.cols['Progress']: return False def gv(key): return model.get_value(tree_iter, ShowListStore.column(key)) - lines = [] - lines.append("Watched: %d" % gv('stat')) + lines = ["Watched: %d" % gv(TreeConstants.MY_PROGRESS)] - aired = gv('subvalue') - status = gv('status') + aired = gv(TreeConstants.AIRED_EPS) + status = gv(TreeConstants.SHOW_STATUS) if aired and not status == utils.Status.NOTYET: lines.append("Aired%s: %d" % ( ' (estimated)' if status == utils.Status.AIRING else '', aired)) - avail_eps = gv('avail-eps') + avail_eps = gv(TreeConstants.AVAILABLE_EPS) if len(avail_eps) > 0: lines.append("Available: %d" % max(avail_eps)) - lines.append("Total: %s" % (gv('total-eps') or '?')) + lines.append("Total: %s" % (gv(TreeConstants.TOTAL_EPS) or '?')) tip.set_markup('\n'.join(lines)) renderer = next(iter(col.get_cells())) @@ -363,7 +529,7 @@ def _header_menu_item(self, w, column_name, visible): def select(self, show): """Select specified row or first if not found""" for row in self.get_model(): - if int(row[0]) == show['id']: + if int(row[TreeConstants.SHOW_ID]) == show['id']: selection = self.get_selection() selection.select_iter(row.iter) return @@ -419,7 +585,7 @@ def do_get_property(self, pspec): return getattr(self, pspec.name) def do_render(self, cr, widget, background_area, cell_area, flags): - (x, y, w, h) = self.do_get_size(widget, cell_area) + (x, y, w, h) = self._do_get_size(widget, cell_area) # set_source_rgb(0.9, 0.9, 0.9) cr.set_source_rgb(*self.__get_color(self.colors['progress_bg'])) @@ -438,7 +604,7 @@ def do_render(self, cr, widget, background_area, cell_area, flags): # set_source_rgb(0.7, 0.7, 0.7) cr.set_source_rgb( *self.__get_color(self.colors['progress_sub_bg'])) - cr.rectangle(x, y+h-self._subheight, mid, h-(h-self._subheight)) + cr.rectangle(x, y + h - self._subheight, mid, h - (h - self._subheight)) cr.fill() if self.value: @@ -463,11 +629,12 @@ def do_render(self, cr, widget, background_area, cell_area, flags): if 0 < episode <= self.total: start = int(w / float(self.total) * (episode - 1)) finish = int(w / float(self.total) * episode) - cr.rectangle(x+start, y+h-self._subheight, - finish-start, h-(h-self._subheight)) + cr.rectangle(x + start, y + h - self._subheight, + finish - start, h - (h - self._subheight)) cr.fill() - def do_get_size(self, widget, cell_area): + @classmethod + def _do_get_size(cls, widget, cell_area): if cell_area is None: return 0, 0, 0, 0 x = cell_area.x diff --git a/trackma/utils.py b/trackma/utils.py index c4ddd309..f4ed7069 100644 --- a/trackma/utils.py +++ b/trackma/utils.py @@ -510,9 +510,64 @@ def show(): 'image': '', 'image_thumb': '', 'queued': False, + 'next_ep_time': None } +# Function that calculates the relative time between 2 datetime objects. +# If full=False, it returns only the greatest nonzero time unit +def calculate_relative_time(time_end: datetime, utc: bool, fulltime: bool = True) -> str: + if time_end: + try: + if utc: + time_end = time_end.replace(tzinfo=datetime.UTC) + current_time = datetime.datetime.now(datetime.UTC) + else: + current_time = datetime.datetime.now() + time_difference = time_end - current_time + days = time_difference.days + hours, remainder = divmod(time_difference.seconds, 3600) + minutes, seconds = divmod(remainder, 60) + + time_units = [("days", days), ("hours", hours), ("minutes", minutes)] + + # Filter out units with a value of 0 + non_zero_units = [(unit, value) for unit, value in time_units if value != 0] + + if fulltime: + result = ", ".join([ + f"in {value + (1 if remainder > 0 else 0)} " + f"{unit if value + (1 if remainder > 0 else 0) != 1 else unit[:-1]}" + for unit, value in non_zero_units + ]) + else: + # Display only the greatest non-zero unit + result = next( + ( + f"in {value + (1 if remainder > 0 else 0)} " + f"{unit if value + (1 if remainder > 0 else 0) != 1 else unit[:-1]}" + for unit, value in non_zero_units + ), + "error" + ) + return result + except ValueError: + return '?' + else: + return '-' + + +def parse_time_interval(value): + # Parse the time interval string and return it as a tuple (days, hours, minutes) + match = re.match(r'in\s*(?:(\d+) day(?:s)?)?(?:,\s*)?(?:(\d+) hour(?:s)?)?(?:,\s*)?(?:(\d+) minute(?:s)?)?|[-?]', value) + if match: + days = int(match.group(1) or 0) + hours = int(match.group(2) or 0) + minutes = int(match.group(3) or 0) + return days, hours, minutes + return 0, 0, 0 # Return a default value if the format is not matched + + class TrackmaError(Exception): pass @@ -665,7 +720,7 @@ class APIFatal(TrackmaFatal): 'remember_geometry': False, 'last_width': 740, 'last_height': 480, - 'visible_columns': ['Title', 'Progress', 'Score', 'Percent'], + 'visible_columns': ['Title', 'Watched', 'Score', 'Progress'], 'episodebar_style': 1, 'colors': { 'is_airing': '#0099CC', From bbe7177c9fdc6df021175bb96cafabbec49cecde Mon Sep 17 00:00:00 2001 From: BigBoyBarney <barni.benke99@gmail.com> Date: Sat, 30 Dec 2023 14:46:44 +0100 Subject: [PATCH 02/16] Added method descriptions --- trackma/ui/gtk/showtreeview.py | 2 +- trackma/utils.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/trackma/ui/gtk/showtreeview.py b/trackma/ui/gtk/showtreeview.py index 258450fe..7d6a42b3 100644 --- a/trackma/ui/gtk/showtreeview.py +++ b/trackma/ui/gtk/showtreeview.py @@ -458,9 +458,9 @@ def _header_button_press(self, button, event): return False - # Time based sort function for the "Next episode" column. Always sorts "-" and "?" below everything. @staticmethod def _next_episode_sort_func(model, iter1, iter2, user_data) -> int: + """Time based sort function for the "Next episode" column. Always sorts "-" and "?" below everything.""" # Get the values from the "Next episode" column for the two rows value1 = model.get_value(iter1, TreeConstants.NEXT_EPISODE_AIR_TIME_RELATIVE) value2 = model.get_value(iter2, TreeConstants.NEXT_EPISODE_AIR_TIME_RELATIVE) diff --git a/trackma/utils.py b/trackma/utils.py index f4ed7069..1f340470 100644 --- a/trackma/utils.py +++ b/trackma/utils.py @@ -514,9 +514,9 @@ def show(): } -# Function that calculates the relative time between 2 datetime objects. -# If full=False, it returns only the greatest nonzero time unit def calculate_relative_time(time_end: datetime, utc: bool, fulltime: bool = True) -> str: + """Function that calculates the relative time between 2 datetime objects. + If full=False, it returns only the greatest nonzero time unit""" if time_end: try: if utc: @@ -558,7 +558,7 @@ def calculate_relative_time(time_end: datetime, utc: bool, fulltime: bool = True def parse_time_interval(value): - # Parse the time interval string and return it as a tuple (days, hours, minutes) + """Parse the time interval string and return it as a tuple (days, hours, minutes)""" match = re.match(r'in\s*(?:(\d+) day(?:s)?)?(?:,\s*)?(?:(\d+) hour(?:s)?)?(?:,\s*)?(?:(\d+) minute(?:s)?)?|[-?]', value) if match: days = int(match.group(1) or 0) From ec5ee2e63ba425da2258f571fafdf907183a3ee7 Mon Sep 17 00:00:00 2001 From: BigBoyBarney <barni.benke99@gmail.com> Date: Sat, 30 Dec 2023 14:54:39 +0100 Subject: [PATCH 03/16] Fixed datetime.utcnow deprecation in Qt - Replaced deprecated datetime.utcnow() with timezone aware datetime.now(datetime.UTC). --- trackma/ui/qt/models.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/trackma/ui/qt/models.py b/trackma/ui/qt/models.py index f5610451..dce0bef7 100644 --- a/trackma/ui/qt/models.py +++ b/trackma/ui/qt/models.py @@ -83,13 +83,12 @@ def _calculate_color(self, row, show): del self.colors[row] def _calculate_next_ep(self, row, show): - if self.mediainfo.get('date_next_ep'): - if 'next_ep_time' in show: - delta = show['next_ep_time'] - datetime.datetime.utcnow() - self.next_ep[row] = "%i days, %02d hrs." % ( - delta.days, delta.seconds/3600) - elif row in self.next_ep: - del self.next_ep[row] + if self.mediainfo.get('date_next_ep') and show['next_ep_time'] is not None: + delta = show['next_ep_time'].replace(tzinfo=datetime.UTC) - datetime.datetime.now(datetime.UTC) + self.next_ep[row] = "%i days, %02d hrs." % ( + delta.days, delta.seconds / 3600) + elif row in self.next_ep: + del self.next_ep[row] def _calculate_eps(self, row, show): aired_eps = utils.estimate_aired_episodes(show) From 5c9a3ef95853044c514add43b8fcafb55298f781 Mon Sep 17 00:00:00 2001 From: BigBoyBarney <barni.benke99@gmail.com> Date: Sat, 30 Dec 2023 15:35:18 +0100 Subject: [PATCH 04/16] (GTK) Updated default columns - Added 'New episode' as default column - Reset visible columns config to accommodate the new column names --- trackma/ui/gtk/mainview.py | 14 ++++++++++++++ trackma/ui/gtk/showtreeview.py | 3 ++- trackma/utils.py | 3 ++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/trackma/ui/gtk/mainview.py b/trackma/ui/gtk/mainview.py index 6ebaa6ac..aecace27 100644 --- a/trackma/ui/gtk/mainview.py +++ b/trackma/ui/gtk/mainview.py @@ -97,6 +97,7 @@ def load_account_mediatype(self, account, mediatype, extern_widget): self._engine_reload(account, mediatype, extern_widget) def _init_widgets(self): + self._visible_column_reset() self.image_box = ImageBox(100, 150) self.image_box.show() self.image_container_box.pack_start(self.image_box, False, False, 0) @@ -559,6 +560,19 @@ def _on_column_toggled(self, page, column_name, visible): utils.save_config(self._config, self._configfile) + def _visible_column_reset(self): + """Should be called when column naming scheme changes to make sure that the default columns are + visible by default, regardless of config + + In 1.1: + Default column 'Progress' was renamed to 'Watched', + Default column 'Percent' was renamed to 'Progress'. + 'New episode' added to default columns""" + column_version = '1.1' # Column naming version number + if self._config['column_version'] != column_version: + self._config['visible_columns'] = utils.gtk_defaults['visible_columns'] + self._config['column_version'] = column_version + class NotebookPage(Gtk.ScrolledWindow): __gtype_name__ = 'NotebookPage' diff --git a/trackma/ui/gtk/showtreeview.py b/trackma/ui/gtk/showtreeview.py index 7d6a42b3..68c412b8 100644 --- a/trackma/ui/gtk/showtreeview.py +++ b/trackma/ui/gtk/showtreeview.py @@ -265,7 +265,8 @@ def __init__(self, colors, visible_columns, status, _list, progress_style=1): self.connect('query-tooltip', self.show_tooltip) self.cols = dict() - # Defines the default column order as well + # Defines the default column order as well. If the default visible columns are renamed or otherwise changed, + # _default_column_reset() in mainview.py should be changed to accommodate the new names. self.available_columns = ( ('Title', TreeConstants.TITLE), ('Watched', TreeConstants.MY_PROGRESS), diff --git a/trackma/utils.py b/trackma/utils.py index 1f340470..4d4a1e3b 100644 --- a/trackma/utils.py +++ b/trackma/utils.py @@ -720,7 +720,7 @@ class APIFatal(TrackmaFatal): 'remember_geometry': False, 'last_width': 740, 'last_height': 480, - 'visible_columns': ['Title', 'Watched', 'Score', 'Progress'], + 'visible_columns': ['Title', 'Watched', 'Score', 'Progress', 'Next episode'], 'episodebar_style': 1, 'colors': { 'is_airing': '#0099CC', @@ -734,6 +734,7 @@ class APIFatal(TrackmaFatal): 'progress_sub_fg': '#668099', 'progress_complete': '#99CCB3', }, + 'column_version': ['1.1'] } qt_defaults = { From 9e7f1d2ae337594ed1d0e1c7f836c085859fa8f3 Mon Sep 17 00:00:00 2001 From: BigBoyBarney <barni.benke99@gmail.com> Date: Sat, 30 Dec 2023 15:44:36 +0100 Subject: [PATCH 05/16] Commented code - Remove redundant comments / commented code blocks - Add comments to new code - Refactored relative time regex to multiple shorter lines --- trackma/ui/gtk/mainview.py | 8 --- trackma/ui/gtk/showtreeview.py | 94 ++-------------------------------- trackma/utils.py | 16 +++++- 3 files changed, 18 insertions(+), 100 deletions(-) diff --git a/trackma/ui/gtk/mainview.py b/trackma/ui/gtk/mainview.py index aecace27..59b6a2a1 100644 --- a/trackma/ui/gtk/mainview.py +++ b/trackma/ui/gtk/mainview.py @@ -611,14 +611,6 @@ def _init_widgets(self, page_num, status, config): self._list, config['episodebar_style'], ) - # self._show_tree_view.set_model( - # Gtk.TreeModelSort( - # model=ShowListFilter( - # status=self.status, - # child_model=self._list - # ) - # ) - # ) self._title.set_text('%s (%d)' % ( self._title_text, len(self._show_tree_view.props.model) diff --git a/trackma/ui/gtk/showtreeview.py b/trackma/ui/gtk/showtreeview.py index 68c412b8..255c2f7e 100644 --- a/trackma/ui/gtk/showtreeview.py +++ b/trackma/ui/gtk/showtreeview.py @@ -184,8 +184,6 @@ def update(self, show, row=None): row[TreeConstants.MY_STATUS] = show['my_status'] return - # print("Warning: Show ID not found in ShowView (%d)" % show['id']) - def update_title(self, show, altname=None): for row in self: if int(row[TreeConstants.SHOW_ID]) == show['id']: @@ -242,6 +240,7 @@ class ShowTreeView(Gtk.TreeView): GObject.TYPE_PYOBJECT, (GObject.TYPE_STRING, GObject.TYPE_BOOLEAN))} def __init__(self, colors, visible_columns, status, _list, progress_style=1): + # Sets up the tree Gtk.TreeView.__init__(self) self.colors = colors @@ -279,6 +278,7 @@ def __init__(self, colors, visible_columns, status, _list, progress_style=1): ('Progress', TreeConstants.PROGRESS), ) + # Creates pre-defined columns for (name, key) in self.available_columns: self.cols[name] = Gtk.TreeViewColumn() self.cols[name].set_sort_column_id(key) @@ -351,96 +351,9 @@ def __init__(self, colors, visible_columns, status, _list, progress_style=1): w = w.get_parent() w.connect('button-press-event', self._header_button_press) + # Appends populated columns self.append_column(self.cols[name]) - # for (name, key) in self.available_columns: - # self.cols[name] = Gtk.TreeViewColumn(name) - # self.cols[name].set_sort_column_id(key) - # self.cols[name].set_resizable(False) - # self.cols[name].set_reorderable(True) - # - # if name == 'Title': - # self.cols[name].set_alignment(0.0) - # self.cols[name].set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) - # else: - # self.cols[name].set_alignment(0.5) - # self.cols[name].set_sizing(Gtk.TreeViewColumnSizing.FIXED) - # - # # This is a hack to allow for right-clickable header - # label = Gtk.Label(name) - # label.show() - # self.cols[name].set_widget(label) - # - # w = self.cols[name].get_widget() - # while not isinstance(w, Gtk.Button): - # w = w.get_parent() - # - # w.connect('button-press-event', self._header_button_press) - # - # if name not in self.visible_columns: - # self.cols[name].set_visible(False) - # - # self.append_column(self.cols[name]) - - # renderer_title = Gtk.CellRendererText() - # self.cols['Title'].pack_start(renderer_title, True) - # self.cols['Title'].set_resizable(True) - # self.cols['Title'].set_expand(True) - # self.cols['Title'].add_attribute(renderer_title, 'text', RowConstants.TITLE) - # # Using foreground-gdk does not work, possibly due to the timing of it being set - # self.cols['Title'].add_attribute(renderer_title, 'foreground', RowConstants.COLOR) - # renderer_title.set_property('ellipsize', Pango.EllipsizeMode.END) - # - # renderer_progress = Gtk.CellRendererText() - # renderer_progress.set_alignment(0.5, 0.5) - # self.cols['Progress'].set_reorderable(True) - # self.cols['Progress'].pack_start(renderer_progress, False) - # self.cols['Progress'].add_attribute(renderer_progress, 'text', RowConstants.EPISODES) - # - # if self.progress_style == 0: - # renderer_percent = Gtk.CellRendererProgress() - # self.cols['Percent'].pack_start(renderer_percent, False) - # self.cols['Percent'].add_attribute(renderer_percent, 'value', RowConstants.PROGRESS) - # else: - # renderer_percent = ProgressCellRenderer(self.colors) - # self.cols['Percent'].pack_start(renderer_percent, False) - # self.cols['Percent'].add_attribute(renderer_percent, 'value', RowConstants.MY_PROGRESS) - # self.cols['Percent'].add_attribute(renderer_percent, 'total', RowConstants.TOTAL_EPS) - # self.cols['Percent'].add_attribute(renderer_percent, 'subvalue', RowConstants.AIRED_EPS) - # self.cols['Percent'].add_attribute(renderer_percent, 'eps', RowConstants.AVAILABLE_EPS) - # renderer_percent.set_fixed_size(100, -1) - # self.cols['Percent'].set_min_width(100) - # - # renderer_score = Gtk.CellRendererText() - # renderer_score.set_alignment(0.5, 0.5) - # self.cols['Score'].pack_end(renderer_score, False) - # self.cols['Score'].add_attribute(renderer_score, 'text', RowConstants.SCORE) - # - # renderer_start = Gtk.CellRendererText() - # renderer_start.set_alignment(0.5, 0.5) - # self.cols['Start'].pack_start(renderer_start, False) - # self.cols['Start'].add_attribute(renderer_start, 'text', RowConstants.START_DATE) - # - # renderer_end = Gtk.CellRendererText() - # renderer_end.set_alignment(0.5, 0.5) - # self.cols['End'].pack_start(renderer_end, False) - # self.cols['End'].add_attribute(renderer_end, 'text', RowConstants.END_DATE) - # - # renderer_my_start = Gtk.CellRendererText() - # renderer_my_start.set_alignment(0.5, 0.5) - # self.cols['My start'].pack_start(renderer_my_start, False) - # self.cols['My start'].add_attribute(renderer_my_start, 'text', RowConstants.MY_START_DATE) - # - # renderer_my_end = Gtk.CellRendererText() - # renderer_my_end.set_alignment(0.5, 0.5) - # self.cols['My end'].pack_start(renderer_my_end, False) - # self.cols['My end'].add_attribute(renderer_my_end, 'text', RowConstants.MY_FINISH_DATE) - # - # renderer_next_episode = Gtk.CellRendererText() - # renderer_next_episode.set_alignment(0.5, 0.5) - # self.cols['Next episode'].pack_start(renderer_next_episode, False) - # self.cols['Next episode'].add_attribute(renderer_next_episode, 'text', RowConstants.NEXT_EPISODE_AIR_TIME) - def _header_button_press(self, button, event): if event.button == 3: menu = Gtk.Menu() @@ -462,6 +375,7 @@ def _header_button_press(self, button, event): @staticmethod def _next_episode_sort_func(model, iter1, iter2, user_data) -> int: """Time based sort function for the "Next episode" column. Always sorts "-" and "?" below everything.""" + # Get the values from the "Next episode" column for the two rows value1 = model.get_value(iter1, TreeConstants.NEXT_EPISODE_AIR_TIME_RELATIVE) value2 = model.get_value(iter2, TreeConstants.NEXT_EPISODE_AIR_TIME_RELATIVE) diff --git a/trackma/utils.py b/trackma/utils.py index 4d4a1e3b..b1a9c3f3 100644 --- a/trackma/utils.py +++ b/trackma/utils.py @@ -517,10 +517,11 @@ def show(): def calculate_relative_time(time_end: datetime, utc: bool, fulltime: bool = True) -> str: """Function that calculates the relative time between 2 datetime objects. If full=False, it returns only the greatest nonzero time unit""" + if time_end: try: if utc: - time_end = time_end.replace(tzinfo=datetime.UTC) + time_end = time_end.replace(tzinfo=datetime.UTC) # Make sure that time_end is timezone aware in UTC current_time = datetime.datetime.now(datetime.UTC) else: current_time = datetime.datetime.now() @@ -559,7 +560,18 @@ def calculate_relative_time(time_end: datetime, utc: bool, fulltime: bool = True def parse_time_interval(value): """Parse the time interval string and return it as a tuple (days, hours, minutes)""" - match = re.match(r'in\s*(?:(\d+) day(?:s)?)?(?:,\s*)?(?:(\d+) hour(?:s)?)?(?:,\s*)?(?:(\d+) minute(?:s)?)?|[-?]', value) + + pattern = ( + r'in\s*' + r'(?:(\d+) day(?:s)?)?' + r'(?:,\s*)?' + r'(?:(\d+) hour(?:s)?)?' + r'(?:,\s*)?' + r'(?:(\d+) minute(?:s)?)?' + r'|[-?]' + ) + + match = re.match(pattern, value) if match: days = int(match.group(1) or 0) hours = int(match.group(2) or 0) From 5f90bcb2a06f1aac8938270bf26b806bf7c1ab36 Mon Sep 17 00:00:00 2001 From: BigBoyBarney <barni.benke99@gmail.com> Date: Sat, 30 Dec 2023 20:59:50 +0100 Subject: [PATCH 06/16] Improved code clarity - Added comments to tree references - Renamed some local tree variables to be more intuitive - Changed percentage progress variable type to int --- trackma/ui/gtk/showtreeview.py | 64 ++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/trackma/ui/gtk/showtreeview.py b/trackma/ui/gtk/showtreeview.py index 255c2f7e..aa848e6c 100644 --- a/trackma/ui/gtk/showtreeview.py +++ b/trackma/ui/gtk/showtreeview.py @@ -22,24 +22,25 @@ # Declare named constants for the tree references, so it's more unified and easier to read # Putting them in their own class also enables whatever we might want to do with it in the future. class TreeConstants: - SHOW_ID = 0 - TITLE = 1 - MY_PROGRESS = 2 - MY_SCORE = 3 - EPISODES = 4 - SCORE = 5 - TOTAL_EPS = 6 - AIRED_EPS = 7 - AVAILABLE_EPS = 8 - COLOR = 9 - PROGRESS = 10 - START_DATE = 11 - END_DATE = 12 - MY_START_DATE = 13 - MY_FINISH_DATE = 14 - MY_STATUS = 15 - SHOW_STATUS = 16 - NEXT_EPISODE_AIR_TIME_RELATIVE = 17 + SHOW_ID = 0 # Show ID + TITLE = 1 # Show title + MY_PROGRESS = 2 # Number of watched episodes + MY_SCORE = 3 # User given score + WATCHED_EPISODES_FRACTION = 4 # Watched episodes / total episodes. E.g: 7 / 13 + MY_SCORE_STRING = 5 # User given score (string), with 'self.decimals' decimals + TOTAL_EPS = 6 # Total number of episodes + AIRED_EPS = 7 # (Estimated) number of episodes aired. + # If no number is provided by the lib, 1 episode / week is assumed and calculated accordingly + AVAILABLE_EPS = 8 # Number of available episodes in the local library + COLOR = 9 # Used with the _get_color method to return the color preset for a show + PROGRESS_PERCENTAGE = 10 # % of episodes watched. 7 / 13 -> + START_DATE = 11 # Start date of the show + END_DATE = 12 # End date of the show + MY_START_DATE = 13 # Date when the user started watching the show + MY_FINISH_DATE = 14 # Date when the user finished the show + MY_STATUS = 15 # User's show status (watching, paused, completed etc.) + SHOW_STATUS = 16 # Show's status (airing, upcoming etc.) + NEXT_EPISODE_AIR_TIME_RELATIVE = 17 # Relative time until the next episode as 'X days / hours / minutes' class ShowListStore(Gtk.ListStore): @@ -50,13 +51,13 @@ class ShowListStore(Gtk.ListStore): (TreeConstants.TITLE, str), (TreeConstants.MY_PROGRESS, int), (TreeConstants.MY_SCORE, float), - (TreeConstants.EPISODES, str), - (TreeConstants.SCORE, str), + (TreeConstants.WATCHED_EPISODES_FRACTION, str), + (TreeConstants.MY_SCORE_STRING, str), (TreeConstants.TOTAL_EPS, int), (TreeConstants.AIRED_EPS, int), (TreeConstants.AVAILABLE_EPS, GObject.TYPE_PYOBJECT), (TreeConstants.COLOR, str), - (TreeConstants.PROGRESS, int), + (TreeConstants.PROGRESS_PERCENTAGE, int), (TreeConstants.START_DATE, str), (TreeConstants.END_DATE, str), (TreeConstants.MY_START_DATE, str), @@ -108,10 +109,11 @@ def _get_color(self, show, eps): return None def append(self, show, altname=None, eps=None): - episodes_str = "{} / {}".format(show['my_progress'], - show['total'] or '?') + watched_episodes_fraction = "{} / {}".format(show['my_progress'], + show['total'] or '?') if show['total'] and show['my_progress'] <= show['total']: - progress = (float(show['my_progress']) / show['total']) * 100 + progress_float = (show['my_progress'] / show['total']) * 100 + progress = int(progress_float) else: progress = 0 @@ -141,7 +143,7 @@ def append(self, show, altname=None, eps=None): title_str, show['my_progress'], show['my_score'], - episodes_str, + watched_episodes_fraction, score_str, show['total'], aired_eps, @@ -174,12 +176,12 @@ def update(self, show, row=None): episodes_str = "{} / {}".format(show['my_progress'], show['total'] or '?') row[TreeConstants.MY_PROGRESS] = show['my_progress'] - row[TreeConstants.EPISODES] = episodes_str + row[TreeConstants.WATCHED_EPISODES_FRACTION] = episodes_str score_str = "%0.*f" % (self.decimals, show['my_score']) row[TreeConstants.MY_SCORE] = show['my_score'] - row[TreeConstants.SCORE] = score_str + row[TreeConstants.MY_SCORE_STRING] = score_str row[TreeConstants.COLOR] = self._get_color(show, row[TreeConstants.AVAILABLE_EPS]) row[TreeConstants.MY_STATUS] = show['my_status'] return @@ -275,7 +277,7 @@ def __init__(self, colors, visible_columns, status, _list, progress_style=1): ('End', TreeConstants.END_DATE), ('My start', TreeConstants.MY_START_DATE), ('My end', TreeConstants.MY_FINISH_DATE), - ('Progress', TreeConstants.PROGRESS), + ('Progress', TreeConstants.PROGRESS_PERCENTAGE), ) # Creates pre-defined columns @@ -288,7 +290,7 @@ def __init__(self, colors, visible_columns, status, _list, progress_style=1): if self.progress_style == 0: renderer = Gtk.CellRendererProgress() self.cols[name].pack_start(renderer, False) - self.cols[name].add_attribute(renderer, 'value', TreeConstants.PROGRESS) + self.cols[name].add_attribute(renderer, 'value', TreeConstants.PROGRESS_PERCENTAGE) else: renderer = ProgressCellRenderer(self.colors) self.cols[name].pack_start(renderer, False) @@ -314,13 +316,13 @@ def __init__(self, colors, visible_columns, status, _list, progress_style=1): renderer.set_property('ellipsize', Pango.EllipsizeMode.END) case 'Watched': - self.cols[name].add_attribute(renderer, 'text', TreeConstants.EPISODES) + self.cols[name].add_attribute(renderer, 'text', TreeConstants.WATCHED_EPISODES_FRACTION) case 'Progress': self.cols[name].set_min_width(200) case 'Score': - self.cols[name].add_attribute(renderer, 'text', TreeConstants.SCORE) + self.cols[name].add_attribute(renderer, 'text', TreeConstants.MY_SCORE_STRING) case 'Start': self.cols[name].add_attribute(renderer, 'text', TreeConstants.START_DATE) From 8bb4aa3425083064246670a8b8a68236a8bf6246 Mon Sep 17 00:00:00 2001 From: BigBoyBarney <barni.benke99@gmail.com> Date: Sun, 31 Dec 2023 12:58:04 +0100 Subject: [PATCH 07/16] Rewrote (GTK) list sorting setup - columns are only sorted when the header is clicked - Disabled sorting indicators for inactive columns - Centred columns and contents (except Title) --- trackma/ui/gtk/showtreeview.py | 86 ++++++++++++++++++++++++++-------- 1 file changed, 67 insertions(+), 19 deletions(-) diff --git a/trackma/ui/gtk/showtreeview.py b/trackma/ui/gtk/showtreeview.py index aa848e6c..6a06c1d3 100644 --- a/trackma/ui/gtk/showtreeview.py +++ b/trackma/ui/gtk/showtreeview.py @@ -22,25 +22,25 @@ # Declare named constants for the tree references, so it's more unified and easier to read # Putting them in their own class also enables whatever we might want to do with it in the future. class TreeConstants: - SHOW_ID = 0 # Show ID - TITLE = 1 # Show title - MY_PROGRESS = 2 # Number of watched episodes - MY_SCORE = 3 # User given score - WATCHED_EPISODES_FRACTION = 4 # Watched episodes / total episodes. E.g: 7 / 13 - MY_SCORE_STRING = 5 # User given score (string), with 'self.decimals' decimals - TOTAL_EPS = 6 # Total number of episodes - AIRED_EPS = 7 # (Estimated) number of episodes aired. + SHOW_ID = 0 # Show ID + TITLE = 1 # Show title + MY_PROGRESS = 2 # Number of watched episodes + MY_SCORE = 3 # User given score + WATCHED_EPISODES_FRACTION = 4 # Watched episodes / total episodes. E.g: 7 / 13 + MY_SCORE_STRING = 5 # User given score (string), with 'self.decimals' decimals + TOTAL_EPS = 6 # Total number of episodes + AIRED_EPS = 7 # (Estimated) number of episodes aired. # If no number is provided by the lib, 1 episode / week is assumed and calculated accordingly - AVAILABLE_EPS = 8 # Number of available episodes in the local library - COLOR = 9 # Used with the _get_color method to return the color preset for a show - PROGRESS_PERCENTAGE = 10 # % of episodes watched. 7 / 13 -> - START_DATE = 11 # Start date of the show - END_DATE = 12 # End date of the show - MY_START_DATE = 13 # Date when the user started watching the show - MY_FINISH_DATE = 14 # Date when the user finished the show - MY_STATUS = 15 # User's show status (watching, paused, completed etc.) - SHOW_STATUS = 16 # Show's status (airing, upcoming etc.) - NEXT_EPISODE_AIR_TIME_RELATIVE = 17 # Relative time until the next episode as 'X days / hours / minutes' + AVAILABLE_EPS = 8 # Number of available episodes in the local library + COLOR = 9 # Used with the _get_color method to return the color preset for a show + PROGRESS_PERCENTAGE = 10 # % of episodes watched. 7 / 13 -> + START_DATE = 11 # Start date of the show + END_DATE = 12 # End date of the show + MY_START_DATE = 13 # Date when the user started watching the show + MY_FINISH_DATE = 14 # Date when the user finished the show + MY_STATUS = 15 # User's show status (watching, paused, completed etc.) + SHOW_STATUS = 16 # Show's status (airing, upcoming etc.) + NEXT_EPISODE_AIR_TIME_RELATIVE = 17 # Relative time until the next episode as 'X days / hours / minutes' class ShowListStore(Gtk.ListStore): @@ -265,6 +265,7 @@ def __init__(self, colors, visible_columns, status, _list, progress_style=1): self.set_property('has-tooltip', True) self.connect('query-tooltip', self.show_tooltip) + self.previous_sort_column = None self.cols = dict() # Defines the default column order as well. If the default visible columns are renamed or otherwise changed, # _default_column_reset() in mainview.py should be changed to accommodate the new names. @@ -283,7 +284,10 @@ def __init__(self, colors, visible_columns, status, _list, progress_style=1): # Creates pre-defined columns for (name, key) in self.available_columns: self.cols[name] = Gtk.TreeViewColumn() - self.cols[name].set_sort_column_id(key) + self.cols[name].set_clickable(True) + self.cols[name].connect("clicked", lambda _, column_key=key, column=self.cols[name]: + self._on_column_clicked(column_key, column)) + self.cols[name].set_alignment(0.5) # Set up the percent / progress bar if name == 'Progress': @@ -301,6 +305,7 @@ def __init__(self, colors, visible_columns, status, _list, progress_style=1): else: renderer = Gtk.CellRendererText() self.cols[name].pack_start(renderer, False) + renderer.set_alignment(0.5, 0.5) if name not in self.visible_columns: self.cols[name].set_visible(False) @@ -314,6 +319,9 @@ def __init__(self, colors, visible_columns, status, _list, progress_style=1): self.cols[name].add_attribute(renderer, 'text', TreeConstants.TITLE) self.cols[name].add_attribute(renderer, 'foreground', TreeConstants.COLOR) renderer.set_property('ellipsize', Pango.EllipsizeMode.END) + self.cols[name].set_alignment(0) + renderer.set_alignment(0, 0.5) + self.cols[name].set_sort_indicator(True) case 'Watched': self.cols[name].add_attribute(renderer, 'text', TreeConstants.WATCHED_EPISODES_FRACTION) @@ -404,6 +412,46 @@ def _next_episode_sort_func(model, iter1, iter2, user_data) -> int: else: return 1 + def _on_column_clicked(self, key, column): + """Sets up sorting for the clicked column, based on what the previous sorting was. + We can't simply use self.cols[name].set_sort_order_column_id(key) because that inevitably allocates screen + space for the sort indicator. This way, the sort indicator is only visible (and takes up space) for the + currently sorted column + + We're saving the sorted column because it's not possible to get the column purely from the ID without + iterating through the columns, so this is more efficient""" + + sort_order_column = column.get_sort_order() # Sort order passed by the column (defaults to Ascending) + sort_column_id_model, sort_order_model = self.get_model().get_sort_column_id() + sort_column_id_model = sort_column_id_model if sort_column_id_model is not None else 1 + # Sort order and ID passed by the model. Order defaults to None, ID is the previously sorted column ID, + # and NOT the clicked column. Set sort_column_id_model to '1', as that's the default Sort column. + + if sort_column_id_model == key: # Check if we're trying to sort the same column that's already being sorted + match sort_order_column: + case Gtk.SortType.ASCENDING: # It was ascending -> set to descending + column.set_sort_order(Gtk.SortType.DESCENDING) + column.set_sort_indicator(True) + self.get_model().set_sort_column_id(key, Gtk.SortType.DESCENDING) + + case Gtk.SortType.DESCENDING: # It was descending -> set to ascending + column.set_sort_order(Gtk.SortType.ASCENDING) + column.set_sort_indicator(True) + self.get_model().set_sort_column_id(key, Gtk.SortType.ASCENDING) + + case _: # This isn't really necessary, but for completeness' sake + pass + else: # We're trying to sort a column that's different from the previously sorted one -> default to Ascending + # TODO: Change default sort order based on what makes sense for the column + column.set_sort_order(Gtk.SortType.ASCENDING) + column.set_sort_indicator(True) + self.get_model().set_sort_column_id(key, Gtk.SortType.ASCENDING) + + # If this isn't the first time sorting, and we're sorting a different column, remove the sort indicator + if self.previous_sort_column is not None and self.previous_sort_column != column: + self.previous_sort_column.set_sort_indicator(False) + self.previous_sort_column = column # Save the sorted column + @property def filter(self): return self.props.model.props.model From 0a4501e5b6ca93b4be7c61f8ef44e9d69e7c1132 Mon Sep 17 00:00:00 2001 From: BigBoyBarney <barni.benke99@gmail.com> Date: Sun, 31 Dec 2023 16:18:56 +0100 Subject: [PATCH 08/16] Sorting update - Slightly rewrote code for the sorting setup again - Fixed multiple sort indicator issue - Sort indicators now have uniform default direction --- trackma/ui/gtk/showtreeview.py | 88 ++++++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 25 deletions(-) diff --git a/trackma/ui/gtk/showtreeview.py b/trackma/ui/gtk/showtreeview.py index 6a06c1d3..9679f595 100644 --- a/trackma/ui/gtk/showtreeview.py +++ b/trackma/ui/gtk/showtreeview.py @@ -265,8 +265,9 @@ def __init__(self, colors, visible_columns, status, _list, progress_style=1): self.set_property('has-tooltip', True) self.connect('query-tooltip', self.show_tooltip) - self.previous_sort_column = None + self.previous_sort_column = 'Title' # Sets the default "0th" previous column for sorting self.cols = dict() + # Defines the default column order as well. If the default visible columns are renamed or otherwise changed, # _default_column_reset() in mainview.py should be changed to accommodate the new names. self.available_columns = ( @@ -288,6 +289,7 @@ def __init__(self, colors, visible_columns, status, _list, progress_style=1): self.cols[name].connect("clicked", lambda _, column_key=key, column=self.cols[name]: self._on_column_clicked(column_key, column)) self.cols[name].set_alignment(0.5) + self.cols[name].set_title(name) # Set up the percent / progress bar if name == 'Progress': @@ -390,9 +392,9 @@ def _next_episode_sort_func(model, iter1, iter2, user_data) -> int: value1 = model.get_value(iter1, TreeConstants.NEXT_EPISODE_AIR_TIME_RELATIVE) value2 = model.get_value(iter2, TreeConstants.NEXT_EPISODE_AIR_TIME_RELATIVE) - sort_order = user_data.cols['Next episode'].get_sort_order() - special_cases = ('-', '?') + sort_id, sort_order = user_data.get_model().get_sort_column_id() # Get the current sort order of the model + special_cases = ('-', '?') if value1 in special_cases: return 1 if sort_order == Gtk.SortType.ASCENDING else -1 elif value2 in special_cases: @@ -413,44 +415,80 @@ def _next_episode_sort_func(model, iter1, iter2, user_data) -> int: return 1 def _on_column_clicked(self, key, column): + # Todo: "Title sort" should sort shows with available episodes first -> custom sort function needed """Sets up sorting for the clicked column, based on what the previous sorting was. We can't simply use self.cols[name].set_sort_order_column_id(key) because that inevitably allocates screen - space for the sort indicator. This way, the sort indicator is only visible (and takes up space) for the - currently sorted column + space for the sort indicator arrow. This way, the sort indicator is only visible (and takes up space) for the + currently sorted column. + + In order to make the list look uniform while keeping the default sort direction logical, + e.g: highest score first, we need to dynamically change the sort indicator directions. We're saving the sorted column because it's not possible to get the column purely from the ID without iterating through the columns, so this is more efficient""" - sort_order_column = column.get_sort_order() # Sort order passed by the column (defaults to Ascending) sort_column_id_model, sort_order_model = self.get_model().get_sort_column_id() - sort_column_id_model = sort_column_id_model if sort_column_id_model is not None else 1 - # Sort order and ID passed by the model. Order defaults to None, ID is the previously sorted column ID, - # and NOT the clicked column. Set sort_column_id_model to '1', as that's the default Sort column. + sort_column_id_model = sort_column_id_model if sort_column_id_model is not None else TreeConstants.TITLE + sort_order_model = sort_order_model if sort_order_model is not None else Gtk.SortType.ASCENDING + # Sort order and ID passed by the model. Order and ID default to none and correspond to the previously sorted + # column ID, NOT the clicked column. Set sort_column_id_model to 'TreeConstants.TITLE', + # as that's the default sort column. + + column_title = column.get_title() # Gets the clicked column title + sort_order_column = column.get_sort_order() # Sort order passed by the column (indicator direction) if sort_column_id_model == key: # Check if we're trying to sort the same column that's already being sorted - match sort_order_column: - case Gtk.SortType.ASCENDING: # It was ascending -> set to descending - column.set_sort_order(Gtk.SortType.DESCENDING) - column.set_sort_indicator(True) + self._reverse_sort_order(column, sort_order_model, sort_order_column, key) # Reverse sorting + + else: # We're trying to sort a new column + match column.get_title(): # Check which column and set the sort indicator accordingly + case 'Score': # Default should be large -> small, e.g: 10, 8, 7, 3 + column.set_sort_order(Gtk.SortType.ASCENDING) self.get_model().set_sort_column_id(key, Gtk.SortType.DESCENDING) - case Gtk.SortType.DESCENDING: # It was descending -> set to ascending + case 'Watched': # Default should be large -> small, e.g: 18 / 23, 12 / 13, 7 / 13 + column.set_sort_order(Gtk.SortType.ASCENDING) + self.get_model().set_sort_column_id(key, Gtk.SortType.DESCENDING) + + case 'Progress': # Default should be large -> small + column.set_sort_order(Gtk.SortType.ASCENDING) + self.get_model().set_sort_column_id(key, Gtk.SortType.DESCENDING) + + case _: # Everything else should be normal column.set_sort_order(Gtk.SortType.ASCENDING) - column.set_sort_indicator(True) self.get_model().set_sort_column_id(key, Gtk.SortType.ASCENDING) - case _: # This isn't really necessary, but for completeness' sake - pass - else: # We're trying to sort a column that's different from the previously sorted one -> default to Ascending - # TODO: Change default sort order based on what makes sense for the column - column.set_sort_order(Gtk.SortType.ASCENDING) - column.set_sort_indicator(True) + self.cols[self.previous_sort_column].set_sort_indicator(False) # Disable the previous sort indicator + column.set_sort_indicator(True) # Enable the new sort indicator + self.previous_sort_column = column.get_title() # Save the new sorting column + + def _reverse_sort_order(self, column, sort_order_model, sort_order_column, key): + """Reverses both the actual sort order and the visual sorting indicator arrow direction""" + + if sort_order_model == Gtk.SortType.ASCENDING: + self._reverse_sort_indicator(column, sort_order_column) + self.get_model().set_sort_column_id(key, Gtk.SortType.DESCENDING) + + elif sort_order_model == Gtk.SortType.DESCENDING: + self._reverse_sort_indicator(column, sort_order_column) self.get_model().set_sort_column_id(key, Gtk.SortType.ASCENDING) - # If this isn't the first time sorting, and we're sorting a different column, remove the sort indicator - if self.previous_sort_column is not None and self.previous_sort_column != column: - self.previous_sort_column.set_sort_indicator(False) - self.previous_sort_column = column # Save the sorted column + else: + raise ValueError("Invalid sort order. Must be Gtk.SortType.ASCENDING or Gtk.SortType.DESCENDING") + + @staticmethod + def _reverse_sort_indicator(column, sort_order_column): + """Reverses the visual sorting indicator direction""" + if sort_order_column == Gtk.SortType.ASCENDING: + column.set_sort_order(Gtk.SortType.DESCENDING) + + elif sort_order_column == Gtk.SortType.DESCENDING: + column.set_sort_order(Gtk.SortType.ASCENDING) + + else: + raise ValueError("Invalid sort order. Must be Gtk.SortType.ASCENDING or Gtk.SortType.DESCENDING") + + @property def filter(self): From e0a264125cd685d23326d928edf74bf74619b8e2 Mon Sep 17 00:00:00 2001 From: BigBoyBarney <barni.benke99@gmail.com> Date: Sun, 31 Dec 2023 18:52:33 +0100 Subject: [PATCH 09/16] Python 3.10 - Bump required python version to 3.10, as 3.8 is outdated and 3.10 brings a lot of QoL changes. The "oldest" main distro is Ubuntu 22.04 and even that ships with Python 3.10 by default. Pretty much every other distro should be 3.10 or newer as well. --- pyproject.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1b22b5b0..f404e929 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,8 +17,6 @@ classifiers = [ "Topic :: Internet", "Topic :: Multimedia", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Operating System :: POSIX", @@ -27,7 +25,7 @@ classifiers = [ [tool.poetry.dependencies] -python = "^3.8" +python = "^3.10" requests = "^2.28.1" inotify = { version = "^0.2.10", optional = true } pillow = { version = "^9.2.0", optional = true } From f99d68d0b336ab8c4f309a4b155a29484ad126d5 Mon Sep 17 00:00:00 2001 From: BigBoyBarney <barni.benke99@gmail.com> Date: Sun, 31 Dec 2023 23:51:58 +0100 Subject: [PATCH 10/16] Increased (Qt and GTK) default window size - Increased window size from 740x480 to 1080x720. The default was last changed and set in 2015, which made sense back then, but it's way too small for most modern displays. --- trackma/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/trackma/utils.py b/trackma/utils.py index b1a9c3f3..cc41dd43 100644 --- a/trackma/utils.py +++ b/trackma/utils.py @@ -730,8 +730,8 @@ class APIFatal(TrackmaFatal): 'start_in_tray': False, 'tray_api_icon': False, 'remember_geometry': False, - 'last_width': 740, - 'last_height': 480, + 'last_width': 1080, + 'last_height': 720, 'visible_columns': ['Title', 'Watched', 'Score', 'Progress', 'Next episode'], 'episodebar_style': 1, 'colors': { @@ -759,8 +759,8 @@ class APIFatal(TrackmaFatal): 'remember_columns': False, 'last_x': 0, 'last_y': 0, - 'last_width': 740, - 'last_height': 480, + 'last_width': 1080, + 'last_height': 720, 'visible_columns': ['Title', 'Progress', 'Score', 'Percent'], 'inline_edit': True, 'columns_state': None, From 251956174c3112b0245dcf7238273e7ca4f47095 Mon Sep 17 00:00:00 2001 From: BigBoyBarney <barni.benke99@gmail.com> Date: Mon, 1 Jan 2024 11:18:35 +0100 Subject: [PATCH 11/16] Updated poetry.lock --- poetry.lock | 521 ++++++++++++++++++++++++++++------------------------ 1 file changed, 279 insertions(+), 242 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5a00378d..43ae4778 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,122 +1,124 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "certifi" -version = "2023.7.22" +version = "2023.11.17" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, ] [[package]] name = "charset-normalizer" -version = "3.0.1" +version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = "*" +python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"}, - {file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"}, - {file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"}, - {file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"}, - {file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"}, - {file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"}, - {file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"}, - {file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"}, + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] @@ -163,112 +165,105 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] [[package]] name = "pillow" -version = "9.4.0" +version = "9.5.0" description = "Python Imaging Library (Fork)" optional = true python-versions = ">=3.7" files = [ - {file = "Pillow-9.4.0-1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1"}, - {file = "Pillow-9.4.0-1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12"}, - {file = "Pillow-9.4.0-1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd"}, - {file = "Pillow-9.4.0-1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9"}, - {file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"}, - {file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"}, - {file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"}, - {file = "Pillow-9.4.0-2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0"}, - {file = "Pillow-9.4.0-2-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f"}, - {file = "Pillow-9.4.0-2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c"}, - {file = "Pillow-9.4.0-2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848"}, - {file = "Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1"}, - {file = "Pillow-9.4.0-2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33"}, - {file = "Pillow-9.4.0-2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9"}, - {file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"}, - {file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5"}, - {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070"}, - {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28"}, - {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35"}, - {file = "Pillow-9.4.0-cp310-cp310-win32.whl", hash = "sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a"}, - {file = "Pillow-9.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391"}, - {file = "Pillow-9.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133"}, - {file = "Pillow-9.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eaef5d2de3c7e9b21f1e762f289d17b726c2239a42b11e25446abf82b26ac132"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6abfb51a82e919e3933eb137e17c4ae9c0475a25508ea88993bb59faf82f3b35"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6663977496d616b618b6cfa43ec86e479ee62b942e1da76a2c3daa1c75933ef4"}, - {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60e7da3a3ad1812c128750fc1bc14a7ceeb8d29f77e0a2356a8fb2aa8925287d"}, - {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8"}, - {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a"}, - {file = "Pillow-9.4.0-cp311-cp311-win32.whl", hash = "sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c"}, - {file = "Pillow-9.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee"}, - {file = "Pillow-9.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5f532a2ad4d174eb73494e7397988e22bf427f91acc8e6ebf5bb10597b49c493"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dd5a9c3091a0f414a963d427f920368e2b6a4c2f7527fdd82cde8ef0bc7a327"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:653d7fb2df65efefbcbf81ef5fe5e5be931f1ee4332c2893ca638c9b11a409c4"}, - {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5"}, - {file = "Pillow-9.4.0-cp37-cp37m-win32.whl", hash = "sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e"}, - {file = "Pillow-9.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6"}, - {file = "Pillow-9.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9"}, - {file = "Pillow-9.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d"}, - {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b"}, - {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f"}, - {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628"}, - {file = "Pillow-9.4.0-cp38-cp38-win32.whl", hash = "sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d"}, - {file = "Pillow-9.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a"}, - {file = "Pillow-9.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569"}, - {file = "Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503"}, - {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6"}, - {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2"}, - {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153"}, - {file = "Pillow-9.4.0-cp39-cp39-win32.whl", hash = "sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c"}, - {file = "Pillow-9.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df"}, - {file = "Pillow-9.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a"}, - {file = "Pillow-9.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9"}, - {file = "Pillow-9.4.0.tar.gz", hash = "sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e"}, + {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, + {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, + {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, + {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, + {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, + {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, + {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, + {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, + {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, + {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, + {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, + {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, + {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, + {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, + {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, + {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, + {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] [[package]] name = "pycairo" -version = "1.23.0" +version = "1.25.1" description = "Python interface for cairo" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pycairo-1.23.0-cp310-cp310-win32.whl", hash = "sha256:564601e5f528531c6caec1c0177c3d0709081e1a2a5cccc13561f715080ae535"}, - {file = "pycairo-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:e7cde633986435d87a86b6118b7b6109c384266fd719ef959883e2729f6eafae"}, - {file = "pycairo-1.23.0-cp311-cp311-win32.whl", hash = "sha256:3a71f758e461180d241e62ef52e85499c843bd2660fd6d87cec99c9833792bfa"}, - {file = "pycairo-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:2dec5378133778961993fb59d66df16070e03f4d491b67eb695ca9ad7a696008"}, - {file = "pycairo-1.23.0-cp37-cp37m-win32.whl", hash = "sha256:d6bacff15d688ed135b4567965a4b664d9fb8de7417a7865bb138ad612043c9f"}, - {file = "pycairo-1.23.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ec305fc7f2f0299df78aadec0eaf6eb9accb90eda242b5d3492544d3f2b28027"}, - {file = "pycairo-1.23.0-cp38-cp38-win32.whl", hash = "sha256:1a6d8e0f353062ad92954784e33dbbaf66c880c9c30e947996c542ed9748aaaf"}, - {file = "pycairo-1.23.0-cp38-cp38-win_amd64.whl", hash = "sha256:82e335774a17870bc038e0c2fb106c1e5e7ad0c764662023886dfcfce5bb5a52"}, - {file = "pycairo-1.23.0-cp39-cp39-win32.whl", hash = "sha256:a4b1f525bbdf637c40f4d91378de36c01ec2b7f8ecc585b700a079b9ff83298e"}, - {file = "pycairo-1.23.0-cp39-cp39-win_amd64.whl", hash = "sha256:87efd62a7b7afad9a0a420f05b6008742a6cfc59077697be65afe8dc73ae15ad"}, - {file = "pycairo-1.23.0.tar.gz", hash = "sha256:9b61ac818723adc04367301317eb2e814a83522f07bbd1f409af0dada463c44c"}, + {file = "pycairo-1.25.1-cp310-cp310-win32.whl", hash = "sha256:cacb5c2abbfdfc79c728ab261ff791511e4957b606c660f9b380975b678b728f"}, + {file = "pycairo-1.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:109ebbeb5bbc510b726fc31251071264dec241e5084d0668f846d7e17e5af8e0"}, + {file = "pycairo-1.25.1-cp310-cp310-win_arm64.whl", hash = "sha256:b19269a8bf9ab5e3c617f2699bed00977fd02ff304339a233654456c0236f7c6"}, + {file = "pycairo-1.25.1-cp311-cp311-win32.whl", hash = "sha256:b10e58a3ce41e487aae15050b630742e880d4135cee7a69cee2c0ea2a0b4bd0a"}, + {file = "pycairo-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:fcf5511b05a652a0ef87f626bf26bfc1b796a67f0d1bd40781c62986fb41c356"}, + {file = "pycairo-1.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:4133ba3ef6d875aa1b16643dc0801846f463b8e78750f5308c41902dfeac5b9a"}, + {file = "pycairo-1.25.1-cp312-cp312-win32.whl", hash = "sha256:56fee2837a07ecd914f4fbf78ff59445f78becd658fe36125101925dd489eb94"}, + {file = "pycairo-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:fb31eec2c41ec74e23dc0fc9feb4007b4c37f78ec76220ed92530b342e09821a"}, + {file = "pycairo-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:27011d822952d7817130fc17f490de94328590bc8d45bdbca9ec4a47039fca22"}, + {file = "pycairo-1.25.1-cp38-cp38-win32.whl", hash = "sha256:9a7c5ed92fe87f60e9796777d5255f2df2deeb8ab1e3c296e67a1d8c9790808c"}, + {file = "pycairo-1.25.1-cp38-cp38-win_amd64.whl", hash = "sha256:48603ad31616140ad6fa097f13086d0ce8f29ead35ad6a215962f3b0496a5a70"}, + {file = "pycairo-1.25.1-cp39-cp39-win32.whl", hash = "sha256:97666c084e9eb1c08c7fd6d306d153767acdf03c0d80349ec55863cecd4138e0"}, + {file = "pycairo-1.25.1-cp39-cp39-win_amd64.whl", hash = "sha256:ac5437d140eccd97af12a618cc1ace0d9a85f1269f29e963751949f132828b21"}, + {file = "pycairo-1.25.1-cp39-cp39-win_arm64.whl", hash = "sha256:bda5d10adbf1f5eba6b524b5a70ccf7f659680b77e691ff94b312f25a6fcc91f"}, + {file = "pycairo-1.25.1.tar.gz", hash = "sha256:7e2be4fbc3b4536f16db7a11982cbf713e75069a4d73d44fe5a49b68423f5c0c"}, ] [[package]] @@ -284,12 +279,12 @@ files = [ [[package]] name = "pygobject" -version = "3.42.2" +version = "3.46.0" description = "Python bindings for GObject Introspection" optional = true -python-versions = ">=3.6, <4" +python-versions = ">=3.8, <4" files = [ - {file = "PyGObject-3.42.2.tar.gz", hash = "sha256:21524cef33100c8fd59dc135948b703d79d303e368ce71fa60521cc971cd8aa7"}, + {file = "PyGObject-3.46.0.tar.gz", hash = "sha256:481437b05af0a66b7c366ea052710eb3aacbb979d22d30b797f7ec29347ab1e6"}, ] [package.dependencies] @@ -307,74 +302,73 @@ files = [ [[package]] name = "pypresence" -version = "4.2.1" +version = "4.3.0" description = "Discord RPC client written in Python" optional = true -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "pypresence-4.2.1-py2.py3-none-any.whl", hash = "sha256:12197b5f51c21e3e555b17f85d3e55023f4ad83b6fff72cd6387659ffd484a02"}, - {file = "pypresence-4.2.1.tar.gz", hash = "sha256:691daf98c8189fd216d988ebfc67779e0f664211512d9843f37ab0d51d4de066"}, + {file = "pypresence-4.3.0-py2.py3-none-any.whl", hash = "sha256:af878c6d49315084f1b108aec86b31915080614d9421d6dd3a44737aba9ff13f"}, + {file = "pypresence-4.3.0.tar.gz", hash = "sha256:a6191a3af33a9667f2a4ef0185577c86b962ee70aa82643c472768a6fed1fbf3"}, ] [[package]] name = "pyqt5" -version = "5.15.9" +version = "5.15.10" description = "Python bindings for the Qt cross platform application toolkit" optional = true python-versions = ">=3.7" files = [ - {file = "PyQt5-5.15.9-cp37-abi3-macosx_10_13_x86_64.whl", hash = "sha256:883ba5c8a348be78c8be6a3d3ba014c798e679503bce00d76c666c2dc6afe828"}, - {file = "PyQt5-5.15.9-cp37-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:dd5ce10e79fbf1df29507d2daf99270f2057cdd25e4de6fbf2052b46c652e3a5"}, - {file = "PyQt5-5.15.9-cp37-abi3-win32.whl", hash = "sha256:e45c5cc15d4fd26ab5cb0e5cdba60691a3e9086411f8e3662db07a5a4222a696"}, - {file = "PyQt5-5.15.9-cp37-abi3-win_amd64.whl", hash = "sha256:e030d795df4cbbfcf4f38b18e2e119bcc9e177ef658a5094b87bb16cac0ce4c5"}, - {file = "PyQt5-5.15.9.tar.gz", hash = "sha256:dc41e8401a90dc3e2b692b411bd5492ab559ae27a27424eed4bd3915564ec4c0"}, + {file = "PyQt5-5.15.10-cp37-abi3-macosx_10_13_x86_64.whl", hash = "sha256:93288d62ebd47b1933d80c27f5d43c7c435307b84d480af689cef2474e87e4c8"}, + {file = "PyQt5-5.15.10-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:862cea3be95b4b0a2b9678003b3a18edf7bd5eafd673860f58820f246d4bf616"}, + {file = "PyQt5-5.15.10-cp37-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:b89478d16d4118664ff58ed609e0a804d002703c9420118de7e4e70fa1cb5486"}, + {file = "PyQt5-5.15.10-cp37-abi3-win32.whl", hash = "sha256:ff99b4f91aa8eb60510d5889faad07116d3340041916e46c07d519f7cad344e1"}, + {file = "PyQt5-5.15.10-cp37-abi3-win_amd64.whl", hash = "sha256:501355f327e9a2c38db0428e1a236d25ebcb99304cd6e668c05d1188d514adec"}, + {file = "PyQt5-5.15.10.tar.gz", hash = "sha256:d46b7804b1b10a4ff91753f8113e5b5580d2b4462f3226288e2d84497334898a"}, ] [package.dependencies] PyQt5-Qt5 = ">=5.15.2" -PyQt5-sip = ">=12.11,<13" +PyQt5-sip = ">=12.13,<13" [[package]] name = "pyqt5-qt5" -version = "5.15.2" +version = "5.15.12" description = "The subset of a Qt installation needed by PyQt5." optional = true python-versions = "*" files = [ - {file = "PyQt5_Qt5-5.15.2-py3-none-macosx_10_13_intel.whl", hash = "sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154"}, - {file = "PyQt5_Qt5-5.15.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a"}, - {file = "PyQt5_Qt5-5.15.2-py3-none-win32.whl", hash = "sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327"}, - {file = "PyQt5_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962"}, + {file = "PyQt5_Qt5-5.15.12-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:7adff02a33f2f82b409ca115d57fc7e39b22e673d29e35c9d2594164039f0f71"}, + {file = "PyQt5_Qt5-5.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b4c56566683ae905d5f04dd6ab925f02a384d6ce5ff4986eea0720d453666c22"}, ] [[package]] name = "pyqt5-sip" -version = "12.11.1" +version = "12.13.0" description = "The sip module support for PyQt5" optional = true python-versions = ">=3.7" files = [ - {file = "PyQt5_sip-12.11.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a40a39a6136a90e10c31510295c2be924564fc6260691501cdde669bdc5edea5"}, - {file = "PyQt5_sip-12.11.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:19b06164793177146c7f7604fe8389f44221a7bde196f2182457eb3e4229fa88"}, - {file = "PyQt5_sip-12.11.1-cp310-cp310-win32.whl", hash = "sha256:3afb1d1c07adcfef5c8bb12356a2ec2ec094f324af4417735d43b1ecaf1bb1a4"}, - {file = "PyQt5_sip-12.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:54dad6c2e5dab14e46f6822a889bbb1515bbd2061762273af10d26566d649bd9"}, - {file = "PyQt5_sip-12.11.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7218f6a1cefeb0b2fc26b89f15011f841aa4cd77786ccd863bf9792347fa38a8"}, - {file = "PyQt5_sip-12.11.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6b1113538082a7dd63b908587f61ce28ba4c7b8341e801fdf305d53a50a878ab"}, - {file = "PyQt5_sip-12.11.1-cp311-cp311-win32.whl", hash = "sha256:ac5f7ed06213d3bb203e33037f7c1a0716584c21f4f0922dcc044750e3659b80"}, - {file = "PyQt5_sip-12.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:4f0497e2f5eeaea9f5a67b0e55c501168efa86df4e53aace2a46498b87bc55c1"}, - {file = "PyQt5_sip-12.11.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b355d56483edc79dcba30be947a6b700856bb74beb90539e14cc4d92b9bad152"}, - {file = "PyQt5_sip-12.11.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dd163d9cffc4a56ebb9dd6908c0f0cb0caff8080294d41f4fb60fc3be63ca434"}, - {file = "PyQt5_sip-12.11.1-cp37-cp37m-win32.whl", hash = "sha256:b714f550ea6ddae94fd7acae531971e535f4a4e7277b62eb44e7c649cf3f03d0"}, - {file = "PyQt5_sip-12.11.1-cp37-cp37m-win_amd64.whl", hash = "sha256:d09b2586235deab7a5f2e28e4bde9a70c0b3730fa84f2590804a9932414136a3"}, - {file = "PyQt5_sip-12.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9a6f9c058564d0ac573561036299f54c452ae78b7d2a65d7c2d01685e6dca50d"}, - {file = "PyQt5_sip-12.11.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fc920c0e0d5050474d2d6282b478e4957548bf1dce58e1b0678914514dc70064"}, - {file = "PyQt5_sip-12.11.1-cp38-cp38-win32.whl", hash = "sha256:3358c584832f0ac9fd1ad3623d8a346c705f43414df1fcd0cb285a6ef51fec08"}, - {file = "PyQt5_sip-12.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:f9691c6f4d899ca762dd54442a1be158c3e52017f583183da6ef37d5bae86595"}, - {file = "PyQt5_sip-12.11.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0bc81cb9e171d29302d393775f95cfa01b7a15f61b199ab1812976e5c4cb2cb9"}, - {file = "PyQt5_sip-12.11.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b077fb4383536f51382f5516f0347328a4f338c6ccc4c268cc358643bef1b838"}, - {file = "PyQt5_sip-12.11.1-cp39-cp39-win32.whl", hash = "sha256:5c152878443c3e951d5db7df53509d444708dc06a121c267b548146be06b87f8"}, - {file = "PyQt5_sip-12.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:bd935cc46dfdbb89c21042c1db2e46a71f25693af57272f146d6d9418e2934f1"}, - {file = "PyQt5_sip-12.11.1.tar.gz", hash = "sha256:97d3fbda0f61edb1be6529ec2d5c7202ae83aee4353e4b264a159f8c9ada4369"}, + {file = "PyQt5_sip-12.13.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a7e3623b2c743753625c4650ec7696362a37fb36433b61824cf257f6d3d43cca"}, + {file = "PyQt5_sip-12.13.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6e4ac714252370ca037c7d609da92388057165edd4f94e63354f6d65c3ed9d53"}, + {file = "PyQt5_sip-12.13.0-cp310-cp310-win32.whl", hash = "sha256:d5032da3fff62da055104926ffe76fd6044c1221f8ad35bb60804bcb422fe866"}, + {file = "PyQt5_sip-12.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:9a8cdd6cb66adcbe5c941723ed1544eba05cf19b6c961851b58ccdae1c894afb"}, + {file = "PyQt5_sip-12.13.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0f85fb633a522f04e48008de49dce1ff1d947011b48885b8428838973fbca412"}, + {file = "PyQt5_sip-12.13.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ec60162e034c42fb99859206d62b83b74f987d58937b3a82bdc07b5c3d190dec"}, + {file = "PyQt5_sip-12.13.0-cp311-cp311-win32.whl", hash = "sha256:205cd449d08a2b024a468fb6100cd7ed03e946b4f49706f508944006f955ae1a"}, + {file = "PyQt5_sip-12.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:1c8371682f77852256f1f2d38c41e2e684029f43330f0635870895ab01c02f6c"}, + {file = "PyQt5_sip-12.13.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7fe3375b508c5bc657d73b9896bba8a768791f1f426c68053311b046bcebdddf"}, + {file = "PyQt5_sip-12.13.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:773731b1b5ab1a7cf5621249f2379c95e3d2905e9bd96ff3611b119586daa876"}, + {file = "PyQt5_sip-12.13.0-cp312-cp312-win32.whl", hash = "sha256:fb4a5271fa3f6bc2feb303269a837a95a6d8dd16be553aa40e530de7fb81bfdf"}, + {file = "PyQt5_sip-12.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:3a4498f3b1b15f43f5d12963accdce0fd652b0bcaae6baf8008663365827444c"}, + {file = "PyQt5_sip-12.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b984c2620a7a7eaf049221b09ae50a345317add2624c706c7d2e9e6632a9587"}, + {file = "PyQt5_sip-12.13.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3188a06956aef86f604fb0d14421a110fad70d2a9e943dbacbfc3303f651dade"}, + {file = "PyQt5_sip-12.13.0-cp38-cp38-win32.whl", hash = "sha256:108a15f603e1886988c4b0d9d41cb74c9f9815bf05cefc843d559e8c298a10ce"}, + {file = "PyQt5_sip-12.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:db228cd737f5cbfc66a3c3e50042140cb80b30b52edc5756dbbaa2346ec73137"}, + {file = "PyQt5_sip-12.13.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5338773bbaedaa4f16a73c142fb23cc18c327be6c338813af70260b756c7bc92"}, + {file = "PyQt5_sip-12.13.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:29fa9cc964517c9fc3f94f072b9a2aeef4e7a2eda1879cb835d9e06971161cdf"}, + {file = "PyQt5_sip-12.13.0-cp39-cp39-win32.whl", hash = "sha256:96414c93f3d33963887cf562d50d88b955121fbfd73f937c8eca46643e77bf61"}, + {file = "PyQt5_sip-12.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:bbc7cd498bf19e0862097be1ad2243e824dea56726f00c11cff1b547c2d31d01"}, + {file = "PyQt5_sip-12.13.0.tar.gz", hash = "sha256:7f321daf84b9c9dbca61b80e1ef37bdaffc0e93312edae2cd7da25b953971d91"}, ] [[package]] @@ -432,30 +426,73 @@ certifi = "*" [[package]] name = "urllib3" -version = "1.26.14" +version = "2.1.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.8" files = [ - {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, - {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, + {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, + {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "urwid" -version = "2.1.2" +version = "2.3.4" description = "A full-featured console (xterm et al.) user interface library" optional = true -python-versions = "*" +python-versions = ">=3.7.0" files = [ - {file = "urwid-2.1.2.tar.gz", hash = "sha256:588bee9c1cb208d0906a9f73c613d2bd32c3ed3702012f51efe318a3f2127eae"}, + {file = "urwid-2.3.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:232b64678248c489e0dddadccfc2483a54567b74fcf74d140f3f4b4e9d15ddba"}, + {file = "urwid-2.3.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce538f0e5c8ee2341f3e38239a1c65a5a042ee993577093067a4419c10615030"}, + {file = "urwid-2.3.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81a17afedc1f0ec7ee6af9ebae0c21f5caddf050831ffc5dfbd7e61a4966388a"}, + {file = "urwid-2.3.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbbb8c21920be76630d1ae03eb67dced3d362a56eabf05d619a0ce3f0488cb0d"}, + {file = "urwid-2.3.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1a0e6bbd4e70805519e843fa982ba322d819939eb7049e6ece5ed6aa132c96b"}, + {file = "urwid-2.3.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b0802494b1baaf691313cd24aaf5e4ec00bf8a6f4ba664bdf088119517db0d4"}, + {file = "urwid-2.3.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0be1a86fa279850bd167a031dd71aa401b26f7e9fdcc99360785f1a292938c10"}, + {file = "urwid-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de28459d08020a07fe4d918a9ca5ff069102fa5455356cc5a1695b55ebb5bafe"}, + {file = "urwid-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4dbeb404751341f354f7d7bf5857cacb7ba415335427c78ee00991cdcc1b5bb0"}, + {file = "urwid-2.3.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e61a5be847ab36a5188aacd96554e0f354367dcae2f6cbc284de7fbbb3f075"}, + {file = "urwid-2.3.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d518d3cb428c9e0c03076dc6b83996bbfe0595d4612c3c2f572d8edbc260e9c"}, + {file = "urwid-2.3.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67bdc9c3f8a834b848e4400d91b5ab82620e1f963e4736600a304930433ea8fe"}, + {file = "urwid-2.3.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d91a4f2abaa022d6bcd8f4d7b179a1bb03afcb83be4707e5599131f322dbce"}, + {file = "urwid-2.3.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:278ffe0c8366c03da533a983eb85ac80e325eec09f78988fd37fc830dc563eb2"}, + {file = "urwid-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0080ad86d37792faeda2ddaf7eaab711861c34f19996876dde649e28139a741d"}, + {file = "urwid-2.3.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c141f5e6a32e03e78ef28083691588d37f60fb5ffb2d96aecba3cda7fa38bb1"}, + {file = "urwid-2.3.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:085b4b1ff4c0df96e2e82331a2353a56abbd5b7456e838baa995be4f12644347"}, + {file = "urwid-2.3.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ca3e5fbdddc3b4394cc93835a79358c56a54b05538edc7e8b66d2ff13c4689f"}, + {file = "urwid-2.3.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c79f55558dc50f8c19d1e1468e63942fc7577cbdb2ff948768931ef28548a5af"}, + {file = "urwid-2.3.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c692bb9a314216c0cfe1fdd8787858e2e916e25f97c95e8411ba5933c0fe3c39"}, + {file = "urwid-2.3.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:90440ef37b50ad5e947a095bfec7a048dfae938daa0d2355274accd886474932"}, + {file = "urwid-2.3.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:205c7aa020c92797f65465e1a226fbf2122159b1565b936a5fd6ed6bd34b4440"}, + {file = "urwid-2.3.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0a263d8a90450166e0a195cf751b46d5081a48a3c00afa45a4ae582a34a6785e"}, + {file = "urwid-2.3.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a6fc3651d5aff40d53b52cae1951d17c62126ffcc11c7f0d53583d28198b0a4"}, + {file = "urwid-2.3.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e4a3500b7166f27ca830df8bcfb9969d97a98f340cd4b0f0299557dee8c39b3"}, + {file = "urwid-2.3.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:963e89099b0438416b550161750a1e2fb52a328008732197d29e3675baacb150"}, + {file = "urwid-2.3.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:448d248bd3cbe34f0422108db0cecb7d24336703677bce06b7ed67dc4892d925"}, + {file = "urwid-2.3.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae83944dc78bd178b0ad8cab9ff7d72b03ac01dcd0e1e01725142e241196f04"}, + {file = "urwid-2.3.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6cc8db27989f6166602a45f6382357827cba966e3bd607a9e409cf867f7b0ec6"}, + {file = "urwid-2.3.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0c7e3f21b4427ecfffd6588fe5119eab7e12abd03576e8ba111fdfe2a78e3fdc"}, + {file = "urwid-2.3.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9dae4dbccae166ee9ff03bda58c36369a566d03274c1fbf559cff11117539bc7"}, + {file = "urwid-2.3.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c4a213be475ae81b250e401e670d74d5d4ffb3b034ff6fc52e721759788eb05"}, + {file = "urwid-2.3.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d38afb5bbcbca365fbd86746990c22167fbeb7f85d756bad33892f364028975"}, + {file = "urwid-2.3.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fbb765783120d5dc835424f6870190b6d73c020b44ab650b682fd9ffbc41a85"}, + {file = "urwid-2.3.4.tar.gz", hash = "sha256:18b9f84cc80a4fcda55ea29f0ea260d31f8c1c721ff6a0396a396020e6667738"}, ] +[package.extras] +glib = ["PyGObject"] +lcd = ["pyserial"] +serial = ["pyserial"] +tornado = ["tornado"] +trio = ["exceptiongroup", "trio (>=0.22.0)"] +twisted = ["twisted"] +zmq = ["zmq"] + [extras] curses = ["urwid"] discord-rpc = ["pypresence"] @@ -467,5 +504,5 @@ ui = ["pillow", "pycairo", "pygobject", "pyqt5", "urwid"] [metadata] lock-version = "2.0" -python-versions = "^3.8" -content-hash = "a2397940e92875df0212b6bdda7eaf3848d76aff0a97178f8ff81f06d43ef681" +python-versions = "^3.10" +content-hash = "c3cc208b5a57da93409b8c4eb3c1c0f3e7de4263d547a126e8b440375f4c0914" From 04b303bc8b5f8d7ccab20126d2654abe4a732069 Mon Sep 17 00:00:00 2001 From: BigBoyBarney <barni.benke99@gmail.com> Date: Mon, 1 Jan 2024 13:38:24 +0100 Subject: [PATCH 12/16] Python 3.10 compliance fix - Replaced python >=3.11 exclusive `datetime.timezone.utc` alias `datetime.UTC`. Should run in 3.10 now. --- trackma/ui/qt/models.py | 12 +++++++----- trackma/utils.py | 5 +++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/trackma/ui/qt/models.py b/trackma/ui/qt/models.py index dce0bef7..d2fdb58c 100644 --- a/trackma/ui/qt/models.py +++ b/trackma/ui/qt/models.py @@ -84,7 +84,8 @@ def _calculate_color(self, row, show): def _calculate_next_ep(self, row, show): if self.mediainfo.get('date_next_ep') and show['next_ep_time'] is not None: - delta = show['next_ep_time'].replace(tzinfo=datetime.UTC) - datetime.datetime.now(datetime.UTC) + delta = show['next_ep_time'].replace(tzinfo=datetime.timezone.utc) - \ + datetime.datetime.now(datetime.timezone.utc) # Make sure it's UTC self.next_ep[row] = "%i days, %02d hrs." % ( delta.days, delta.seconds / 3600) elif row in self.next_ep: @@ -137,7 +138,7 @@ def update(self, showid, is_playing=None): self._calculate_color(row, show) self.dataChanged.emit(self.index( - row, 0), self.index(row, len(self.columns)-1)) + row, 0), self.index(row, len(self.columns) - 1)) def rowCount(self, parent): if self.showlist: @@ -184,8 +185,8 @@ def data(self, index, role): if show['total']: total = show['total'] else: - total = (int(show['my_progress']/12)+1) * \ - 12 # Round up to the next cour + total = (int(show['my_progress'] / 12) + 1) * \ + 12 # Round up to the next cour if row in self.eps: return (show['my_progress'], total, self.eps[row][0], self.eps[row][1]) @@ -394,7 +395,8 @@ def setFilterColumns(self, columns): self.invalidateFilter() def filterAcceptsRow(self, source_row, source_parent): - if self.filter_status is not None and self.sourceModel().showlist[source_row]['my_status'] != self.filter_status: + if self.filter_status is not None and self.sourceModel().showlist[source_row]['my_status'] \ + != self.filter_status: return False if self.filter_columns: diff --git a/trackma/utils.py b/trackma/utils.py index cc41dd43..f6e2a10b 100644 --- a/trackma/utils.py +++ b/trackma/utils.py @@ -521,8 +521,9 @@ def calculate_relative_time(time_end: datetime, utc: bool, fulltime: bool = True if time_end: try: if utc: - time_end = time_end.replace(tzinfo=datetime.UTC) # Make sure that time_end is timezone aware in UTC - current_time = datetime.datetime.now(datetime.UTC) + time_end = time_end.replace(tzinfo=datetime.timezone.utc) + # Make sure that time_end is timezone aware in UTC + current_time = datetime.datetime.now(datetime.timezone.utc) else: current_time = datetime.datetime.now() time_difference = time_end - current_time From 5b44f708457ec46abac214db49803885f5b20c63 Mon Sep 17 00:00:00 2001 From: BigBoyBarney <barni.benke99@gmail.com> Date: Wed, 3 Jan 2024 21:37:15 +0100 Subject: [PATCH 13/16] Replaced 2 get_title() calls with a variable --- trackma/ui/gtk/showtreeview.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trackma/ui/gtk/showtreeview.py b/trackma/ui/gtk/showtreeview.py index 9679f595..1be94b2f 100644 --- a/trackma/ui/gtk/showtreeview.py +++ b/trackma/ui/gtk/showtreeview.py @@ -441,7 +441,7 @@ def _on_column_clicked(self, key, column): self._reverse_sort_order(column, sort_order_model, sort_order_column, key) # Reverse sorting else: # We're trying to sort a new column - match column.get_title(): # Check which column and set the sort indicator accordingly + match column_title: # Check which column and set the sort indicator accordingly case 'Score': # Default should be large -> small, e.g: 10, 8, 7, 3 column.set_sort_order(Gtk.SortType.ASCENDING) self.get_model().set_sort_column_id(key, Gtk.SortType.DESCENDING) @@ -460,7 +460,7 @@ def _on_column_clicked(self, key, column): self.cols[self.previous_sort_column].set_sort_indicator(False) # Disable the previous sort indicator column.set_sort_indicator(True) # Enable the new sort indicator - self.previous_sort_column = column.get_title() # Save the new sorting column + self.previous_sort_column = column_title # Save the new sorting column def _reverse_sort_order(self, column, sort_order_model, sort_order_column, key): """Reverses both the actual sort order and the visual sorting indicator arrow direction""" From 669551336263e79c3878cfe89ab7b911e1e96a26 Mon Sep 17 00:00:00 2001 From: BigBoyBarney <barni.benke99@gmail.com> Date: Wed, 10 Jan 2024 23:51:11 +0100 Subject: [PATCH 14/16] Added best possible MAL support - Displays 'Next episode' for MAL, where applicable - Added a bunch of comments for clarity - Indents in `libmal.py` were cleaned up a bit - Renamed `header_button_press` to `header_right_click` for clarity. - The state of MAL is pretty sad, it does not give an actual weekly airing time, just a general broadcast schedule, so if an episode gets delayed, there is no way of knowing without 3rd party sites. This is not an issue with the code, but with MAL. Use Anilist. --- trackma/lib/libmal.py | 179 +++++++++++++++++++++++---------- trackma/ui/gtk/showtreeview.py | 44 ++++---- trackma/ui/qt/models.py | 2 +- trackma/utils.py | 6 +- 4 files changed, 151 insertions(+), 80 deletions(-) diff --git a/trackma/lib/libmal.py b/trackma/lib/libmal.py index 887ad002..90449dd3 100644 --- a/trackma/lib/libmal.py +++ b/trackma/lib/libmal.py @@ -53,7 +53,7 @@ class libmal(lib): 'statuses_start': ['watching'], 'statuses_finish': ['completed'], 'statuses_library': ['watching', 'on_hold', 'plan_to_watch'], - 'statuses': ['watching', 'completed', 'on_hold', 'dropped', 'plan_to_watch'], + 'statuses': ['watching', 'completed', 'on_hold', 'dropped', 'plan_to_watch'], 'statuses_dict': { 'watching': 'Watching', 'completed': 'Completed', @@ -76,7 +76,7 @@ class libmal(lib): 'can_date': True, 'statuses_start': ['reading'], 'statuses_finish': ['completed'], - 'statuses': ['reading', 'completed', 'on_hold', 'dropped', 'plan_to_read'], + 'statuses': ['reading', 'completed', 'on_hold', 'dropped', 'plan_to_read'], 'statuses_dict': { 'reading': 'Reading', 'completed': 'Completed', @@ -106,13 +106,13 @@ class libmal(lib): 'one_shot': utils.Type.ONE_SHOT, 'doujinshi': utils.Type.MANGA, } - + status_translate = { 'currently_airing': utils.Status.AIRING, 'finished_airing': utils.Status.FINISHED, 'not_yet_aired': utils.Status.NOTYET, } - + season_translate = { utils.Season.WINTER: 'winter', utils.Season.SPRING: 'spring', @@ -127,7 +127,7 @@ class libmal(lib): query_url = "https://api.myanimelist.net/v2" client_id = "32c510ab2f47a1048a8dd24de266dc0c" user_agent = 'Trackma/{}'.format(utils.VERSION) - + library_page_limit = 1000 search_page_limit = 100 season_page_limit = 500 @@ -136,15 +136,15 @@ def __init__(self, messenger, account, userconfig): super(libmal, self).__init__(messenger, account, userconfig) self.pin = account['password'].strip() - + if 'extra' not in account or 'code_verifier' not in account['extra']: raise utils.APIFatal( "This account seems to be using the old MyAnimeList API." "Please re-create and authorize the account.") - + self.code_verifier = account['extra']['code_verifier'] self.userid = self._get_userconfig('userid') - + if self.mediatype == 'manga': self.total_str = "num_chapters" self.watched_str = self.watched_send_str = "num_chapters_read" @@ -155,10 +155,10 @@ def __init__(self, messenger, account, userconfig): self.opener = urllib.request.build_opener() self.opener.addheaders = [ - ('User-Agent', self.user_agent), - ('Accept', 'application/json'), + ('User-Agent', self.user_agent), + ('Accept', 'application/json'), ('Accept-Encoding', 'gzip'), - ('Accept-Charset', 'utf-8'), + ('Accept-Charset', 'utf-8'), ] def _request(self, method, url, get=None, post=None, auth=False): @@ -191,7 +191,7 @@ def _request(self, method, url, get=None, post=None, auth=False): response = gzip.GzipFile(fileobj=response).read().decode('utf-8') else: response = response.read().decode('utf-8') - + return json.loads(response) except urllib.error.HTTPError as e: raise utils.APIError("Connection error: %s" % e) @@ -199,13 +199,13 @@ def _request(self, method, url, get=None, post=None, auth=False): raise utils.APIError("URL error: %s" % e) except socket.timeout: raise utils.APIError("Operation timed out.") - + def _request_access_token(self, refresh=False): """ Requests or refreshes the access token through OAuth2 """ params = { - 'client_id': self.client_id, + 'client_id': self.client_id, } if refresh: @@ -219,25 +219,25 @@ def _request_access_token(self, refresh=False): params['code'] = self.pin params['code_verifier'] = self.code_verifier params['grant_type'] = 'authorization_code' - + data = self._request('POST', self.auth_url, post=params) timestamp = int(time.time()) - self._set_userconfig('access_token', data['access_token']) - self._set_userconfig('token_type', data['token_type']) - self._set_userconfig('expires', timestamp + data['expires_in']) + self._set_userconfig('access_token', data['access_token']) + self._set_userconfig('token_type', data['token_type']) + self._set_userconfig('expires', timestamp + data['expires_in']) self._set_userconfig('refresh_token', data['refresh_token']) self.logged_in = True self._emit_signal('userconfig_changed') - + def check_credentials(self): timestamp = int(time.time()) - + if not self._get_userconfig('access_token'): self._request_access_token(False) - elif (timestamp+60) > self._get_userconfig('expires'): + elif (timestamp + 60) > self._get_userconfig('expires'): try: self._request_access_token(True) except utils.APIError: @@ -253,18 +253,18 @@ def check_credentials(self): def fetch_list(self): self.check_credentials() shows = {} - - fields = 'id,alternative_titles,title,start_date,main_picture,status,' + self.total_str + + fields = 'id,alternative_titles,title,start_date,main_picture,status,broadcast,' + self.total_str listfields = 'score,status,start_date,finish_date,' + self.watched_str params = { 'fields': '%s,list_status{%s}' % (fields, listfields), 'limit': self.library_page_limit, 'nsfw': 'true' } - + url = "{}/users/@me/{}list?{}".format(self.query_url, self.mediatype, urllib.parse.urlencode(params)) i = 1 - + while url: self.msg.info('Downloading list (page %d)...' % i) data = self._request('GET', url, auth=True) @@ -286,8 +286,9 @@ def fetch_list(self): 'my_status': item['list_status']['status'], 'my_start_date': self._str2date(item['list_status'].get('start_date')), 'my_finish_date': self._str2date(item['list_status'].get('finish_date')), + 'next_ep_time': self._next_episode_to_utc_datetime(item['node']) }) - + url = data['paging'].get('next') i += 1 @@ -302,49 +303,50 @@ def update_show(self, item): self.check_credentials() self.msg.info("Updating item %s..." % item['title']) self._update_entry(item) - + def delete_show(self, item): self.check_credentials() self.msg.info("Deleting item %s..." % item['title']) - data = self._request('DELETE', self.query_url + '/%s/%d/my_list_status' % (self.mediatype, item['id']), auth=True) - + data = self._request('DELETE', self.query_url + '/%s/%d/my_list_status' % (self.mediatype, item['id']), + auth=True) + def search(self, criteria, method): self.check_credentials() self.msg.info("Searching for {}...".format(criteria)) - + fields = 'alternative_titles,end_date,genres,id,main_picture,mean,media_type,' + self.total_str + ',popularity,rating,start_date,status,studios,synopsis,title' params = {'fields': fields, 'nsfw': 'true'} - + if method == utils.SearchMethod.KW: url = '/%s' % self.mediatype params['q'] = criteria params['limit'] = self.search_page_limit elif method == utils.SearchMethod.SEASON: - season, season_year = criteria + season, season_year = criteria url = '/%s/season/%d/%s' % (self.mediatype, season_year, self.season_translate[season]) params['limit'] = self.season_page_limit else: raise utils.APIError("Invalid search method.") - + results = [] data = self._request('GET', self.query_url + url, get=params, auth=True) for item in data['data']: results.append(self._parse_info(item['node'])) - + self._emit_signal('show_info_changed', results) return results - + def request_info(self, itemlist): self.check_credentials() infolist = [] - + fields = 'alternative_titles,end_date,genres,id,main_picture,mean,media_type,' + self.total_str + ',popularity,rating,start_date,status,studios,synopsis,title' params = {'fields': fields, 'nsfw': 'true'} for item in itemlist: data = self._request('GET', self.query_url + '/%s/%d' % (self.mediatype, item['id']), get=params, auth=True) infolist.append(self._parse_info(data)) - + self._emit_signal('show_info_changed', infolist) return infolist @@ -361,17 +363,19 @@ def _update_entry(self, item): if 'my_finish_date' in item: values['finish_date'] = item['my_finish_date'] or "" - data = self._request('PATCH', self.query_url + '/%s/%d/my_list_status' % (self.mediatype, item['id']), post=values, auth=True) + data = self._request('PATCH', self.query_url + '/%s/%d/my_list_status' % (self.mediatype, item['id']), + post=values, auth=True) def _get_aliases(self, item): - aliases = [item['alternative_titles']['en'], item['alternative_titles']['ja']] + item['alternative_titles']['synonyms'] - + aliases = [item['alternative_titles']['en'], item['alternative_titles']['ja']] + item['alternative_titles'][ + 'synonyms'] + return aliases - + def _parse_info(self, item): info = utils.show() showid = item['id'] - + info.update({ 'id': showid, 'title': item['title'], @@ -384,27 +388,94 @@ def _parse_info(self, item): 'start_date': self._str2date(item.get('start_date')), 'end_date': self._str2date(item.get('end_date')), 'extra': [ - ('English', item['alternative_titles'].get('en')), - ('Japanese', item['alternative_titles'].get('ja')), - ('Synonyms', item['alternative_titles'].get('synonyms')), - ('Synopsis', item.get('synopsis')), - ('Type', item.get('media_type')), - ('Mean score', item.get('mean')), - ('Status', self._translate_status(item['status'])), + ('English', item['alternative_titles'].get('en')), + ('Japanese', item['alternative_titles'].get('ja')), + ('Synonyms', item['alternative_titles'].get('synonyms')), + ('Synopsis', item.get('synopsis')), + ('Type', item.get('media_type')), + ('Mean score', item.get('mean')), + ('Status', self._translate_status(item['status'])), ] }) - + return info - + def _translate_status(self, orig_status): return self.status_translate.get(orig_status, utils.Status.UNKNOWN) - def _str2date(self, string): + @staticmethod + def _str2date(string): if string is None: return None try: return datetime.datetime.strptime(string, "%Y-%m-%d") - except Exception: - self.msg.debug('Invalid date {}'.format(string)) + except ValueError: return None # Ignore date if it's invalid + + @classmethod + def _next_episode_to_utc_datetime(cls, mal_response_node: dict) -> datetime: + """MAL is pretty rubbish, and does not provide an actual date for the next episode. + Instead, it gives a data for when the show started airing, and the general broadcast schedule as a string. + E.g.: {'day_of_the_week': 'friday', 'start_time': '23:00'} — *this is assumed to be JST* + It does not update for when a show goes on hiatus or anything like that. + + This function takes that string and assumes that the 'day of the week' is this week for an airing show. + There is no other way to do this without a 3rd party site. If the show is not airing, this does not matter, + as MAL doesn't return anything anyway.""" + airing_status = mal_response_node['status'] # Get the status of the show + next_broadcast_utc = None # Default 'Next episode' value, in case MAL doesn't return anything + + # Define JST time-zone and current datetime in JST (there is no DST) + jst_timezone = datetime.timezone(datetime.timedelta(hours=9)) + current_jst_time = datetime.datetime.now(tz=jst_timezone) + + # Potentially return the 'Next episode' time in UTC depending on the status + match airing_status: + case 'finished_airing': # There are no new episodes + pass + + case 'currently_airing': # The UTC datetime object needs to be stitched together + broadcast_response = mal_response_node.get('broadcast') # Returned by MAL + if broadcast_response is not None: + # Read and strip MAL response, generally looks like this (given in JST): 'friday 23:00' + jst_broadcast_time_day_str = broadcast_response['day_of_the_week'] # 'friday' + jst_broadcast_time_time_str = broadcast_response['start_time'] # '23:00' + + if jst_broadcast_time_time_str is not None: + jst_broadcast_time_hour, jst_broadcast_time_minute = ( + map(int, jst_broadcast_time_time_str.split(':'))) + + else: # Sometimes MAL doesn't have an exact time entry + jst_broadcast_time_hour, jst_broadcast_time_minute = ('0', '0') + + # Map weekday names to corresponding indices. Lowercase because MAL uses lowercase for some reason + weekday_indices = {'monday': 1, 'tuesday': 2, 'wednesday': 3, 'thursday': 4, 'friday': 5, + 'saturday': 6, 'sunday': 7} + + # Calculate the days until the next broadcast weekday + days_until_next_broadcast = (weekday_indices[jst_broadcast_time_day_str] + - current_jst_time.isoweekday() + 7) % 7 + + # Set the correct day and time + next_broadcast_jst = current_jst_time + datetime.timedelta(days=days_until_next_broadcast) + next_broadcast_jst = next_broadcast_jst.replace(hour=jst_broadcast_time_hour, + minute=jst_broadcast_time_minute, + second=0, microsecond=0) + + # Convert to UTC + next_broadcast_utc = next_broadcast_jst.astimezone(datetime.timezone.utc) + + # These shows are a mess and return one of the following formats: 'None'/'2024'/'2024 04'/'2024 04 12' + case 'not_yet_aired': + # _str2date returns 'None' for anything that's not '%Y-%m-%d' compliant, so we don't have to check + next_broadcast_jst = cls._str2date(mal_response_node.get('start_date')) + if next_broadcast_jst is not None: + # Assign JST timezone, set the broadcast time to 12pm, then convert to UTC + next_broadcast_jst = next_broadcast_jst.replace(tzinfo=jst_timezone, hour=12, microsecond=0) + next_broadcast_utc = next_broadcast_jst.astimezone(tz=datetime.timezone.utc) + + case _: + raise ValueError + + return next_broadcast_utc diff --git a/trackma/ui/gtk/showtreeview.py b/trackma/ui/gtk/showtreeview.py index 1be94b2f..bc13650d 100644 --- a/trackma/ui/gtk/showtreeview.py +++ b/trackma/ui/gtk/showtreeview.py @@ -33,7 +33,7 @@ class TreeConstants: # If no number is provided by the lib, 1 episode / week is assumed and calculated accordingly AVAILABLE_EPS = 8 # Number of available episodes in the local library COLOR = 9 # Used with the _get_color method to return the color preset for a show - PROGRESS_PERCENTAGE = 10 # % of episodes watched. 7 / 13 -> + PROGRESS_PERCENTAGE = 10 # % of episodes watched. 7 / 13 -> 53 START_DATE = 11 # Start date of the show END_DATE = 12 # End date of the show MY_START_DATE = 13 # Date when the user started watching the show @@ -138,25 +138,25 @@ def append(self, show, altname=None, eps=None): next_episode_air_time_relative = utils.calculate_relative_time(show['next_ep_time'], utc=True, fulltime=False) - # Corresponds to __cols, but is used locally to store the data before appending. - row = [show['id'], - title_str, - show['my_progress'], - show['my_score'], - watched_episodes_fraction, - score_str, - show['total'], - aired_eps, - available_eps, - self._get_color(show, available_eps), - progress, - start_date, - end_date, - my_start_date, - my_finish_date, - show['my_status'], - show['status'], - next_episode_air_time_relative, + # Corresponds to __cols, but is used locally to store the data before appending. Comments are TreeConstants + row = [show['id'], # SHOW_ID + title_str, # TITLE + show['my_progress'], # MY_PROGRESS + show['my_score'], # MY_SCORE + watched_episodes_fraction, # WATCHED_EPISODES_FRACTION + score_str, # MY_SCORE_STRING + show['total'], # TOTAL_EPS + aired_eps, # AIRED_EPS + available_eps, # AVAILABLE_EPS + self._get_color(show, available_eps), # COLOR + progress, # PROGRESS_PERCENTAGE + start_date, # START_DATE + end_date, # END_DATE + my_start_date, # MY_START_DATE + my_finish_date, # MY_FINISH_DATE + show['my_status'], # MY_STATUS + show['status'], # SHOW_STATUS + next_episode_air_time_relative, # NEXT_EPISODE_AIR_TIME_RELATIVE ] super().append(row) @@ -361,12 +361,12 @@ def __init__(self, colors, visible_columns, status, _list, progress_style=1): w = self.cols[name].get_widget() while not isinstance(w, Gtk.Button): w = w.get_parent() - w.connect('button-press-event', self._header_button_press) + w.connect('button-press-event', self._header_right_click) # Appends populated columns self.append_column(self.cols[name]) - def _header_button_press(self, button, event): + def _header_right_click(self, button, event): if event.button == 3: menu = Gtk.Menu() for name, sort in self.available_columns: diff --git a/trackma/ui/qt/models.py b/trackma/ui/qt/models.py index d2fdb58c..a1640ba7 100644 --- a/trackma/ui/qt/models.py +++ b/trackma/ui/qt/models.py @@ -85,7 +85,7 @@ def _calculate_color(self, row, show): def _calculate_next_ep(self, row, show): if self.mediainfo.get('date_next_ep') and show['next_ep_time'] is not None: delta = show['next_ep_time'].replace(tzinfo=datetime.timezone.utc) - \ - datetime.datetime.now(datetime.timezone.utc) # Make sure it's UTC + datetime.datetime.now(tz=datetime.timezone.utc) # 'next_ep_time has to be UTC' by definition self.next_ep[row] = "%i days, %02d hrs." % ( delta.days, delta.seconds / 3600) elif row in self.next_ep: diff --git a/trackma/utils.py b/trackma/utils.py index f6e2a10b..45e4d160 100644 --- a/trackma/utils.py +++ b/trackma/utils.py @@ -510,7 +510,7 @@ def show(): 'image': '', 'image_thumb': '', 'queued': False, - 'next_ep_time': None + 'next_ep_time': None # Must be a time-zone aware datetime object in UTC } @@ -522,8 +522,8 @@ def calculate_relative_time(time_end: datetime, utc: bool, fulltime: bool = True try: if utc: time_end = time_end.replace(tzinfo=datetime.timezone.utc) - # Make sure that time_end is timezone aware in UTC - current_time = datetime.datetime.now(datetime.timezone.utc) + # Make sure that time_end is time-zone aware in UTC + current_time = datetime.datetime.now(tz=datetime.timezone.utc) else: current_time = datetime.datetime.now() time_difference = time_end - current_time From efbea98ea777387d2657871137fbe72e1da243e8 Mon Sep 17 00:00:00 2001 From: BigBoyBarney <barni.benke99@gmail.com> Date: Wed, 10 Jan 2024 23:51:11 +0100 Subject: [PATCH 15/16] Added best possible MAL support - Displays 'Next episode' for MAL, where applicable (new in Qt) - Added a bunch of comments for clarity - Indents in `libmal.py` were cleaned up a bit - Renamed `header_button_press` to `header_right_click` for clarity. - The state of MAL is pretty sad, it does not give an actual weekly airing time, just a general broadcast schedule, so if an episode gets delayed, there is no way of knowing without 3rd party sites. This is not an issue with the code, but with MAL. Use Anilist. --- trackma/lib/libmal.py | 179 +++++++++++++++++++++++---------- trackma/ui/gtk/showtreeview.py | 44 ++++---- trackma/ui/qt/models.py | 2 +- trackma/utils.py | 6 +- 4 files changed, 151 insertions(+), 80 deletions(-) diff --git a/trackma/lib/libmal.py b/trackma/lib/libmal.py index 887ad002..90449dd3 100644 --- a/trackma/lib/libmal.py +++ b/trackma/lib/libmal.py @@ -53,7 +53,7 @@ class libmal(lib): 'statuses_start': ['watching'], 'statuses_finish': ['completed'], 'statuses_library': ['watching', 'on_hold', 'plan_to_watch'], - 'statuses': ['watching', 'completed', 'on_hold', 'dropped', 'plan_to_watch'], + 'statuses': ['watching', 'completed', 'on_hold', 'dropped', 'plan_to_watch'], 'statuses_dict': { 'watching': 'Watching', 'completed': 'Completed', @@ -76,7 +76,7 @@ class libmal(lib): 'can_date': True, 'statuses_start': ['reading'], 'statuses_finish': ['completed'], - 'statuses': ['reading', 'completed', 'on_hold', 'dropped', 'plan_to_read'], + 'statuses': ['reading', 'completed', 'on_hold', 'dropped', 'plan_to_read'], 'statuses_dict': { 'reading': 'Reading', 'completed': 'Completed', @@ -106,13 +106,13 @@ class libmal(lib): 'one_shot': utils.Type.ONE_SHOT, 'doujinshi': utils.Type.MANGA, } - + status_translate = { 'currently_airing': utils.Status.AIRING, 'finished_airing': utils.Status.FINISHED, 'not_yet_aired': utils.Status.NOTYET, } - + season_translate = { utils.Season.WINTER: 'winter', utils.Season.SPRING: 'spring', @@ -127,7 +127,7 @@ class libmal(lib): query_url = "https://api.myanimelist.net/v2" client_id = "32c510ab2f47a1048a8dd24de266dc0c" user_agent = 'Trackma/{}'.format(utils.VERSION) - + library_page_limit = 1000 search_page_limit = 100 season_page_limit = 500 @@ -136,15 +136,15 @@ def __init__(self, messenger, account, userconfig): super(libmal, self).__init__(messenger, account, userconfig) self.pin = account['password'].strip() - + if 'extra' not in account or 'code_verifier' not in account['extra']: raise utils.APIFatal( "This account seems to be using the old MyAnimeList API." "Please re-create and authorize the account.") - + self.code_verifier = account['extra']['code_verifier'] self.userid = self._get_userconfig('userid') - + if self.mediatype == 'manga': self.total_str = "num_chapters" self.watched_str = self.watched_send_str = "num_chapters_read" @@ -155,10 +155,10 @@ def __init__(self, messenger, account, userconfig): self.opener = urllib.request.build_opener() self.opener.addheaders = [ - ('User-Agent', self.user_agent), - ('Accept', 'application/json'), + ('User-Agent', self.user_agent), + ('Accept', 'application/json'), ('Accept-Encoding', 'gzip'), - ('Accept-Charset', 'utf-8'), + ('Accept-Charset', 'utf-8'), ] def _request(self, method, url, get=None, post=None, auth=False): @@ -191,7 +191,7 @@ def _request(self, method, url, get=None, post=None, auth=False): response = gzip.GzipFile(fileobj=response).read().decode('utf-8') else: response = response.read().decode('utf-8') - + return json.loads(response) except urllib.error.HTTPError as e: raise utils.APIError("Connection error: %s" % e) @@ -199,13 +199,13 @@ def _request(self, method, url, get=None, post=None, auth=False): raise utils.APIError("URL error: %s" % e) except socket.timeout: raise utils.APIError("Operation timed out.") - + def _request_access_token(self, refresh=False): """ Requests or refreshes the access token through OAuth2 """ params = { - 'client_id': self.client_id, + 'client_id': self.client_id, } if refresh: @@ -219,25 +219,25 @@ def _request_access_token(self, refresh=False): params['code'] = self.pin params['code_verifier'] = self.code_verifier params['grant_type'] = 'authorization_code' - + data = self._request('POST', self.auth_url, post=params) timestamp = int(time.time()) - self._set_userconfig('access_token', data['access_token']) - self._set_userconfig('token_type', data['token_type']) - self._set_userconfig('expires', timestamp + data['expires_in']) + self._set_userconfig('access_token', data['access_token']) + self._set_userconfig('token_type', data['token_type']) + self._set_userconfig('expires', timestamp + data['expires_in']) self._set_userconfig('refresh_token', data['refresh_token']) self.logged_in = True self._emit_signal('userconfig_changed') - + def check_credentials(self): timestamp = int(time.time()) - + if not self._get_userconfig('access_token'): self._request_access_token(False) - elif (timestamp+60) > self._get_userconfig('expires'): + elif (timestamp + 60) > self._get_userconfig('expires'): try: self._request_access_token(True) except utils.APIError: @@ -253,18 +253,18 @@ def check_credentials(self): def fetch_list(self): self.check_credentials() shows = {} - - fields = 'id,alternative_titles,title,start_date,main_picture,status,' + self.total_str + + fields = 'id,alternative_titles,title,start_date,main_picture,status,broadcast,' + self.total_str listfields = 'score,status,start_date,finish_date,' + self.watched_str params = { 'fields': '%s,list_status{%s}' % (fields, listfields), 'limit': self.library_page_limit, 'nsfw': 'true' } - + url = "{}/users/@me/{}list?{}".format(self.query_url, self.mediatype, urllib.parse.urlencode(params)) i = 1 - + while url: self.msg.info('Downloading list (page %d)...' % i) data = self._request('GET', url, auth=True) @@ -286,8 +286,9 @@ def fetch_list(self): 'my_status': item['list_status']['status'], 'my_start_date': self._str2date(item['list_status'].get('start_date')), 'my_finish_date': self._str2date(item['list_status'].get('finish_date')), + 'next_ep_time': self._next_episode_to_utc_datetime(item['node']) }) - + url = data['paging'].get('next') i += 1 @@ -302,49 +303,50 @@ def update_show(self, item): self.check_credentials() self.msg.info("Updating item %s..." % item['title']) self._update_entry(item) - + def delete_show(self, item): self.check_credentials() self.msg.info("Deleting item %s..." % item['title']) - data = self._request('DELETE', self.query_url + '/%s/%d/my_list_status' % (self.mediatype, item['id']), auth=True) - + data = self._request('DELETE', self.query_url + '/%s/%d/my_list_status' % (self.mediatype, item['id']), + auth=True) + def search(self, criteria, method): self.check_credentials() self.msg.info("Searching for {}...".format(criteria)) - + fields = 'alternative_titles,end_date,genres,id,main_picture,mean,media_type,' + self.total_str + ',popularity,rating,start_date,status,studios,synopsis,title' params = {'fields': fields, 'nsfw': 'true'} - + if method == utils.SearchMethod.KW: url = '/%s' % self.mediatype params['q'] = criteria params['limit'] = self.search_page_limit elif method == utils.SearchMethod.SEASON: - season, season_year = criteria + season, season_year = criteria url = '/%s/season/%d/%s' % (self.mediatype, season_year, self.season_translate[season]) params['limit'] = self.season_page_limit else: raise utils.APIError("Invalid search method.") - + results = [] data = self._request('GET', self.query_url + url, get=params, auth=True) for item in data['data']: results.append(self._parse_info(item['node'])) - + self._emit_signal('show_info_changed', results) return results - + def request_info(self, itemlist): self.check_credentials() infolist = [] - + fields = 'alternative_titles,end_date,genres,id,main_picture,mean,media_type,' + self.total_str + ',popularity,rating,start_date,status,studios,synopsis,title' params = {'fields': fields, 'nsfw': 'true'} for item in itemlist: data = self._request('GET', self.query_url + '/%s/%d' % (self.mediatype, item['id']), get=params, auth=True) infolist.append(self._parse_info(data)) - + self._emit_signal('show_info_changed', infolist) return infolist @@ -361,17 +363,19 @@ def _update_entry(self, item): if 'my_finish_date' in item: values['finish_date'] = item['my_finish_date'] or "" - data = self._request('PATCH', self.query_url + '/%s/%d/my_list_status' % (self.mediatype, item['id']), post=values, auth=True) + data = self._request('PATCH', self.query_url + '/%s/%d/my_list_status' % (self.mediatype, item['id']), + post=values, auth=True) def _get_aliases(self, item): - aliases = [item['alternative_titles']['en'], item['alternative_titles']['ja']] + item['alternative_titles']['synonyms'] - + aliases = [item['alternative_titles']['en'], item['alternative_titles']['ja']] + item['alternative_titles'][ + 'synonyms'] + return aliases - + def _parse_info(self, item): info = utils.show() showid = item['id'] - + info.update({ 'id': showid, 'title': item['title'], @@ -384,27 +388,94 @@ def _parse_info(self, item): 'start_date': self._str2date(item.get('start_date')), 'end_date': self._str2date(item.get('end_date')), 'extra': [ - ('English', item['alternative_titles'].get('en')), - ('Japanese', item['alternative_titles'].get('ja')), - ('Synonyms', item['alternative_titles'].get('synonyms')), - ('Synopsis', item.get('synopsis')), - ('Type', item.get('media_type')), - ('Mean score', item.get('mean')), - ('Status', self._translate_status(item['status'])), + ('English', item['alternative_titles'].get('en')), + ('Japanese', item['alternative_titles'].get('ja')), + ('Synonyms', item['alternative_titles'].get('synonyms')), + ('Synopsis', item.get('synopsis')), + ('Type', item.get('media_type')), + ('Mean score', item.get('mean')), + ('Status', self._translate_status(item['status'])), ] }) - + return info - + def _translate_status(self, orig_status): return self.status_translate.get(orig_status, utils.Status.UNKNOWN) - def _str2date(self, string): + @staticmethod + def _str2date(string): if string is None: return None try: return datetime.datetime.strptime(string, "%Y-%m-%d") - except Exception: - self.msg.debug('Invalid date {}'.format(string)) + except ValueError: return None # Ignore date if it's invalid + + @classmethod + def _next_episode_to_utc_datetime(cls, mal_response_node: dict) -> datetime: + """MAL is pretty rubbish, and does not provide an actual date for the next episode. + Instead, it gives a data for when the show started airing, and the general broadcast schedule as a string. + E.g.: {'day_of_the_week': 'friday', 'start_time': '23:00'} — *this is assumed to be JST* + It does not update for when a show goes on hiatus or anything like that. + + This function takes that string and assumes that the 'day of the week' is this week for an airing show. + There is no other way to do this without a 3rd party site. If the show is not airing, this does not matter, + as MAL doesn't return anything anyway.""" + airing_status = mal_response_node['status'] # Get the status of the show + next_broadcast_utc = None # Default 'Next episode' value, in case MAL doesn't return anything + + # Define JST time-zone and current datetime in JST (there is no DST) + jst_timezone = datetime.timezone(datetime.timedelta(hours=9)) + current_jst_time = datetime.datetime.now(tz=jst_timezone) + + # Potentially return the 'Next episode' time in UTC depending on the status + match airing_status: + case 'finished_airing': # There are no new episodes + pass + + case 'currently_airing': # The UTC datetime object needs to be stitched together + broadcast_response = mal_response_node.get('broadcast') # Returned by MAL + if broadcast_response is not None: + # Read and strip MAL response, generally looks like this (given in JST): 'friday 23:00' + jst_broadcast_time_day_str = broadcast_response['day_of_the_week'] # 'friday' + jst_broadcast_time_time_str = broadcast_response['start_time'] # '23:00' + + if jst_broadcast_time_time_str is not None: + jst_broadcast_time_hour, jst_broadcast_time_minute = ( + map(int, jst_broadcast_time_time_str.split(':'))) + + else: # Sometimes MAL doesn't have an exact time entry + jst_broadcast_time_hour, jst_broadcast_time_minute = ('0', '0') + + # Map weekday names to corresponding indices. Lowercase because MAL uses lowercase for some reason + weekday_indices = {'monday': 1, 'tuesday': 2, 'wednesday': 3, 'thursday': 4, 'friday': 5, + 'saturday': 6, 'sunday': 7} + + # Calculate the days until the next broadcast weekday + days_until_next_broadcast = (weekday_indices[jst_broadcast_time_day_str] + - current_jst_time.isoweekday() + 7) % 7 + + # Set the correct day and time + next_broadcast_jst = current_jst_time + datetime.timedelta(days=days_until_next_broadcast) + next_broadcast_jst = next_broadcast_jst.replace(hour=jst_broadcast_time_hour, + minute=jst_broadcast_time_minute, + second=0, microsecond=0) + + # Convert to UTC + next_broadcast_utc = next_broadcast_jst.astimezone(datetime.timezone.utc) + + # These shows are a mess and return one of the following formats: 'None'/'2024'/'2024 04'/'2024 04 12' + case 'not_yet_aired': + # _str2date returns 'None' for anything that's not '%Y-%m-%d' compliant, so we don't have to check + next_broadcast_jst = cls._str2date(mal_response_node.get('start_date')) + if next_broadcast_jst is not None: + # Assign JST timezone, set the broadcast time to 12pm, then convert to UTC + next_broadcast_jst = next_broadcast_jst.replace(tzinfo=jst_timezone, hour=12, microsecond=0) + next_broadcast_utc = next_broadcast_jst.astimezone(tz=datetime.timezone.utc) + + case _: + raise ValueError + + return next_broadcast_utc diff --git a/trackma/ui/gtk/showtreeview.py b/trackma/ui/gtk/showtreeview.py index 1be94b2f..bc13650d 100644 --- a/trackma/ui/gtk/showtreeview.py +++ b/trackma/ui/gtk/showtreeview.py @@ -33,7 +33,7 @@ class TreeConstants: # If no number is provided by the lib, 1 episode / week is assumed and calculated accordingly AVAILABLE_EPS = 8 # Number of available episodes in the local library COLOR = 9 # Used with the _get_color method to return the color preset for a show - PROGRESS_PERCENTAGE = 10 # % of episodes watched. 7 / 13 -> + PROGRESS_PERCENTAGE = 10 # % of episodes watched. 7 / 13 -> 53 START_DATE = 11 # Start date of the show END_DATE = 12 # End date of the show MY_START_DATE = 13 # Date when the user started watching the show @@ -138,25 +138,25 @@ def append(self, show, altname=None, eps=None): next_episode_air_time_relative = utils.calculate_relative_time(show['next_ep_time'], utc=True, fulltime=False) - # Corresponds to __cols, but is used locally to store the data before appending. - row = [show['id'], - title_str, - show['my_progress'], - show['my_score'], - watched_episodes_fraction, - score_str, - show['total'], - aired_eps, - available_eps, - self._get_color(show, available_eps), - progress, - start_date, - end_date, - my_start_date, - my_finish_date, - show['my_status'], - show['status'], - next_episode_air_time_relative, + # Corresponds to __cols, but is used locally to store the data before appending. Comments are TreeConstants + row = [show['id'], # SHOW_ID + title_str, # TITLE + show['my_progress'], # MY_PROGRESS + show['my_score'], # MY_SCORE + watched_episodes_fraction, # WATCHED_EPISODES_FRACTION + score_str, # MY_SCORE_STRING + show['total'], # TOTAL_EPS + aired_eps, # AIRED_EPS + available_eps, # AVAILABLE_EPS + self._get_color(show, available_eps), # COLOR + progress, # PROGRESS_PERCENTAGE + start_date, # START_DATE + end_date, # END_DATE + my_start_date, # MY_START_DATE + my_finish_date, # MY_FINISH_DATE + show['my_status'], # MY_STATUS + show['status'], # SHOW_STATUS + next_episode_air_time_relative, # NEXT_EPISODE_AIR_TIME_RELATIVE ] super().append(row) @@ -361,12 +361,12 @@ def __init__(self, colors, visible_columns, status, _list, progress_style=1): w = self.cols[name].get_widget() while not isinstance(w, Gtk.Button): w = w.get_parent() - w.connect('button-press-event', self._header_button_press) + w.connect('button-press-event', self._header_right_click) # Appends populated columns self.append_column(self.cols[name]) - def _header_button_press(self, button, event): + def _header_right_click(self, button, event): if event.button == 3: menu = Gtk.Menu() for name, sort in self.available_columns: diff --git a/trackma/ui/qt/models.py b/trackma/ui/qt/models.py index d2fdb58c..a1640ba7 100644 --- a/trackma/ui/qt/models.py +++ b/trackma/ui/qt/models.py @@ -85,7 +85,7 @@ def _calculate_color(self, row, show): def _calculate_next_ep(self, row, show): if self.mediainfo.get('date_next_ep') and show['next_ep_time'] is not None: delta = show['next_ep_time'].replace(tzinfo=datetime.timezone.utc) - \ - datetime.datetime.now(datetime.timezone.utc) # Make sure it's UTC + datetime.datetime.now(tz=datetime.timezone.utc) # 'next_ep_time has to be UTC' by definition self.next_ep[row] = "%i days, %02d hrs." % ( delta.days, delta.seconds / 3600) elif row in self.next_ep: diff --git a/trackma/utils.py b/trackma/utils.py index f6e2a10b..45e4d160 100644 --- a/trackma/utils.py +++ b/trackma/utils.py @@ -510,7 +510,7 @@ def show(): 'image': '', 'image_thumb': '', 'queued': False, - 'next_ep_time': None + 'next_ep_time': None # Must be a time-zone aware datetime object in UTC } @@ -522,8 +522,8 @@ def calculate_relative_time(time_end: datetime, utc: bool, fulltime: bool = True try: if utc: time_end = time_end.replace(tzinfo=datetime.timezone.utc) - # Make sure that time_end is timezone aware in UTC - current_time = datetime.datetime.now(datetime.timezone.utc) + # Make sure that time_end is time-zone aware in UTC + current_time = datetime.datetime.now(tz=datetime.timezone.utc) else: current_time = datetime.datetime.now() time_difference = time_end - current_time From 96facff389ab20918f3bf3e4aa179ffecb6029fa Mon Sep 17 00:00:00 2001 From: BigBoyBarney <barni.benke99@gmail.com> Date: Thu, 11 Jan 2024 00:16:43 +0100 Subject: [PATCH 16/16] Fixed comment typo --- trackma/lib/libmal.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/trackma/lib/libmal.py b/trackma/lib/libmal.py index d03aadcc..d173a2d9 100644 --- a/trackma/lib/libmal.py +++ b/trackma/lib/libmal.py @@ -418,13 +418,14 @@ def _str2date(string): @classmethod def _next_episode_to_utc_datetime(cls, mal_response_node: dict) -> datetime: """MAL is pretty rubbish, and does not provide an actual date for the next episode. - Instead, it gives a data for when the show started airing, and the general broadcast schedule as a string. + Instead, it gives a date for when the show started airing, and the general broadcast schedule as a string. E.g.: {'day_of_the_week': 'friday', 'start_time': '23:00'} — *this is assumed to be JST* It does not update for when a show goes on hiatus or anything like that. - This function takes that string and assumes that the 'day of the week' is this week for an airing show. - There is no other way to do this without a 3rd party site. If the show is not airing, this does not matter, - as MAL doesn't return anything anyway.""" + This function takes that string and assumes that the 'day of the week' is the upcoming weekday for an airing + show. There is no other way to do this without a 3rd party site. If the show is not airing, this does not + matter, as MAL doesn't return anything anyway.""" + airing_status = mal_response_node['status'] # Get the status of the show next_broadcast_utc = None # Default 'Next episode' value, in case MAL doesn't return anything
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor