Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
43a2bfa
Rework page loading
Nokse22 Aug 4, 2025
db9a640
Add HTShortcutsWidget for new shortcuts in the home page
Nokse22 Aug 4, 2025
9b81013
Start to unify pages in a single one
Nokse22 Aug 4, 2025
561098c
Fix issue when favouriting with new tidalapi version
Nokse22 Aug 4, 2025
24749e4
Add caching (Fix #101)
Nokse22 Aug 6, 2025
4e06a57
Siplify ExplorePage by inheriting from GenericPage
Nokse22 Aug 6, 2025
c008894
Use cache when we can
Nokse22 Aug 6, 2025
add8ea6
Add import for HTCache
Nokse22 Aug 6, 2025
09b7886
Add connect_signal function
Nokse22 Aug 6, 2025
7be9f4c
Fix bug when creator is None
Nokse22 Aug 6, 2025
a0dc0ef
Add and improve docstrings
Nokse22 Aug 7, 2025
7b0b553
Add almost all docstrings
Nokse22 Aug 7, 2025
39d5258
Remove download action, rename app object
Nokse22 Aug 7, 2025
ebde0b8
Set page title to empty string if page title is None
Nokse22 Aug 7, 2025
d810e15
General formatting
Nokse22 Aug 7, 2025
c506fb4
Add missing window docstrings
Nokse22 Aug 7, 2025
5ee0748
carousel: improve API and docstrings
Nokse22 Aug 7, 2025
b2ef8f7
page: remove unused functions
Nokse22 Aug 7, 2025
29eecdc
track widget: improve API
Nokse22 Aug 7, 2025
2dc576c
add .new_from_id classmethod to Page for all pages that need an id
Nokse22 Aug 7, 2025
ee4cae5
Add more type hints
Nokse22 Aug 8, 2025
97cef82
remove __repr__ from some objects
Nokse22 Aug 8, 2025
e3c1b5d
Fixed bug when playing a song from auto load widget
Nokse22 Aug 8, 2025
2722b2a
Revert add Shortcuts
Nokse22 Aug 8, 2025
5329391
Fix bio not showing in artist page
Nokse22 Aug 8, 2025
69c4625
Apply suggested changes
Nokse22 Aug 8, 2025
f9a99a4
Improve typing, remove *args: Any
Nokse22 Aug 8, 2025
49dc237
Fix import error in login
Nokse22 Aug 8, 2025
6a7a50e
Fix explore page not showing featured items
Nokse22 Aug 8, 2025
1f579df
Fix Featured in explore page
Nokse22 Aug 9, 2025
3b13037
Fix docstring
Nokse22 Aug 9, 2025
96b61e6
Fix link carousel buttons not working
Nokse22 Aug 9, 2025
d76b80c
utils: improve docstring
Nokse22 Aug 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 36 additions & 10 deletions src/disconnectable_iface.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
# SPDX-License-Identifier: GPL-3.0-or-later


from typing import List, Tuple, Any


class IDisconnectable:
"""
A class that provides automatic resource cleanup for GTK widgets and other objects.
Expand All @@ -32,16 +35,14 @@ class IDisconnectable:
1. Inherit from IDisconnectable alongside your main class:

>>> class MyWidget(Gtk.Box, IDisconnectable):
... def __init__(self):
... Gtk.Box.__init__(self)
... IDisconnectable.__init__(self)
... def __init__(self):
... Gtk.Box.__init__(self)
... IDisconnectable.__init__(self)

2. When connecting signals, store them in self.signals as a tuple of the object
and the handler id:

>>> self.signals.append((
... some_object,
... some_object.connect("signal-name", callback)))
>>> self.signals.append((some_object, some_object.connect("signal-name", callback)))

3. When creating bindings, store them in self.bindings:

Expand All @@ -54,13 +55,35 @@ class IDisconnectable:

5. Call disconnect_all() when the widget is being destroyed:
"""

def __init__(self) -> None:
self.signals = []
self.bindings = []
self.disconnectables = []
self.signals: List[Tuple[Any, int]] = []
self.bindings: List[Any] = []
self.disconnectables: List["IDisconnectable"] = []

def connect_signal(
self, g_object: Any, signal_name: str, callback_func: Any, *args
) -> None:
"""Connect a signal and track it for later disconnection.

Args:
g_object: The GObject to connect the signal to
signal_name (str): Name of the signal to connect
callback_func: The callback function to execute when signal is emitted
*args: Additional arguments to pass to the callback function
"""
self.signals.append((
g_object,
g_object.connect(signal_name, callback_func, *args),
))

def disconnect_all(self, *_args) -> None:
"""Disconnects all signals so that the class can be deleted"""
"""Disconnect all tracked signals and child disconnectable objects.

This method should be called when the widget is being removed to ensure
proper cleanup. It disconnects all tracked signal connections and
recursively calls disconnect_all on child disconnectable objects.
"""

for obj, signal_id in self.signals:
if obj.handler_is_connected(signal_id):
Expand All @@ -85,5 +108,8 @@ def disconnect_all(self, *_args) -> None:
self.bindings = []
self.disconnectables = []

def __repr__(self, *args) -> str | None:
return self.__gtype_name__ if self.__gtype_name__ else None

# def __del__(self):
# print(f"DELETING {self}")
1 change: 1 addition & 0 deletions src/lib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
from .utils import *
from .secret_storage import SecretStore
from .discord_rpc import *
from .cache import HTCache
112 changes: 112 additions & 0 deletions src/lib/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# cache.py
#
# Copyright 2025 Nokse <[email protected]>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from typing import Dict, Any

from tidalapi.artist import Artist
from tidalapi.album import Album
from tidalapi.media import Track
from tidalapi.playlist import Playlist
from tidalapi.mix import Mix


class HTCache:
artists: Dict[str, Artist] = {}
albums: Dict[str, Album] = {}
tracks: Dict[str, Track] = {}
playlists: Dict[str, Playlist] = {}
mixes: Dict[str, Mix] = {}

def __init__(self, session: Any) -> None:
self.session = session

def get_artist(self, artist_id: str) -> Artist:
"""Get an artist from cache or fetch from TIDAL API if not cached.

Args:
artist_id (str): The TIDAL artist ID

Returns:
Artist: The artist object from TIDAL API
"""
if artist_id in self.artists:
return self.artists[artist_id]
artist = Artist(self.session, artist_id)
self.artists[artist_id] = artist
return artist

def get_album(self, album_id: str) -> Album:
"""Get an album from cache or fetch from TIDAL API if not cached.

Args:
album_id (str): The TIDAL album ID

Returns:
Album: The album object from TIDAL API
"""
if album_id in self.albums:
return self.albums[album_id]
album = Album(self.session, album_id)
self.albums[album_id] = album
return album

def get_track(self, track_id: str) -> Track:
"""Get a track from cache or fetch from TIDAL API if not cached.

Args:
track_id (str): The TIDAL track ID

Returns:
Track: The track object from TIDAL API
"""
if track_id in self.tracks:
return self.tracks[track_id]
track = Track(self.session, track_id)
self.tracks[track_id] = track
return track

def get_playlist(self, playlist_id: str) -> Playlist:
"""Get a playlist from cache or fetch from TIDAL API if not cached.

Args:
playlist_id (str): The TIDAL playlist ID

Returns:
Playlist: The playlist object from TIDAL API
"""
if playlist_id in self.playlists:
return self.playlists[playlist_id]
playlist = Playlist(self.session, playlist_id)
self.playlists[playlist_id] = playlist
return playlist

def get_mix(self, mix_id: str) -> Mix:
"""Get a mix from cache or fetch from TIDAL API if not cached.

Args:
mix_id (str): The TIDAL mix ID

Returns:
Mix: The mix object from TIDAL API
"""
if mix_id in self.mixes:
return self.mixes[mix_id]
mix = Mix(self.session, mix_id)
self.mixes[mix_id] = mix
return mix
31 changes: 27 additions & 4 deletions src/lib/discord_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,15 @@ class State(Enum):
disconnect_thread: threading.Thread | None = None


def connect():
def connect() -> bool:
"""Connect to Discord Rich Presence IPC.

Attempts to establish a connection to Discord's IPC server for
Rich Presence functionality.

Returns:
bool: True if connection successful, False otherwise
"""
global state

if not has_pypresence:
Expand All @@ -45,7 +53,14 @@ def connect():
return True


def disconnect():
def disconnect() -> bool:
"""Disconnect from Discord Rich Presence IPC.

Closes the connection to Discord's IPC server and updates the state.

Returns:
bool: True if disconnection successful, False otherwise
"""
global state

if not has_pypresence:
Expand All @@ -62,7 +77,15 @@ def disconnect():
return True


def set_activity(track: Track | None = None, offset_ms: int = 0):
def set_activity(track: Track | None = None, offset_ms: int = 0) -> None:
"""Set the Discord Rich Presence activity status.

Updates Discord with the current playing track information and playback position.

Args:
track: The currently playing Track object, or None to clear activity
offset_ms: Current playback position in milliseconds (default: 0)
"""
global state
global disconnect_thread

Expand Down Expand Up @@ -90,7 +113,7 @@ def set_activity(track: Track | None = None, offset_ms: int = 0):
)
state = State.IDLE

def disconnect_function():
def disconnect_function() -> None:
for _ in range(5 * 60):
time.sleep(1)
if state != State.IDLE:
Expand Down
Loading