import os import time import curses import subprocess import requester class CommandHandler(object): def __init__(self, stdscreen): track_list_length = 120 track_list_height = 33 search_buffer_length = 100 search_buffer_height = 1 help_window_length = 120 help_window_height = 5 self.country_id = None self.stdscreen = stdscreen self.track_list = None self.back_track_history = [] self.forward_track_history = [] self.track_start = 2 self.curr_position = self.track_start self.track_window = stdscreen.subwin(track_list_height, track_list_length, 0, 0) self.help_window = stdscreen.subwin(help_window_height, help_window_length, self.track_window.getmaxyx()[0], 1) self.prompt_area = self.help_window self.search_window = stdscreen.subwin(search_buffer_height, search_buffer_length, self.track_window.getmaxyx()[0], 10) self.input_prompt = stdscreen.subwin(1, 15, self.track_window.getmaxyx()[0], 1) self.now_playing_window = stdscreen.subwin(1, 120, stdscreen.getmaxyx()[0] - 1, 0) self.command_list_hint = stdscreen.subwin(1, 30, stdscreen.getmaxyx()[0] - 3, 0) self.command_list_hint.addstr(0, 0, "Press C for Command List") self.command_list_hint.refresh() def print_command_list(self): """Display all possible commands available to the user.""" command_menu = """[<Up>/K: Go Up] [<Down>/J: Go Down] [<Left>/H: Prev Track] [<Right>/L: Next Track] [<Enter>: Play Selected Track] [<Space>: Toggle Play/Pause] [Q: Quit] [Y: Change Country Code] [S: Search] [I: Play Track at Index] [F: Bring Spotify Client to Front] [C: Show Command List] [A: Go to Album of Selected Track] [T: Top Tracks of Artist of Selected Track] [V: Set Volume] [B: Previous track listing ] [N: Next track listing] [O: Decrease Volume] [P: Increase Volume]""" command_menu = '\n'.join(' '.join(line.split()) for line in command_menu.split('\n')) self.help_window.clear() self.help_window.addstr(0, 0, command_menu) self.help_window.refresh() def set_curr_position(self, curr_position): """Set the current position of the track list cursor.""" self.curr_position = curr_position def move_up(self): """Move the track list cursor position up one.""" if self.track_list != None and self.curr_position > self.track_start: self.curr_position -= 1 self.draw_track_list() def move_down(self): """Move the track list cursor position down one.""" if self.track_list != None and self.curr_position < (len(self.track_list) + self.track_start - 1): self.curr_position += 1 self.draw_track_list() def next_song(self): """Play the next song in the track list (based on current cursor position).""" self.move_down() self.current_song() def prev_song(self): """Play the previous song in the track list (based on current cursor position).""" self.move_up() self.current_song() def play_at_index(self): """Play song located at a specific index within the current track list. User will be prompted for desired index.""" desired_index = self.get_input(" Index:") try: desired_index = int(desired_index) screen_index = desired_index + self.track_start - 1 if self.track_list != None and screen_index <= (len(self.track_list) + self.track_start - 1) and screen_index >= self.track_start: self.curr_position = screen_index self.current_song() self.draw_track_list() except ValueError: #Case: Invalid Index pass def current_song(self): """Play song track list cursor is currently on.""" if self.track_list != None: self.play_song(self.track_list[self.curr_position - self.track_start]) def toggle_play_pause(self): """Send command to Spotify desktop client to pause/play.""" apple_script_call = ['osascript', '-e', 'tell application "Spotify" to playpause'] subprocess.call(apple_script_call) def play_song(self, track): """Given track info, send command to Spotify desktop client to play it.""" track_spotify_uri = track[4] apple_script_call = ['osascript', '-e', 'tell application "Spotify" to play track "{0}"'.format(track_spotify_uri)] subprocess.call(apple_script_call) self.update_now_playing(track) def update_now_playing(self, track): """Update the 'Now Playing' string to reflect currently playing track.""" now_playing = ">>> Now Playing: {0} --- {1} <<<".format(track[1][:50], track[2][:40]) self.now_playing_window.clear() self.now_playing_window.addstr(0, 0, now_playing) self.now_playing_window.refresh() def show_client(self): """Bring Spotify desktop client to the front of the screen.""" get_client_command = 'tell application "Spotify" \n activate \n end tell' apple_script_call = ['osascript', '-e', get_client_command] subprocess.call(apple_script_call) def prev_track_list(self): """Go back to the previously displayed track listing.""" if len(self.back_track_history) > 1: self.forward_track_history.append(self.track_list) self.track_list = self.back_track_history.pop() self.curr_position = self.track_start self.draw_track_list() def next_track_list(self): """Go ahead one track listing in the user's track listing history.""" if len(self.forward_track_history) > 0: self.back_track_history.append(self.track_list) self.track_list = self.forward_track_history.pop() self.curr_position = self.track_start self.draw_track_list() def search_content(self): """Fulfill user's search request for music by keywords.""" user_search = self.get_input("Search:") if len(user_search) > 0: if not self.back_track_history or self.track_list != self.back_track_history[-1] and self.track_list: self.forward_track_history = [] self.back_track_history.append(self.track_list) self.track_list = requester.execute_search(user_search, self.country_id, self.track_window.getmaxyx()[0]-3) self.curr_position = self.track_start self.draw_track_list() def get_artist_top(self): """Display the top tracks (according to Spotify) by the artist of the currently selected track.""" if self.track_list != None: track = self.track_list[self.curr_position - self.track_start] artist_name = track[2] artist_id = track[7] artist_uri = track[6] if not self.back_track_history or self.track_list != self.back_track_history[-1] and self.track_list: self.forward_track_history = [] self.back_track_history.append(self.track_list) self.track_list = requester.get_artist_top(artist_name, artist_id, artist_uri, self.country_id) self.curr_position = self.track_start self.draw_track_list() def get_album_tracks(self): """Display all tracks in the album of the currently selected track.""" if self.track_list != None: track = self.track_list[self.curr_position - self.track_start] album_name = track[3] album_id = track[8] album_uri = track[5] if not self.back_track_history or self.track_list != self.back_track_history[-1] and self.track_list: self.forward_track_history = [] self.back_track_history.append(self.track_list) self.track_list = requester.get_album_tracks(album_name, album_id, album_uri) self.curr_position = self.track_start self.draw_track_list() def draw_track_list(self): """Handles all of the track list displaying.""" self.track_window.clear() result_line = '{0:<2} | {1:<40} | {2:<25} | {3:<40}' result_header = result_line.format('#', 'Song Name', 'Artist', 'Album') separator_bar = '=' * (self.track_window.getmaxyx()[1] - 5) self.track_window.addstr(0, 0, result_header) self.track_window.addstr(1, 0, separator_bar) for song_index, track in enumerate(self.track_list, start=1): if (self.curr_position - self.track_start) == track[0]: mode = curses.A_REVERSE else: mode = curses.A_NORMAL song_index = str(song_index) if len(song_index) == 1: song_index = '0' + song_index track_string = result_line.format(song_index, track[1][:40], track[2][:25], track[3][:40]) self.track_window.addstr(track[0] + self.track_start, 0, track_string, mode) bottom_bar_position = self.track_start + len(self.track_list) self.track_window.addstr(bottom_bar_position, 0, separator_bar) self.track_window.refresh() def country_check(self): """Ensure a valid country ISO code is inputted by the user.""" valid_countries = [line.strip() for line in open(os.path.dirname(os.path.realpath(__file__)) + "/country_iso_codes.txt", 'r')] self.country_check_prompt() while self.country_id not in valid_countries: self.flash_message(":: Invalid Country ISO Code ::", 0.7) self.country_check_prompt() def country_check_prompt(self): """Country check helper method. Gets user input and properly formats it for validation.""" user_input = self.get_input("Country:") if len(user_input) > 0: self.country_id = user_input.split()[0].upper() def increment_volume(self): """Increase the volume of the Spotify desktop client.""" set_volume_command = 'tell application "Spotify" \n set sound volume to (get sound volume + 5) \n end tell' apple_script_call = ['osascript', '-e', set_volume_command] subprocess.call(apple_script_call) self.flash_message(":: Volume ++ ::", 0.1) def decrement_volume(self): """Decrease the volume of the Spotify desktop client.""" set_volume_command = 'tell application "Spotify" \n set sound volume to (get sound volume - 5) \n end tell' apple_script_call = ['osascript', '-e', set_volume_command] subprocess.call(apple_script_call) self.flash_message(":: Volume -- ::", 0.1) def user_volume_input(self): """Allows the user to set desired volume level.""" while True: try: desired_volume = self.get_input(" Volume:") #If no user input, RETURN if not desired_volume: return desired_volume = int(desired_volume) if desired_volume < 0 or desired_volume > 100: self.flash_message(":: Volume Range 1-100 ::", 0.8) else: break except ValueError: #Case: Unable to convert user input to type Int self.flash_message(":: Volume Range 1-100 ::", 0.8) self.set_curr_volume(desired_volume) def set_curr_volume(self, volume_level): """Sets Spotify desktop client to 'volume_level'""" set_volume_command = 'tell application "Spotify" \n set sound volume to {0} \n end tell'.format(volume_level) apple_script_call = ['osascript', '-e', set_volume_command] subprocess.call(apple_script_call) def flash_message(self, message, flash_speed): """Takes in a message string and flashes it on screen for 'flash_speed' seconds.""" self.prompt_area.clear() self.prompt_area.addstr(message) self.prompt_area.refresh() time.sleep(flash_speed) self.prompt_area.clear() self.prompt_area.refresh() def get_input(self, prompt): """Get user input through the user interface and return it.""" curses.curs_set(2) self.prompt_area.clear() self.input_prompt.addstr(0, 0, prompt) self.search_window.clear() self.prompt_area.refresh() curses.echo() user_input = self.search_window.getstr().decode(encoding="utf-8") curses.noecho() self.prompt_area.clear() self.prompt_area.refresh() curses.curs_set(0) return user_input