#!/usr/bin/python
#
# audio-converter.py
# Convert wave audio files using standard command-line tools
# to lossless and lossy audio formats using drag-and-drop :)
#
# Author: Thomas Perl <thp@thpinfo.com>
# Sun, 08 Apr 2007 20:29:41 +0200
#
# Released under the terms of the
# GNU General Public License Version 2
#

import gtk
import gobject
import pango
import os
import os.path
import signal
import subprocess
import threading

class AudioConverter(gtk.Window):
    drop_targets = [( 'text/uri-list', 0, 2 ),]
    file_types = [
            {
                'title': 'FLAC (lossless)',
                'type': 'Flac',
                'command': [ 'flac', '{infile}', '-o', '{outfile}' ],
                'extension': '.flac',
            },
            {
                'title': 'Ogg Vorbis (lossy)',
                'type': 'Ogg Vorbis',
                'command': [ 'oggenc', '{infile}', '-o', '{outfile}' ],
                'extension': '.ogg',
            },
            {
                'title': 'MPEG Layer 3 (lossy)',
                'type': 'MP3',
                'command': [ 'lame', '{infile}', '-o', '{outfile}' ],
                'extension': '.mp3',
            },
            # this is how you do a separator:
            {
                'title': None,
            },
            # here follow two specials that are used by me :)
            {
                'title': 'Ogg Vorbis (quality 0.5, lossy)',
                'type': 'Ogg Vorbis',
                'command': [ 'oggenc', '-q0.5', '{infile}', '-o', '{outfile}' ],
                'extension': '.ogg',
            },
            {
                'title': 'FLAC (using flake, lossless)',
                'type': 'Flac',
                'command': [ 'flake', '{infile}', '-o', '{outfile}' ],
                'extension': '.flac',
            },
            # add new types here as you desire
    ]

    def __init__( self):
        gobject.threads_init()

        self.running_processes = {}

        gtk.Window.__init__( self)
        self.set_title( 'Audio converter')
        self.set_icon_name( 'audio-x-generic')
        self.connect( 'destroy', self.quit)

        vb = gtk.VBox()
        vb.set_spacing( 6)
        
        # file type selector
        selector = gtk.HBox()
        selector.set_spacing( 6)
        selector.pack_start( gtk.Label('Convert to filetype:'), expand = False)
        self.combo_file_type = gtk.ComboBox( self.get_file_type_model())
        self.combo_file_type.set_row_separator_func( lambda model, iter: model.get_value( iter, 0) == None)
        text_renderer = gtk.CellRendererText()
        self.combo_file_type.pack_start( text_renderer)
        self.combo_file_type.add_attribute( text_renderer, 'text', 0)
        self.combo_file_type.set_active( 0)
        selector.pack_start( self.combo_file_type)
        vb.pack_start( selector, expand = False, fill = True)

        # treeview status
        self.items_liststore_iter_basket = []
        self.items_liststore = gtk.ListStore( gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_INT)
        self.treeview = gtk.TreeView( self.items_liststore)
        self.treeview.append_column( gtk.TreeViewColumn( '  ', gtk.CellRendererPixbuf(), **{'icon-name':0}))
        renderer = gtk.CellRendererText()
        renderer.set_property( 'ellipsize', pango.ELLIPSIZE_END)
        self.treeview.append_column( gtk.TreeViewColumn( 'Filename', renderer, text = 1))
        self.treeview.append_column( gtk.TreeViewColumn( 'Filetype', gtk.CellRendererText(), text = 2))
        self.treeview.append_column( gtk.TreeViewColumn( 'Progress', gtk.CellRendererProgress(), value = 3, text = 4))

        for column in self.treeview.get_columns():
            column.set_resizable( True)
            column.set_reorderable( True)

        sw = gtk.ScrolledWindow()
        sw.set_policy( gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
        sw.set_shadow_type( gtk.SHADOW_IN)
        sw.add( self.treeview)
        vb.pack_start( sw)

        # drop destination
        dropdest = gtk.HBox()
        dropdest.set_spacing( 6)
        dropimg = gtk.image_new_from_icon_name( 'folder', gtk.ICON_SIZE_DND)
        dropdest.pack_start( dropimg, expand = False)
        lbl = gtk.Label('Drag wave audio files into this window')
        lbl.set_alignment( 0.0, 0.5)
        dropdest.pack_start( lbl)
        cleanup_button = gtk.Button( label = 'Clean')
        cleanup_button.set_image( gtk.image_new_from_stock( gtk.STOCK_CLEAR, gtk.ICON_SIZE_BUTTON))
        cleanup_button.connect( 'clicked', self.cleanup_done)
        dropdest.pack_end( cleanup_button, expand = False, fill = True)
        abort_button = gtk.Button( label = 'Cancel')
        abort_button.set_image( gtk.image_new_from_stock( gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON))
        abort_button.connect( 'clicked', self.cancel_selection)
        dropdest.pack_end( abort_button, expand = False, fill = True)
        self.drag_dest_set( gtk.DEST_DEFAULT_ALL, self.drop_targets, gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY)
        self.connect( 'drag_data_received', self.drag_data_received)
        self.connect( 'drag-motion', lambda *args: dropimg.set_from_icon_name( 'folder-drag-accept', gtk.ICON_SIZE_DND))
        self.connect( 'drag-leave', lambda *args: dropimg.set_from_icon_name( 'folder', gtk.ICON_SIZE_DND))
        vb.pack_start( dropdest, expand = False, fill = True)

        self.add( vb)
        self.set_border_width( 6)
        self.resize( 600, 300)
        self.show_all()
        gtk.main()

    def quit( self, widget):
        for pid in self.running_processes:
            try:
                os.kill( pid, signal.SIGKILL)
            except:
                pass
        gtk.main_quit()

    def get_file_type_model( self):
        model = gtk.ListStore( gobject.TYPE_STRING)

        for type in self.file_types:
            iter = model.append()
            model.set_value( iter, 0, type['title'])

        return model

    def drag_data_received( self, widget, context, x, y, sel, ttype, time):
        files = sel.data.split('\n')
        for f in files:
            if not f:
                continue

            f = f.strip()

            if f.startswith('file:///') and f.lower().endswith('.wav') and os.path.isfile(f[7:]):
                self.convert_file( f[7:])
            else:
                dlg = gtk.MessageDialog( parent = self, buttons = gtk.BUTTONS_OK, message_format = 'Cannot convert file')
                dlg.format_secondary_markup( 'Cannot convert <b>%s</b>. Only wave files are supported at the moment.' % ( os.path.basename( f), ))
                dlg.run()
                dlg.destroy()

    def convert_file( self, filename):
        file_type = self.file_types[self.combo_file_type.get_active()]

        infile = filename
        outfile = os.path.splitext(filename)[0] + file_type['extension']

        command = []
        for c in file_type['command']:
            if c == '{infile}':
                command.append( infile)
            elif c == '{outfile}':
                command.append( outfile)
            else:
                command.append( c)

        iter = self.items_liststore.append()
        self.items_liststore.set_value( iter, 0, 'audio-x-generic')
        self.items_liststore.set_value( iter, 1, os.path.basename( filename))
        self.items_liststore.set_value( iter, 2, file_type['type'])
        self.items_liststore.set_value( iter, 3, 0)
        self.items_liststore.set_value( iter, 4, None)
        self.items_liststore.set_value( iter, 5, 0)
        self.treeview.columns_autosize()

        threading.Thread( target = self.convert_proc, args = ( command, iter, outfile, )).start()

    def convert_proc( self, command, iter, outfile):
        try:
            proc = subprocess.Popen( args = command, stderr = subprocess.PIPE)
        except:
            gobject.idle_add( self.items_liststore.set_value, iter, 3, 0)
            gobject.idle_add( self.items_liststore.set_value, iter, 4, 'Execution of "%s" failed' % ( command[0], ))
            self.items_liststore_iter_basket.append( iter)
            return

        self.items_liststore.set_value( iter, 5, proc.pid)
        self.running_processes[proc.pid] = iter

        old_percentage = 0
        while proc.poll() == None:
            s =  proc.stderr.readline( 80)
            pos = s.find('%')
            if pos != -1:
                try:
                    if s[pos-2] == '.':
                        # oggenc returns a float value (00.0%)
                        percentage = int(s[pos-4:pos-2])
                    else:
                        # we expect everything else to be int (00%)
                        percentage = int(s[pos-2:pos])
                    if percentage > old_percentage:
                        gobject.idle_add( self.items_liststore.set_value, iter, 3, percentage)
                        old_percentage = percentage
                except:
                    pass

        if proc.returncode == 0:
            gobject.idle_add( self.items_liststore.set_value, iter, 3, 100)
            gobject.idle_add( self.items_liststore.set_value, iter, 4, 'Done')
        else:
            #gobject.idle_add( self.items_liststore.set_value, iter, 3, 0)
            if proc.pid in self.running_processes:
                gobject.idle_add( self.items_liststore.set_value, iter, 4, 'Error while converting')
            else:
                gobject.idle_add( self.items_liststore.set_value, iter, 4, 'Cancelled')
            os.unlink( outfile)

        self.items_liststore_iter_basket.append( iter)

        if proc.pid in self.running_processes:
            del self.running_processes[proc.pid]

    def cleanup_done( self, widget):
        for iter in self.items_liststore_iter_basket:
            try:
                self.items_liststore.remove( iter)
            except:
                pass

        self.items_liststore_iter_basket = []

    def cancel_selection( self, widget):
        (model, iter) = self.treeview.get_selection().get_selected()

        if iter:
            pid = model.get_value( iter, 5)
            if pid in self.running_processes:
                del self.running_processes[pid]
            os.kill( pid, signal.SIGKILL)


if __name__ == '__main__':
    AudioConverter()

