├── .gitignore ├── README.md └── snek ├── .gitignore ├── header.py ├── loop.py ├── mainwidget.py ├── misc.py ├── playercontrols.py ├── snek.py ├── soundloader ├── __init__.py └── soundloader.py ├── soundplayer ├── __init__.py ├── customplayer.py └── player.py ├── topwidget.py └── tracklist.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # snek 2 | A terminal based music player written in Python. 3 | 4 | ## Installing Dependencies 5 | The easiest way to install snek's dependencies is through [pip](https://pypi.python.org/pypi/pip). 6 | 7 | * Install [urwid](http://urwid.org/) console interface library 8 | ``` 9 | pip install urwid 10 | ``` 11 | 12 | * Install [pyglet](https://bitbucket.org/pyglet/pyglet/wiki/Home) multimedia library 13 | ``` 14 | pip install pyglet 15 | ``` 16 | 17 | * Install [AVbin](avbin.github.io/) audio decoding library (required for mp3 playback) 18 | 19 | * binary release available [here](http://avbin.github.io/AVbin/Download.html) 20 | 21 | ## Using snek 22 | * Clone the repository 23 | ``` 24 | git clone https://github.com/azablan/snek.git 25 | ``` 26 | * cd into the directory and run snek.py, specifying a directory that contains audio files 27 | ``` 28 | python snek.py 29 | ``` 30 | ## Screenshot 31 | 32 | ![Screenshot](https://cloud.githubusercontent.com/assets/14065730/21290374/c45b35dc-c485-11e6-88d1-340387bf15fb.png) 33 | -------------------------------------------------------------------------------- /snek/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | -------------------------------------------------------------------------------- /snek/header.py: -------------------------------------------------------------------------------- 1 | import urwid 2 | 3 | import misc 4 | 5 | 6 | class Header(urwid.Columns): 7 | 8 | def __init__(self, path, on_path_change): 9 | quit = self.quit_button() 10 | edit = self.path_edit(path, on_path_change) 11 | 12 | widgets = [ 13 | edit, 14 | quit 15 | ] 16 | 17 | urwid.Columns.__init__(self, widgets) 18 | 19 | 20 | def quit_button(self): 21 | def quit(w): 22 | raise urwid.ExitMainLoop() 23 | 24 | widget = urwid.Button(u"Quit", on_press=quit) 25 | widget._label.wrap = 'clip' 26 | widget._label.align = 'center' 27 | 28 | return widget 29 | 30 | 31 | def path_edit(self, path, enter_cb): 32 | edit = misc.CustomEdit(u"Path: ", enter_cb) 33 | edit.set_edit_text(path) 34 | widget = urwid.AttrMap(edit, None, focus_map='reversed') 35 | return widget 36 | -------------------------------------------------------------------------------- /snek/loop.py: -------------------------------------------------------------------------------- 1 | import urwid 2 | 3 | 4 | class TaskLoop: 5 | def __init__(self, main_loop): 6 | self.tasks = [] 7 | self.main_loop = main_loop 8 | self.handle = None 9 | self.run_tasks() 10 | 11 | 12 | def add_task(self, cb): 13 | self.tasks.append(cb) 14 | 15 | 16 | def run_tasks(self, loop=None, user_data=None): 17 | for cb in self.tasks: 18 | cb() 19 | 20 | self.handle = self.main_loop.set_alarm_in(1, self.run_tasks) 21 | 22 | 23 | def stop(self): 24 | if self.handle is not None: 25 | print 'stop' 26 | self.main_loop.remove_alarm(self.handle) 27 | -------------------------------------------------------------------------------- /snek/mainwidget.py: -------------------------------------------------------------------------------- 1 | import urwid 2 | 3 | from soundloader.soundloader import Loader 4 | from soundplayer.customplayer import CustomPlayer 5 | from tracklist import TrackList 6 | from playercontrols import PlayerControls 7 | 8 | 9 | class MainWidget(urwid.Pile): 10 | def __init__(self, path, task_loop): 11 | self.loader = Loader(path) 12 | self.player = CustomPlayer(self.loader) 13 | self.task_loop = task_loop 14 | 15 | track_data = self.loader.get_all_source_info() 16 | track_window = urwid.LineBox(TrackList(self.player, track_data)) 17 | controls_window = urwid.LineBox(PlayerControls(self.player, self.task_loop)) 18 | 19 | widgets = [ 20 | ('weight', 1, track_window), 21 | (8, controls_window) 22 | ] 23 | 24 | urwid.Pile.__init__(self, widgets) 25 | -------------------------------------------------------------------------------- /snek/misc.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import urwid 4 | 5 | 6 | class CustomEdit(urwid.Edit): 7 | def __init__(self, caption, cb): 8 | urwid.Edit.__init__(self, caption) 9 | self.on_enter = cb 10 | 11 | def keypress(self, size, key): 12 | if key == 'enter': 13 | self.on_enter(self.edit_text) 14 | urwid.Edit.keypress(self, size, key) 15 | 16 | 17 | def format_time(seconds): 18 | m, s = divmod(math.floor(seconds), 60) 19 | m, s = str(int(m)), str(int(s)) 20 | time = '{0}:{1}'.format(m.zfill(2), s.zfill(2)) 21 | return time 22 | 23 | 24 | def reverse_focus_color(widget): 25 | return urwid.AttrMap(widget, None, focus_map='reversed') 26 | -------------------------------------------------------------------------------- /snek/playercontrols.py: -------------------------------------------------------------------------------- 1 | import urwid 2 | 3 | import misc 4 | 5 | 6 | class PlayerControls(urwid.ListBox): 7 | def __init__(self, player, task_loop): 8 | self.player = player 9 | self.task_loop = task_loop 10 | 11 | info_widget = urwid.Columns([self.current_track_name(), self.current_track_time()]) 12 | progress_widget = self.current_track_progress() 13 | media_widget = self.get_media_buttons() 14 | volume_widget = self.get_volume_buttons() 15 | divider = urwid.Divider(u"-") 16 | 17 | list_walker = urwid.SimpleFocusListWalker([ 18 | info_widget, 19 | divider, 20 | progress_widget, 21 | divider, 22 | media_widget, 23 | volume_widget 24 | ]) 25 | 26 | urwid.ListBox.__init__(self, list_walker) 27 | 28 | 29 | def get_volume_buttons(self): 30 | volTxt = urwid.Text(u"Volume: 10", align='center') 31 | 32 | def wrapper(widget, value): 33 | vol = self.player.volume(value) 34 | volTxt.set_text('Volume: ' + str(int(vol * 10))) 35 | 36 | volUp = urwid.Button(u"Vol \u25B2", on_press=wrapper, user_data=True) 37 | volDown = urwid.Button(u"Vol \u25BC", on_press=wrapper, user_data=False) 38 | shuffleb = urwid.CheckBox(u"Shuffle", state=False, on_state_change=self.player.toggle_shuffle) 39 | 40 | volUp._label.align = 'center' 41 | volDown._label.align = 'center' 42 | shuffleb._label.align = 'center' 43 | 44 | 45 | volUp = misc.reverse_focus_color(volUp) 46 | volDown = misc.reverse_focus_color(volDown) 47 | shuffleb = misc.reverse_focus_color(shuffleb) 48 | 49 | widget = urwid.Columns([ 50 | ('weight', 2, volTxt), 51 | ('weight', 2, volDown), 52 | ('weight', 2, volUp), 53 | ('weight', 12, shuffleb), 54 | ]) 55 | return widget 56 | 57 | 58 | def get_media_buttons(self): 59 | toggleb = urwid.Button(u"\u25B6 / \u275A\u275A", on_press=self.player.toggle_play) 60 | prevb = urwid.Button(u"\u25C0\u25C0", on_press=self.player.previous) 61 | nextb = urwid.Button(u"\u25B6\u25B6", on_press=self.player.next) 62 | 63 | toggleb._label.align = 'center' 64 | prevb._label.align = 'center' 65 | nextb._label.align = 'center' 66 | 67 | toggleb = misc.reverse_focus_color(toggleb) 68 | prevb = misc.reverse_focus_color(prevb) 69 | nextb = misc.reverse_focus_color(nextb) 70 | 71 | widget = urwid.Columns([ 72 | ('weight', 6, prevb), 73 | ('weight', 6, toggleb), 74 | ('weight', 6, nextb) 75 | ]) 76 | return widget 77 | 78 | 79 | def current_track_name(self): 80 | widget = urwid.Text(u"", wrap='clip') 81 | 82 | def wrapper(): 83 | name = self.player.current_track_name() 84 | widget.set_text(name) 85 | 86 | self.task_loop.add_task(wrapper) 87 | return widget 88 | 89 | 90 | def current_track_progress(self): 91 | widget = urwid.ProgressBar(None, 'reversed') 92 | 93 | def wrapper(): 94 | t = self.player.track_progress() 95 | widget.set_completion(t) 96 | 97 | self.task_loop.add_task(wrapper) 98 | return widget 99 | 100 | 101 | def current_track_time(self): 102 | widget = urwid.Text(u"", align='right', wrap='clip') 103 | 104 | def wrapper(): 105 | current, duration = self.player.track_time() 106 | current, duration = misc.format_time(current), misc.format_time(duration) 107 | formatted = u"{0} / {1}".format(current, duration) 108 | widget.set_text(formatted) 109 | 110 | self.task_loop.add_task(wrapper) 111 | return widget 112 | 113 | 114 | def pad(widget): 115 | widget = urwid.Padding(widget, left=1, right=1) 116 | return widget 117 | -------------------------------------------------------------------------------- /snek/snek.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import urwid 4 | 5 | import loop 6 | from topwidget import TopWidget 7 | 8 | 9 | palette = [ 10 | ('reversed', 'standout', 'default'), 11 | ('b', 'bold', 'default') 12 | ] 13 | 14 | 15 | main_loop = urwid.MainLoop(urwid.SolidFill('s'), palette) 16 | 17 | if len(sys.argv) == 2: 18 | path = sys.argv[1] 19 | else: 20 | path = '.' 21 | 22 | top = TopWidget(main_loop, path) 23 | main_loop.widget = top 24 | main_loop.run() 25 | -------------------------------------------------------------------------------- /snek/soundloader/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvin-the-programmer/snek/e403d5d5921b91705acf034e41b14ec6fd860948/snek/soundloader/__init__.py -------------------------------------------------------------------------------- /snek/soundloader/soundloader.py: -------------------------------------------------------------------------------- 1 | import os 2 | import math 3 | 4 | import pyglet 5 | 6 | 7 | class Loader: 8 | SOUND_EXTS = ('.mp3', '.wav') 9 | 10 | 11 | def __init__(self, path): 12 | resource_paths = self.get_resource_paths(path) 13 | self.loader = pyglet.resource.Loader(resource_paths) 14 | self.sound_names = self.get_sound_names(path) 15 | self.path = path 16 | 17 | 18 | def get_resource_paths(self, root): 19 | resource_paths = [] 20 | 21 | for root, dirs, files in os.walk(root): 22 | resource_paths.append(root) 23 | 24 | return resource_paths 25 | 26 | 27 | def get_sound_names(self, root): 28 | sound_names = [] 29 | 30 | for root, dirs, files in os.walk(root): 31 | sounds = [f for f in files if f.endswith(self.SOUND_EXTS)] 32 | 33 | if sounds: 34 | sound_names.extend(sounds) 35 | 36 | return sound_names 37 | 38 | 39 | def get_source(self, source_name): 40 | source = self.loader.media(source_name) 41 | return source 42 | 43 | 44 | def get_tracks_info(self, source_names): 45 | tracks_info = [self.source_info(s) for s in source_names] 46 | return tracks_info 47 | 48 | 49 | def get_all_source_info(self): 50 | tracks_info = [self.source_info(s) for s in self.sound_names] 51 | return tracks_info 52 | 53 | 54 | def source_info(self, source_name): 55 | source = self.get_source(source_name) 56 | 57 | info = { 58 | 'source_name' : source_name, 59 | 'title' : source.info.title, 60 | 'author' : source.info.author, 61 | 'album' : source.info.album, 62 | 'duration' : self.format_time(source.duration) 63 | } 64 | 65 | return info 66 | 67 | 68 | def source_title(self, source_name): 69 | source = self.get_source(source_name) 70 | return source.info.title or name 71 | 72 | 73 | def format_time(self, seconds): 74 | m, s = divmod(math.floor(seconds), 60) 75 | m, s = str(int(m)), str(int(s)) 76 | time = '{0}:{1}'.format(m.zfill(2), s.zfill(2)) 77 | return time 78 | -------------------------------------------------------------------------------- /snek/soundplayer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvin-the-programmer/snek/e403d5d5921b91705acf034e41b14ec6fd860948/snek/soundplayer/__init__.py -------------------------------------------------------------------------------- /snek/soundplayer/customplayer.py: -------------------------------------------------------------------------------- 1 | from player import Player 2 | 3 | 4 | class CustomPlayer(Player): 5 | def __init__(self, loader): 6 | Player.__init__(self, loader) 7 | 8 | def play(self, widget, number): 9 | Player.play(self, number) 10 | 11 | 12 | def next(self, widget): 13 | Player.next(self) 14 | 15 | 16 | def toggle_shuffle(self, widget, state): 17 | Player.toggle_shuffle(self) 18 | 19 | 20 | def previous(self, widget): 21 | Player.previous(self) 22 | 23 | 24 | def toggle_play(self, widget): 25 | Player.toggle_play(self) 26 | -------------------------------------------------------------------------------- /snek/soundplayer/player.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | 4 | import pyglet 5 | 6 | 7 | class Player: 8 | def __init__(self, loader): 9 | self.loader = loader 10 | self.track_queue = loader.get_all_source_info() 11 | self.now_playing = pyglet.media.Player() 12 | self.track_queue = None 13 | self.track_num = None 14 | self.shuffle_order = None 15 | self.shuffle = False 16 | 17 | 18 | def set_queue(self, source_names): 19 | if self.now_playing.source: 20 | self.now_playing.pause() 21 | self.now_playing.next_source() 22 | 23 | self.track_queue = [self.loader.source_info(s) for s in source_names] 24 | 25 | 26 | def play(self, number): 27 | self.track_num = number 28 | 29 | if self.now_playing.source: 30 | self.now_playing.pause() 31 | self.now_playing.next_source() 32 | 33 | track = self.track_queue[number] 34 | source = self.loader.get_source(track['source_name']) 35 | self.now_playing.queue(source) 36 | self.now_playing.play() 37 | 38 | 39 | def next(self): 40 | if self.now_playing.source is None: 41 | return 42 | 43 | if self.shuffle: 44 | num = self.next_shuffle_num() 45 | else: 46 | num = self.next_inorder_num() 47 | 48 | self.play(None, num) 49 | 50 | 51 | def toggle_shuffle(self): 52 | if self.shuffle: 53 | self.shuffle = False 54 | else: 55 | self.shuffle = True 56 | self.shuffle_tracks() 57 | 58 | 59 | def next_inorder_num(self): 60 | num = self.track_num + 1 61 | 62 | if num == len(self.track_queue): 63 | num = 0 64 | 65 | return num 66 | 67 | 68 | def next_shuffle_num(self): 69 | current = self.shuffle_order.index(self.track_num) 70 | self.shuffle_num = current + 1 71 | 72 | if self.shuffle_num == len(self.shuffle_order): 73 | self.shuffle_num = 0 74 | 75 | num = self.shuffle_order[self.shuffle_num] 76 | return num 77 | 78 | 79 | def previous(self): 80 | if self.now_playing.source is None: 81 | return 82 | 83 | num = self.track_num - 1 84 | 85 | if num == -1: 86 | num = len(self.track_queue) - 1 87 | 88 | self.play(None, num) 89 | 90 | 91 | def toggle_play(self): 92 | if self.now_playing.playing: 93 | self.now_playing.pause() 94 | else: 95 | self.now_playing.play() 96 | 97 | 98 | def pause(self): 99 | if self.now_playing.playing: 100 | self.now_playing.pause() 101 | 102 | 103 | def volume(self, increase): 104 | if increase: 105 | new_volume = min(1, round(self.now_playing.volume + 0.1, 1)) 106 | else: 107 | new_volume = max(0, round(self.now_playing.volume - 0.1, 1)) 108 | 109 | self.now_playing.volume = new_volume 110 | return self.now_playing.volume 111 | 112 | 113 | def shuffle_tracks(self): 114 | self.shuffle_order = [i for i in range(0, len(self.track_queue))] 115 | random.shuffle(self.shuffle_order) 116 | 117 | 118 | def autoplay(self): 119 | if self.current_time() == self.track_duration(): 120 | self.next(None) 121 | 122 | 123 | def track_progress(self): 124 | if self.now_playing.source is None: 125 | return 0 126 | percentage = (self.current_time() / self.track_duration()) * 100 127 | return math.floor(percentage) 128 | 129 | 130 | def current_track_name(self): 131 | if self.now_playing.source is None: 132 | return '' 133 | else: 134 | track = self.track_queue[self.track_num] 135 | author = track['author'] or 'unknown' 136 | title = track['title'] or track['source_name'] 137 | name = '{} - {}'.format(author, title) 138 | return name 139 | 140 | 141 | def track_time(self): 142 | return self.current_time(), self.track_duration() 143 | 144 | 145 | def current_time(self): 146 | if self.now_playing.source is None: 147 | return 0 148 | else: 149 | return math.floor(self.now_playing.time) 150 | 151 | 152 | def track_duration(self): 153 | if self.now_playing.source is None: 154 | return 0 155 | else: 156 | return math.floor(self.now_playing.source.duration) 157 | -------------------------------------------------------------------------------- /snek/topwidget.py: -------------------------------------------------------------------------------- 1 | import urwid 2 | 3 | from mainwidget import MainWidget 4 | from header import Header 5 | from loop import TaskLoop 6 | 7 | class TopWidget(urwid.Frame): 8 | def __init__(self, main_loop, initial_path): 9 | self.task_loop = TaskLoop(main_loop) 10 | 11 | body = MainWidget(initial_path, self.task_loop) 12 | header = Header(initial_path, self.change_path) 13 | 14 | urwid.Frame.__init__(self, body, header=urwid.LineBox(header)) 15 | 16 | 17 | def change_path(self, path): 18 | self.task_loop.stop() 19 | self.contents['body'][0].player.pause() 20 | 21 | new_body = MainWidget(path, self.task_loop) 22 | self.contents['body'] = (new_body, None) 23 | -------------------------------------------------------------------------------- /snek/tracklist.py: -------------------------------------------------------------------------------- 1 | import urwid 2 | 3 | 4 | class TrackList(urwid.Frame): 5 | def __init__(self, player, track_data): 6 | self.track_data = track_data 7 | self.player = player 8 | 9 | header = self.get_column_header() 10 | body = self.get_track_list('album') 11 | urwid.Frame.__init__(self, body, header=header) 12 | 13 | 14 | def get_track_list(self, sort_key): 15 | track_data = self.sort_tracks(sort_key) 16 | self.player.set_queue([t['source_name']for t in track_data]) 17 | tracks = [] 18 | 19 | for num, t in enumerate(track_data): 20 | title = urwid.Button( 21 | t['title'] or t['source_name'], 22 | on_press=self.player.play, user_data=num 23 | ) 24 | author = urwid.Button( 25 | t['author'] or 'unknown', 26 | on_press=self.player.play, user_data=num 27 | ) 28 | album = urwid.Button( 29 | t['album'] or 'unknown', 30 | on_press=self.player.play, user_data=num 31 | ) 32 | duration = urwid.Button( 33 | t['duration'], 34 | on_press=self.player.play, user_data=num 35 | ) 36 | 37 | title._label.wrap = 'clip' 38 | author._label.wrap = 'clip' 39 | album._label.wrap = 'clip' 40 | duration._label.wrap = 'clip' 41 | 42 | track = urwid.Columns([ 43 | ('weight', 8, pad(title)), 44 | ('weight', 3, pad(author)), 45 | ('weight', 5, pad(album)), 46 | ('weight', 3, pad(duration)) 47 | ], min_width=0) 48 | 49 | attr_track = urwid.AttrMap(track, None, focus_map='reversed') 50 | tracks.append(attr_track) 51 | 52 | widget = urwid.ListBox(urwid.SimpleFocusListWalker(tracks)) 53 | return widget 54 | 55 | 56 | def get_column_header(self): 57 | title = urwid.Button( 58 | ('b', u"Title"), 59 | on_press=self.update, user_data='title' 60 | ) 61 | artist = urwid.Button( 62 | ('b', u"Artist"), 63 | on_press=self.update, user_data='author' 64 | ) 65 | album = urwid.Button( 66 | ('b', u"Album"), 67 | on_press=self.update, user_data='album' 68 | ) 69 | duration = urwid.Button( 70 | ('b', u"Duration"), 71 | on_press=self.update, user_data='duration' 72 | ) 73 | 74 | title._label.wrap = 'clip' 75 | artist._label.wrap = 'clip' 76 | album._label.wrap = 'clip' 77 | duration._label.wrap = 'clip' 78 | 79 | header = urwid.Columns([ 80 | ('weight', 8, pad(title)), 81 | ('weight', 3, pad(artist)), 82 | ('weight', 5, pad(album)), 83 | ('weight', 3, pad(duration)) 84 | ], min_width=0) 85 | 86 | widget = urwid.Pile([header, urwid.Divider(u"-")]) 87 | return widget 88 | 89 | 90 | def sort_tracks(self, sort_key): 91 | sorted_tracks = sorted(self.track_data, key=lambda track:track[sort_key]) 92 | return sorted_tracks 93 | 94 | 95 | def update(self, widget, sort_key): 96 | body = (self.get_track_list(sort_key), None) 97 | self.contents['body'] = body 98 | 99 | 100 | def pad(widget): 101 | widget = urwid.Padding(widget, left=1, right=1) 102 | return widget 103 | --------------------------------------------------------------------------------