diff --git a/melodine/__main__.py b/melodine/__main__.py index 33ad622..054cb56 100644 --- a/melodine/__main__.py +++ b/melodine/__main__.py @@ -7,20 +7,21 @@ import time from curses import panel, wrapper from curses.textpad import Textbox, rectangle - from ffpyplayer.player import MediaPlayer from ffpyplayer.tools import loglevels, set_log_callback from melodine import innertube, spotify, utils, ytmusic from melodine.configs import APP_DIR from melodine.player import Player + # endregion # region to make ffpyplayer not output anything -loglevel_emit = 'error' +loglevel_emit = "error" + def log_callback(message, level): if message and loglevels[level] <= loglevels[loglevel_emit]: - print ("error") + print("error") set_log_callback(log_callback) @@ -30,9 +31,11 @@ def log_callback(message, level): global ffs -class Menu(): + + +class Menu: search_type = "tracks" - with open(os.path.join(APP_DIR,"ffs.txt"), 'w') as ffs: + with open(os.path.join(APP_DIR, "ffs.txt"), "w") as ffs: ffs.write(search_type) def __init__(self, items, stdscreen): @@ -74,7 +77,7 @@ def display(self): if key in [curses.KEY_ENTER, ord("\n")]: search_type = self.items[self.position][0].lower() - with open(os.path.join(APP_DIR,"ffs.txt"), 'w') as ffs: + with open(os.path.join(APP_DIR, "ffs.txt"), "w") as ffs: ffs.write(search_type) break @@ -88,25 +91,29 @@ def display(self): self.panel.hide() panel.update_panels() curses.doupdate() + + # endregion -def runner(stdscr): +def runner(stdscr): curses.noecho() stdscr.keypad(True) global height global width - global playing global player + global player_state global index - index = 0 - playing = False + global autoplay + player_state = "Nothing Playing" + autoplay = True + index = -1 height, width = stdscr.getmaxyx() # print(height) # Quits if terminal is too small - if height < 14 or width < 40: + if height < 14 or width < 60: curses.nocbreak() stdscr.keypad(False) curses.echo() @@ -118,127 +125,257 @@ def runner(stdscr): curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) curses.init_pair(2, curses.COLOR_MAGENTA, curses.COLOR_BLACK) # Initialises search box and player window + # Variables required box, playerwin, menu, autotoggle, typewin, + box, playerwin, menu, autotoggle, typewin = init_screen(stdscr) + typewin.refresh() + playerwin.refresh() + stdscr.refresh() + autotoggle.refresh() + player = Player() + """ box = draw_search_box(stdscr) playerwin = draw_player(stdscr) menu = draw_type_menu(stdscr) + autotoggle = stdscr.subwin(3, 9, 1,width - 23) + autotoggle.border() + autotoggle.addstr("autoplay") + autotoggle.addstr(1, 1, "ON ") + autotoggle.refresh() typewin = stdscr.subwin(3, 9, 1, width - 13) typewin.border() typewin.addstr(1, 1, menu.search_type) typewin.refresh() playerwin.refresh() + """ curses.curs_set(0) while True: c = stdscr.getch() # If window resized redraw elements # FIXME: Does not redraw correctly if c == curses.KEY_RESIZE: - if height < 14 or width < 40: + stdscr.clear() + height, width = stdscr.getmaxyx() + if height < 14 or width < 60: curses.nocbreak() stdscr.keypad(False) curses.echo() curses.endwin() - # print("Terminal too small!") + print("Terminal too small!") quit() - if box.gather(): - results = search(box, stdscr, menu) + init_screen(stdscr) + typewin.refresh() + playerwin.refresh() + stdscr.refresh() + autotoggle.refresh() + """ box = draw_search_box(stdscr) + menu = draw_type_menu(stdscr) + autotoggle = stdscr.subwin(3, 6, 1,width - 23) + autotoggle.border() + autotoggle.addstr("Radio") + autotoggle.addstr(1, 1, "ON ") + autotoggle.refresh() + typewin = stdscr.subwin(3, 9, 1, width - 13) + typewin.border() + typewin.addstr(1, 1, menu.search_type) + typewin.refresh() playerwin = draw_player(stdscr) playerwin.refresh() + stdscr.refresh() + """ - elif c == ord('/'): + elif c == ord("/"): results = search(box, stdscr, menu) - elif c == ord('q'): + elif c == ord("x"): curses.nocbreak() stdscr.keypad(False) curses.echo() curses.endwin() - break - elif c >= ord('0') and c <= ord('9'): + quit() + elif c == curses.KEY_UP: + if os.path.exists(os.path.join(APP_DIR, "History.txt")): + with open(os.path.join(APP_DIR, "History.txt"), "r") as history: + search_term = history.readlines()[index] + newline = search_term.index("\n") + search_term = search_term[:newline] + if len(search_term) > 30: + search_term = search_term[:30] + results = search(box, stdscr, menu, search_term) + stdscr.addstr(2, 1, search_term) + if abs(index) < len(history.readlines()) - 1: + index -= 1 + + elif c == curses.KEY_DOWN: + if os.path.exists(os.path.join(APP_DIR, "History.txt")): + with open(os.path.join(APP_DIR, "History.txt"), "r") as history: + search_term = history.readlines()[index] + results = search(box, stdscr, menu, search_term) + stdscr.addstr(2, 1, search_term[:30]) + if index < len(history.readlines()) - 1: + index += 1 + + elif c >= ord("0") and c <= ord("9"): selected = results[int(chr(c))] # utils.put_notification(selected) - player_update(playerwin, selected.artists[0].name, selected.name, selected.duration) + player_state = "Playing" + player_update( + playerwin, + selected.artists[0].name, + selected.name, + selected.duration, + ) # url = innertube.InnerTube().player(selected.id)['streamingData']['formats'][-1]['url'] - player = Player() + history = open(os.path.join(APP_DIR, "History.txt"), "a") + history.write(selected.name + "\n") + history.close() + if player.now_playing: + player_state = "Nothing Playing" + player._Player__now_playing.close_player() player.play(selected) - global bar - bar = threading.Thread(target=progressbar, args=(playerwin, selected.duration, player)).start() + bar = threading.Thread( + target=progressbar, args=(playerwin, selected.duration) + ).start() + + elif c == ord("a"): + if autoplay == True: + autoplay = False + autotoggle.addstr(1, 1, "ON ") + else: + autoplay = True + autotoggle.addstr(1, 1, "OFF") + autotoggle.refresh() - elif c == ord('p'): + elif c == ord("l"): + liked_path = os.path.join(APP_DIR, "Liked.txt") + if not os.path.exists(os.path.join(APP_DIR, "Liked.txt")): + with open(os.path.join(APP_DIR, "Liked.txt"), "a") as liked: + liked.write(selected.name + "\n") + else: + with open(os.path.join(APP_DIR, "Liked.txt")) as liked: + if selected.name not in liked.readlines(): + liked.write(selected.name + "\n") + + elif c == ord("p"): if player: - player.toggle_pause() - player_update(playerwin) - bar = threading.Thread(target=progressbar, args=(playerwin, selected.duration, player)).start() - elif c == ord('t'): + player_state = "Paused" + player.toggle_state() + player_update( + playerwin, + selected.artists[0].name, + selected.name, + selected.duration, + ) + elif c == ord("t"): menu.display() - typewin = stdscr.subwin(3, 9, 1, width - 13) typewin.border() ffs = "" - with open(os.path.join(APP_DIR,"ffs.txt"), "r") as help: + with open(os.path.join(APP_DIR, "ffs.txt"), "r") as help: ffs = help.read() typewin.addstr(1, 1, ffs) typewin.refresh() - elif c == ord("l"): - with open(os.path.join(APP_DIR, "Liked.txt")) as liked: - if selected.name not in liked.readlines(): - liked.write(selected.name) + # TODO: Display queue + elif c == ord("q"): + continue - elif c == curses.KEY_UP: - with open(os.path.join(APP_DIR,"History.txt"), "r") as history: - search_term = history.readlines()[index] - results = search(box, stdscr, menu, search_term) - stdscr.addstr(2, 1, search_term[:30]) - index += 1 # region Curses Functions # Initialises player window with "Nothing Playing" + def draw_player(stdscr): playerwin = curses.newwin(5, width - 1, height - 5, 0) playerwin.border() playerwin.addstr(0, 1, "Nothing Playing", curses.color_pair(1) | curses.A_BOLD) - playerwin.refresh() - playing = False return playerwin -# Method to draw panel for search type -def draw_type_menu(stdscr): - menu_items = [("Tracks", curses.flash), ("Artists", curses.flash), ("Albums", curses.flash)] +def draw_queue(tracks): + result_num = height - 11 + if result_num > 10: + result_num = 10 + result_box = curses.newwin(result_num + 2, width - 1, 4, 0) + result_box.border() - menu = Menu(menu_items, stdscr) - return menu + result_box.addstr(0, 1, "Queue", curses.color_pair(1) | curses.A_BOLD) + line = 1 + for track in tracks[:result_num]: + name = str(result.name) + artists_list = [] + for artist in result.artists: + artists_list.append(str(artist.name)) + artists = " & ".join(artists_list) + if len(artists) > width / 3: + artists = result.artists[0].name + if len(name) > width / 3: + name = name[: (int(width / 3))] + + duration_minute, duration_seconds = math.floor( + result.duration / 60 + ), "{:02d}".format(math.floor(result.duration % 60)) + result_box.addstr( + line, + 1, + f"{line - 1}. |{name}{(int(width/3) - len(name)) * ' '}|{artists}{(int(width/3) - len(artists)) * ' '}|{duration_minute}:{duration_seconds}", + ) -# Method to draw search box +# Method to draw search box def draw_search_box(stdscr): stdscr.addstr(0, 0, "Search (/ to focus):", curses.color_pair(1) | curses.A_BOLD) stdscr.keypad(True) - editwin = curses.newwin(1,30, 2,1) - rectangle(stdscr, 1,0, 1+1+1, 1+30+1) + editwin = curses.newwin(1, 30, 2, 1) + rectangle(stdscr, 1, 0, 1 + 1 + 1, 1 + 30 + 1) stdscr.refresh() box = Textbox(editwin) return box + +# Method to draw panel for search type +def draw_type_menu(stdscr): + menu_items = [ + ("Tracks", curses.flash), + ("Artists", curses.flash), + ("Albums", curses.flash), + ] + + menu = Menu(menu_items, stdscr) + return menu + + +# Initialise screen + + +def init_screen(stdscr): + stdscr.clear() + box = draw_search_box(stdscr) + playerwin = draw_player(stdscr) + menu = draw_type_menu(stdscr) + autotoggle = stdscr.subwin(3, 9, 1, width - 23) + autotoggle.border() + autotoggle.addstr("autoplay") + autotoggle.addstr(1, 1, "ON ") + typewin = stdscr.subwin(3, 9, 1, width - 13) + typewin.border() + typewin.addstr(1, 1, menu.search_type) + return box, playerwin, menu, autotoggle, typewin + + # Updates player window with track data and progressbar -def player_update(player, artist=None, title=None, duration=None): + +def player_update(player,artist=None, title=None, duration=None): minutes = math.floor(duration / 60) seconds = math.floor(duration % 60) playerwin = curses.newwin(5, width - 1, height - 5, 0) length = width - 16 playerwin.border() - if playing == True: - playerwin.addstr(0, 1, "Paused", curses.color_pair(1) | curses.A_BOLD) - playing == False - else: - playerwin.addstr(0, 1, "Playing", curses.color_pair(1) | curses.A_BOLD) - playing == True + playerwin.addstr(0, 1, player_state, curses.color_pair(1) | curses.A_BOLD) if artist and title: playerwin.addstr(1, 2, title, curses.color_pair(2) | curses.A_BOLD) playerwin.addstr(2, 2, artist, curses.color_pair(1)) @@ -247,11 +384,13 @@ def player_update(player, artist=None, title=None, duration=None): playerwin.addstr(3, 8, "-" * length) playerwin.refresh() + # Updates progressbar -def progressbar(playerwin, duration, player): - '''pts = 0 - while True: + +def progressbar(playerwin, duration): + while player_state != "Nothing Playing": + pts = player.get_current_timestamp() if pts == duration: break length = width - 16 @@ -264,21 +403,15 @@ def progressbar(playerwin, duration, player): playerwin.addstr(3, 2, f"{minutes}:{'{:02d}'.format(seconds)}") playerwin.refresh() pts += 1 - time.sleep(1)''' - pts = player.get_current_timestamp() - while playing and pts != duration: - length = width - 16 - percent = duration / length - position = math.floor(pts / percent) - # playerwin.addstr(1, 30, f"dur: {duration}%: {round(percent,2)}, len: {round(length,2)}, pts%%: {round(pts % percent, 2)}, pos: {position}, pts: {'{:03d}'.format(pts)}") - playerwin.addstr(3, 8, "=" * position) - minutes = math.floor(pts / 60) - seconds = math.floor(pts % 60) - playerwin.addstr(3, 2, f"{minutes}:{'{:02d}'.format(seconds)}") - playerwin.refresh() time.sleep(1) -def search(box, stdscr, menu, search=None,): + +def search( + box, + stdscr, + menu, + search=None, +): curses.curs_set(1) if not search: box.edit() @@ -286,20 +419,22 @@ def search(box, stdscr, menu, search=None,): search = box.gather() # Print the window to the screen - with open(os.path.join(APP_DIR,"ffs.txt"), "r") as help: - ffs = help.read() + with open(os.path.join(APP_DIR, "ffs.txt"), "r") as help: + ffs = help.read() search_type = ffs results = spotify.search(search, types=[search_type]) color = curses.has_colors() # Print results based on screensize, maxed at 10 - result_num = height - 9 - if height - 9 > 10: + result_num = height - 11 + if result_num > 10: result_num = 10 result_box = curses.newwin(result_num + 2, width - 1, 4, 0) result_box.border() - result_box.addstr(0, 1, "Enter index number to play", curses.color_pair(1) | curses.A_BOLD) + result_box.addstr( + 0, 1, "Enter index number to play", curses.color_pair(1) | curses.A_BOLD + ) line = 1 if search_type == "tracks": @@ -314,22 +449,28 @@ def search(box, stdscr, menu, search=None,): artists_list = [] for artist in result.artists: artists_list.append(str(artist.name)) - artists = ' & '.join(artists_list) - if len(artists) > width/3: + artists = " & ".join(artists_list) + if len(artists) > width / 3: artists = result.artists[0].name - if len(name) > width/3: - name = name[:(int(width/3))] - - duration_minute, duration_seconds = math.floor(result.duration / 60), "{:02d}".format(math.floor(result.duration % 60)) - result_box.addstr(line, 1, f"{line - 1}. |{name}{(int(width/3) - len(name)) * ' '}|{artists}{(int(width/3) - len(artists)) * ' '}|{duration_minute}:{duration_seconds}") + if len(name) > width / 3: + name = name[: (int(width / 3))] + + duration_minute, duration_seconds = math.floor( + result.duration / 60 + ), "{:02d}".format(math.floor(result.duration % 60)) + result_box.addstr( + line, + 1, + f"{line - 1}. |{name}{(int(width/3) - len(name)) * ' '}|{artists}{(int(width/3) - len(artists)) * ' '}|{duration_minute}:{duration_seconds}", + ) line += 1 curses.curs_set(0) result_box.refresh() elif search_type == "artists": name = str(result.name) - if len(name) > width/3: - name = name[:(int(width/3))] + if len(name) > width / 3: + name = name[: (int(width / 3))] result_box.addstr(line, 1, f"{line - 1}. |{name}") line += 1 curses.curs_set(0) @@ -339,21 +480,28 @@ def search(box, stdscr, menu, search=None,): artists_list = [] for artist in result.artists: artists_list.append(str(artist.name)) - artists = ' & '.join(artists_list) - if len(artists) > width/3: + artists = " & ".join(artists_list) + if len(artists) > width / 3: artists = result.artists[0].name - if len(name) > width/3: - name = name[:(int(width/3))] - - duration_minute, duration_seconds = math.floor(result.duration / 60), "{:02d}".format(math.floor(result.duration % 60)) - result_box.addstr(line, 1, f"{line - 1}. |{name}{(int(width/3) - len(name)) * ' '}|{artists}{(int(width/3) - len(artists)) * ' '}|{duration_minute}:{duration_seconds}") + if len(name) > width / 3: + name = name[: (int(width / 3))] + + duration_minute, duration_seconds = math.floor( + result.duration / 60 + ), "{:02d}".format(math.floor(result.duration % 60)) + result_box.addstr( + line, + 1, + f"{line - 1}. |{name}{(int(width/3) - len(name)) * ' '}|{artists}{(int(width/3) - len(artists)) * ' '}|{duration_minute}:{duration_seconds}", + ) line += 1 curses.curs_set(0) result_box.refresh() return results + # endregion if __name__ == "__main__": - wrapper(runner) \ No newline at end of file + wrapper(runner) diff --git a/melodine/models/spotify/client.py b/melodine/models/spotify/client.py index 94c2c42..824c1e0 100644 --- a/melodine/models/spotify/client.py +++ b/melodine/models/spotify/client.py @@ -64,8 +64,8 @@ def authorize(self) -> None: SPOTIFY = spotipy.Spotify( auth_manager=spotipy.SpotifyOAuth( scope=SCOPES, - client_id="22e27810dff0451bb93a71beb5e4b70d", - client_secret="6254b7703d8540a48b4795d82eae9300", + client_id="b647fdf572ad4403ba50017e9d09344c", + client_secret="16e816fe4ac341c6bf6f07f34881d5a8", redirect_uri="http://localhost:8080/", cache_handler=spotipy.CacheFileHandler( cache_path=os.path.join( diff --git a/melodine/player/helpers.py b/melodine/player/helpers.py index 85ab782..da36806 100644 --- a/melodine/player/helpers.py +++ b/melodine/player/helpers.py @@ -39,7 +39,9 @@ def player_fade_in(player: MediaPlayer, fade: int = 0): delta = fade - player.get_pts() player.set_volume(step) #print(player.get_volume(), player.get_pts(), delta, step) - time.sleep(delta / SMOOTHNESS_FACTOR) + sleep_time = delta / SMOOTHNESS_FACTOR + if sleep_time > 0: + time.sleep(sleep_time) def player_fade_out(player: MediaPlayer): @@ -50,7 +52,9 @@ def player_fade_out(player: MediaPlayer): delta = DURATION - player.get_pts() player.set_volume(step) #print(player.get_volume(), player.get_pts(), delta, step) - time.sleep(delta / SMOOTHNESS_FACTOR) + sleep_time = delta / SMOOTHNESS_FACTOR + if sleep_time > 0: + time.sleep(sleep_time) else: close_player(player) diff --git a/melodine/player/players.py b/melodine/player/players.py index aa7f0bf..e7082d1 100644 --- a/melodine/player/players.py +++ b/melodine/player/players.py @@ -96,7 +96,6 @@ def player_handler(self): sleep_time = 0 while True: for track in self.queue: - print(f'new track - {track}') self.queue.remove(track) self.now_playing = track self.__now_playing, self.__now_playing_thread = play( @@ -105,11 +104,8 @@ def player_handler(self): fade=self.crossfade, fade_in=True if self.queue else False ) - print(self.__now_playing) - print(self.now_playing) self.__now_playing_thread.run() self.recently_played.append(track) - print(self.queue) time.sleep(0.4) # pause state