#!/usr/bin/env python import sys, os import pygtk, gtk, gobject import pygst pygst.require("0.10") import gst import random import urllib import mutagen import warnings import threading class Preferences: def __init__(self): # Default values self.volume = 1.0 self.repeat = True self.shuffle = False self.last_directory = os.path.expanduser("~/Music") self.xpos = 0 self.ypos = 0 self.width = 480 self.height = 640 self.playlist = [] def load_from_file(self, instance): #Config files by preference: self.config_paths = [os.path.expanduser("~/.justplay.conf"),"./.justplay.conf"] config_file = None self.path_to_config = None for path in self.config_paths: try: config_file = open(path, 'r') self.path_to_config = path except: continue if config_file != None: config_data = config_file.read() config_items = config_data.splitlines() for line in config_items: key,value = line.split("=") if key == "repeat": self.repeat = bool(int(value)) elif key == "shuffle": self.shuffle = bool(int(value)) elif key == "volume": self.volume = float(value) elif key == "last_directory": self.last_directory = str(value) elif key == "width": self.width = int(value) elif key == "height": self.height = int(value) elif key == "xpos": self.xpos = int(value) elif key == "ypos": self.ypos = int(value) elif key == "playlist": track_paths = value.split(";") for t in track_paths: self.playlist.append(t) def write_to_file(self, instance): if self.path_to_config == None: self.path_to_config = self.config_paths[0] config_file = open(self.path_to_config, "w") config_data = "volume=" + str(instance.volume_button.get_value()) + "\n" config_data += "repeat=" + str(int(instance.repeat_check.get_active())) + "\n" config_data += "shuffle=" + str(int(instance.shuffle_check.get_active())) + "\n" config_data += "last_directory=" + self.last_directory + "\n" config_data += "width=" + str(instance.window_object.get_size()[0]) + "\n" config_data += "height=" + str(instance.window_object.get_size()[1]) + "\n" config_data += "xpos=" + str(instance.window_object.get_position()[0]) + "\n" config_data += "ypos=" + str(instance.window_object.get_position()[1]) + "\n" config_data += "playlist=" saved_playlist = [] instance.playlist_store.foreach(self.playlist_helper, saved_playlist) joined = "" for item in saved_playlist: joined += item[0] + ";" config_data += str(joined) config_file.write(config_data) def playlist_helper(self, model, path, iter, saved_playlist): saved_playlist.append(model.get(iter, 1)) class JustPlay: def __init__(self): # init Preferences object self.preferences = Preferences() self.preferences.load_from_file(self) # init GST object self.player = gst.element_factory_make("playbin2", "player") fakesink = gst.element_factory_make("fakesink", "fakesink") self.player.set_property("video-sink", fakesink) bus = self.player.get_bus() bus.add_signal_watch() bus.connect("message", self.on_message) # init target types for drag and drop self.drop_target_types = ["UTF8_STRING","text/uri-list"] self.supported_media_formats = ["mp3","MP3","wav","WAV","ogg","OGG","m4a","M4A","FLAC","flac","mp4","MP4","mp2","MP2","aac","AAC","FLV","flv"] # init GTK+ Interface window = gtk.Window(gtk.WINDOW_TOPLEVEL) window.drag_dest_set(0, [], 0) window.connect("drag-drop", self.data_dropped) window.connect("drag-motion", self.drag_hover) window.connect("drag-data-received", self.data_received) window.set_title("JustPlay") window.set_default_size(self.preferences.width, self.preferences.height) window.connect("delete_event", self.capture_delete) window.connect("destroy", gtk.main_quit) vbox = gtk.VBox() window.add(vbox) media_button_box = gtk.HBox() vbox.pack_start(media_button_box, False, True) self.play_button = gtk.ToggleButton() temp_stock = gtk.Image() temp_stock.set_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_BUTTON) self.play_button.add(temp_stock) self.play_button.connect("toggled", self.play_track) media_button_box.pack_start(self.play_button, False, False) self.next_button = gtk.Button() temp_stock = gtk.Image() temp_stock.set_from_stock(gtk.STOCK_MEDIA_NEXT, gtk.ICON_SIZE_BUTTON) self.next_button.add(temp_stock) self.next_button.connect("clicked", self.next_track) media_button_box.pack_start(self.next_button, False, False) self.track_progress = gtk.HScale() media_button_box.pack_start(self.track_progress, True, True) self.track_progress.set_draw_value(False) self.track_progress.connect("value-changed", self.on_seek) self.time_label = gtk.Label("") media_button_box.pack_start(self.time_label, False, False) self.volume_button = gtk.ScaleButton(size=gtk.ICON_SIZE_BUTTON, min=0, max=1, step=0.02) self.volume_button.set_value(float(self.preferences.volume)) self.volume_button.connect("value-changed", self.volume_changed) self.volume_changed(None, self.preferences.volume) self.volume_button.set_use_stock(False) self.volume_button.remove(self.volume_button.get_child()) temp_stock = gtk.Label("Vol.") self.volume_button.add(temp_stock) media_button_box.pack_end(self.volume_button, False, False) self.playlist_store = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_BOOLEAN) ''' Fields: 0 - The title of the track, obtained from metadata (or fallback to leaf node of file path) 1 - The file path 2 - The status of this particular item FALSE = NULL TRUE = PLAYING ''' self.playlist_view = gtk.TreeView(model=self.playlist_store) self.playlist_view.get_selection().set_mode(gtk.SELECTION_MULTIPLE) self.playlist_view.connect("row-activated", self.new_track_selected) self.playlist_view.connect("button-press-event", self.block_input_func) state_column = gtk.TreeViewColumn(">") self.playlist_view.append_column(state_column) state_renderer = gtk.CellRendererToggle() state_column.pack_start(state_renderer) state_column.add_attribute(state_renderer, "active", 2) title_column = gtk.TreeViewColumn("Title") self.playlist_view.append_column(title_column) title_renderer = gtk.CellRendererText() title_column.pack_start(title_renderer) title_column.add_attribute(title_renderer, "text", 0) self.playlist_view.set_enable_search(False) # interferes with keyboard shortcuts scrolledwin = gtk.ScrolledWindow() scrolledwin.add_with_viewport(self.playlist_view) vbox.pack_start(scrolledwin, True, True) file_ops_box = gtk.HBox() add_files_button = gtk.Button() temp_stock = gtk.Image() temp_stock.set_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_BUTTON) add_files_button.add(temp_stock) add_files_button.connect("clicked", self.add_files_dialog) file_ops_box.pack_start(add_files_button, False, False) remove_files_button = gtk.Button() temp_stock = gtk.Image() temp_stock.set_from_stock(gtk.STOCK_REMOVE, gtk.ICON_SIZE_BUTTON) remove_files_button.add(temp_stock) remove_files_button.connect("clicked", self.remove_files) file_ops_box.pack_start(remove_files_button, False, False) clear_files_button = gtk.Button() temp_stock = gtk.Image() temp_stock.set_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_BUTTON) clear_files_button.add(temp_stock) clear_files_button.connect("clicked", self.clear_files) file_ops_box.pack_start(clear_files_button, False, False) self.repeat_check = gtk.CheckButton(label="Repeat") self.repeat_check.set_active(self.preferences.repeat) file_ops_box.pack_start(self.repeat_check, False, False) self.shuffle_check = gtk.CheckButton(label="Shuffle") self.shuffle_check.set_active(self.preferences.shuffle) file_ops_box.pack_start(self.shuffle_check, False, False) vbox.pack_start(file_ops_box, False, True) shortcuts = gtk.AccelGroup() shortcuts.connect_group(gtk.keysyms.c, 0, 0, self.play_pause_keyboard) shortcuts.connect_group(gtk.keysyms.b, 0, 0, self.next_track) shortcuts.connect_group(gtk.keysyms.f, 0, 0, self.add_files_dialog) shortcuts.connect_group(gtk.keysyms.r, 0, 0, self.toggle_repeat) shortcuts.connect_group(gtk.keysyms.s, 0, 0, self.toggle_shuffle) shortcuts.connect_group(gtk.keysyms.Delete, 0, 0, self.remove_files) window.add_accel_group(shortcuts) self.window_object = window adding_files = threading.Thread(target=self.add_files, args=[self.preferences.playlist]) adding_files.start() window.move(self.preferences.xpos, self.preferences.ypos) window.show_all() # helper functions def volume_changed(self, w, data=1): self.player.set_property("volume", float(data)) def stop_all_tracks(self, model, path, iter, data=None): model.set(iter, 2, False) def block_input_func(self, w, event, data=None): if event.button == 3: return True def update_progress(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") if gst.STATE_PLAYING not in self.player.get_state(): return False try: position = self.player.query_position(gst.FORMAT_TIME, None)[0] / gst.SECOND duration = self.player.query_duration(gst.FORMAT_TIME, None)[0] / gst.SECOND position_label = str(position / 60) + ":" + "%.2d" % (position % 60) duration_label = str(duration / 60) + ":" + "%.2d" % (duration % 60) self.time_label.set_text(str(position_label) + "/" + str(duration_label)) self.track_progress.handler_block_by_func(self.on_seek) self.track_progress.set_range(0, duration) self.track_progress.set_value(position) self.track_progress.handler_unblock_by_func(self.on_seek) return True except gst.QueryError: return True def new_track_selected(self, treeview, path, view_column): self.player.set_state(gst.STATE_NULL) self.playlist_store.foreach(self.stop_all_tracks) filepath = self.playlist_store.get_value(self.playlist_store.get_iter(path), 1) new_title = self.playlist_store.get_value(self.playlist_store.get_iter(path), 0) self.window_object.set_title("JustPlay - " + new_title) if os.path.isfile(filepath): self.player.set_property("uri", "file://" + filepath) self.play_button.set_active(True) self.player.set_state(gst.STATE_PLAYING) gobject.timeout_add(250, self.update_progress) self.playlist_store.set(self.playlist_store.get_iter(path), 2, True) else: print "could not read file " + filepath + "\nfile doesn't exist" # Main interface widget callbacks def play_pause_keyboard(self, w=None, x=None, y=None, z=None): with warnings.catch_warnings(): warnings.simplefilter("ignore") if gst.STATE_PLAYING in self.player.get_state(): # currently playing, set to pause self.play_button.set_active(False) else: self.play_button.set_active(True) def play_track(self, w=None, x=None, y=None, z=None): with warnings.catch_warnings(): warnings.simplefilter("ignore") if gst.STATE_PLAYING in self.player.get_state(): # currently playing, set to pause self.player.set_state(gst.STATE_PAUSED) #self.play_button.set_active(False) else: # currently paused, set to playing if self.player.get_property("uri") != None: self.player.set_state(gst.STATE_PLAYING) #self.play_button.set_active(True) gobject.timeout_add(250, self.update_progress) def next_track_helper(self, model, path, iter, current_track_iter): if model.get_value(iter, 2) == True: current_track_iter[0] = iter else: # for shuffle mode current_track_iter[1].append(iter) def next_track(self, w=None, x=None, y=None, z=None): current_track_iter = [None,[]] self.playlist_store.foreach(self.next_track_helper, current_track_iter) new_iter = None if self.shuffle_check.get_active(): new_iter = current_track_iter[1][random.randint(0, len(current_track_iter[1])-1)] else: if current_track_iter[0] != None: new_iter = self.playlist_store.iter_next(current_track_iter[0]) if new_iter != None: self.playlist_view.row_activated(self.playlist_store.get_path(new_iter), self.playlist_view.get_column(0)) else: new_iter = self.playlist_store.get_iter_first() # if repeat is turned on and playlist is not empty if self.repeat_check.get_active() == True and new_iter != None: self.playlist_view.row_activated(self.playlist_store.get_path(new_iter), self.playlist_view.get_column(0)) def add_files(self, uri_list): for filename in uri_list: decoded = urllib.unquote(filename) if decoded.split("://")[0] == "file": # path is already in uri form (from drag and drop) decoded = decoded.split("://")[1] extension = decoded.split(".")[-1] if extension in self.supported_media_formats: # only add files with supported file extension (skip album covers, etc.) disp_name = None tagged_file = mutagen.File(decoded) if tagged_file == None: # if file type could not be determined, just use the file name # this means the file is named with a supported extension, but # may well not be the type it claims to be, but whatever. # Users of JustPlay probably know what they're doing. disp_name = decoded.split("/")[-1] else: taglist = tagged_file.keys() # MP3 ID3 Tags if "TIT2" in taglist: if "TPE1" in taglist: disp_name = str(tagged_file.get("TPE1")) + " - " + str(tagged_file.get("TIT2")) else: # Has track title, but no artist name disp_name = str(tagged_file.get("TIT2")) # m4a tags elif "\xa9nam" in taglist: if "aART" in taglist: disp_name = str(tagged_file.get("aART")[0]) + " - " + str(tagged_file.get("\xa9nam")[0]) else: # Has track title, but no artist name disp_name = str(tagged_file.get("\xa9nam")[0]) # flac or ogg vorbis tags elif "title" in taglist: if "artist" in taglist: disp_name = str(tagged_file.get("artist")[0]) + " - " + str(tagged_file.get("title")[0]) else: disp_name = str(tagged_file.get("title")[0]) else: print tagged_file.keys() print tagged_file.pprint() disp_name = decoded.split("/")[-1] self.playlist_store.append([disp_name, decoded, False]) elif os.path.isdir(decoded): subfiles = sorted(os.listdir(decoded)) subfiles_full = [] for item in subfiles: subfiles_full.append(decoded + "/" + item) self.add_files(subfiles_full) def add_files_dialog_helper(self, widget, chooser): new_files = chooser.get_filenames() adding_files = threading.Thread(target=self.add_files, args=[new_files]) adding_files.start() self.preferences.last_directory = chooser.get_current_folder() chooser.destroy() def add_files_dialog(self, w=None, x=None, y=None, z=None): chooser = gtk.FileChooserDialog(title="Add Files",action=gtk.FILE_CHOOSER_ACTION_OPEN, buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN, gtk.RESPONSE_OK)) add_button = gtk.Button(stock=gtk.STOCK_ADD) add_button.connect("clicked", self.add_files_dialog_helper, chooser) chooser.get_action_area().pack_start(add_button, False, False) add_button.show() chooser.set_select_multiple(True) chooser.set_current_folder(self.preferences.last_directory) response = chooser.run() if response == gtk.RESPONSE_OK: # add selected files to playlist new_files = chooser.get_filenames() adding_files = threading.Thread(target=self.add_files, args=[new_files]) adding_files.start() self.preferences.last_directory = chooser.get_current_folder() chooser.destroy() else: chooser.destroy() def remove_files_helper(self, model, path, iter, pathlist): pathlist.append(path) def remove_files(self, w=None, x=None, y=None, z=None): selection_list = self.playlist_view.get_selection() pathlist = [] selection_list.selected_foreach(self.remove_files_helper, pathlist) pathlist.reverse() for item in pathlist: self.playlist_store.remove(self.playlist_store.get_iter(item)) def clear_files(self, w=None): self.playlist_store.clear() def save_playlist(self, data=None): chooser = gtk.FileChooserDiaflog(title="Save Playlist",action=gtk.FILE_CHOOSER_ACTION_SAVE, buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_SAVE,gtk.RESPONSE_OK)) chooser.set_select_multiple(False) chooser.set_current_folder(self.preferences.last_directory) response = chooser.run() if response == gtk.RESPONSE_OK: # save current tracklist to .m3u format file self.preferences.last_directory = chooser.get_current_folder() chooser.destroy() def on_seek(self, data=None): self.player.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, self.track_progress.get_value() * gst.SECOND) def toggle_repeat(self, w=None, x=None, y=None, z=None): if self.repeat_check.get_active() == True: self.repeat_check.set_active(False) else: self.repeat_check.set_active(True) def toggle_shuffle(self, w=None, x=None, y=None, z=None): if self.shuffle_check.get_active() == True: self.shuffle_check.set_active(False) else: self.shuffle_check.set_active(True) def drag_hover(self, wid, context, x, y, time): context.drag_status(gtk.gdk.ACTION_COPY, time) return True def data_received(self, widget, context, x, y, data, info, time): dropped_data = None try: dropped_data = data.get_text().splitlines() except: try: dropped_data = data.get_uris() except: dropped_data = None if dropped_data != None: adding_files = threading.Thread(target=self.add_files, args=[dropped_data]) adding_files.start() context.finish(True, False, time) def data_dropped(self, wid, context, x, y, time): for t in context.targets: print t if t in self.drop_target_types: wid.drag_get_data(context, t) break # only want to process the dropped data once return True def on_message(self, bus, message): t = message.type if t == gst.MESSAGE_EOS: self.player.set_state(gst.STATE_NULL) self.next_track(None) elif t == gst.MESSAGE_ERROR: self.player.set_state(gst.STATE_NULL) err, debug = message.parse_error() print "Error: %s" % err, debug def capture_delete(self, w, data=None): self.preferences.write_to_file(self) return False def main(self): gtk.gdk.threads_init() gtk.main() # If the program is run directly or passed as an argument to the python # interpreter then create a HelloWorld instance and show it if __name__ == "__main__": justplay = JustPlay() justplay.main()