374 lines
12 KiB
Python
374 lines
12 KiB
Python
import logging
|
|
import os
|
|
import io
|
|
|
|
LOG_FILE = 'textual.log'
|
|
LOGGING_LEVEL = logging.DEBUG
|
|
LOG_FORMAT = '[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
|
|
|
|
# Clear Log File
|
|
os.remove(LOG_FILE)
|
|
|
|
logging.basicConfig(filename=LOG_FILE, level=LOGGING_LEVEL, format=LOG_FORMAT)
|
|
|
|
import SubSonic
|
|
|
|
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
|
|
import pygame
|
|
|
|
from textual import on, events
|
|
from textual.app import App, ComposeResult
|
|
from textual.widgets import Footer, Header, OptionList, DataTable, ProgressBar, Label
|
|
from textual.containers import Horizontal, Vertical
|
|
from textual.screen import Screen
|
|
from textual.widgets.option_list import Option
|
|
|
|
u = 'pseudo'
|
|
p = 'telegram9dock-MICHEL'
|
|
|
|
conn = SubSonic.SubpyConn(u, p, "https://music.0x01fe.net")
|
|
|
|
pygame.mixer.init()
|
|
|
|
class AlbumSongs(Screen):
|
|
BINDINGS = [
|
|
('escape', 'die()', "Back"),
|
|
('p', 'play()', 'Play Song'),
|
|
('q' 'queue()', 'Queue Song')
|
|
]
|
|
|
|
selected_song_id: str | None = None
|
|
songs = None
|
|
|
|
def __init__(self, album_id: str):
|
|
self.album_id = album_id
|
|
super().__init__()
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield DataTable(id="albumSongs")
|
|
yield Footer()
|
|
|
|
def on_mount(self) -> None:
|
|
self.songs = conn.getAlbumSongs(self.album_id)
|
|
songsTable: DataTable = self.query_one(DataTable)
|
|
|
|
songsTable.cursor_type = 'row'
|
|
songsTable.add_columns("#", "Title", "Artist", "Duration", "Bitrate")
|
|
|
|
for song in self.songs:
|
|
songsTable.add_row(song.trackNumber, song.title, song.artist, song.duration, song.bitRate, key=song.id)
|
|
|
|
@on(DataTable.RowHighlighted)
|
|
def row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
|
self.selected_song_id = event.row_key.value
|
|
|
|
def action_play(self) -> None:
|
|
app.play_song(self._find_selected_song())
|
|
|
|
def action_queue(self) -> None:
|
|
app.queue_song(self._find_selected_song())
|
|
|
|
# Find the song that was selected
|
|
def _find_selected_song(self) -> SubSonic.Song:
|
|
for song in self.songs:
|
|
if song.id == self.selected_song_id:
|
|
return song
|
|
|
|
|
|
# Pops and uninstalls self
|
|
def action_die(self) -> None:
|
|
app.pop_screen()
|
|
app.uninstall_screen("albumSongs")
|
|
|
|
class ArtistAlbums(Screen):
|
|
BINDINGS = [
|
|
('escape', 'die()', "Back")
|
|
]
|
|
|
|
def __init__(self, artist_id: str):
|
|
self.artist_id = artist_id
|
|
super().__init__()
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield DataTable(id="Albums")
|
|
yield Footer()
|
|
|
|
def on_mount(self) -> None:
|
|
albums = conn.getArtistAlbums(self.artist_id)
|
|
albumTable: DataTable = self.query_one(DataTable)
|
|
|
|
albumTable.cursor_type = 'row'
|
|
|
|
albumTable.add_columns("Title", "Songs", "Plays", "Year", "Time")
|
|
|
|
for album in albums:
|
|
albumTable.add_row(album.title, album.songCount, album.playCount, album.year, album.duration, key=album.id)
|
|
|
|
@on(DataTable.RowSelected)
|
|
async def album_selected(self, event: DataTable.RowSelected) -> None:
|
|
app.install_screen(AlbumSongs(event.row_key.value), "albumSongs")
|
|
app.push_screen("albumSongs")
|
|
|
|
# Pops and uninstalls self
|
|
def action_die(self) -> None:
|
|
app.pop_screen()
|
|
app.uninstall_screen("artistAlbums")
|
|
|
|
class Artists(Screen):
|
|
BINDINGS = [
|
|
('escape', 'app.pop_screen', "Back")
|
|
]
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield OptionList(id="Artists")
|
|
yield Footer()
|
|
|
|
def on_mount(self) -> None:
|
|
artists = conn.getArtists()
|
|
artistList = self.query_one(OptionList)
|
|
|
|
for artist in artists:
|
|
artistList.add_option(Option(artist.name, artist.id))
|
|
|
|
@on(OptionList.OptionSelected)
|
|
def artist_selected(self, event: OptionList.OptionSelected) -> None:
|
|
app.install_screen(ArtistAlbums(event.option_id), 'artistAlbums')
|
|
app.push_screen('artistAlbums')
|
|
|
|
class Albums(Screen):
|
|
BINDINGS = [
|
|
('escape', 'app.pop_screen', "Back")
|
|
]
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield DataTable()
|
|
yield Footer()
|
|
|
|
def on_mount(self) -> None:
|
|
albums = conn.getAllAlbums()
|
|
albumTable = self.query_one(DataTable)
|
|
|
|
albumTable.cursor_type = 'row'
|
|
albumTable.add_columns("Title", "Songs", "Plays", "Year", "Time")
|
|
|
|
for album in albums:
|
|
albumTable.add_row(album.title, album.songCount, album.playCount, album.year, album.duration, key=album.id)
|
|
|
|
@on(DataTable.RowSelected)
|
|
def album_selected(self, event: DataTable.RowSelected) -> None:
|
|
app.install_screen(AlbumSongs(event.row_key.value), 'albumSongs')
|
|
app.push_screen('albumSongs')
|
|
|
|
class Playlists(Screen):
|
|
BINDINGS = [
|
|
('escape', 'app.pop_screen', "Back")
|
|
]
|
|
|
|
playlists: list[SubSonic.Playlist] | None = None
|
|
selected_playlist_id: str = None
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield Header(icon="")
|
|
yield DataTable()
|
|
yield Footer()
|
|
|
|
def on_mount(self) -> None:
|
|
app.sub_title = "Playlists"
|
|
self.playlists = conn.getPlaylists()
|
|
playlistTable = self.query_one(DataTable)
|
|
|
|
playlistTable.cursor_type = "row"
|
|
playlistTable.add_columns("Name", "Songs", "Time", "Owner")
|
|
|
|
for playlist in self.playlists:
|
|
playlistTable.add_row(playlist.name, playlist.songCount, playlist.duration, playlist.owner, key=playlist.id)
|
|
|
|
@on(DataTable.RowHighlighted)
|
|
def row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
|
self.selected_playlist_id = event.row_key.value
|
|
|
|
@on(DataTable.RowSelected)
|
|
def playlist_selected(self, event: DataTable.RowSelected) -> None:
|
|
app.install_screen(PlaylistSongs(self._find_selected_playlist()), "playlistSongs")
|
|
app.push_screen('playlistSongs')
|
|
|
|
def _find_selected_playlist(self) -> SubSonic.Playlist:
|
|
for playlist in self.playlists:
|
|
if playlist.id == self.selected_playlist_id:
|
|
return playlist
|
|
|
|
|
|
class PlaylistSongs(Screen):
|
|
BINDINGS = [
|
|
('escape', 'die()', "Back"),
|
|
('p', 'play()', 'Play Song'),
|
|
('q', 'queue()', 'Queue Song')
|
|
]
|
|
|
|
selected_song_id: str | None = None
|
|
|
|
def __init__(self, playlist: SubSonic.Playlist):
|
|
self.playlist = playlist
|
|
self.songs = conn.getPlaylist(playlist.id)
|
|
super().__init__()
|
|
|
|
def compose(self) -> ComposeResult:
|
|
self.sub_title = self.playlist.name
|
|
yield Header(icon="")
|
|
yield DataTable()
|
|
yield Footer()
|
|
|
|
def on_mount(self) -> None:
|
|
songTable: DataTable = self.query_one(DataTable)
|
|
|
|
songTable.cursor_type = "row"
|
|
songTable.add_columns("Title", "Album", "Artist", "Plays", "Duration", "Bitrate")
|
|
|
|
for song in self.songs:
|
|
songTable.add_row(song.title, song.album, song.artist, song.playCount, song.duration, song.bitRate, key=song.id)
|
|
|
|
# Pops and uninstalls self
|
|
def action_die(self) -> None:
|
|
app.pop_screen()
|
|
app.uninstall_screen("playlistSongs")
|
|
|
|
@on(DataTable.RowHighlighted)
|
|
def row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
|
self.selected_song_id = event.row_key.value
|
|
|
|
def action_play(self) -> None:
|
|
app.play_song(self._find_selected_song())
|
|
|
|
def action_queue(self) -> None:
|
|
app.queue_song(self._find_selected_song())
|
|
|
|
# Find the song that was selected
|
|
def _find_selected_song(self) -> SubSonic.Song:
|
|
for song in self.songs:
|
|
if song.id == self.selected_song_id:
|
|
return song
|
|
|
|
class Favourites(Screen):
|
|
BINDINGS = [
|
|
('escape', 'app.pop_screen', "Back")
|
|
]
|
|
|
|
songs: list[SubSonic.Song] | None = None
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield Header(icon="")
|
|
yield DataTable()
|
|
yield Footer()
|
|
|
|
def on_mount(self) -> None:
|
|
app.sub_title = "Favourites"
|
|
self.songs = conn.getStarredSongs()
|
|
songTable = self.query_one(DataTable)
|
|
|
|
songTable.cursor_type = "row"
|
|
songTable.add_columns("Title", "Album", "Artist", "Plays", "Duration", "Bitrate")
|
|
|
|
for song in self.songs:
|
|
songTable.add_row(song.title, song.album, song.artist, song.playCount, song.duration, song.bitRate, key=song.id)
|
|
|
|
class TextualSubpyPlayer(App):
|
|
BINDINGS = [
|
|
("space", "play_pause_song()", "⏯️"),
|
|
("f1", "push_screen('artists')", "Artists"),
|
|
("f2", "push_screen('albums')", "Albums"),
|
|
("f3", "push_screen('favourites')", "Favourites"),
|
|
("f4", "push_screen('playlists')", "Playlists")
|
|
]
|
|
|
|
currently_playing: SubSonic.Song | None = None
|
|
song_queue: list[SubSonic.Song] = []
|
|
isPlaying: bool = False
|
|
place: int = 0
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield Header(icon="")
|
|
with Horizontal():
|
|
with Vertical(classes="column"):
|
|
yield Label(id="playing")
|
|
yield ProgressBar()
|
|
yield DataTable()
|
|
yield Footer()
|
|
|
|
def on_mount(self) -> None:
|
|
logging.debug("mount called")
|
|
self.title = "SubPy"
|
|
self.install_screen(Artists(), name="artists")
|
|
self.install_screen(Albums(), name="albums")
|
|
self.install_screen(Favourites(), name="favourites")
|
|
self.install_screen(Playlists(), name="playlists")
|
|
|
|
queueTable: DataTable = self.query_one(DataTable)
|
|
queueTable.cursor_type = "row"
|
|
queueTable.add_columns("Title", "Album", "Artist", "Plays", "Duration", "Bitrate")
|
|
|
|
def on_key(self, event: events.Key) -> None:
|
|
# logging.debug(f'Key Pressed {event.name}')
|
|
pass
|
|
|
|
@on(events.ScreenSuspend)
|
|
def sus(self, event: events.ScreenSuspend) -> None:
|
|
logging.debug('sus')
|
|
|
|
def on_screen_suspend(self, event: events.ScreenSuspend) -> None:
|
|
logging.debug('sus3')
|
|
|
|
@on(events.ScreenResume)
|
|
def screen_resumed(self, event: events.ScreenResume) -> None:
|
|
logging.debug('Main screen resumed')
|
|
queueTable: DataTable = self.query_one(DataTable)
|
|
for row_key, row in queueTable.rows.items():
|
|
queueTable.remove_row(row_key)
|
|
|
|
for song in self.song_queue:
|
|
queueTable.add_row(song.title, song.album, song.artist, song.playCount, song.duration, song.bitRate)
|
|
|
|
def play_song(self, song: SubSonic.Song) -> None:
|
|
self.currently_playing = song
|
|
self.isPlaying = True
|
|
logging.debug(f"Playing song {song.title} by {song.artist}.")
|
|
|
|
self.notify(f"Playing \"{song.title}\" by {song.artist}.")
|
|
|
|
r = conn.stream(song.id)
|
|
audio_data = io.BytesIO(r.content)
|
|
|
|
pygame.mixer.music.load(audio_data)
|
|
pygame.mixer.music.play()
|
|
|
|
# playingLabel: Label = self.query_one(Label)
|
|
# playingLabel.
|
|
|
|
# Set callback loop for progress bar
|
|
# self.progress_timer = self.set_interval(1 / 10, self.update_progress, pause=False)
|
|
|
|
# def update_progress(self) -> None:
|
|
# self.query_one(ProgressBar).advance(1)
|
|
|
|
def queue_song(self, song: SubSonic.Song) -> None:
|
|
logging.debug(f'Queue\'d song "{song.title}" by {song.artist}.')
|
|
self.song_queue.append(song)
|
|
|
|
def action_play_pause_song(self) -> None:
|
|
self.play_pause_song()
|
|
|
|
def play_pause_song(self) -> None:
|
|
if not self.currently_playing:
|
|
return None
|
|
|
|
if self.isPlaying:
|
|
self.place = pygame.mixer.music.get_pos()
|
|
pygame.mixer.music.pause()
|
|
self.isPlaying = False
|
|
else:
|
|
pygame.mixer.music.unpause()
|
|
self.isPlaying = True
|
|
|
|
if __name__ == '__main__':
|
|
app = TextualSubpyPlayer()
|
|
app.run()
|