
import gtk
import plugins
import os
import os.path
import threading
import time
import signal

PLUGIN_NAME = 'Audio CD Ripper Plugin'
PLUGIN_AUTHORS = ['kani <jkani4@gmail.com>']
PLUGIN_VERSION = '0.1'
PLUGIN_ENABLED = False
PLUGIN_DESCRIPTION = r'''This plugin converts audio data into various format. But now, this can convert to Ogg Vorbis only. '''


b = gtk.Button()
PLUGIN_ICON = b.render_icon(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU)
b.destroy()

STR_RIPPER_COMMAND = 'cdparanoia'
STR_OGG_ENCODER_COMMAND = 'oggenc'

#
# Functions for geting/setting configuration
#

##
def get_conf_str(name, defStr):
    return APP.settings.get_str(name,
                                default=defStr,
                                plugin=plugins.name(__file__))

##
def set_conf_str(name, someStr):
    APP.settings.set_str(name,
                         someStr,
                         plugin=plugins.name(__file__))

##
def get_conf_int(name, defVal):
    return APP.settings.get_int(name,
                                default=defVal,
                                plugin=plugins.name(__file__))
    
##
def set_conf_int(name, someVal):
    APP.settings.set_int(name,
                         someVal,
                         plugin=plugins.name(__file__))
    
##
def get_conf_bool(name, defVal):
    return APP.settings.get_boolean(name,
                                    default=defVal,
                                    plugin=plugins.name(__file__))
    
##
def set_conf_bool(name, someFlag):
    APP.settings.set_boolean(name,
                             someFlag,
                             plugin=plugins.name(__file__))

##
def get_conf_tmp_path():
    return get_conf_str('TEMP_DIRECTORY_PATH', '~/')
##
def set_conf_tmp_path(newPath):
    set_conf_str('TEMP_DIRECTORY_PATH', newPath)

##
def get_conf_out_path():
    return get_conf_str('OUT_DIRECTORY_PATH', '~/')
##
def set_conf_out_path(newPath):
    set_conf_str('OUT_DIRECTORY_PATH', newPath)

##
def get_conf_delete_wav():
    return get_conf_bool('IS_DELETE_WAV', True)
##
def set_conf_delete_wav(newFlag):
    set_conf_bool('IS_DELETE_WAV', newFlag)

##
def get_conf_ripper_path():
    return get_conf_str('RIPPER_PATH', '/usr/bin')
##
def set_conf_ripper_path(newPath):
    set_conf_str('RIPPER_PATH', newPath)

##
def get_conf_ogg_encoder_path():
    return get_conf_str('OGG_ENCODER_PATH', '/usr/bin')
##
def set_conf_ogg_encoder_path(newPath):
    set_conf_str('OGG_ENCODER_PATH', newPath)

##
def get_conf_ogg_encoder_quality():
    return get_conf_int('OGG_ENCODER_QUALITY', 5)
##
def set_conf_ogg_encoder_quality(newVal):
    set_conf_int('OGG_ENCODER_QUALITY', newVal)

##
def get_conf_ripp_only():
    return get_conf_bool('RIPP_ONLY', False)
##
def set_conf_ripp_only(newFlag):
    set_conf_bool('RIPP_ONLY', newFlag)


gDictOggOutputDir = {'OGG_OPT_INTO_OUTDIR': 0,
                      'OGG_OPT_INTO_ARTIST_ALBUML_DIR': 1}

##
def get_conf_ogg_output_dir():
    return get_conf_int('OGG_ENCODER_OUTPUT_DIR',
                        gDictOggOutputDir['OGG_OPT_INTO_OUTDIR'])
##
def set_conf_ogg_output_dir(newVal):
    set_conf_int('OGG_ENCODER_OUTPUT_DIR', newVal)


#
#
class RipperConfigDlg:
    ##
    def __init__(self, parent=None):
        self.tmpPathStr = get_conf_tmp_path()
        self.outPathStr = get_conf_out_path()
        self.rippCmdPathStr = get_conf_ripper_path()
        self.oggEncCmdPathStr = get_conf_ogg_encoder_path()
        self.oggQuality = get_conf_ogg_encoder_quality()
        self.isDeleteWav = get_conf_delete_wav()
        self.rippOnly = get_conf_ripp_only()
        self.oggOutputDir = get_conf_ogg_output_dir()

        # Create 'Config' dialog.
        self.dlg = gtk.Dialog('Config',
                              parent,
                              gtk.DIALOG_DESTROY_WITH_PARENT,
                              (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
                               gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))

        self.notebook = gtk.Notebook()
        self.notebook.set_tab_pos(gtk.POS_TOP)
        # Create general setting page
        widget, label = self.create_general_page()
        self.notebook.append_page(widget, label)
        
        # Create ripper setting page
        widget, label = self.create_ripper_page()
        self.notebook.append_page(widget, label)

        # Create encoder setting page
        widget, label = self.create_encoder_page()
        self.notebook.append_page(widget, label)

        self.dlg.vbox.pack_start(self.notebook, False, False, 2)
        self.dlg.vbox.show_all()

    ##
    def create_general_page(self):
        contents = gtk.VBox()
        pageLabel = gtk.Label('General')

        label = gtk.Label('Temp Directory:')
        label.set_alignment(0.0, 0.5)
        contents.pack_start(label, False, False, 2)

        self.entryTmpPath = gtk.Entry()
        self.entryTmpPath.set_text(self.tmpPathStr)
        contents.pack_start(self.entryTmpPath, False, False, 2)

        HSep = gtk.HSeparator()
        contents.pack_start(HSep, False, False, 2)

        label = gtk.Label('Directory for ogg files output:')
        label.set_alignment(0.05, 0.5)
        contents.pack_start(label, False, False, 2)
        
        self.entryOutPath = gtk.Entry()
        self.entryOutPath.set_text(self.outPathStr)
        contents.pack_start(self.entryOutPath)
        
        self.checkRippOnly = gtk.CheckButton('Ripping wav only')
        self.checkDelWav = gtk.CheckButton('Delete ".wav" files after conversion finished.')

        self.checkRippOnly.connect('clicked', self.clicked_ripp_only, None)
        self.checkRippOnly.set_active(self.rippOnly)
        contents.pack_start(self.checkRippOnly, False, False, 2)

        self.checkDelWav.connect('clicked', self.clicked_del_wav, None)
        if self.rippOnly:
            self.checkDelWav.set_active(False)
        else:
            self.checkDelWav.set_active(self.isDeleteWav)
        contents.pack_start(self.checkDelWav, False, False, 2)

        return contents, pageLabel

    ##
    def create_ripper_page(self):
        contents = gtk.VBox()
        pageLabel = gtk.Label('Ripper')

        label = gtk.Label('Command path:')
        label.set_alignment(0.0, 0.5)
        contents.pack_start(label, False, False, 2)

        self.entryRippCmd = gtk.Entry()
        self.entryRippCmd.set_text(self.rippCmdPathStr)
        contents.pack_start(self.entryRippCmd, False, False, 2)

        return contents, pageLabel

    ##
    def create_encoder_page(self):
        contents = gtk.VBox()
        pageLabel = gtk.Label('Encoder')

        label = gtk.Label('Encoder path:')
        label.set_alignment(0.0, 0.5)
        contents.pack_start(label, False, False, 2)

        self.entryOggEncPath = gtk.Entry()
        self.entryOggEncPath.set_text(self.oggEncCmdPathStr)
        contents.pack_start(self.entryOggEncPath, False, False, 2)
        
        separator = gtk.HSeparator()
        contents.pack_start(separator, False, False, 2)
        
        hbox = gtk.HBox()
        label = gtk.Label('Quality:')
        hbox.pack_start(label, False, False, 2)
        
        self.comboOggQuality = gtk.combo_box_new_text()
        for i in range(1, 11):
            self.comboOggQuality.append_text(str(i))
        self.comboOggQuality.set_active(self.oggQuality - 1)
        hbox.pack_start(self.comboOggQuality, True, False, 0)
        
        contents.pack_start(hbox, False, False, 2)

        frame = gtk.Frame(' Output options ')
        contents.pack_start(frame, False, False, 0)

        radiobox = gtk.VBox()
        frame.add(radiobox)

        self.oggOutRadio1 = gtk.RadioButton(None, 'Put ogg files into the ogg output directory.')
        radiobox.pack_start(self.oggOutRadio1, False, False, 0)
        
        self.oggOutRadio2 = gtk.RadioButton(self.oggOutRadio1, 'Put ogg files into "artist-name/album-name/" directory \nunder the ogg output directory.')
        radiobox.pack_start(self.oggOutRadio2, False, False, 0)

        dir_opt = get_conf_ogg_output_dir()
        if dir_opt == gDictOggOutputDir['OGG_OPT_INTO_OUTDIR']:
            self.oggOutRadio1.set_active(True)
        elif dir_opt == gDictOggOutputDir['OGG_OPT_INTO_ARTIST_ALBUML_DIR']:
            self.oggOutRadio2.set_active(True)

        return contents, pageLabel

    ##
    def clicked_ripp_only(self, widget, data=None):
        if self.checkRippOnly.get_active():
            self.checkDelWav.set_active(False)

    ##
    def clicked_del_wav(self, widget, data=None):
        if self.checkDelWav.get_active():
            self.checkRippOnly.set_active(False)

    ##
    def run(self):
        return self.dlg.run()

    ##
    def destroy(self):
        self.dlg.destroy()

    ##
    def save_config(self):
        set_conf_tmp_path(self.entryTmpPath.get_text())
        set_conf_out_path(self.entryOutPath.get_text())
        set_conf_delete_wav(self.checkDelWav.get_active())
        set_conf_ripper_path(self.entryRippCmd.get_text())
        set_conf_ogg_encoder_path(self.entryOggEncPath.get_text())
        set_conf_ogg_encoder_quality(self.comboOggQuality.get_active()+1)
        set_conf_ripp_only(self.checkRippOnly.get_active())
        if self.oggOutRadio1.get_active():
            set_conf_ogg_output_dir(gDictOggOutputDir['OGG_OPT_INTO_OUTDIR'])
        elif self.oggOutRadio2.get_active():
            set_conf_ogg_output_dir(gDictOggOutputDir['OGG_OPT_INTO_ARTIST_ALBUML_DIR'])


#
#
class CmdThread(threading.Thread):
    ##
    def __init__(self, cmd, opts, targets=None, outArray=None):
        threading.Thread.__init__(self)

        self.cmd = cmd
        self.cmdOpts = opts
        self.cmdTargets = targets
        self.outArray = outArray
        self.counter = 0
        self.cmd_pid = 0
        self.killed = False
        self.cbProgressFunc = None
        self.cbEndFunc = None
        print '***** thread created *****'

    ##
    def run(self):
        print '****** thread start ******'

        self.preparation_func()
        
        while self.killed == False:

            if self.cmdTargets != None and len(self.cmdTargets) == 0:
                if self.cbProgressFunc != None:
                    self.cbProgressFunc(self.counter, False)
                time.sleep(1)
                continue

            if self.cbProgressFunc != None:
                self.cbProgressFunc(self.counter, True)

            args = self.generate_cmd_args(self.cmd, self.cmdOpts, self.cmdTargets)
            
            self.cmd_pid = os.spawnv(os.P_NOWAIT, args[0], args)
            print 'pid = %d' % self.cmd_pid

            os.waitpid(self.cmd_pid, 0)
            self.cmd_pid = 0

            if self.outArray != None:
                self.outArray.append(self.generate_output_name(args))

            self.counter += 1

        self.cleanup_func()

        if self.cbEndFunc() != None:
            self.cbEndFunc()

    ##
    def preparation_func(self):
        pass

    ##
    def cleanup_func(self):
        pass
    
    ##
    def set_cb_progress_func(self, func):
        self.cbProgressFunc = func

    ##
    def set_cb_end_func(self, func):
        self.cbEndFunc = func

    ##
    def stop(self):
        print 'pid = %d' % self.cmd_pid
        if self.cmd_pid:
            os.kill(self.cmd_pid, signal.SIGTERM)
            print '****** killed(pid:%d) ******' % self.cmd_pid
            self.cmd_pid = 0
        self.killed = True

    ##
    def generate_cmd_args(self, cmd, opts, targets):
        arr = [cmd] + opts
        if targets and len(targets) > 0:
            arr.append(targets.pop(0))
        return arr

    ##
    def generate_output_name(self, target):
        return target
        
    ##
    def get_counter_value(self):
        return self.counter


#
#
class WavCmdThread(CmdThread):
    ##
    def generate_cmd_args(self, cmd, opts, targets):
        arr = [cmd]
        if targets and len(targets) > 0:
            tup = targets.pop(0)
            arr.append(str(tup[0]))
            arr.append(tup[1])

        return arr

    def generate_output_name(self, seq):
        return seq[len(seq) - 1]

    ##
    def set_working_directory(self, workPath):
        self.workPath = workPath

    ##
    def preparation_func(self):
        self.savePath = os.getcwd()
        os.chdir(self.workPath)

    ##
    def cleanup_func(self):
        os.chdir(self.savePath)


#
#
class OggCmdThread(CmdThread):
    ##
    def generate_cmd_args(self, cmd, opts, targets):
        arr = [cmd] + opts
        if targets != None and len(targets) > 0:
            name = targets.pop(0)
            head, ext = os.path.splitext(name)
            if len(head) > 0:
                outpath = self.outputPath + '/' + head + '.ogg'
                arr += [name, '-o', outpath]

        return arr

    ##
    def set_output_path(self, path):
        self.outputPath = path

#
#
class CmdThreadKiller(threading.Thread):
    ##
    def __init__(self, max_count):
        threading.Thread.__init__(self)

        self.cmdThreads = []
        self.cmdMaxCount = max_count
        self.forceKill = False
        self.cbEnd = None
        print '**** Killer Initialized ****'

    ##
    def run(self):
        print '**** Killer started! *****'
        while True:
            if self.forceKill:
                break
            
            cnt = len(self.cmdThreads)
            if cnt == 0:
                time.sleep(0.5)
                continue

            tmp = []
            for i in range(cnt):
                cmd = self.cmdThreads.pop(0)
                
                if cmd.get_counter_value() >= self.cmdMaxCount:
                    cmd.stop()
                else:
                    tmp.append(cmd)
                    
            self.cmdThreads = tmp
            if len(self.cmdThreads) == 0:
                break
        
        if self.forceKill:
            print '****** force kill ******'
            for cmd in self.cmdThreads:
                cmd.stop()

        if self.cbEnd != None:
            self.cbEnd()

    ##
    def add_target(self, target):
        if target == None:
            return
        self.cmdThreads.append(target)
    
    ##
    def kill_all(self):
        self.forceKill = True

    ##
    def set_cb_end(self, func):
        self.cbEnd = func


#
#
class RipperWindow:
    ##
    def __init__(self, artist='', album='', targets=[]):
        '''
        artist  : artist name of the CD
        albume  : album name of the CD
        targets : sequance of the items which must contain
                  the track number and the track name.
                         
                  Ex. [(1, "track#1"), (2, "track#2")] or
                      [[2, "track#2"], [3, "track#3"]] so so..
        '''
        self.artistName = artist
        self.albumName = album
        self.targets = targets
        self.oggTargets = []

        self.wind = gtk.Window(gtk.WINDOW_TOPLEVEL)
        self.wind.set_size_request(250, -1)
        self.wind.set_deletable(False)
        self.wind.set_modal(True)

        #
        # Add widgets that shows progress of Ripping
        #
        frameWav = gtk.Frame(' To WAV ')
        wavbox = gtk.VBox()
        frameWav.add(wavbox)
        
        self.wavName = gtk.Label('---')
        self.wavName.set_alignment(0.05, 0.5)
        wavbox.pack_start(self.wavName, False, False, 2)

        self.wavProgress = gtk.ProgressBar()
        self.wavProgress.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
        wavbox.pack_start(self.wavProgress, False, False, 4)

        vbox = gtk.VBox()
        vbox.pack_start(frameWav, False, False, 2)

        #
        # Add widgets that shows Progress of converting ogg
        #
        if get_conf_ripp_only() == False:
            frameOgg = gtk.Frame(' To OGG ')
            oggbox = gtk.VBox()
            frameOgg.add(oggbox)
        
            self.oggName = gtk.Label('---')
            self.oggName.set_alignment(0.05, 0.5)
            oggbox.pack_start(self.oggName, False, False, 2)

            self.oggProgress = gtk.ProgressBar()
            self.oggProgress.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
            oggbox.pack_start(self.oggProgress, False, False, 4)

            vbox.pack_start(frameOgg, False, False, 2)
        
        self.bt = gtk.Button('Cancel')
        self.bt.connect('clicked', self.close, None)
        vbox.pack_start(self.bt, False, False, 2)

        self.wind.add(vbox)
        self.wind.show_all()

        #
        # Create thread for killing threads(wav ripping thread, ogg converting thread)
        # 
        self.cmdKiller = CmdThreadKiller(len(self.targets))
        self.cmdKiller.set_cb_end(self.cb_func_killer_end)
        self.cmdKiller.start()

        #
        # Create wav ripping thread
        #
        wavThread = self.setup_ripper_thread()
        self.cmdKiller.add_target(wavThread)
        wavThread.start()

        #
        # Create ogg converting thread
        #
        if get_conf_ripp_only() == False:
            oggThread = self.setup_ogg_thread()
            self.cmdKiller.add_target(oggThread)
            oggThread.start()

    ##
    def setup_ripper_thread(self):
        ripperCmd = os.path.join(get_conf_ripper_path(), STR_RIPPER_COMMAND)
        ripperCmd = os.path.normpath(ripperCmd)
        targetCopy = self.targets[:]
        wavThread = WavCmdThread(ripperCmd, [], targetCopy, self.oggTargets)

        tmpPath = os.path.normpath(os.path.expanduser(get_conf_tmp_path()))
        wavThread.set_working_directory(tmpPath)
        wavThread.set_cb_progress_func(self.cb_progress_func_wav)
        wavThread.set_cb_end_func(self.cb_func_wav_end)

        return wavThread
        
    ##
    def setup_ogg_thread(self):
        oggEncCmd = os.path.join(get_conf_ogg_encoder_path(),
                                 STR_OGG_ENCODER_COMMAND)
        oggEncCmd = os.path.normpath(oggEncCmd)
        quality = get_conf_ogg_encoder_quality()
        oggThread = OggCmdThread(oggEncCmd, ['-q', str(quality)], self.oggTargets)
        oggThread.set_cb_progress_func(self.cb_progress_func_ogg)
        oggThread.set_cb_end_func(self.cb_func_ogg_end)

        outPath = get_conf_out_path()
        
        if get_conf_ogg_output_dir() == gDictOggOutputDir['OGG_OPT_INTO_ARTIST_ALBUML_DIR']:
            if self.artistName == '':
                self.artistName = 'Unknown_Artist'
            if self.albumName == '':
                self.albumName = 'Unknown_album'
            outPath += '/%s/%s/' % (self.artistName, self.albumName)

        outPath = os.path.normpath(os.path.expanduser(outPath))
        oggThread.set_output_path(outPath)

        return oggThread
        
    ##
    def cb_progress_func_wav(self, curStep, hasTarget):
        totalSteps = len(self.targets)
        self.wavProgress.set_fraction(float(curStep) / float(totalSteps))
        self.wavProgress.set_text('%d / %d' % (curStep, totalSteps))

        if hasTarget == False:
            self.wavName.set_text('..waiting..')
            return
            
        if curStep < totalSteps:
            self.wavName.set_text(self.targets[curStep][1])

    ##
    def cb_progress_func_ogg(self, curStep, hasTarget):
        totalSteps = len(self.targets)
        self.oggProgress.set_fraction(float(curStep) / float(totalSteps))
        self.oggProgress.set_text('%d / %d' % (curStep, totalSteps))

        if hasTarget == False:
            self.oggName.set_text('..waiting..')
            return
            
        if curStep < totalSteps:
            name, ext = os.path.splitext(self.targets[curStep][1])
            self.oggName.set_text(name + '.ogg')

    ##
    def cb_func_wav_end(self):
        self.wavName.set_text('!! Complete !!')
    
    ##
    def cb_func_ogg_end(self):
        self.oggName.set_text('!! Complete !!')

    ##
    def remove_wav_files(self, targets):
        if len(targets) == 0:
            return
 
        for t in targets:
            delPath = os.path.join(os.path.expanduser(get_conf_tmp_path()), t[1])
            delPath = os.path.normpath(delPath)
            try:
                os.remove(delPath)
            except:
                pass

    ##
    def cb_func_killer_end(self):
        # Remove all wav files if option is on.
        if get_conf_delete_wav():
            self.remove_wav_files(self.targets)
        self.bt.set_label('Close')
        
        print '!! Finished !!'
    
    ##
    def close(self, widget, data=None):
        self.cmdKiller.kill_all()
        self.wind.destroy()


#
#
def convert_to_ogg(widget, wind):
    if wind == None:
        return
    
    artist = ''
    album = ''
    convertTargets = []

    #
    # Get the selection of the track(s) for ripping.
    #
    selected = wind.tracks.get_selected_tracks()
    if len(selected) > 0:
        artist = selected[0].get_artist()
        album = selected[0].get_album()
        convertTargets = [(sel.get_track(), sel.get_title() + '.wav') for sel in selected]

    #
    # Ready..Go!!
    #
    progWind = RipperWindow(artist, album, convertTargets)


###
#
# Below describes functions for exaile plugin.
#
###

gToolBar = None
gCnvButton = None
#
#
def initialize():
    '''
       Called when the plugin is enabled.
    '''

    #
    # Append custom button for ripping.
    #
    global gToolBar
    global gCnvButton
    if gCnvButton == None:
        gToolBar = APP.xml.get_widget('play_toolbar')
        gCnvButton = gtk.Button('Conv2Ogg')
        gToolBar.pack_start(gCnvButton, True, False, 0)
        gToolBar.show_all()

        gCnvButton.connect('clicked', convert_to_ogg, APP)

    return True


#
#
def destroy():
    '''
       Called when the plugin is disabled.
    '''

    global gToolBar
    global gCnvButton

    if gToolBar and gCnvButton:
        gToolBar.remove(gCnvButton)
        gToolBar.show_all()
        gCnvButton = None


#
#
def configure():
    '''
       Called when the user click Configure button in the Plugin Manager.
    '''
    configDlg = RipperConfigDlg()
    response = configDlg.run()

    if response == gtk.RESPONSE_ACCEPT:
        print 'response is OK'
        configDlg.save_config()
            
    configDlg.destroy()
