ポケット倉庫@ Wiki

オーディオプレーヤ "Exaile"のリッピングプラグイン

最終更新:

kani_3

- view
メンバー限定 登録/ログイン

オーディオプレーヤ "Exaile"のリッピングプラグイン(pyRipper)


Exaileのプラグインを作ってみました。(Linux環境でのみ動作確認を行っています)

pyRipper.pyというファイルのみです。
pyRipper.pyを ~/.exaile/plugins/に置き、Exaileを再起動してください。

Exaile起動後、PluginManagerを開いて、Enableチェックボックスを
ONにすることで有効になります。

実行するためには、以下のものが必要です。
  • exaile
  • python
  • pygtk
  • cdparanoia
  • oggenc

日本語の扱いについて
Exaileでアルバム名やトラック名が文字化けしている場合には
エンコードしたOGGファイルの名前も文字化けしているかもしれません。
日本語の扱いについては今後対応していこうと思っています。


このページの下のほうにスクリプトファイルを添付しました。



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()
記事メニュー
目安箱バナー