// Big thanks to Mark Kretschmann for this fine piece of software
// The file belongs to the great copycover script http://www.kde-apps.org/content/show.php?content=22517
// I needed to modify it a bit so it writes the cover also in the tag of the mp3 file being played.
// To show up covers on mobile devices which read the cover out of the mp3 tag. e.g.: SE C905
// There is also a second script in bash which is basicly an "intro" function for amarok 1.*
import ConfigParser
import os
import Queue
import sys
import threading
import urllib
import commands
import shutil
import subprocess
import mutagen
from mutagen.id3 import APIC
try:
from qt import *
except:
os.system( "kdialog --sorry 'CopyCover error: PyQt (Qt bindings for Python) is required for this script.'" )
raise
debug_prefix = "[AmaroK CopyCover Script]"
class ConfigDialog( QDialog ):
""" Configuration widget """
def __init__( self ):
self.load()
QDialog.__init__( self )
self.setWFlags( Qt.WDestructiveClose )
self.setCaption( "Copy cover script - amaroK" )
self.lay = QHBoxLayout( self )
self.vbox = QVBox( self )
self.lay.addWidget( self.vbox )
self.hbox1 = QHBox( self.vbox )
self.hbox1.setMargin(5)
QLabel( "What filename should I use for album covers ?", self.hbox1 )
self.hbox2 = QHBox( self.vbox )
self.hbox2.setMargin(5)
self.albumName = QCheckBox( QString("Use album name"), self.hbox2 )
self.hbox3 = QHBox( self.vbox )
self.hbox3.setMargin(5)
QLabel( "Always use this filename : ", self.hbox3 )
self.filenameEdit = QLineEdit( self.hbox3 )
self.connect( self.albumName, SIGNAL("toggled(bool)"), self.filenameEdit, SLOT('setDisabled(bool)') )
self.connect( self.albumName, SIGNAL("toggled(bool)"), self.filenameEdit, SLOT('setDisabled(bool)') )
if self.filename == "albumname":
self.filenameEdit.setText("cover.png")
self.albumName.setChecked(True)
else:
self.albumName.setChecked(False)
self.filenameEdit.setText(self.filename)
self.filenameEdit.setFocus()
self.hbox4 = QHBox( self.vbox )
self.hbox4.setMargin(5)
self.desktopEntry = QCheckBox( QString("Change folder icon"), self.hbox4 )
if self.createDE:
self.desktopEntry.setChecked(True)
else:
self.desktopEntry.setChecked(False)
self.hbox5 = QHBox( self.vbox )
self.hbox5.setMargin(5)
self.removeCoverBox = QCheckBox( QString("Remove from amaroK's cache after copy"), self.hbox5 )
if self.removeCover:
self.removeCoverBox.setChecked(True)
else:
self.removeCoverBox.setChecked(False)
self.hboxOk = QHBox( self.vbox )
self.hboxOk.setMargin(5)
self.ok = QPushButton( self.hboxOk )
self.ok.setText( "Ok" )
self.cancel = QPushButton( self.hboxOk )
self.cancel.setText( "Cancel" )
self.cancel.setDefault( True )
self.connect( self.ok, SIGNAL( "clicked()" ), self.save )
self.connect( self.cancel, SIGNAL( "clicked()" ), self, SLOT( "reject()" ) )
self.adjustSize()
def load( self ):
""" Loads configuration from file """
self.config = ConfigParser.ConfigParser()
try:
self.config.read( "copycoverrc" )
except:
debug( "No config file found, using defaults." )
self.filename = "cover.png"
self.createDE = False
self.removeCover = False
if self.config.has_option("General", "filename"):
self.filename = self.config.get("General", "filename")
else:
self.filename = "cover.png"
if self.config.has_option("General", "createDesktopEntry"):
self.createDE = self.config.getboolean("General", "createDesktopEntry")
else:
self.createDE = False
if self.config.has_option("General", "removeCover"):
self.removeCover = self.config.getboolean("General", "removeCover")
else:
self.removeCover = False
def save( self ):
""" Saves configuration to file """
self.file = open( "copycoverrc", 'w' )
self.config = ConfigParser.ConfigParser()
self.config.add_section( "General" )
if self.albumName.isChecked():
self.config.set( "General", "filename", "albumname" )
else:
filename = str( self.filenameEdit.text() )
self.config.set( "General", "filename", filename )
if self.desktopEntry.isChecked():
self.config.set( "General", "createDesktopEntry", "yes" )
else:
self.config.set( "General", "createDesktopEntry", "no" )
if self.removeCoverBox.isChecked():
self.config.set( "General", "removeCover", "yes" )
else:
self.config.set( "General", "removeCover", "no" )
self.config.write( self.file )
self.file.close()
self.accept()
class CopyCover( QApplication ):
""" The main application, also sets up the Qt event loop """
def __init__( self, args ):
QApplication.__init__( self, args )
debug( "Started." )
self.queue = Queue.Queue()
self.startTimer( 100 )
self.stdinReader = threading.Thread( target = self.readStdin )
self.stdinReader.start()
self.readSettings()
def saveState(self):
sessionmanager.setRestartHint(QSessionManager.RestartNever)
def readSettings( self ):
""" Reads settings from configuration file """
self.filename = "cover.png"
self.createDE = False
self.removeCover = False
config = ConfigParser.ConfigParser()
try:
config.read( "copycoverrc" )
except:
debug( "No config file found, using defaults." )
if config.has_option("General", "filename"):
self.filename = config.get("General", "filename")
if config.has_option("General", "createDesktopEntry"):
self.createDE = config.getboolean("General", "createDesktopEntry")
if config.has_option("General", "removeCover"):
self.removeCover = config.getboolean("General", "removeCover")
def readStdin( self ):
""" Reads incoming notifications from stdin """
while True:
line = sys.stdin.readline()
if line:
self.queue.put_nowait( line )
else:
break
def timerEvent( self, event ):
""" Polls the notification queue at regular interval """
if not self.queue.empty():
string = QString( self.queue.get_nowait() )
debug( "Received notification: " + str( string ) )
if string.contains( "configure" ):
self.configure()
if string.contains( "engineStateChange: play" ):
self.engineStatePlay()
if string.contains( "engineStateChange: idle" ):
self.engineStateIdle()
if string.contains( "engineStateChange: pause" ):
self.engineStatePause()
if string.contains( "engineStateChange: empty" ):
self.engineStatePause()
if string.contains( "trackChange" ):
self.trackChange()
def configure( self ):
debug( "configuration" )
self.dia = ConfigDialog()
self.dia.show()
self.connect( self.dia, SIGNAL( "destroyed()" ), self.readSettings )
def engineStatePlay( self ):
""" Called when Engine state changes to Play """
pass
def engineStateIdle( self ):
""" Called when Engine state changes to Idle """
pass
def engineStatePause( self ):
""" Called when Engine state changes to Pause """
pass
def engineStateEmpty( self ):
""" Called when Engine state changes to Empty """
pass
def trackChange( self ):
""" Called when a new track starts """
try:
self.copyCover()
except Exception, e:
message = "The CopyCover amaroK script has run into an unhandled error. " \
+"I'm sorry about it, but please tell me about this error, " \
+"and help improve the script !\nThe error message was:\n" \
+"%s\nPlease look at the end of your ~/.xsession-errors " % e \
+"file for error messages too. Thanks."
os.system("kdialog --title \"AmaroK CopyCover Script\" --sorry \"%s\"" % message)
def copyCover(self):
""" Copy a cover to a song's directory """
cover = commands.getoutput("dcop amarok player coverImage")
str_cacheFileName = os.path.basename(cover)
str_cacheDir = os.path.dirname(cover)
str_baseDir = os.path.dirname(str_cacheDir)
str_largeDir = os.path.join(str_baseDir, "large")
str_largeFileName = str_cacheFileName.split("@")[1]
if os.path.exists(os.path.join(str_largeDir, str_largeFileName)):
cover = os.path.join(str_largeDir, str_largeFileName)
if cover.endswith('nocover.png'):
return
songDir = self.getAlbumDir()
if not os.path.exists(songDir):
debug("ERROR: Invalid dirname: %s" % songDir, level=2)
return
filename = self.getCoverName()
target = os.path.join(songDir, filename)
target_jpg = os.path.join(songDir, "cover.jpg")
alreadyThere = False
"""for file in os.listdir(songDir):
if file.lower()[-4:] in ['.png', '.jpg', '.gif']:
alreadyThere = True"""
if not alreadyThere:
if os.path.exists(cover):
debug("copy %s to %s" % (cover, target))
if not os.access(songDir, os.W_OK):
debug("ERROR: No write access to %s" % songDir, level=2)
return
shutil.copyfile(cover, target)
"""os.system("kdialog --title \"AmaroK CopyCover Script\" --sorry \"%s\"" % target)"""
cmd=["convert", target, target_jpg]
subprocess.call(cmd)
imagedata = open(target_jpg, 'rb').read()
mp3file = commands.getoutput("dcop amarok player path")
audio = mutagen.File(mp3file)
audio.tags.add(APIC(3, 'image/jpg', 3, 'Front cover', imagedata))
audio.tags.save()
audio.save
if self.removeCover:
os.remove(cover)
if self.createDE:
self.createDesktopEntry()
def getAlbumDir(self):
""" Finds the album directory """
songURL = commands.getoutput("dcop amarok player encodedURL")
song = urllib.unquote(songURL)
# A song URL usually looks like file:///path/to/song,
# but sometimes it can be file:/path/to/song
song = song.replace("file://", "").replace("file:", "")
return os.path.dirname(song)
def getCoverName(self):
""" Finds the cover name (could be from the album name) """
if self.filename != "albumname":
return self.filename
albumName = commands.getoutput("dcop amarok player album")
if not albumName:
return "cover.png"
illegal_chars = [' ', '/', '"', '*', ':', '<', '>', '?', '\\', '|' ]
for char in illegal_chars:
if albumName.count(char) > 0:
albumName = albumName.replace(char, "")
return albumName + ".png"
def createDesktopEntry(self):
""" Creates the desktop entry to change the folder's icon to the album cover """
songDir = self.getAlbumDir()
alreadyThere = False
for file in os.listdir(songDir):
if file == ".directory":
alreadyThere = True
if not alreadyThere:
desktopEntry = open(os.path.join(songDir, ".directory"), "w")
desktopEntry.write("[Desktop Entry]\nIcon=./%s\n" % self.getCoverName())
desktopEntry.close()
debug("desktop file created in %s" % songDir)
def debug( message, level=2 ):
""" Prints debug message to stdout """
if level == 1:
sys.stdout.write(debug_prefix + " " + message + "\n")
if level >= 2:
sys.stderr.write(debug_prefix + " " + message + "\n")
if level >= 3:
os.system("kdialog --title \"%s\" --sorry \"%s\"" % (debug_prefix, message))
def main( args ):
app = CopyCover( args )
app.exec_loop()
if __name__ == "__main__":
main( sys.argv )