Skip to content

Commit

Permalink
Fixed random crashes, freezes and UI blanking
Browse files Browse the repository at this point in the history
- Implemented more logging
- Background activities now have success/error status
  • Loading branch information
aleiepure committed Oct 2, 2023
1 parent 1eea79b commit 89bb326
Show file tree
Hide file tree
Showing 15 changed files with 441 additions and 181 deletions.
2 changes: 2 additions & 0 deletions data/icons/symbolic/warning-symbolic.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 53 additions & 11 deletions src/background_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,18 @@ class BackgroundActivity(GObject.GObject):

title = GObject.Property(type=str, default='')
activity_type = GObject.Property(type=str, default='')
callback = GObject.Property(type=object, default=None)
task_function = GObject.Property(type=object, default=None)
completed = GObject.Property(type=bool, default=False)
has_error = GObject.Property(type=bool, default=False)

def __init__(self, activity_type: ActivityType, title: str = '', callback: Callable | None = None):
def __init__(self, activity_type: ActivityType, title: str = '', task_function: Callable | None = None):
super().__init__()
self.activity_type = activity_type.name
self.title = title
self.callback = callback
self.task_function = task_function
self._cancellable = Gio.Cancellable()

def start(self) -> None:
def start(self, on_done: Callable) -> None:
"""
Runs self.callback in a separate thread. The callback must call end() to mark the activity as completed.
Expand All @@ -62,11 +64,38 @@ def start(self) -> None:
None
"""

try:
GLib.Thread.try_new(self.title, self.callback, self)
except GLib.Error as err:
shared.logging.error(err)
raise err
task = Gio.Task.new(self, None, on_done, self._cancellable, self)
task.set_return_on_cancel(True)
task.run_in_thread(self._run_in_thread)

def _run_in_thread(self,
task: Gio.Task,
source_object: GObject.Object,
task_data: object | None,
cancelable: Gio.Cancellable):
"""Callback to run self.task_function in a thread"""

if task.return_error_if_cancelled():
return
outcome = self.task_function(self) # type: ignore
task.return_value(outcome)

def activity_finish(self, result: Gio.AsyncResult, caller: GObject.Object):
"""
Completes the async operation and marks the activity as completed.
Args:
None
Returns:
None
"""

if not Gio.Task.is_valid(result, caller):
return -1

# self.completed = True
return result.propagate_value().value

def end(self) -> None:
"""
Expand All @@ -81,6 +110,19 @@ def end(self) -> None:

self.completed = True

def error(self) -> None:
"""
Marks the activity with an error.
Args:
None
Returns:
None
"""

self.has_error = True


class BackgroundQueue:
"""
Expand All @@ -100,7 +142,7 @@ class BackgroundQueue:
_queue = Gio.ListStore.new(item_type=BackgroundActivity)

@staticmethod
def add(activity: BackgroundActivity) -> None:
def add(activity: BackgroundActivity, on_done: Callable) -> None:
"""
Adds an activity to the queue and starts its execution.
Expand All @@ -112,7 +154,7 @@ def add(activity: BackgroundActivity) -> None:
"""

BackgroundQueue._queue.append(activity)
activity.start()
activity.start(on_done)

@staticmethod
def get_queue() -> Gio.ListStore:
Expand Down
8 changes: 8 additions & 0 deletions src/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@
border-radius: 6px;
}

.progress_complete progress {
background-color: @success_color;
}

.progress_error progress {
background-color: @error_color;
}

/* Theme Switcher
* Modified from https://gitlab.gnome.org/tijder/blueprintgtk
* Original header below
Expand Down
99 changes: 65 additions & 34 deletions src/dialogs/add_manual_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later

import logging
import os
import shutil
from datetime import date, datetime
Expand All @@ -10,7 +11,7 @@
from typing import List
from urllib.parse import unquote

from gi.repository import Adw, GLib, GObject, Gtk
from gi.repository import Adw, Gio, GLib, GObject, Gtk

from .. import shared # type: ignore
from ..background_queue import (ActivityType, BackgroundActivity,
Expand Down Expand Up @@ -83,10 +84,13 @@ def __init__(self,
self.edit_mode = edit_mode
self._content = content

logging.info(f'Edit mode: {self.edit_mode}')
if edit_mode and type(self._content) is MovieModel:
self.set_title(_('Edit Movie'))
logging.debug(f'Editing [Movie] {self._content.title}')
elif edit_mode and type(self._content) is SeriesModel:
self.set_title(_('Edit TV Series'))
logging.debug(f'Editing [TV Series] {self._content.title}')

@Gtk.Template.Callback('_on_map')
def _on_map(self, user_data: object | None) -> None:
Expand All @@ -104,26 +108,30 @@ def _on_map(self, user_data: object | None) -> None:
self._overview_text.remove_css_class('view')

languages = local.get_all_languages()
languages.insert(0, languages.pop(len(languages)-6)) # move 'no language' to 1st place
# move 'no language' to 1st place
languages.insert(0, languages.pop(len(languages)-6))
for language in languages:
self._language_model.append(language.name)

self._poster.set_blank_image(f'resource://{shared.PREFIX}/blank_poster.jpg')
self._poster.set_blank_image(
f'resource://{shared.PREFIX}/blank_poster.jpg')

if not self._content:
if shared.schema.get_string('win-tab') == 'movies':
self._movies_btn.set_active(True)
else:
self._series_btn.set_active(True)

self._release_date_menu_btn.set_label(self._calendar.get_date().format('%x'))
self._release_date_menu_btn.set_label(
self._calendar.get_date().format('%x'))
return

# Both Movies and TV Series
self._poster.set_image(self._content.poster_path)
self._title_entry.set_text(self._content.title)
self._title_entry.grab_focus()
self._release_date_menu_btn.set_label(date.fromisoformat(self._content.release_date).strftime('%x'))
self._release_date_menu_btn.set_label(
date.fromisoformat(self._content.release_date).strftime('%x'))
self._calendar.select_day(GLib.DateTime.new_from_iso8601(
datetime.fromisoformat(self._content.release_date).isoformat()+'Z'))
self._genres_entry.set_text(', '.join(self._content.genres))
Expand Down Expand Up @@ -264,7 +272,8 @@ def _on_calendar_day_selected(self, user_data: object | None) -> None:
None
"""

self._release_date_menu_btn.set_label(self._calendar.get_date().format('%x'))
self._release_date_menu_btn.set_label(
self._calendar.get_date().format('%x'))

@Gtk.Template.Callback('_on_season_add_btn_clicked')
def _on_season_add_btn_clicked(self, user_data: object | None) -> None:
Expand All @@ -280,7 +289,9 @@ def _on_season_add_btn_clicked(self, user_data: object | None) -> None:
"""

# TRANSLATORS: {num} is the season number
dialog = EditSeasonDialog(self, title=_('Season {num}').format(num=len(self.seasons)+1))
logging.debug('Edit season dialog open')
dialog = EditSeasonDialog(self, title=_(
'Season {num}').format(num=len(self.seasons)+1))
dialog.connect('edit-saved', self._on_edit_saved)
dialog.present()

Expand All @@ -299,6 +310,7 @@ def _on_edit_saved(self, source: Gtk.Widget, title: str, poster_uri: str, episod
None
"""

logging.info(f'Updated {title}, {poster_uri}, {episodes}')
self.seasons.append((title, poster_uri, episodes))
self.update_seasons_ui()

Expand All @@ -316,11 +328,17 @@ def _on_done_btn_clicked(self, user_data: object | None) -> None:
"""

if not self.edit_mode:
BackgroundQueue.add(BackgroundActivity( # TRANSLATORS: {title} is the content's title
ActivityType.ADD, C_('Background activity title', 'Add {title}').format(title=self._title_entry.get_text()), self._add_content_to_db))
BackgroundQueue.add(
activity=BackgroundActivity( # TRANSLATORS: {title} is the content's title
activity_type=ActivityType.ADD,
title=C_('Background activity title', 'Add {title}').format(title=self._title_entry.get_text()), task_function=self._add_content_to_db),
on_done=self._on_add_done)
else:
BackgroundQueue.add(BackgroundActivity( # TRANSLATORS: {title} is the content's title
ActivityType.UPDATE, C_('Background activity title', 'Update {title}').format(title=self._title_entry.get_text()), self._add_content_to_db))
BackgroundQueue.add(
activity=BackgroundActivity( # TRANSLATORS: {title} is the content's title
activity_type=ActivityType.UPDATE,
title=C_('Background activity title', 'Update {title}').format(title=self._title_entry.get_text()), task_function=self._add_content_to_db),
on_done=self._on_add_done)

self.close()

Expand All @@ -345,7 +363,15 @@ def _add_content_to_db(self, activity: BackgroundActivity) -> None:
else:
self._save_series(poster_uri)

self.get_ancestor(Adw.Window).get_transient_for().activate_action('win.refresh', None)
def _on_add_done(self,
source: GObject.Object,
result: Gio.AsyncResult,
cancellable: Gio.Cancellable,
activity: BackgroundActivity):
"""Callback to complete async activity"""

self.get_ancestor(Adw.Window).get_transient_for(
).activate_action('win.refresh', None)
activity.end()

def _save_movie(self, poster_uri: str) -> None:
Expand All @@ -365,24 +391,24 @@ def _save_movie(self, poster_uri: str) -> None:
overview = buffer.get_text(start_iter, end_iter, False)

movie = MovieModel(t=(
datetime.now(), # add date
'', # background
int(self._budget_spinrow.get_value()), # budget
''.join(self._genres_entry.get_text().split()), # genres
local.get_next_manual_movie() if not self.edit_mode else self._content.id, # id # type: ignore
True, # manual
datetime.now(), # add date
'', # background
int(self._budget_spinrow.get_value()), # budget
''.join(self._genres_entry.get_text().split()), # genres
local.get_next_manual_movie() if not self.edit_mode else self._content.id, # id
True, # manual
local.get_language_by_name(self._original_language_comborow.get_selected_item(
).get_string()).iso_name, # type: ignore # original language
self._original_title_entry.get_text(), # original title
overview, # overview
poster_uri, # poster
self._calendar.get_date().format('%Y-%m-%d'), # release date
int(self._revenue_spinrow.get_value()), # revenue
int(self._runtime_spinrow.get_value()), # runtime
self._status_entry.get_text(), # status
self._tagline_entry.get_text(), # tagline
self._title_entry.get_text(), # title
False if not self.edit_mode else self._content.watched # type: ignore # watched
).get_string()).iso_name, # original language
self._original_title_entry.get_text(), # original title
overview, # overview
poster_uri, # poster
self._calendar.get_date().format('%Y-%m-%d'), # release date
int(self._revenue_spinrow.get_value()), # revenue
int(self._runtime_spinrow.get_value()), # runtime
self._status_entry.get_text(), # status
self._tagline_entry.get_text(), # tagline
self._title_entry.get_text(), # title
False if not self.edit_mode else self._content.watched # watched
))

if not self.edit_mode:
Expand Down Expand Up @@ -411,7 +437,8 @@ def _save_series(self, series_poster_uri: str) -> None:

# Create folder to store the images, if needed
if not os.path.exists(f'{shared.series_dir}/{show_id}/{self._increment_manual_id(base_season_id, idx)}'):
os.makedirs(f'{shared.series_dir}/{show_id}/{self._increment_manual_id(base_season_id, idx)}')
os.makedirs(
f'{shared.series_dir}/{show_id}/{self._increment_manual_id(base_season_id, idx)}')

# Copy the season poster
poster_uri = self._copy_image_to_data(season[1],
Expand Down Expand Up @@ -442,7 +469,8 @@ def _save_series(self, series_poster_uri: str) -> None:
watched # watched
)))

base_episode_id = self._increment_manual_id(base_episode_id, len(episodes)+1)
base_episode_id = self._increment_manual_id(
base_episode_id, len(episodes)+1)
season_id = self._increment_manual_id(base_season_id, idx)
season_number = idx+1

Expand Down Expand Up @@ -541,7 +569,8 @@ def _copy_image_to_data(self, src_uri: str, dest_folder: str, filename: str) ->

if src_uri.startswith('file'):
extension = src_uri[src_uri.rindex('.'):]
shutil.copy2(src_uri[7:], f'{dest_folder}/{unquote(filename)}{extension}')
shutil.copy2(
src_uri[7:], f'{dest_folder}/{unquote(filename)}{extension}')
return f'file://{dest_folder}/{unquote(filename)}{extension}'
return src_uri

Expand All @@ -557,12 +586,14 @@ def update_seasons_ui(self) -> None:
"""

# Empty PreferencesGroup
list_box = self._seasons_group.get_first_child().get_last_child().get_first_child() # ugly workaround
list_box = self._seasons_group.get_first_child(
).get_last_child().get_first_child() # ugly workaround
list_box.remove_all()

# Fill PreferencesGroup
for season in self.seasons:
self._seasons_group.add(SeasonExpander(season_title=season[0], poster_uri=season[1], episodes=season[2]))
self._seasons_group.add(SeasonExpander(
season_title=season[0], poster_uri=season[1], episodes=season[2]))

self._enable_save_btn()

Expand Down
32 changes: 19 additions & 13 deletions src/models/episode_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,21 +86,27 @@ def _download_still(self, path: str) -> str:
return f'resource://{shared.PREFIX}/blank_still.jpg'

if not os.path.exists(f'{shared.series_dir}/{self.show_id}/{self.season_number}'):
os.makedirs(f'{shared.series_dir}/{self.show_id}/{self.season_number}')
os.makedirs(
f'{shared.series_dir}/{self.show_id}/{self.season_number}')

files = glob.glob(f'{path[1:-4]}.jpg', root_dir=f'{shared.series_dir}/{self.show_id}/{self.season_number}')
files = glob.glob(
f'{path[1:-4]}.jpg', root_dir=f'{shared.series_dir}/{self.show_id}/{self.season_number}')
if files:
return f'file://{shared.series_dir}/{self.show_id}/{self.season_number}/{files[0]}'

url = f'https://image.tmdb.org/t/p/w500{path}'
r = requests.get(url)
if r.status_code == 200:
with open(f'{shared.series_dir}/{self.show_id}/{self.season_number}{path}', 'wb') as f:
f.write(r.content)

with Image.open(f'{shared.series_dir}/{self.show_id}/{self.season_number}{path}') as img:
img = img.resize((500, 281))
img.save(f'{shared.series_dir}/{self.show_id}/{self.season_number}{path}', 'JPEG')
return f'file://{shared.series_dir}/{self.show_id}/{self.season_number}{path}'

return f'resource://{shared.PREFIX}/blank_still.jpg'
try:
r = requests.get(url)
if r.status_code == 200:
with open(f'{shared.series_dir}/{self.show_id}/{self.season_number}{path}', 'wb') as f:
f.write(r.content)

with Image.open(f'{shared.series_dir}/{self.show_id}/{self.season_number}{path}') as img:
img = img.resize((500, 281))
img.save(
f'{shared.series_dir}/{self.show_id}/{self.season_number}{path}', 'JPEG')
return f'file://{shared.series_dir}/{self.show_id}/{self.season_number}{path}'
else:
return f'resource://{shared.PREFIX}/blank_still.jpg'
except (requests.exceptions.ConnectionError, requests.exceptions.SSLError):
return f'resource://{shared.PREFIX}/blank_still.jpg'
Loading

0 comments on commit 89bb326

Please sign in to comment.