Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1from dataclasses import dataclass, field 

2from datetime import timedelta 

3from enum import Enum 

4from typing import Any, Callable, Dict, Optional, Set, Tuple, Type 

5 

6from ..adapters import AlbumSearchQuery 

7from ..adapters.api_objects import Genre, Song 

8from ..util import this_decade 

9 

10 

11class RepeatType(Enum): 

12 NO_REPEAT = 0 

13 REPEAT_QUEUE = 1 

14 REPEAT_SONG = 2 

15 

16 @property 

17 def icon(self) -> str: 

18 """ 

19 Get the icon for the repeat type. 

20 

21 >>> RepeatType.NO_REPEAT.icon, RepeatType.REPEAT_QUEUE.icon 

22 ('media-playlist-repeat-symbolic', 'media-playlist-repeat-symbolic') 

23 >>> RepeatType.REPEAT_SONG.icon 

24 'media-playlist-repeat-song-symbolic' 

25 """ 

26 song_str = "-song" if self == RepeatType.REPEAT_SONG else "" 

27 return f"media-playlist-repeat{song_str}-symbolic" 

28 

29 def as_mpris_loop_status(self) -> str: 

30 return ["None", "Playlist", "Track"][self.value] 

31 

32 @staticmethod 

33 def from_mpris_loop_status(loop_status: str) -> "RepeatType": 

34 return { 

35 "None": RepeatType.NO_REPEAT, 

36 "Track": RepeatType.REPEAT_SONG, 

37 "Playlist": RepeatType.REPEAT_QUEUE, 

38 }[loop_status] 

39 

40 

41@dataclass 

42class UIState: 

43 """Represents the UI state of the application.""" 

44 

45 @dataclass(unsafe_hash=True) 

46 class UINotification: 

47 markup: str 

48 actions: Tuple[Tuple[str, Callable[[], None]], ...] = field( 

49 default_factory=tuple 

50 ) 

51 icon: Optional[str] = None 

52 

53 version: int = 1 

54 

55 # Play state 

56 playing: bool = False 

57 current_song_index: int = -1 

58 play_queue: Tuple[str, ...] = field(default_factory=tuple) 

59 old_play_queue: Tuple[str, ...] = field(default_factory=tuple) 

60 _volume: Dict[str, float] = field(default_factory=lambda: {"this device": 100.0}) 

61 is_muted: bool = False 

62 repeat_type: RepeatType = RepeatType.NO_REPEAT 

63 shuffle_on: bool = False 

64 song_progress: timedelta = timedelta() 

65 song_stream_cache_progress: Optional[timedelta] = timedelta() 

66 current_device: str = "this device" 

67 connecting_to_device: bool = False 

68 connected_device_name: Optional[str] = None 

69 available_players: Dict[Type, Set[Tuple[str, str]]] = field(default_factory=dict) 

70 

71 # UI state 

72 current_tab: str = "albums" 

73 selected_album_id: Optional[str] = None 

74 selected_artist_id: Optional[str] = None 

75 selected_browse_element_id: Optional[str] = None 

76 selected_playlist_id: Optional[str] = None 

77 album_sort_direction: str = "ascending" 

78 album_page_size: int = 30 

79 album_page: int = 0 

80 current_notification: Optional[UINotification] = None 

81 playlist_details_expanded: bool = True 

82 artist_details_expanded: bool = True 

83 loading_play_queue: bool = False 

84 

85 # State for Album sort. 

86 class _DefaultGenre(Genre): 

87 def __init__(self): 

88 self.name = "Rock" 

89 

90 current_album_search_query: AlbumSearchQuery = AlbumSearchQuery( 

91 AlbumSearchQuery.Type.RANDOM, 

92 genre=_DefaultGenre(), 

93 year_range=this_decade(), 

94 ) 

95 

96 active_playlist_id: Optional[str] = None 

97 

98 def __getstate__(self): 

99 state = self.__dict__.copy() 

100 del state["song_stream_cache_progress"] 

101 del state["current_notification"] 

102 del state["playing"] 

103 del state["available_players"] 

104 return state 

105 

106 def __setstate__(self, state: Dict[str, Any]): 

107 self.__dict__.update(state) 

108 self.song_stream_cache_progress = None 

109 self.current_notification = None 

110 self.playing = False 

111 

112 def __init_available_players__(self): 

113 from sublime_music.players import PlayerManager 

114 

115 self.available_players = { 

116 pt: set() for pt in PlayerManager.available_player_types 

117 } 

118 

119 def migrate(self): 

120 pass 

121 

122 _current_song: Optional[Song] = None 

123 

124 @property 

125 def current_song(self) -> Optional[Song]: 

126 if not self.play_queue or self.current_song_index < 0: 

127 return None 

128 

129 from sublime_music.adapters import AdapterManager 

130 

131 current_song_id = self.play_queue[self.current_song_index] 

132 

133 if not self._current_song or self._current_song.id != current_song_id: 

134 self._current_song = AdapterManager.get_song_details( 

135 current_song_id 

136 ).result() 

137 

138 return self._current_song 

139 

140 @property 

141 def next_song_index(self) -> Optional[int]: 

142 # If nothing is playing there is no next song 

143 if self.current_song_index < 0: 

144 return None 

145 

146 if self.repeat_type == RepeatType.REPEAT_SONG: 

147 return self.current_song_index 

148 

149 # If we are at the end of the play queue 

150 if self.current_song_index == len(self.play_queue) - 1: 

151 

152 # If we are repeating the queue, jump back to the beginning 

153 if self.repeat_type == RepeatType.REPEAT_QUEUE: 

154 return 0 

155 

156 # Otherwise, there isn't a next song 

157 return None 

158 

159 # In all other cases, it's the song after the current one 

160 return self.current_song_index + 1 

161 

162 @property 

163 def volume(self) -> float: 

164 return self._volume.get(self.current_device, 100.0) 

165 

166 @volume.setter 

167 def volume(self, value: float): 

168 self._volume[self.current_device] = value