Skip to content

Commit ccc9e35

Browse files
authored
Merge pull request #128
Page loading rework and more
2 parents 793432a + d76b80c commit ccc9e35

36 files changed

+1552
-901
lines changed

src/disconnectable_iface.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
# SPDX-License-Identifier: GPL-3.0-or-later
1919

2020

21+
from typing import List, Tuple, Any
22+
23+
2124
class IDisconnectable:
2225
"""
2326
A class that provides automatic resource cleanup for GTK widgets and other objects.
@@ -32,16 +35,14 @@ class IDisconnectable:
3235
1. Inherit from IDisconnectable alongside your main class:
3336
3437
>>> class MyWidget(Gtk.Box, IDisconnectable):
35-
... def __init__(self):
36-
... Gtk.Box.__init__(self)
37-
... IDisconnectable.__init__(self)
38+
... def __init__(self):
39+
... Gtk.Box.__init__(self)
40+
... IDisconnectable.__init__(self)
3841
3942
2. When connecting signals, store them in self.signals as a tuple of the object
4043
and the handler id:
4144
42-
>>> self.signals.append((
43-
... some_object,
44-
... some_object.connect("signal-name", callback)))
45+
>>> self.signals.append((some_object, some_object.connect("signal-name", callback)))
4546
4647
3. When creating bindings, store them in self.bindings:
4748
@@ -54,13 +55,35 @@ class IDisconnectable:
5455
5556
5. Call disconnect_all() when the widget is being destroyed:
5657
"""
58+
5759
def __init__(self) -> None:
58-
self.signals = []
59-
self.bindings = []
60-
self.disconnectables = []
60+
self.signals: List[Tuple[Any, int]] = []
61+
self.bindings: List[Any] = []
62+
self.disconnectables: List["IDisconnectable"] = []
63+
64+
def connect_signal(
65+
self, g_object: Any, signal_name: str, callback_func: Any, *args
66+
) -> None:
67+
"""Connect a signal and track it for later disconnection.
68+
69+
Args:
70+
g_object: The GObject to connect the signal to
71+
signal_name (str): Name of the signal to connect
72+
callback_func: The callback function to execute when signal is emitted
73+
*args: Additional arguments to pass to the callback function
74+
"""
75+
self.signals.append((
76+
g_object,
77+
g_object.connect(signal_name, callback_func, *args),
78+
))
6179

6280
def disconnect_all(self, *_args) -> None:
63-
"""Disconnects all signals so that the class can be deleted"""
81+
"""Disconnect all tracked signals and child disconnectable objects.
82+
83+
This method should be called when the widget is being removed to ensure
84+
proper cleanup. It disconnects all tracked signal connections and
85+
recursively calls disconnect_all on child disconnectable objects.
86+
"""
6487

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

111+
def __repr__(self, *args) -> str | None:
112+
return self.__gtype_name__ if self.__gtype_name__ else None
113+
88114
# def __del__(self):
89115
# print(f"DELETING {self}")

src/lib/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
from .utils import *
33
from .secret_storage import SecretStore
44
from .discord_rpc import *
5+
from .cache import HTCache

src/lib/cache.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# cache.py
2+
#
3+
# Copyright 2025 Nokse <[email protected]>
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
# SPDX-License-Identifier: GPL-3.0-or-later
19+
20+
from typing import Dict, Any
21+
22+
from tidalapi.artist import Artist
23+
from tidalapi.album import Album
24+
from tidalapi.media import Track
25+
from tidalapi.playlist import Playlist
26+
from tidalapi.mix import Mix
27+
28+
29+
class HTCache:
30+
artists: Dict[str, Artist] = {}
31+
albums: Dict[str, Album] = {}
32+
tracks: Dict[str, Track] = {}
33+
playlists: Dict[str, Playlist] = {}
34+
mixes: Dict[str, Mix] = {}
35+
36+
def __init__(self, session: Any) -> None:
37+
self.session = session
38+
39+
def get_artist(self, artist_id: str) -> Artist:
40+
"""Get an artist from cache or fetch from TIDAL API if not cached.
41+
42+
Args:
43+
artist_id (str): The TIDAL artist ID
44+
45+
Returns:
46+
Artist: The artist object from TIDAL API
47+
"""
48+
if artist_id in self.artists:
49+
return self.artists[artist_id]
50+
artist = Artist(self.session, artist_id)
51+
self.artists[artist_id] = artist
52+
return artist
53+
54+
def get_album(self, album_id: str) -> Album:
55+
"""Get an album from cache or fetch from TIDAL API if not cached.
56+
57+
Args:
58+
album_id (str): The TIDAL album ID
59+
60+
Returns:
61+
Album: The album object from TIDAL API
62+
"""
63+
if album_id in self.albums:
64+
return self.albums[album_id]
65+
album = Album(self.session, album_id)
66+
self.albums[album_id] = album
67+
return album
68+
69+
def get_track(self, track_id: str) -> Track:
70+
"""Get a track from cache or fetch from TIDAL API if not cached.
71+
72+
Args:
73+
track_id (str): The TIDAL track ID
74+
75+
Returns:
76+
Track: The track object from TIDAL API
77+
"""
78+
if track_id in self.tracks:
79+
return self.tracks[track_id]
80+
track = Track(self.session, track_id)
81+
self.tracks[track_id] = track
82+
return track
83+
84+
def get_playlist(self, playlist_id: str) -> Playlist:
85+
"""Get a playlist from cache or fetch from TIDAL API if not cached.
86+
87+
Args:
88+
playlist_id (str): The TIDAL playlist ID
89+
90+
Returns:
91+
Playlist: The playlist object from TIDAL API
92+
"""
93+
if playlist_id in self.playlists:
94+
return self.playlists[playlist_id]
95+
playlist = Playlist(self.session, playlist_id)
96+
self.playlists[playlist_id] = playlist
97+
return playlist
98+
99+
def get_mix(self, mix_id: str) -> Mix:
100+
"""Get a mix from cache or fetch from TIDAL API if not cached.
101+
102+
Args:
103+
mix_id (str): The TIDAL mix ID
104+
105+
Returns:
106+
Mix: The mix object from TIDAL API
107+
"""
108+
if mix_id in self.mixes:
109+
return self.mixes[mix_id]
110+
mix = Mix(self.session, mix_id)
111+
self.mixes[mix_id] = mix
112+
return mix

src/lib/discord_rpc.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,15 @@ class State(Enum):
2525
disconnect_thread: threading.Thread | None = None
2626

2727

28-
def connect():
28+
def connect() -> bool:
29+
"""Connect to Discord Rich Presence IPC.
30+
31+
Attempts to establish a connection to Discord's IPC server for
32+
Rich Presence functionality.
33+
34+
Returns:
35+
bool: True if connection successful, False otherwise
36+
"""
2937
global state
3038

3139
if not has_pypresence:
@@ -45,7 +53,14 @@ def connect():
4553
return True
4654

4755

48-
def disconnect():
56+
def disconnect() -> bool:
57+
"""Disconnect from Discord Rich Presence IPC.
58+
59+
Closes the connection to Discord's IPC server and updates the state.
60+
61+
Returns:
62+
bool: True if disconnection successful, False otherwise
63+
"""
4964
global state
5065

5166
if not has_pypresence:
@@ -62,7 +77,15 @@ def disconnect():
6277
return True
6378

6479

65-
def set_activity(track: Track | None = None, offset_ms: int = 0):
80+
def set_activity(track: Track | None = None, offset_ms: int = 0) -> None:
81+
"""Set the Discord Rich Presence activity status.
82+
83+
Updates Discord with the current playing track information and playback position.
84+
85+
Args:
86+
track: The currently playing Track object, or None to clear activity
87+
offset_ms: Current playback position in milliseconds (default: 0)
88+
"""
6689
global state
6790
global disconnect_thread
6891

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

93-
def disconnect_function():
116+
def disconnect_function() -> None:
94117
for _ in range(5 * 60):
95118
time.sleep(1)
96119
if state != State.IDLE:

0 commit comments

Comments
 (0)