Esempio n. 1
0
    def onAppStarted(self):
        """ This is the real initialization function, called when the module has been loaded """
        wTree                  = tools.prefs.getWidgetsTree()
        self.playtime          = 0
        self.bufferedTrack     = None
        # Retrieve widgets
        self.window     = wTree.get_object('win-main')

        columns = (('',   [(gtk.CellRendererPixbuf(), gtk.gdk.Pixbuf), (gtk.CellRendererText(), TYPE_STRING)], True),
                   (None, [(None, TYPE_PYOBJECT)], False),
                  )

        self.tree = TrackTreeView(columns, use_markup=True)
        self.tree.enableDNDReordering()
        self.tree.setDNDSources([DND_INTERNAL_TARGET])

        wTree.get_object('scrolled-tracklist').add(self.tree)

        # GTK handlers
        self.tree.connect('exttreeview-button-pressed', self.onMouseButton)
        self.tree.connect('tracktreeview-dnd', self.onDND)
        self.tree.connect('key-press-event', self.onKeyboard)
        self.tree.get_model().connect('row-deleted', self.onRowDeleted)

        (options, args) = prefs.getCmdLine()

        self.savedPlaylist = os.path.join(consts.dirCfg, 'saved-playlist')
        self.paused = False


        # Populate the playlist with the saved playlist
        dump = None
        if os.path.exists(self.savedPlaylist):
            try:
                dump = pickleLoad(self.savedPlaylist)
            except:
                msg = '[%s] Unable to restore playlist from %s\n\n%s'
                log.logger.error(msg % (MOD_INFO[modules.MODINFO_NAME],
                                self.savedPlaylist, traceback.format_exc()))

        if dump:
            self.restoreTreeDump(dump)
            log.logger.info('[%s] Restored playlist' % MOD_INFO[modules.MODINFO_NAME])
            self.tree.collapse_all()
            self.select_last_played_track()
            self.onListModified()

        commands, args = tools.separate_commands_and_tracks(args)

        # Add commandline tracks to the playlist
        if args:
            log.logger.info('[%s] Filling playlist with files given on command line' % MOD_INFO[modules.MODINFO_NAME])
            tracks  = media.getTracks([os.path.abspath(arg) for arg in args])
            playNow = not 'stop' in commands and not 'pause' in commands
            modules.postMsg(consts.MSG_CMD_TRACKLIST_ADD, {'tracks': tracks, 'playNow': playNow})
        elif 'play' in commands:
            modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE)

        # Automatically save the content at regular intervals
        gobject.timeout_add_seconds(SAVE_INTERVAL, self.save_track_tree)
Esempio n. 2
0
class Tracktree(modules.Module):
    """ This module manages the tracklist """

    def __init__(self):
        """ Constructor """
        handlers = {
            consts.MSG_CMD_NEXT:              self.jumpToNext,
            consts.MSG_EVT_PAUSED:            self.onPaused,
            consts.MSG_EVT_STOPPED:           self.onStopped,
            consts.MSG_EVT_UNPAUSED:          self.onUnPaused,
            consts.MSG_CMD_PREVIOUS:          self.jumpToPrevious,
            consts.MSG_EVT_NEED_BUFFER:       self.onBufferingNeeded,
            consts.MSG_EVT_APP_STARTED:       self.onAppStarted,
            consts.MSG_EVT_APP_QUIT:          self.onAppQuit,
            consts.MSG_CMD_TOGGLE_PAUSE:      self.togglePause,
            consts.MSG_CMD_TRACKLIST_DEL:     self.remove,
            consts.MSG_CMD_TRACKLIST_ADD:     self.insert,
            consts.MSG_CMD_TRACKLIST_SET:     self.set,
            consts.MSG_CMD_TRACKLIST_CLR:     lambda: self.set(None, None),
            consts.MSG_EVT_TRACK_ENDED_OK:    lambda: self.onTrackEnded(False),
            consts.MSG_EVT_TRACK_ENDED_ERROR: lambda: self.onTrackEnded(True),
            consts.MSG_CMD_FILE_EXPLORER_DRAG_BEGIN: self.onDragBegin,
            consts.MSG_EVT_SEARCH_START:      self.onSearchStart,
            consts.MSG_EVT_SEARCH_RESET:      self.onSearchReset,
            }

        modules.Module.__init__(self, handlers)


    def getTreeDump(self, path=None):
        """ Recursively dump the given tree starting at path (None for the root of the tree) """
        list = []

        for child in self.tree.iterChildren(path):
            row = self.tree.getRow(child)

            if self.tree.getNbChildren(child) == 0:
                grandChildren = None
            else:
                grandChildren = self.getTreeDump(child)

            name = row[ROW_NAME].replace('<b>', '').replace('</b>', '')

            list.append([(name, row[ROW_TRK]), grandChildren])

        return list


    def restoreTreeDump(self, dump, parent=None):
        """ Recursively restore the dump under the given parent (None for the root of the tree) """
        for item in dump:
            (name, track) = item[0]

            if track:
                self.tree.appendRow((icons.nullMenuIcon(), name, track), parent)
            else:
                newNode = self.tree.appendRow((icons.mediaDirMenuIcon(), name, None), parent)

                if item[1] is not None:
                    if len(item[1]) != 0:
                        # We must expand the row before adding the real children,
                        # but this works only if there is already at least one child
                        self.restoreTreeDump(item[1], newNode)


    def select_last_played_track(self):
        last_path = prefs.get(__name__, 'last-played-track', None)
        if last_path:
            parent_path = (last_path[0],)
            gobject.idle_add(self.tree.scroll_to_cell, parent_path)
            self.tree.get_selection().select_path(parent_path)


    def getTrackDir(self, root=None):
        flat = False if root else True
        name = self.tree.getLabel(root) if root else 'playtree'
        name = name.replace('<b>', '').replace('</b>', '')
        trackdir = media.TrackDir(name=name, flat=flat)

        for iter in self.tree.iter_children(root):
            track = self.tree.getTrack(iter)
            if track:
                trackdir.tracks.append(track)
            else:
                subdir = self.getTrackDir(iter)
                trackdir.subdirs.append(subdir)

        return trackdir


    def get_m3u_text(self, root=None):
        text = ''
        for iter in self.tree.iter_children(root):
            track = self.tree.getTrack(iter)
            if track:
                text += '%s\n' % track.getFilePath()
            else:
                dirname = self.tree.getLabel(iter).replace('<b>', '').replace('</b>', '')
                text += '# %s\n%s\n' % (dirname, self.get_m3u_text(iter))
        return text


    def __getNextTrackIter(self):
        """ Return the index of the next track, or -1 if there is none """
        next = None
        while True:
            next = self.tree.get_next_iter(next)
            if not next:
                return None

            # Check track
            error = self.tree.getItem(next, ROW_ICO) == icons.errorMenuIcon()
            track = self.tree.getItem(next, ROW_TRK)
            if track and not error:
                # Row is not a directory
                return next


    def __hasNextTrack(self):
        """ Return whether there is a next track """
        return self.__getNextTrackIter() is not None


    def __getPreviousTrackIter(self):
        """ Return the index of the previous track, or -1 if there is none """
        prev = None
        while True:
            prev = self.tree.get_prev_iter(prev)
            if not prev:
                return None

            # Check track
            error = self.tree.getItem(prev, ROW_ICO) == icons.errorMenuIcon()
            track = self.tree.getItem(prev, ROW_TRK)
            if track and not error:
                # Row is not a directory
                return prev


    def __hasPreviousTrack(self):
        """ Return whether there is a previous track """
        return self.__getPreviousTrackIter() is not None


    def jumpToNext(self):
        """ Jump to the next track, if any """
        where = self.__getNextTrackIter()
        if where:
            self.jumpTo(where)


    def jumpToPrevious(self):
        """ Jump to the previous track, if any """
        where = self.__getPreviousTrackIter()
        if where:
            self.jumpTo(where)


    def set_track_playing(self, iter, playing):
        if not iter:
            return
        track = self.tree.getTrack(iter)
        if not track:
            return

        for parent in self.tree.get_all_parents(iter):
            parent_label = self.tree.getLabel(parent)
            parent_label = tools.htmlUnescape(parent_label)
            is_bold = parent_label.startswith('<b>') and parent_label.endswith('</b>')
            if playing and not is_bold:
                parent_label = tools.htmlEscape(parent_label)
                self.tree.setLabel(parent, '<b>%s</b>' % parent_label)
            elif not playing and is_bold:
                parent_label = tools.htmlEscape(parent_label[3:-4])
                self.tree.setLabel(parent, parent_label)

        parent = self.tree.store.iter_parent(iter)
        parent_label = self.tree.getLabel(parent) if parent else None
        label = track.get_label(parent_label=parent_label, playing=playing)
        if playing:
            self.tree.setLabel(iter, label)
            self.tree.setItem(iter, ROW_ICO, icons.playMenuIcon())
            self.tree.expand(iter)
        else:
            self.tree.setLabel(iter, label)
            icon = self.tree.getItem(iter, ROW_ICO)
            has_error = (icon == icons.errorMenuIcon())
            is_dir = (icon == icons.mediaDirMenuIcon())
            if not is_dir and not has_error:
                self.tree.setItem(iter, ROW_ICO, icons.nullMenuIcon())


    def jumpTo(self, iter, sendPlayMsg=True, forced=True):
        """ Jump to the track located at the given iter """
        if not iter:
            return

        mark = self.tree.getMark()
        if mark:
            self.set_track_playing(mark, False)

        self.tree.setMark(iter)
        self.tree.scroll(iter)

        # Check track
        track = self.tree.getTrack(iter)
        if not track:
            # Row may be a directory
            self.jumpTo(self.__getNextTrackIter())
            return

        self.set_track_playing(iter, True)
        self.paused = False

        if sendPlayMsg:
            modules.postMsg(consts.MSG_CMD_PLAY, {'uri': track.getURI(), 'forced': forced})

        modules.postMsg(consts.MSG_EVT_NEW_TRACK,   {'track': track})
        modules.postMsg(consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__hasPreviousTrack(), 'hasNext': self.__hasNextTrack()})


    def insert(self, tracks, target=None, drop_mode=None, playNow=True, highlight=False):
        if type(tracks) == list:
            trackdir = media.TrackDir(None, flat=True)
            trackdir.tracks = tracks
            tracks = trackdir

        children_before = self.tree.store.iter_n_children(target)

        self.tree.get_selection().unselect_all()
        self.insertDir(tracks, target, drop_mode, highlight)
        self.onListModified()

        # We only want to start playback if tracks are appended from DBus
        # or appended (not inserted) into the playlist.
        # In that case target is None. Also don't interrupt playing songs.
        logging.info('playNow: %s, target: %s, self.tree.hasMark(): %s, self.paused: %s' % (playNow, target, self.tree.hasMark(), self.paused))
        if playNow and target is None and (not self.tree.hasMark() or self.paused):
            # If the target is None, the tracks have to be appended to the top
            # level and the first new track is the one after the original tracks
            new = self.tree.store.iter_nth_child(target, children_before)
            if new:
                # If new is None, the tracks could not be added
                self.jumpTo(new)


    def insertDir(self, trackdir, target=None, drop_mode=None, highlight=False):
        '''
        Insert a directory recursively, return the iter of the first
        added element
        '''
        model = self.tree.store
        if trackdir.flat:
            new = target
        else:
            string = trackdir.dirname.replace('_', ' ')
            string = tools.htmlEscape(string)
            source_row = (icons.mediaDirMenuIcon(), string, None)

            new = self.tree.insert(target, source_row, drop_mode)
            drop_mode = gtk.TREE_VIEW_DROP_INTO_OR_AFTER
            if highlight:
                self.tree.select(new)

        dest = new
        for index, subdir in enumerate(trackdir.subdirs):
            drop = drop_mode if index == 0 else gtk.TREE_VIEW_DROP_AFTER
            dest = self.insertDir(subdir, dest, drop, highlight)

        dest = new
        for index, track in enumerate(trackdir.tracks):
            drop = drop_mode if index == 0 else gtk.TREE_VIEW_DROP_AFTER
            highlight &= trackdir.flat
            dest = self.insertTrack(track, dest, drop, highlight)

        if not trackdir.flat:
            # Open albums on the first layer
            if target is None or model.iter_depth(new) == 0:
                self.tree.expand(new)

        return new


    def insertTrack(self, track, target=None, drop_mode=None, highlight=False):
        '''
        Insert a new track into the tracktree under parentPath
        '''
        length = track.getLength()
        self.playtime += length

        name = track.get_label()

        row = (icons.nullMenuIcon(), name, track)
        new_iter = self.tree.insert(target, row, drop_mode)
        parent = self.tree.store.iter_parent(new_iter)
        if parent:
            # adjust track label to parent
            parent_label = self.tree.getLabel(parent)
            new_label = track.get_label(parent_label)
            self.tree.setLabel(new_iter, new_label)
        if highlight:
            self.tree.select(new_iter)
        return new_iter


    def set(self, tracks, playNow):
        """ Replace the tracklist, clear it if tracks is None """
        self.playtime = 0

        if type(tracks) == list:
            trackdir = media.TrackDir(None, flat=True)
            trackdir.tracks = tracks
            tracks = trackdir

        if self.tree.hasMark() and ((not playNow) or (tracks is None) or tracks.empty()):
            modules.postMsg(consts.MSG_CMD_STOP)

        self.tree.clear()

        if tracks is not None and not tracks.empty():
            self.insert(tracks, playNow=playNow)

        self.tree.collapse_all()

        self.onListModified()


    def export_playlist_to_m3u(self):
        """ Save the current tracklist to a playlist """
        outfile = fileChooser.save(self.window, _('Export playlist to file'), 'playlist.m3u')

        if outfile is not None:
            tools.write_file(outfile, self.get_m3u_text())


    def export_playlist_to_dir(self):
        """ Save the current tracklist to a playlist """
        outdir = fileChooser.openDirectory(self.window, _('Export playlist to directory'))

        if outdir is not None:
            trackdir = self.getTrackDir()
            trackdir.export_to_dir(outdir)


    def remove(self, iter=None):
        """ Remove the given track, or the selection if iter is None """
        hadMark = self.tree.hasMark()

        iters = [iter] if iter else list(self.tree.iterSelectedRows())

        prev_iter = self.tree.get_prev_iter_or_parent(iters[0])

        # reverse list, so that we remove children before their fathers
        for iter in reversed(iters):
            track = self.tree.getTrack(iter)
            if track:
                self.playtime -= track.getLength()
            self.tree.removeRow(iter)

        self.tree.selection.unselect_all()

        if hadMark and not self.tree.hasMark():
            modules.postMsg(consts.MSG_CMD_STOP)

        # Select new track when old selected is deleted
        if prev_iter:
            self.tree.select(prev_iter)
        else:
            first_iter = self.tree.get_first_iter()
            if first_iter:
                self.tree.select(first_iter)

        self.onListModified()


    def onShowPopupMenu(self, tree, button, time, path):
        """ The index parameter may be None """
        if path is None:
            iter = None
        else:
            iter = tree.store.get_iter(path)

        popup = gtk.Menu()

        # Remove
        remove = gtk.ImageMenuItem(gtk.STOCK_REMOVE)
        popup.append(remove)

        if iter is None:
            remove.set_sensitive(False)
        else:
            remove.connect('activate', lambda item: self.remove())

        #popup.append(gtk.SeparatorMenuItem())

        # Clear
        clear = gtk.ImageMenuItem(_('Clear Playlist'))
        clear.set_image(gtk.image_new_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_MENU))
        popup.append(clear)

        if len(tree.store) == 0:
            clear.set_sensitive(False)
        else:
            clear.connect('activate', lambda item: modules.postMsg(consts.MSG_CMD_TRACKLIST_CLR))

        # Save to m3u
        export_m3u = gtk.ImageMenuItem(_('Export playlist to file'))
        export_m3u.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE, gtk.ICON_SIZE_MENU))
        popup.append(export_m3u)

        # Save to dir
        export_dir = gtk.ImageMenuItem(_('Export playlist to directory'))
        export_dir.set_image(gtk.image_new_from_stock(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_MENU))
        popup.append(export_dir)

        if len(tree.store) == 0:
            export_m3u.set_sensitive(False)
            export_dir.set_sensitive(False)
        else:
            export_m3u.connect('activate', lambda item: self.export_playlist_to_m3u())
            export_dir.connect('activate', lambda item: self.export_playlist_to_dir())

        popup.show_all()
        popup.popup(None, None, None, button, time)


    def togglePause(self):
        """ Start playing if not already playing """
        if len(self.tree) != 0 and not self.tree.hasMark():
            if self.tree.selection.count_selected_rows() > 0:
                model, sel_rows_list = self.tree.selection.get_selected_rows()
                first_sel_iter = self.tree.store.get_iter(sel_rows_list[0])
                self.jumpTo(first_sel_iter)
            else:
                self.jumpTo(self.tree.get_first_iter())


    def save_track_tree(self):
        # Save playing track
        if self.tree.hasMark():
            last_path = self.tree.mark.get_path()
        else:
            last_path = None
        prefs.set(__name__, 'last-played-track', last_path)

        dump = self.getTreeDump()
        logging.info('Saving playlist')
        pickleSave(self.savedPlaylist, dump)
        # tell gobject to keep saving the content in regular intervals
        return True


    # --== Message handlers ==--


    def onAppStarted(self):
        """ This is the real initialization function, called when the module has been loaded """
        wTree                  = tools.prefs.getWidgetsTree()
        self.playtime          = 0
        self.bufferedTrack     = None
        # Retrieve widgets
        self.window     = wTree.get_object('win-main')

        columns = (('',   [(gtk.CellRendererPixbuf(), gtk.gdk.Pixbuf), (gtk.CellRendererText(), TYPE_STRING)], True),
                   (None, [(None, TYPE_PYOBJECT)], False),
                  )

        self.tree = TrackTreeView(columns, use_markup=True)
        self.tree.enableDNDReordering()
        self.tree.setDNDSources([DND_INTERNAL_TARGET])

        wTree.get_object('scrolled-tracklist').add(self.tree)

        # GTK handlers
        self.tree.connect('exttreeview-button-pressed', self.onMouseButton)
        self.tree.connect('tracktreeview-dnd', self.onDND)
        self.tree.connect('key-press-event', self.onKeyboard)
        self.tree.get_model().connect('row-deleted', self.onRowDeleted)

        (options, args) = prefs.getCmdLine()

        self.savedPlaylist = os.path.join(consts.dirCfg, 'saved-playlist')
        self.paused = False


        # Populate the playlist with the saved playlist
        dump = None
        if os.path.exists(self.savedPlaylist):
            try:
                dump = pickleLoad(self.savedPlaylist)
            except:
                msg = '[%s] Unable to restore playlist from %s\n\n%s'
                log.logger.error(msg % (MOD_INFO[modules.MODINFO_NAME],
                                self.savedPlaylist, traceback.format_exc()))

        if dump:
            self.restoreTreeDump(dump)
            log.logger.info('[%s] Restored playlist' % MOD_INFO[modules.MODINFO_NAME])
            self.tree.collapse_all()
            self.select_last_played_track()
            self.onListModified()

        commands, args = tools.separate_commands_and_tracks(args)

        # Add commandline tracks to the playlist
        if args:
            log.logger.info('[%s] Filling playlist with files given on command line' % MOD_INFO[modules.MODINFO_NAME])
            tracks  = media.getTracks([os.path.abspath(arg) for arg in args])
            playNow = not 'stop' in commands and not 'pause' in commands
            modules.postMsg(consts.MSG_CMD_TRACKLIST_ADD, {'tracks': tracks, 'playNow': playNow})
        elif 'play' in commands:
            modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE)

        # Automatically save the content at regular intervals
        gobject.timeout_add_seconds(SAVE_INTERVAL, self.save_track_tree)


    def onAppQuit(self):
        """ The module is going to be unloaded """
        self.save_track_tree()


    def onTrackEnded(self, withError):
        """ The current track has ended, jump to the next one if any """
        current_iter = self.tree.getMark()

        # If an error occurred with the current track, flag it as such
        if withError and current_iter:
            self.tree.setItem(current_iter, ROW_ICO, icons.errorMenuIcon())

        # Find the next 'playable' track (not already flagged)
        next = self.__getNextTrackIter()
        if next:
            send_play_msg = True
            if current_iter:
                track_name = self.tree.getTrack(current_iter).getURI()
                send_play_msg = (track_name != self.bufferedTrack)
            self.jumpTo(next, sendPlayMsg=send_play_msg, forced=False)
            self.bufferedTrack = None
            return

        self.bufferedTrack = None
        modules.postMsg(consts.MSG_CMD_STOP)


    def onBufferingNeeded(self):
        """ The current track is close to its end, so we try to buffer the next one to avoid gaps """
        where = self.__getNextTrackIter()
        if where:
            self.bufferedTrack = self.tree.getItem(where, ROW_TRK).getURI()
            modules.postMsg(consts.MSG_CMD_BUFFER, {'uri': self.bufferedTrack})


    def onStopped(self):
        """ Playback has been stopped """
        if self.tree.hasMark():
            currTrack = self.tree.getMark()
            self.set_track_playing(currTrack, False)
            self.tree.clearMark()


    def onPausedToggled(self, icon):
        """ Switch between paused and unpaused """
        if self.tree.hasMark():
            self.tree.setItem(self.tree.getMark(), ROW_ICO, icon)


    def highlight(self, query, root=None):
        """Select all rows (and their parents) that contain all parts of *query*."""
        for iter in self.tree.iter_children(root):
            track = self.tree.getTrack(iter)
            if track:
                if all(part in track.get_search_text() for part in query):
                    self.tree.select(iter)
                    # Highlight all parents as well
                    for parent in self.tree.get_all_parents(iter):
                        self.tree.select_synchronously(parent)
            else:
                dirname = self.tree.getLabel(iter).replace('<b>', '').replace('</b>', '')
                if all(part in dirname.lower() for part in query):
                    self.tree.select_synchronously(iter)
                self.highlight(query, iter)


    def onSearchStart(self, query):
        query = [part.strip().lower() for part in query.split()]
        gobject.idle_add(self.highlight, query)
        gobject.idle_add(self.tree.scroll_to_first_selection)

    def onSearchReset(self):
        self.tree.selection.unselect_all()


    def onPaused(self):
        self.paused = True
        self.onPausedToggled(icons.pauseMenuIcon())

    def onUnPaused(self):
        self.paused = False
        self.onPausedToggled(icons.playMenuIcon())


    def onDragBegin(self, paths):
        dir_selected = any(map(os.path.isdir, paths))
        self.tree.dir_selected = dir_selected
        if dir_selected:
            self.tree.collapse_all()


    # --== GTK handlers ==--


    def onMouseButton(self, tree, event, path):
        """ A mouse button has been pressed """
        if event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS and path is not None:
            self.jumpTo(self.tree.store.get_iter(path))
        elif event.button == 3:
            self.onShowPopupMenu(tree, event.button, event.time, path)


    def onKeyboard(self, list, event):
        """ Keyboard shortcuts """
        keyname = gtk.gdk.keyval_name(event.keyval)

        if keyname == 'Delete':   self.remove()
        elif keyname == 'Return': self.jumpTo(self.tree.getFirstSelectedRow())
        elif keyname == 'space':  modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE)
        elif keyname == 'Escape': modules.postMsg(consts.MSG_CMD_STOP)
        elif keyname == 'Left':   modules.postMsg(consts.MSG_CMD_STEP, {'seconds': -5})
        elif keyname == 'Right':  modules.postMsg(consts.MSG_CMD_STEP, {'seconds': 5})


    def onListModified(self):
        """ Some rows have been added/removed/moved """
        # Getting the trackdir takes virtually no time, so we can do it on every
        # paylist change
        tracks = self.getTrackDir()
        self.playtime = tracks.get_playtime()

        modules.postMsg(consts.MSG_EVT_NEW_TRACKLIST, {'tracks': tracks, 'playtime': self.playtime})

        if self.tree.hasMark():
            modules.postMsg(consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__hasPreviousTrack(), 'hasNext':  self.__hasNextTrack()})


    def onDND(self, list, context, x, y, dragData, dndId, time):
        """ External Drag'n'Drop """
        import urllib

        if dragData.data == '':
            context.finish(False, False, time)
            return

        # A list of filenames, without 'file://' at the beginning
        if dndId == consts.DND_POGO_URI:
            tracks = media.getTracks([urllib.url2pathname(uri) for uri in dragData.data.split()])
        # A list of filenames starting with 'file://'
        elif dndId == consts.DND_URI:
            tracks = media.getTracks([urllib.url2pathname(uri)[7:] for uri in dragData.data.split()])
        else:
            assert False

        # dropInfo is tuple (path, drop_pos)
        dropInfo = list.get_dest_row_at_pos(x, y)

        # Insert the tracks, but beware of the AFTER/BEFORE mechanism used by GTK
        if dropInfo is None:
            self.insert(tracks, playNow=False, highlight=True)
        else:
            path, drop_mode = dropInfo
            iter = self.tree.store.get_iter(path)
            self.insert(tracks, iter, drop_mode, playNow=False, highlight=True)

        # We want to allow dropping tracks only when we are sure that no dir is
        # selected. This is needed for dnd from nautilus.
        self.tree.dir_selected = True

        context.finish(True, False, time)

    def onRowDeleted(self, model, path):
        """
        Internal drag and drop cannot be caught in PyGTK since the
        "rows-reordered" signal is not emitted and the work-around proposed by
        katsh doesn't work either
        (http://stackoverflow.com/questions/2831779/catch-pygtk-treeview-reorder).
        After a drag and drop operation PyGTK emits the "row-inserted" and
        afterwards "row-deleted" signals, so we catch the latter and update the
        buttons.
        """
        self.onListModified()
Esempio n. 3
0
class Tracktree(modules.Module):
    """ This module manages the tracklist """

    def __init__(self):
        """ Constructor """
        handlers = {
            consts.MSG_CMD_NEXT:              self.jumpToNext,
            consts.MSG_EVT_PAUSED:            self.onPaused,
            consts.MSG_EVT_STOPPED:           self.onStopped,
            consts.MSG_EVT_UNPAUSED:          self.onUnPaused,
            consts.MSG_CMD_PREVIOUS:          self.jumpToPrevious,
            consts.MSG_EVT_NEED_BUFFER:       self.onBufferingNeeded,
            consts.MSG_EVT_APP_STARTED:       self.onAppStarted,
            consts.MSG_EVT_APP_QUIT:          self.onAppQuit,
            consts.MSG_CMD_TOGGLE_PAUSE:      self.togglePause,
            consts.MSG_CMD_TRACKLIST_DEL:     self.remove,
            consts.MSG_CMD_TRACKLIST_ADD:     self.insert,
            consts.MSG_CMD_TRACKLIST_SET:     self.set,
            consts.MSG_CMD_TRACKLIST_CLR:     lambda: self.set(None, None),
            consts.MSG_EVT_TRACK_ENDED_OK:    lambda: self.onTrackEnded(False),
            consts.MSG_EVT_TRACK_ENDED_ERROR: lambda: self.onTrackEnded(True),
            consts.MSG_CMD_FILE_EXPLORER_DRAG_BEGIN: self.onDragBegin,
            consts.MSG_EVT_SEARCH_START:      self.onSearchStart,
            consts.MSG_EVT_SEARCH_RESET:      self.onSearchReset,
            }

        modules.Module.__init__(self, handlers)


    def getTreeDump(self, path=None):
        """ Recursively dump the given tree starting at path (None for the root of the tree) """
        list = []

        for child in self.tree.iterChildren(path):
            row = self.tree.getRow(child)

            if self.tree.getNbChildren(child) == 0:
                grandChildren = None
            else:
                grandChildren = self.getTreeDump(child)

            name = row[ROW_NAME].replace('<b>', '').replace('</b>', '')

            list.append([(name, row[ROW_TRK]), grandChildren])

        return list


    def restoreTreeDump(self, dump, parent=None):
        """ Recursively restore the dump under the given parent (None for the root of the tree) """
        for item in dump:
            (name, track) = item[0]

            if track:
                self.tree.appendRow((icons.nullMenuIcon(), name, track), parent)
            else:
                newNode = self.tree.appendRow((icons.mediaDirMenuIcon(), name, None), parent)

                if item[1] is not None:
                    if len(item[1]) != 0:
                        # We must expand the row before adding the real children,
                        # but this works only if there is already at least one child
                        self.restoreTreeDump(item[1], newNode)


    def select_last_played_track(self):
        last_path = prefs.get(__name__, 'last-played-track', None)
        if last_path:
            parent_path = (last_path[0],)
            GObject.idle_add(self.tree.scroll_to_cell, parent_path)
            self.tree.get_selection().select_path(parent_path)


    def getTrackDir(self, root=None):
        flat = False if root else True
        name = self.tree.getLabel(root) if root else 'playtree'
        name = name.replace('<b>', '').replace('</b>', '')
        trackdir = media.TrackDir(name=name, flat=flat)

        for iter in self.tree.iter_children(root):
            track = self.tree.getTrack(iter)
            if track:
                trackdir.tracks.append(track)
            else:
                subdir = self.getTrackDir(iter)
                trackdir.subdirs.append(subdir)

        return trackdir


    def get_m3u_text(self, root=None):
        text = ''
        for iter in self.tree.iter_children(root):
            track = self.tree.getTrack(iter)
            if track:
                text += '%s\n' % track.getFilePath()
            else:
                dirname = self.tree.getLabel(iter).replace('<b>', '').replace('</b>', '')
                text += '# %s\n%s\n' % (dirname, self.get_m3u_text(iter))
        return text


    def __getNextTrackIter(self):
        """ Return the index of the next track, or -1 if there is none """
        next = None
        while True:
            next = self.tree.get_next_iter(next)
            if not next:
                return None

            # Check track
            error = self.tree.getItem(next, ROW_ICO) == icons.errorMenuIcon()
            track = self.tree.getItem(next, ROW_TRK)
            if track and not error:
                # Row is not a directory
                return next


    def __hasNextTrack(self):
        """ Return whether there is a next track """
        return self.__getNextTrackIter() is not None


    def __getPreviousTrackIter(self):
        """ Return the index of the previous track, or -1 if there is none """
        prev = None
        while True:
            prev = self.tree.get_prev_iter(prev)
            if not prev:
                return None

            # Check track
            error = self.tree.getItem(prev, ROW_ICO) == icons.errorMenuIcon()
            track = self.tree.getItem(prev, ROW_TRK)
            if track and not error:
                # Row is not a directory
                return prev


    def __hasPreviousTrack(self):
        """ Return whether there is a previous track """
        return self.__getPreviousTrackIter() is not None


    def jumpToNext(self):
        """ Jump to the next track, if any """
        where = self.__getNextTrackIter()
        if where:
            self.jumpTo(where)


    def jumpToPrevious(self):
        """ Jump to the previous track, if any """
        where = self.__getPreviousTrackIter()
        if where:
            self.jumpTo(where)


    def set_track_playing(self, iter, playing):
        if not iter:
            return
        track = self.tree.getTrack(iter)
        if not track:
            return

        for parent in self.tree.get_all_parents(iter):
            parent_label = self.tree.getLabel(parent)
            parent_label = tools.htmlUnescape(parent_label)
            is_bold = parent_label.startswith('<b>') and parent_label.endswith('</b>')
            if playing and not is_bold:
                parent_label = tools.htmlEscape(parent_label)
                self.tree.setLabel(parent, '<b>%s</b>' % parent_label)
            elif not playing and is_bold:
                parent_label = tools.htmlEscape(parent_label[3:-4])
                self.tree.setLabel(parent, parent_label)

        parent = self.tree.store.iter_parent(iter)
        parent_label = self.tree.getLabel(parent) if parent else None
        label = track.get_label(parent_label=parent_label, playing=playing)
        if playing:
            self.tree.setLabel(iter, label)
            self.tree.setItem(iter, ROW_ICO, icons.playMenuIcon())
            self.tree.expand(iter)
        else:
            self.tree.setLabel(iter, label)
            icon = self.tree.getItem(iter, ROW_ICO)
            has_error = (icon == icons.errorMenuIcon())
            is_dir = (icon == icons.mediaDirMenuIcon())
            if not is_dir and not has_error:
                self.tree.setItem(iter, ROW_ICO, icons.nullMenuIcon())


    def jumpTo(self, iter, sendPlayMsg=True, forced=True):
        """ Jump to the track located at the given iter """
        if not iter:
            return

        mark = self.tree.getMark()
        if mark:
            self.set_track_playing(mark, False)

        self.tree.setMark(iter)
        self.tree.scroll(iter)

        # Check track
        track = self.tree.getTrack(iter)
        if not track:
            # Row may be a directory
            self.jumpTo(self.__getNextTrackIter())
            return

        self.set_track_playing(iter, True)
        self.paused = False

        if sendPlayMsg:
            modules.postMsg(consts.MSG_CMD_PLAY, {'uri': track.getURI(), 'forced': forced})

        modules.postMsg(consts.MSG_EVT_NEW_TRACK,   {'track': track})
        modules.postMsg(consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__hasPreviousTrack(), 'hasNext': self.__hasNextTrack()})


    def insert(self, tracks, target=None, drop_mode=None, playNow=True, highlight=False):
        if type(tracks) == list:
            trackdir = media.TrackDir(None, flat=True)
            trackdir.tracks = tracks
            tracks = trackdir

        children_before = self.tree.store.iter_n_children(target)

        self.tree.get_selection().unselect_all()
        self.insertDir(tracks, target, drop_mode, highlight)
        self.onListModified()

        # We only want to start playback if tracks are appended from DBus
        # or appended (not inserted) into the playlist.
        # In that case target is None. Also don't interrupt playing songs.
        logging.info('playNow: %s, target: %s, self.tree.hasMark(): %s, self.paused: %s' % (playNow, target, self.tree.hasMark(), self.paused))
        if playNow and target is None and (not self.tree.hasMark() or self.paused):
            # If the target is None, the tracks have to be appended to the top
            # level and the first new track is the one after the original tracks
            new = self.tree.store.iter_nth_child(target, children_before)
            if new:
                # If new is None, the tracks could not be added
                self.jumpTo(new)


    def insertDir(self, trackdir, target=None, drop_mode=None, highlight=False):
        '''
        Insert a directory recursively, return the iter of the first
        added element
        '''
        model = self.tree.store
        if trackdir.flat:
            new = target
        else:
            string = trackdir.dirname.replace('_', ' ')
            string = tools.htmlEscape(string)
            source_row = (icons.mediaDirMenuIcon(), string, None)

            new = self.tree.insert(target, source_row, drop_mode)
            drop_mode = Gtk.TreeViewDropPosition.INTO_OR_AFTER
            if highlight:
                self.tree.select(new)

        dest = new
        for index, subdir in enumerate(trackdir.subdirs):
            drop = drop_mode if index == 0 else Gtk.TreeViewDropPosition.AFTER
            dest = self.insertDir(subdir, dest, drop, highlight)

        dest = new
        for index, track in enumerate(trackdir.tracks):
            drop = drop_mode if index == 0 else Gtk.TreeViewDropPosition.AFTER
            highlight &= trackdir.flat
            dest = self.insertTrack(track, dest, drop, highlight)

        if not trackdir.flat:
            # Open albums on the first layer
            if target is None or model.iter_depth(new) == 0:
                self.tree.expand(new)

        return new


    def insertTrack(self, track, target=None, drop_mode=None, highlight=False):
        '''
        Insert a new track into the tracktree under parentPath
        '''
        length = track.getLength()
        self.playtime += length

        name = track.get_label()

        row = (icons.nullMenuIcon(), name, track)
        new_iter = self.tree.insert(target, row, drop_mode)
        parent = self.tree.store.iter_parent(new_iter)
        if parent:
            # adjust track label to parent
            parent_label = self.tree.getLabel(parent)
            new_label = track.get_label(parent_label)
            self.tree.setLabel(new_iter, new_label)
        if highlight:
            self.tree.select(new_iter)
        return new_iter


    def set(self, tracks, playNow):
        """ Replace the tracklist, clear it if tracks is None """
        self.playtime = 0

        if type(tracks) == list:
            trackdir = media.TrackDir(None, flat=True)
            trackdir.tracks = tracks
            tracks = trackdir

        if self.tree.hasMark() and ((not playNow) or (tracks is None) or tracks.empty()):
            modules.postMsg(consts.MSG_CMD_STOP)

        self.tree.clear()

        if tracks is not None and not tracks.empty():
            self.insert(tracks, playNow=playNow)

        self.tree.collapse_all()

        self.onListModified()


    def export_playlist_to_m3u(self):
        """ Save the current tracklist to a playlist """
        outfile = fileChooser.save(self.window, _('Export playlist to file'), 'playlist.m3u')

        if outfile is not None:
            tools.write_file(outfile, self.get_m3u_text())


    def export_playlist_to_dir(self):
        """ Save the current tracklist to a playlist """
        outdir = fileChooser.openDirectory(self.window, _('Export playlist to directory'))

        if outdir is not None:
            trackdir = self.getTrackDir()
            trackdir.export_to_dir(outdir)


    def remove(self, iter=None):
        """ Remove the given track, or the selection if iter is None """
        hadMark = self.tree.hasMark()

        iters = [iter] if iter else list(self.tree.iterSelectedRows())

        prev_iter = self.tree.get_prev_iter_or_parent(iters[0])

        # reverse list, so that we remove children before their fathers
        for iter in reversed(iters):
            track = self.tree.getTrack(iter)
            if track:
                self.playtime -= track.getLength()
            self.tree.removeRow(iter)

        self.tree.selection.unselect_all()

        if hadMark and not self.tree.hasMark():
            modules.postMsg(consts.MSG_CMD_STOP)

        # Select new track when old selected is deleted
        if prev_iter:
            self.tree.select(prev_iter)
        else:
            first_iter = self.tree.get_first_iter()
            if first_iter:
                self.tree.select(first_iter)

        self.onListModified()


    def onShowPopupMenu(self, tree, button, time, path):
        # Keep reference after method exits.
        self.popup_menu = Gtk.Menu()

        # Remove
        remove = Gtk.MenuItem.new_with_label(_('Remove'))
        self.popup_menu.append(remove)
        if path is None:
            remove.set_sensitive(False)
        else:
            remove.connect('activate', lambda item: self.remove())

        # Clear
        clear = Gtk.MenuItem.new_with_label(_('Clear Playlist'))
        self.popup_menu.append(clear)

        # Save to m3u
        export_m3u = Gtk.MenuItem.new_with_label(_('Export playlist to file'))
        self.popup_menu.append(export_m3u)

        # Save to dir
        export_dir = Gtk.MenuItem.new_with_label(_('Export playlist to directory'))
        self.popup_menu.append(export_dir)

        if len(tree.store) == 0:
            clear.set_sensitive(False)
            export_m3u.set_sensitive(False)
            export_dir.set_sensitive(False)
        else:
            clear.connect('activate', lambda item: modules.postMsg(consts.MSG_CMD_TRACKLIST_CLR))
            export_m3u.connect('activate', lambda item: self.export_playlist_to_m3u())
            export_dir.connect('activate', lambda item: self.export_playlist_to_dir())

        self.popup_menu.show_all()
        self.popup_menu.popup(None, None, None, None, button, time)


    def togglePause(self):
        """ Start playing if not already playing """
        if len(self.tree) != 0 and not self.tree.hasMark():
            if self.tree.selection.count_selected_rows() > 0:
                model, sel_rows_list = self.tree.selection.get_selected_rows()
                first_sel_iter = self.tree.store.get_iter(sel_rows_list[0])
                self.jumpTo(first_sel_iter)
            else:
                self.jumpTo(self.tree.get_first_iter())


    def save_track_tree(self):
        # Save playing track
        if self.tree.hasMark():
            last_path = tuple(self.tree.mark.get_path())
        else:
            last_path = None
        prefs.set(__name__, 'last-played-track', last_path)

        dump = self.getTreeDump()
        logging.info('Saving playlist')
        pickleSave(self.savedPlaylist, dump)
        # tell gobject to keep saving the content in regular intervals
        return True


    # --== Message handlers ==--


    def onAppStarted(self):
        """ This is the real initialization function, called when the module has been loaded """
        wTree                  = tools.prefs.getWidgetsTree()
        self.playtime          = 0
        self.bufferedTrack     = None
        # Retrieve widgets
        self.window     = wTree.get_object('win-main')

        columns = (('',   [(Gtk.CellRendererPixbuf(), GdkPixbuf.Pixbuf), (Gtk.CellRendererText(), GObject.TYPE_STRING)], True),
                   (None, [(None, GObject.TYPE_PYOBJECT)], False),
                  )

        self.tree = TrackTreeView(columns, use_markup=True)

        self.tree.enableDNDReordering()
        target = Gtk.TargetEntry.new(*DND_INTERNAL_TARGET)
        targets = Gtk.TargetList.new([target])
        self.tree.setDNDSources(targets)

        wTree.get_object('scrolled-tracklist').add(self.tree)

        # GTK handlers
        self.tree.connect('exttreeview-button-pressed', self.onMouseButton)
        self.tree.connect('tracktreeview-dnd', self.onDND)
        self.tree.connect('key-press-event', self.onKeyboard)
        self.tree.get_model().connect('row-deleted', self.onRowDeleted)

        (options, args) = prefs.getCmdLine()

        self.savedPlaylist = os.path.join(consts.dirCfg, 'saved-playlist')
        self.paused = False


        # Populate the playlist with the saved playlist
        dump = None
        if os.path.exists(self.savedPlaylist):
            try:
                dump = pickleLoad(self.savedPlaylist)
            except (EOFError, IOError):
                msg = '[%s] Unable to restore playlist from %s\n\n%s'
                log.logger.error(msg % (MOD_INFO[modules.MODINFO_NAME],
                                self.savedPlaylist, traceback.format_exc()))

        if dump:
            self.restoreTreeDump(dump)
            log.logger.info('[%s] Restored playlist' % MOD_INFO[modules.MODINFO_NAME])
            self.tree.collapse_all()
            self.select_last_played_track()
            self.onListModified()

        commands, args = tools.separate_commands_and_tracks(args)

        # Add commandline tracks to the playlist
        if args:
            log.logger.info('[%s] Filling playlist with files given on command line' % MOD_INFO[modules.MODINFO_NAME])
            tracks  = media.getTracks([os.path.abspath(arg) for arg in args])
            playNow = not 'stop' in commands and not 'pause' in commands
            modules.postMsg(consts.MSG_CMD_TRACKLIST_ADD, {'tracks': tracks, 'playNow': playNow})
        elif 'play' in commands:
            modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE)

        # Automatically save the content at regular intervals
        GObject.timeout_add_seconds(SAVE_INTERVAL, self.save_track_tree)


    def onAppQuit(self):
        """ The module is going to be unloaded """
        self.save_track_tree()


    def onTrackEnded(self, withError):
        """ The current track has ended, jump to the next one if any """
        current_iter = self.tree.getMark()

        # If an error occurred with the current track, flag it as such
        if withError and current_iter:
            self.tree.setItem(current_iter, ROW_ICO, icons.errorMenuIcon())

        # Find the next 'playable' track (not already flagged)
        next = self.__getNextTrackIter()
        if next:
            send_play_msg = True
            if current_iter:
                track_name = self.tree.getTrack(current_iter).getURI()
                send_play_msg = (track_name != self.bufferedTrack)
            self.jumpTo(next, sendPlayMsg=send_play_msg, forced=False)
            self.bufferedTrack = None
            return

        self.bufferedTrack = None
        modules.postMsg(consts.MSG_CMD_STOP)


    def onBufferingNeeded(self):
        """ The current track is close to its end, so we try to buffer the next one to avoid gaps """
        where = self.__getNextTrackIter()
        if where:
            self.bufferedTrack = self.tree.getItem(where, ROW_TRK).getURI()
            modules.postMsg(consts.MSG_CMD_BUFFER, {'uri': self.bufferedTrack})


    def onStopped(self):
        """ Playback has been stopped """
        if self.tree.hasMark():
            currTrack = self.tree.getMark()
            self.set_track_playing(currTrack, False)
            self.tree.clearMark()


    def onPausedToggled(self, icon):
        """ Switch between paused and unpaused """
        if self.tree.hasMark():
            self.tree.setItem(self.tree.getMark(), ROW_ICO, icon)


    def highlight(self, query, root=None):
        """Select all rows (and their parents) that contain all parts of *query*."""
        for iter in self.tree.iter_children(root):
            track = self.tree.getTrack(iter)
            if track:
                if all(part in track.get_search_text() for part in query):
                    self.tree.select(iter)
                    # Highlight all parents as well
                    for parent in self.tree.get_all_parents(iter):
                        self.tree.select_synchronously(parent)
            else:
                dirname = self.tree.getLabel(iter).replace('<b>', '').replace('</b>', '')
                if all(part in dirname.lower() for part in query):
                    self.tree.select_synchronously(iter)
                self.highlight(query, iter)


    def onSearchStart(self, query):
        query = [part.strip().lower() for part in query.split()]
        GObject.idle_add(self.highlight, query)
        GObject.idle_add(self.tree.scroll_to_first_selection)

    def onSearchReset(self):
        self.tree.selection.unselect_all()


    def onPaused(self):
        self.paused = True
        self.onPausedToggled(icons.pauseMenuIcon())

    def onUnPaused(self):
        self.paused = False
        self.onPausedToggled(icons.playMenuIcon())


    def onDragBegin(self, paths):
        dir_selected = any(map(os.path.isdir, paths))
        self.tree.dir_selected = dir_selected
        if dir_selected:
            self.tree.collapse_all()


    # --== GTK handlers ==--


    def onMouseButton(self, tree, event, path):
        """ A mouse button has been pressed """
        if event.button == 1 and event.type == Gdk.EventType._2BUTTON_PRESS and path is not None:
            self.jumpTo(self.tree.store.get_iter(path))
        elif event.button == 3:
            self.onShowPopupMenu(tree, event.button, event.time, path)


    def onKeyboard(self, list, event):
        """ Keyboard shortcuts """
        keyname = Gdk.keyval_name(event.keyval)

        if keyname == 'Delete':   self.remove()
        elif keyname == 'Return': self.jumpTo(self.tree.getFirstSelectedRow())
        elif keyname == 'space':  modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE)
        elif keyname == 'Escape': modules.postMsg(consts.MSG_CMD_STOP)
        elif keyname == 'Left':   modules.postMsg(consts.MSG_CMD_STEP, {'seconds': -5})
        elif keyname == 'Right':  modules.postMsg(consts.MSG_CMD_STEP, {'seconds': 5})


    def onListModified(self):
        """ Some rows have been added/removed/moved """
        # Getting the trackdir takes virtually no time, so we can do it on every
        # paylist change
        tracks = self.getTrackDir()
        self.playtime = tracks.get_playtime()

        modules.postMsg(consts.MSG_EVT_NEW_TRACKLIST, {'tracks': tracks, 'playtime': self.playtime})

        if self.tree.hasMark():
            modules.postMsg(consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__hasPreviousTrack(), 'hasNext':  self.__hasNextTrack()})


    def onDND(self, list, context, x, y, dragData, dndId, time):
        """ External Drag'n'Drop """
        import urllib.request

        uris = dragData.get_uris()

        if not uris:
            context.finish(False, False, time)
            return

        def get_path(uri):
            if uri.startswith('file://'):
                uri = uri[len('file://'):]
            return urllib.request.url2pathname(uri)

        paths = [get_path(uri) for uri in uris]
        tracks = media.getTracks(paths)

        dropInfo = list.get_dest_row_at_pos(x, y)

        # Insert the tracks, but beware of the AFTER/BEFORE mechanism used by GTK
        if dropInfo is None:
            self.insert(tracks, playNow=False, highlight=True)
        else:
            path, drop_mode = dropInfo
            iter = self.tree.store.get_iter(path)
            self.insert(tracks, iter, drop_mode, playNow=False, highlight=True)

        # We want to allow dropping tracks only when we are sure that no dir is
        # selected. This is needed for dnd from nautilus.
        self.tree.dir_selected = True

        context.finish(True, False, time)

    def onRowDeleted(self, model, path):
        """
        Internal drag and drop cannot be caught in PyGTK since the
        "rows-reordered" signal is not emitted and the work-around proposed by
        katsh doesn't work either
        (http://stackoverflow.com/questions/2831779/catch-pygtk-treeview-reorder).
        After a drag and drop operation PyGTK emits the "row-inserted" and
        afterwards "row-deleted" signals, so we catch the latter and update the
        buttons.
        """
        self.onListModified()
Esempio n. 4
0
    def onAppStarted(self):
        """ This is the real initialization function, called when the module has been loaded """
        wTree                  = tools.prefs.getWidgetsTree()
        self.playtime          = 0
        self.bufferedTrack     = None
        # Retrieve widgets
        self.window     = wTree.get_object('win-main')

        columns = (('',   [(Gtk.CellRendererPixbuf(), GdkPixbuf.Pixbuf), (Gtk.CellRendererText(), GObject.TYPE_STRING)], True),
                   (None, [(None, GObject.TYPE_PYOBJECT)], False),
                  )

        self.tree = TrackTreeView(columns, use_markup=True)

        self.tree.enableDNDReordering()
        target = Gtk.TargetEntry.new(*DND_INTERNAL_TARGET)
        targets = Gtk.TargetList.new([target])
        self.tree.setDNDSources(targets)

        wTree.get_object('scrolled-tracklist').add(self.tree)

        # GTK handlers
        self.tree.connect('exttreeview-button-pressed', self.onMouseButton)
        self.tree.connect('tracktreeview-dnd', self.onDND)
        self.tree.connect('key-press-event', self.onKeyboard)
        self.tree.get_model().connect('row-deleted', self.onRowDeleted)

        (options, args) = prefs.getCmdLine()

        self.savedPlaylist = os.path.join(consts.dirCfg, 'saved-playlist')
        self.paused = False


        # Populate the playlist with the saved playlist
        dump = None
        if os.path.exists(self.savedPlaylist):
            try:
                dump = pickleLoad(self.savedPlaylist)
            except (EOFError, IOError):
                msg = '[%s] Unable to restore playlist from %s\n\n%s'
                log.logger.error(msg % (MOD_INFO[modules.MODINFO_NAME],
                                self.savedPlaylist, traceback.format_exc()))

        if dump:
            self.restoreTreeDump(dump)
            log.logger.info('[%s] Restored playlist' % MOD_INFO[modules.MODINFO_NAME])
            self.tree.collapse_all()
            self.select_last_played_track()
            self.onListModified()

        commands, args = tools.separate_commands_and_tracks(args)

        # Add commandline tracks to the playlist
        if args:
            log.logger.info('[%s] Filling playlist with files given on command line' % MOD_INFO[modules.MODINFO_NAME])
            tracks  = media.getTracks([os.path.abspath(arg) for arg in args])
            playNow = not 'stop' in commands and not 'pause' in commands
            modules.postMsg(consts.MSG_CMD_TRACKLIST_ADD, {'tracks': tracks, 'playNow': playNow})
        elif 'play' in commands:
            modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE)

        # Automatically save the content at regular intervals
        GObject.timeout_add_seconds(SAVE_INTERVAL, self.save_track_tree)