Sfoglia il codice sorgente

Integrates gpymusic and mpv-based playback

Caleb Fangmeier 6 anni fa
parent
commit
a115671174
3 ha cambiato i file con 1449 aggiunte e 135 eliminazioni
  1. 0 0
      app/__init__.py
  2. 1084 0
      app/mpv.py
  3. 365 135
      app/ui.py

+ 0 - 0
app/__init__.py


File diff suppressed because it is too large
+ 1084 - 0
app/mpv.py


+ 365 - 135
app/ui.py

@@ -1,57 +1,239 @@
-import urwid
+import signal
 
-songs = ['one', 'another on bites the dust', 'enter sandman', 'piano man', 'dance of death']
-artists = ['metallica', 'iron maiden', 'judas priest', 'haken']
-albums = ['some kind of bird egg', 'electric boogaloo', 'another time', '13']
+import urwid
+from gmusicapi import Mobileclient
 
 log = []
 
+# ######################################################33
+# ######################################################33
+
+def build_search_collection(N):
+    from random import choice
+    songs = ['one', 'another on bites the dust', 'enter sandman', 'piano man', 'dance of death']
+    artists = ['metallica', 'iron maiden', 'judas priest', 'haken']
+    albums = ['some kind of bird egg', 'electric boogaloo', 'another time', '13']
+    years = [1967, 2066, 1924, 1098]
+
+    types = (Song, Artist, Album)
+    collection = []
+    for _ in range(N):
+        type_ = choice(types)
+        if type_ == Song:
+            collection.append(Song(choice(songs), choice(artists), choice(albums), 0))
+        elif type_ == Artist:
+            collection.append(Artist(choice(artists)))
+        else:
+            collection.append(Album(choice(albums), choice(artists), choice(years)))
+    return (list(filter(lambda o: type(o) == Song, collection)),
+            list(filter(lambda o: type(o) == Album, collection)),
+            list(filter(lambda o: type(o) == Artist, collection)))
+# ######################################################33
+# ######################################################33
+
+
+class MusicObject:
+    @staticmethod
+    def to_ui(*txts):
+        first, *rest = [str(txt) for txt in txts]
+        items = [urwid.SelectableIcon(first, 0)]
+        for line in rest:
+            items.append(urwid.Text(line))
+        line = urwid.Columns(items)
+        line = urwid.AttrMap(line, 'search normal', 'search select')
+        return line
+
+    @staticmethod
+    def header_ui(*txts):
+        header = urwid.Columns([('weight', 1, urwid.Text(('header', txt)))
+                                for txt in txts])
+        return urwid.AttrMap(header, 'header_bg')
+
 
-class Song:
-    def __init__(self, title, artist, album):
+class Song(MusicObject):
+    def __init__(self, title, album, artist, id_):
         self.title = title
         self.album = album
         self.artist = artist
+        self.id = id_
 
     def __repr__(self):
         return f'<Song title:{self.title}, album:{self.album}, artist:{self.artist}>'
 
+    def __str__(self):
+        return f'{self.title} by {self.artist}'
+
+    def ui(self):
+        return self.to_ui(self.title, self.album, self.artist)
+
+    @staticmethod
+    def header():
+        return MusicObject.header_ui('Title', 'Album', 'Artist')
+
+
 
-class Album:
-    def __init__(self, title, artist):
+class Album(MusicObject):
+    def __init__(self, title, artist, year, id_):
         self.title = title
         self.artist = artist
+        self.year = year
+        self.id = id_
 
     def __repr__(self):
-        return f'<Album title:{self.title}, artist:{self.artist}>'
+        return f'<Album title:{self.title}, artist:{self.artist}, year:{self.year}>'
 
+    def ui(self):
+        return self.to_ui(self.title, self.artist, self.year)
 
-class Artist:
+    @staticmethod
+    def header():
+        return MusicObject.header_ui('Album', 'Artist', 'Year')
+
+
+class Artist(MusicObject):
     def __init__(self, name):
         self.name = name
 
     def __repr__(self):
         return f'<Artist name:{self.name}>'
 
+    def ui(self):
+        return self.to_ui(self.name)
 
-def build_search_collection(N):
-    from random import choice
-    types = (Song, Artist, Album)
-    collection = []
-    for _ in range(N):
-        type_ = choice(types)
-        if type_ == Song:
-            collection.append(Song(choice(songs), choice(artists), choice(albums)))
-        elif type_ == Artist:
-            collection.append(Artist(choice(artists)))
+    @staticmethod
+    def header():
+        return MusicObject.header_ui('Artist')
+
+
+class CommandInput(urwid.Edit):
+    def __init__(self, app):
+        self.app = app
+        super().__init__('search > ', multiline=False, allow_tab=False)
+
+    def keypress(self, size, key):
+        if key == 'enter':
+            txt = self.edit_text
+            if txt:
+                self.set_edit_text('')
+                self.app.search(txt)
+            return None
         else:
-            collection.append(Album(choice(albums), choice(artists)))
-    return (list(filter(lambda o: type(o) == Song, collection)),
-            list(filter(lambda o: type(o) == Album, collection)),
-            list(filter(lambda o: type(o) == Artist, collection)))
+            size = (size[0],)
+            return super().keypress(size, key)
 
 
-class UI(urwid.Pile):
+class SearchPanel(urwid.ListBox):
+    def __init__(self, app):
+        self.app = app
+        self.walker = urwid.SimpleFocusListWalker([])
+        super().__init__(self.walker)
+
+    def keypress(self, size, key):
+        if key == 'q':
+            selected = self.selected_search_obj()
+            if selected and type(selected) in (Song, Album):
+                self.app.queue_panel.add_to_queue(selected)
+        elif key == 'j':
+            super().keypress(size, 'down')
+        elif key == 'k':
+            super().keypress(size, 'up')
+        else:
+            super().keypress(size, key)
+
+    def set_search_results(self, search_results):
+        self.search_results = search_results
+        songs, albums, artists = search_results
+
+        self.walker.clear()
+
+        if songs:
+            self.walker.append(Song.header())
+        for song in songs:
+            self.walker.append(song.ui())
+
+        if albums:
+            self.walker.append(Album.header())
+        for album in albums:
+            self.walker.append(album.ui())
+
+        if artists:
+            self.walker.append(Artist.header())
+        for artist in artists:
+            self.walker.append(artist.ui())
+
+        if self.walker:
+            self.walker.set_focus(1)
+
+    def selected_search_obj(self):
+        focus_id = self.walker.get_focus()[1]
+        songs, albums, artists = self.search_results
+
+        try:
+            focus_id -= 1
+            if focus_id < len(songs):
+                return songs[focus_id]
+            focus_id -= (1 + len(songs))
+            if focus_id < len(albums):
+                return albums[focus_id]
+            focus_id -= (1 + len(albums))
+            return artists[focus_id]
+        except IndexError:
+            return None
+
+
+class QueuePanel(urwid.ListBox):
+    def __init__(self, app):
+        self.app = app
+        self.walker = urwid.SimpleFocusListWalker([])
+        self.queue = []
+        super().__init__(self.walker)
+
+    def add_to_queue(self, music_obj):
+        # assume Song for now
+        self.queue.append(music_obj)
+        self.walker.append(music_obj.ui())
+
+    def drop(self, idx):
+        if 0 <= idx < len(self.queue):
+            self.queue.pop(idx)
+            self.walker.pop(idx)
+
+    def swap(self, idx1, idx2):
+        if (0 <= idx1 < len(self.queue)) and (0 <= idx2 < len(self.queue)):
+            obj1, obj2 = self.queue[idx1], self.queue[idx2]
+            self.queue[idx1], self.queue[idx2] = obj2, obj1
+
+            ui1, ui2 = self.walker[idx1], self.walker[idx2]
+            self.walker[idx1], self.walker[idx2] = ui2, ui1
+
+    def keypress(self, size, key):
+        focus_id = self.walker.get_focus()[1]
+        if focus_id is None:
+            return super().keypress(size, key)
+
+        if key == 'u':
+            self.swap(focus_id, focus_id-1)
+            self.keypress(size, 'up')
+        elif key == 'd':
+            self.swap(focus_id, focus_id+1)
+            self.keypress(size, 'down')
+        elif key == 'delete':
+            self.drop(focus_id)
+        elif key == 'j':
+            super().keypress(size, 'down')
+        elif key == 'k':
+            super().keypress(size, 'up')
+        elif key == ' ':
+            if self.app.play_state == 'stop':
+                self.walker.pop()
+                self.app.play(self.queue.pop())
+            else:
+                self.app.toggle_play()
+        else:
+            return super().keypress(size, key)
+
+
+class App(urwid.Pile):
 
     palette = [
         ('header', 'white,underline', 'black'),
@@ -64,129 +246,177 @@ class UI(urwid.Pile):
         ('region_bg select', '', 'black'),
     ]
 
-    def click(self, *args, **kwargs):
-        print(args, kwargs)
-
     def __init__(self):
-        self.search_results = urwid.SimpleFocusListWalker([])
-        search_results_box = urwid.ListBox(self.search_results)
-        search_results_box = urwid.LineBox(search_results_box, title='Search Results')
-        search_results_box = urwid.AttrMap(search_results_box, 'region_bg normal', 'region_bg select')
 
-        self.now_playing = urwid.Text('Now Playing')
+        self.g_api = Mobileclient()
+        deviceid = "3d9cf4c429a3e170"
+        self.g_api.login('cfangmeier74@gmail.com', 'fnqugbdwjyfoqbxf', deviceid)
 
-        # urwid.connect_signal(search_results_box.base_widget, 'click', self.click)
+        import app.mpv as mpv
+        self.player = mpv.MPV()
 
-        self.queue = urwid.SimpleFocusListWalker([])
-        queue_box = urwid.ListBox(self.queue)
-        queue_box = urwid.LineBox(queue_box, title='Queue')
-        queue_box = urwid.AttrMap(queue_box, 'region_bg normal', 'region_bg select')
+        self.search_panel = SearchPanel(self)
+        search_panel_wrapped = urwid.LineBox(self.search_panel, title='Search Results')
+        search_panel_wrapped = urwid.AttrMap(search_panel_wrapped, 'region_bg normal', 'region_bg select')
+        self.search_panel_wrapped = search_panel_wrapped
 
-        self.command_input = urwid.Edit('> ', multiline=False, allow_tab=False)
+        self.now_playing = urwid.Text('')
+        self.progress = urwid.Text('0:00/0:00', align='right')
+        status_line = urwid.Columns([('weight', 3, self.now_playing),
+                                     ('weight', 1, self.progress)])
 
-        urwid.Pile.__init__(self, [('weight', 12, search_results_box),
-                                   ('pack', self.now_playing),
-                                   ('weight', 7, queue_box),
-                                   ('pack', self.command_input)
-                                   ])
+        self.queue_panel = QueuePanel(self)
+        queue_panel_wrapped = urwid.LineBox(self.queue_panel, title='Queue')
+        queue_panel_wrapped = urwid.AttrMap(queue_panel_wrapped, 'region_bg normal', 'region_bg select')
+        self.queue_panel_wrapped = queue_panel_wrapped
 
-    def header(self, txts):
-        header = urwid.Columns([('weight', 1, urwid.Text(('header', txt)))
-                                for txt in txts])
-        return urwid.AttrMap(header, 'header_bg')
+        self.command_input = urwid.Edit('> ', multiline=False)
+        self.command_input = CommandInput(self)
 
-    def line(self, txts):
-        first, *rest = txts
-        items = [urwid.SelectableIcon(first, 0)]
-        for line in rest:
-            items.append(urwid.Text(line))
-        line = urwid.Columns(items)
-        line = urwid.AttrMap(line, 'search normal', 'search select')
-        return line
-
-    def selected_search_obj(self):
-        focus_id = self.search_results.get_focus()[1]
-        songs, albums, artists = self.search_list
-
-        focus_id -= 1
-        if focus_id < len(songs):
-            return songs[focus_id]
-        focus_id -= (1 + len(songs))
-        if focus_id < len(albums):
-            return albums[focus_id]
-        focus_id -= (1 + len(albums))
-        return artists[focus_id]
-
-    def enqueue(self, obj):
-        pass
+        urwid.Pile.__init__(self, [('weight', 12, search_panel_wrapped),
+                                   ('pack', status_line),
+                                   ('weight', 7, queue_panel_wrapped),
+                                   ('pack', self.command_input)
+                                   ])
+        self.set_focus(self.command_input)
 
-    def expand(self, obj):
-        pass
+        self.play_state = 'stop'
+        self.current_song = None
 
-    def radio(self, obj):
-        pass
+    @staticmethod
+    def sec_to_min_sec(sec_tot):
+        if sec_tot is None:
+            return 0, 0
+        else:
+            min_ = int(sec_tot // 60)
+            sec = int(sec_tot % 60)
+            return min_, sec
+
+    def update_progress(self):
+        curr_time_s = self.player.time_pos
+        rem_time_s = self.player.time_remaining
+        if curr_time_s is not None and rem_time_s is not None:
+            curr_time = self.sec_to_min_sec(curr_time_s)
+            total_time = self.sec_to_min_sec(curr_time_s+rem_time_s)
+        else:
+            curr_time = (0, 0)
+            total_time = (0, 0)
+        self.progress.set_text(f'{curr_time[0]}:{curr_time[1]:02d}/{total_time[0]}:{total_time[1]:02d}')
+
+    def update_now_playing(self, *args, **kwargs):
+        if self.play_state == 'play':
+            self.update_progress()
+            self.now_playing.set_text(f'Now Playing: {str(self.current_song)}')
+            self.loop.set_alarm_in(1, self.update_now_playing)
+        elif self.play_state == 'pause':
+            self.update_progress()
+            self.now_playing.set_text(f'Paused: {str(self.current_song)}')
+        else:
+            self.now_playing.set_text('')
+
+    def play(self, song):
+        self.current_song = song
+        url = self.g_api.get_stream_url(song.id)
+        self.player.play(url)
+        self.play_state = 'play'
+        self.update_now_playing()
+        self.loop.set_alarm_in(1, self.update_now_playing)
+
+    def stop(self, song):
+        self.current_song = None
+        self.player.quit()
+        self.play_state = 'stop'
+        self.update_now_playing()
+
+    def toggle_play(self):
+        if self.play_state == 'play':
+            self.player.pause = True
+            self.play_state = 'pause'
+            self.update_now_playing()
+        elif self.play_state == 'pause':
+            self.player.pause = False
+            self.play_state = 'play'
+            self.update_now_playing()
+            self.loop.set_alarm_in(1, self.update_now_playing)
 
     def keypress(self, size, key):
-        if key in ('q', 'e', 'r'):
-            focus_obj = self.selected_search_obj()
-            self.now_playing.set_text(repr(focus_obj))
-            if key == 'q' and type(focus_obj) in (Song, Album):
-                self.enqueue(focus_obj)
-            elif key == 'e':
-                self.expand(focus_obj)
+        if key == 'tab':
+            current_focus = self.focus
+            if current_focus == self.search_panel_wrapped:
+                self.set_focus(self.queue_panel_wrapped)
+            elif current_focus == self.queue_panel_wrapped:
+                self.set_focus(self.command_input)
             else:
-                self.radio(focus_obj)
-
+                self.set_focus(self.search_panel_wrapped)
+        elif key == 'shift tab':
+            current_focus = self.focus
+            if current_focus == self.search_panel_wrapped:
+                self.set_focus(self.command_input)
+            elif current_focus == self.queue_panel_wrapped:
+                self.set_focus(self.search_panel_wrapped)
+            else:
+                self.set_focus(self.queue_panel_wrapped)
+        elif key == 'ctrl p':
+            self.toggle_play()
+        elif key == 'ctrl q':
+            self.stop()
         else:
             return self.focus.keypress(size, key)
 
-    def display_search_results(self, search_list=None):
-        if search_list is None:
-            self.search_list = build_search_collection(40)
-        songs, albums, artists = self.search_list
-
-        self.search_results.clear()
-        if songs:
-            self.search_results.append(self.header(['Title', 'Album', 'Artist']))
-        for song in songs:
-            self.search_results.append(self.line([song.title, song.album, song.artist]))
-
-        if albums:
-            self.search_results.append(self.header(['Album', 'Artist']))
-        for album in albums:
-            self.search_results.append(self.line([album.title, album.artist]))
-
-        if artists:
-            self.search_results.append(self.header(['Artist']))
-        for artist in artists:
-            self.search_results.append(self.line([artist.name]))
-
-
-ui = UI()
-
-
-def show_or_exit(key):
-    if key == 'esc':
-        raise urwid.ExitMainLoop()
-    elif key == 'enter':
-        ui.display_search_results()
-    else:
-        # ui.now_playing.set_text(repr(key))
-        # try:
-        #     ui.now_playing.set_text(repr(ui.search_results.get_focus()[0].base_widget))
-        # except AttributeError:
-        ui.now_playing.set_text(repr(ui.search_results.get_focus()))
-        # current = ui.search_results.get_focus()[1]
-        # try:
-        #     ui.search_results.set_focus(current+1)
-        # except IndexError:
-        #     pass
-
-
-loop = urwid.MainLoop(ui, ui.palette, unhandled_input=show_or_exit)
-loop.screen.set_terminal_properties(colors=256)
-try:
-    loop.run()
-except Exception as e:
-    print(log)
-    raise e
+    def search(self, query):
+
+        results = self.g_api.search(query)
+
+        songs = []
+        for hit in results['song_hits']:
+            title = hit['track']['title']
+            album = hit['track']['album']
+            artist = hit['track']['artist']
+            try:
+                id_ = hit['track']['id']
+            except KeyError:
+                id_ = hit['track']['storeId']
+            songs.append(Song(title, album, artist, id_))
+
+        albums = []
+        for hit in results['album_hits']:
+            title = hit['album']['name']
+            artist = hit['album']['albumArtist']
+            year = hit['album']['year']
+            id_ = hit['album']['albumId']
+            albums.append(Album(title, artist, year, id_))
+
+        self.search_panel.set_search_results((songs, albums, []))
+        self.set_focus(self.search_panel_wrapped)
+
+    def cleanup(self):
+        self.player.quit()
+        del self.player
+        self.g_api.logout()
+
+
+def handle_sigint(signum, frame):
+    raise urwid.ExitMainLoop()
+
+
+if __name__ == '__main__':
+    app = App()
+    signal.signal(signal.SIGINT, handle_sigint)
+
+    # def show_or_exit(key):
+    #     if key == 'esc':
+    #         raise urwid.ExitMainLoop()
+    #     elif key == 'enter':
+    #         app.now_playing.set_text('got an enter!')
+    #     else:
+    #         app.now_playing.set_text(repr(app.search_panel.get_focus()))
+    # loop = urwid.MainLoop(app, app.palette, unhandled_input=show_or_exit)
+    loop = urwid.MainLoop(app, app.palette)
+    try:
+        app.loop = loop
+        loop.run()
+        app.cleanup()
+    except Exception as e:
+        print(log)
+        app.cleanup()
+        raise e