Skip to content

Commit

Permalink
Implemented preferences window
Browse files Browse the repository at this point in the history
- localProvider: on delete also remove related files
- tmdbProvider: information is retrieved in the selected language if available
- content is automatically updated after a certain period (selectable in preferences)
- background activities: indicator and cannot exit app while in progress
  • Loading branch information
aleiepure committed Sep 3, 2023
1 parent 9bb08a0 commit 87a7a48
Show file tree
Hide file tree
Showing 15 changed files with 653 additions and 48 deletions.
2 changes: 2 additions & 0 deletions data/icons/symbolic/right-symbolic.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions data/me.iepure.Ticketbooth.gschema.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,24 @@ SPDX-License-Identifier: GPL-3.0-or-later
<key name="offline-mode" type="b">
<default>false</default>
</key>
<key name="update-freq" type="s">
<choices>
<choice value="day" />
<choice value="week" />
<choice value="month" />
<choice value="never" />
</choices>
<default>"week"</default>
<summary>Frequency to check for new data on TMDB</summary>
</key>
<key name="last-update" type="s">
<default>"1970-01-01"</default>
<summary>Last autoupdate date</summary>
</key>
<key name="exit-remove-cache" type="b">
<default>true</default>
<summary>Clear cache on exit</summary>
</key>

</schema>
</schemalist>
263 changes: 258 additions & 5 deletions src/preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,274 @@

import glob
import os
from gettext import gettext as _
from pathlib import Path

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

from . import shared # type: ignore
from .providers.local_provider import LocalProvider as local


@Gtk.Template(resource_path=shared.PREFIX + '/ui/preferences.ui')
class PreferencesWindow(Adw.PreferencesWindow):
__gtype_name__ = 'PreferencesWindow'

_language_comborow = Gtk.Template.Child()
_language_model = Gtk.Template.Child()
_update_freq_comborow = Gtk.Template.Child()
_offline_switch = Gtk.Template.Child()
_housekeeping_group = Gtk.Template.Child()
_exit_cache_row = Gtk.Template.Child()
_cache_row = Gtk.Template.Child()
_data_row = Gtk.Template.Child()
_clear_cache_dialog = Gtk.Template.Child()
_clear_data_dialog = Gtk.Template.Child()
_movies_row = Gtk.Template.Child()
_movies_checkbtn = Gtk.Template.Child()
_series_row = Gtk.Template.Child()
_series_checkbtn = Gtk.Template.Child()

def __init__(self):
super().__init__()
self.language_change_handler = self._language_comborow.connect('notify::selected', self._on_language_changed)
self._update_freq_comborow.connect('notify::selected', self._on_freq_changed)

shared.schema.bind('offline-mode', self._offline_switch, 'active', Gio.SettingsBindFlags.DEFAULT)
shared.schema.bind('exit-remove-cache', self._exit_cache_row, 'active', Gio.SettingsBindFlags.DEFAULT)

@Gtk.Template.Callback('_on_map')
def _on_map(self, user_data: object | None) -> None:
"""
Callback for the "map" signal.
Populates dropdowns and checks if an automatic update of the content is due.

Args:
user_data (object or None): user data passed to the callback.

Returns:
None
"""

# Languages dropdown
self._language_comborow.handler_block(self.language_change_handler)

languages = local.get_all_languages()
languages.pop(len(languages)-6) # remove 'no language'
for language in languages:
self._language_model.append(language.name)

self._language_comborow.set_selected(self._get_selected_language_index(shared.schema.get_string('tmdb-lang')))
self._language_comborow.handler_unblock(self.language_change_handler)

# Update frequency dropdown
match shared.schema.get_string('update-freq'):
case 'never':
self._update_freq_comborow.set_selected(0)
case 'day':
self._update_freq_comborow.set_selected(1)
case 'week':
self._update_freq_comborow.set_selected(2)
case 'month':
self._update_freq_comborow.set_selected(3)

# Update check
self._update_occupied_space()

def _on_language_changed(self, pspec: GObject.ParamSpec, user_data: object | None) -> None:
"""
Callback for "notify::selected" signal.
Updates the prefered TMDB language in GSettings.

Args:
pspec (GObject.ParamSpec): The GParamSpec of the property which changed
user_data (object or None): additional data passed to the callback

Returns:
None
"""

language = self._get_selected_language(self._language_comborow.get_selected_item().get_string())
shared.schema.set_string('tmdb-lang', language)

def _on_freq_changed(self, pspec, user_data: object | None) -> None:
"""
Callback for "notify::selected" signal.
Updates the frequency for content updates in GSettings.

Args:
pspec (GObject.ParamSpec): The GParamSpec of the property which changed
user_data (object or None): additional data passed to the callback

Returns:
None
"""

freq = self._update_freq_comborow.get_selected()
match freq:
case 0:
shared.schema.set_string('update-freq', 'never')
case 1:
shared.schema.set_string('update-freq', 'day')
case 2:
shared.schema.set_string('update-freq', 'week')
case 3:
shared.schema.set_string('update-freq', 'month')

def _get_selected_language_index(self, iso_name: str) -> int:
"""
Loops all available languages and returns the index of the one with the specified iso name. If a result is not found, it returns the index for English (37).

Args:
iso_name: a language's iso name

Return:
int with the index
"""

for idx, language in enumerate(local.get_all_languages()):
if language.iso_name == iso_name:
return idx
return 37

def _get_selected_language(self, name: str) -> str:
"""
Loops all available languages and returns the iso name of the one with the specified name. If a result is not found, it returns the iso name for English (en).

Args:
name: a language's name

Return:
str with the iso name
"""

for language in local.get_all_languages():
if language.name == name:
return language.iso_name
return 'en'

@Gtk.Template.Callback('_on_clear_cache_activate')
def _on_clear_cache_activate(self, user_data: object | None) -> None:
"""
Callback for "activated" signal.
Shows a confirmation dialog to the user.

Args:
user_data (object or None): additional data passed to the callback

@Gtk.Template.Callback('_on_clear_cache_btn_clicked')
def _on_clear_cache_btn_clicked(self, user_data: GObject.GPointer):
files = glob.glob('poster-*.jpg', root_dir=GLib.get_tmp_dir())
Returns:
None
"""

self._clear_cache_dialog.choose(None, self._on_cache_message_dialog_choose, None)

def _on_cache_message_dialog_choose(self,
source: GObject.Object | None,
result: Gio.AsyncResult,
user_data: object | None) -> None:
"""
Callback for the message dialog.
Finishes the async operation and retrieves the user response. If the later is positive, deletes the stored cached data.

Args:
source (Gtk.Widget): object that started the async operation
result (Gio.AsyncResult): a Gio.AsyncResult
user_data (object or None): additional data passed to the callback

Returns:
None
"""

result = Adw.MessageDialog.choose_finish(source, result)
if result == 'cache_cancel':
return

files = glob.glob('*.jpg', root_dir=shared.cache_dir)
for file in files:
os.remove(GLib.get_tmp_dir() + '/' + file)
os.remove(shared.cache_dir / file)

self._update_occupied_space()

@Gtk.Template.Callback('_on_clear_activate')
def _on_clear_btn_clicked(self, user_data: object | None) -> None:
"""
Callback for "activated" signal.
Shows a confirmation dialog to the user.

Args:
user_data (object or None): additional data passed to the callback

Returns:
None
"""

self._movies_row.set_subtitle(_('{number} Titles').format(number=len(local.get_all_movies()))) # type: ignore
self._series_row.set_subtitle(_('{number} Titles').format(number=len(local.get_all_series()))) # type: ignore

self._clear_data_dialog.choose(None, self._on_data_message_dialog_choose, None)

def _on_data_message_dialog_choose(self,
source: GObject.Object | None,
result: Gio.AsyncResult,
user_data: object | None) -> None:
"""
Callback for the message dialog.
Finishes the async operation and retrieves the user response. If the later is positive, deletes the selected data.

Args:
source (Gtk.Widget): object that started the async operation
result (Gio.AsyncResult): a Gio.AsyncResult
user_data (object or None): additional data passed to the callback

Returns:
None
"""

result = Adw.MessageDialog.choose_finish(source, result)
if result == 'cache_cancel':
return

# Movies
if self._movies_checkbtn.get_active():
for movie in local.get_all_movies(): # type: ignore
local.delete_movie(movie.id)

# TV Series
if self._series_checkbtn.get_active():
for serie in local.get_all_series(): # type: ignore
local.delete_series(serie.id)

self._update_occupied_space()
self.get_transient_for().activate_action('win.refresh', None)

def _calculate_space(self, directory: Path) -> float:
"""
Given a directory, calculates the total space occupied on disk.

Args:
directory (Path): the directory to measure

Returns:
float with space occupied in MegaBytes (MB)
"""

return sum(file.stat().st_size for file in directory.rglob('*'))/1024.0/1024.0

def _update_occupied_space(self) -> None:
"""
After calculating space occupied by cache and data, updates the ui labels to reflect the values.

Args:
None

Returns:
None
"""

cache_space = self._calculate_space(shared.cache_dir)
data_space = self._calculate_space(shared.data_dir)

self._housekeeping_group.set_description(
_('Ticket Booth is currently using {total_space:.2f}MB. Use the options below to free some space.').format(total_space=cache_space+data_space))
self._cache_row.set_subtitle(_('{space:.2f}MB occupied').format(space=cache_space))
self._data_row.set_subtitle(_('{space:.2f}MB occupied').format(space=data_space))
26 changes: 24 additions & 2 deletions src/providers/local_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#
# SPDX-License-Identifier: GPL-3.0-or-later

import os
import shutil
import sqlite3
from typing import List

Expand Down Expand Up @@ -414,7 +416,7 @@ def mark_watched_movie(id: int, watched: bool) -> int | None:
@staticmethod
def delete_movie(id: int) -> int | None:
"""
Deletes the movie with the provided id.
Deletes the movie with the provided id, removing associated files too.

Args:
id (int): movie id to delete
Expand All @@ -423,10 +425,19 @@ def delete_movie(id: int) -> int | None:
int or None containing the id of the last modified row
"""

movie = LocalProvider.get_movie_by_id(id)

if movie.backdrop_path.startswith('file'): # type: ignore
os.remove(movie.backdrop_path[7:]) # type: ignore

if movie.poster_path.startswith('file'): # type: ignore
os.remove(movie.poster_path[7:]) # type: ignore

with sqlite3.connect(shared.db) as connection:
sql = """DELETE FROM movies WHERE id = ?"""
result = connection.cursor().execute(sql, (id,))
connection.commit()

return result.lastrowid

@staticmethod
Expand Down Expand Up @@ -552,7 +563,7 @@ def mark_watched_series(id: int, watched: bool) -> int | None:
@staticmethod
def delete_series(id: int) -> int | None:
"""
Deletes the tv series with the provided id.
Deletes the tv series with the provided id, removing associated files too.

Args:
id (int): tv series id to delete
Expand All @@ -561,6 +572,17 @@ def delete_series(id: int) -> int | None:
int or None containing the id of the last modified row
"""

series = LocalProvider.get_series_by_id(id)

if series.backdrop_path.startswith('file'): # type: ignore
os.remove(series.backdrop_path[7:]) # type: ignore

if series.poster_path.startswith('file'): # type: ignore
os.remove(series.poster_path[7:]) # type: ignore

if (shared.series_dir/id).is_dir():
shutil.rmtree(shared.series_dir / id)

with sqlite3.connect(shared.db) as connection:
connection.cursor().execute('PRAGMA foreign_keys = ON;')

Expand Down
Loading

0 comments on commit 87a7a48

Please sign in to comment.