# from the caller's point of view. However, when implementing the class
# you'll obviously want to include this extra argument.
- def get_format():
+ def format():
"""Return the decode/encoding format as a short string
Example: "MP3", "OGG", "AVI", ...
"""
- def get_description():
+ def description():
"""Return a string describing what this decode format provides, is good
for, etc... The description is meant to help the end user decide what
format is good for him/her
"""
- def get_file_extension():
+ def file_extension():
"""Return the filename extension corresponding to this decode format"""
- def get_mime_type():
+ def mime_type():
"""Return the mime type corresponding to this decode format"""
- def set_cache_dir(path):
- """Set the directory where cached files should be stored. Does nothing
- if the decodeer doesn't support caching.
-
- The driver shouldn't assume that this method will always get called. A
- temporary directory should be used if that's not the case.
- """
-
- def process(item_id, source, metadata, options=None):
+ def process(source, options=None):
"""Perform the decoding process and stream the result through a generator
- item_id is the media item id that uniquely identifies this audio/video
- resource
-
source is the audio/video source file absolute path.
- metadata is a tuple containing tuples for each descriptor return by
- the dc.Ressource of the item, in the model order :
- ((name1, value1),(name2, value2),(name1, value3), ...)
-
- The returned file path is not meant to be permanent in any way, it
- should be considered temporary/volatile by the caller.
-
It is highly recommended that decode drivers implement some sort of
cache instead of re-encoding each time process() is called.
different items, using the same driver instance.
"""
+
class DecodeProcessError(TimeSideError):
def __init__(self, message, command, subprocess):
import xml.dom.minidom
import xml.dom.ext
-class DecoderCore(Component):
- """Defines the main parts of the decodeing tools :
- paths, metadata parsing, data streaming thru system command"""
- def __init__(self):
- self.source = ''
- self.collection = ''
- self.verbose = ''
- self.dest = ''
- self.metadata = []
- self.cache_dir = 'cache'
- self.buffer_size = 0xFFFF
+class SubProcessPipe:
- def set_cache_dir(self,path):
- self.cache_dir = path
-
- def normalize(self):
- """ Normalize the source and return its path """
- args = ''
- if self.verbose == '0':
- args = '-q'
- try:
- os.system('normalize-audio '+args+' "'+self.source+'"')
- return self.source
- except:
- raise IOError('DecoderError: cannot normalize, path does not exist.')
-
- def check_md5_key(self):
- """ Check if the md5 key is OK and return a boolean """
- try:
- md5_log = os.popen4('md5sum -c "'+self.dest+ \
- '" "'+self.dest+'.md5"')
- return 'OK' in md5_log.split(':')
- except IOError:
- raise IOError('DecoderError: cannot check the md5 key.')
-
- def get_file_info(self):
- """ Return the list of informations of the dest """
- return self.decode.get_file_info()
-
- def get_wav_length_sec(self) :
- """ Return the length of the audio source file in seconds """
- try:
- file1, file2 = os.popen4('wavinfo "'+self.source+ \
- '" | grep wavDataSize')
- for line in file2.readlines():
- line_split = line.split(':')
- value = int(int(line_split[1])/(4*44100))
- return value
- except:
- raise IOError('DecoderError: cannot get the wav length.')
-
- def compare_md5_key(self, source, dest):
- """ Compare source and dest files wih md5 method """
- f_source = open(source).read()
- f_dest = open(dest).read()
- return md5.new(f_source).digest() == md5.new(f_dest).digest()
-
- def write_metadata_xml(self,path):
- doc = xml.dom.minidom.Document()
- root = doc.createElement('timeside')
- doc.appendChild(root)
- for tag in self.metadata.keys() :
- value = self.metadata[tag]
- node = doc.createElement(tag)
- node.setAttribute('value', str(value))
- #node.setAttribute('type', get_type(value))
- root.appendChild(node)
- xml_file = open(path, "w")
- xml.dom.ext.PrettyPrint(doc, xml_file)
- xml_file.close()
-
- def pre_process(self, item_id, source, metadata, ext,
- cache_dir, options=None):
- """ Pre processing : prepare the decode path and return it"""
- self.item_id = str(item_id)
- self.source = source
- file_name = get_file_name(self.source)
- file_name_wo_ext, file_ext = split_file_name(file_name)
- self.cache_dir = cache_dir
- self.metadata = metadata
- #self.collection = self.metadata['Collection']
- #self.artist = self.metadata['Artist']
- #self.title = self.metadata['Title']
-
- # Normalize if demanded
- if not options is None:
- self.options = options
- if 'normalize' in self.options and \
- self.options['normalize'] == True:
- self.normalize()
-
- # Define the decode directory
- self.ext = self.get_file_extension()
- decode_dir = os.path.join(self.cache_dir,self.ext)
-
- if not os.path.exists(decode_dir):
- decode_dir_split = decode_dir.split(os.sep)
- path = os.sep + decode_dir_split[0]
- for _dir in decode_dir_split[1:]:
- path = os.path.join(path,_dir)
- if not os.path.exists(path):
- os.mkdir(path)
- else:
- path = decode_dir
-
- # Set the target file
- target_file = self.item_id+'.'+self.ext
- dest = os.path.join(path,target_file)
- return dest
-
- def core_process(self, command, buffer_size, dest):
- """Encode and stream audio data through a generator"""
+ def __init__(self, command, stdin=None):
+ """Read media and stream data through a generator.
+ Taken from Telemeta (see http://telemeta.org)"""
- __chunk = 0
- file_out = open(dest,'w')
+ self.buffer_size = 0xFFFF
+
+ if not stdin:
+ stdin = subprocess.PIPE
- proc = subprocess.Popen(command.encode('utf-8'),
+ self.proc = subprocess.Popen(command.encode('utf-8'),
shell = True,
- bufsize = buffer_size,
- stdin = subprocess.PIPE,
+ bufsize = self.buffer_size,
+ stdin = stdin,
stdout = subprocess.PIPE,
close_fds = True)
- # Core processing
+ self.input = self.proc.stdin
+ self.output = self.proc.stdout
+
+
+class DecoderCore(Component):
+ """Defines the main parts of the decoding tools :
+ paths, metadata parsing, data streaming thru system command"""
+
+ def __init__(self):
+ self.command = 'sox "%s" -s -q -b 16 -r 44100 -t wav -c2 -'
+
+ def process(self, source, options=None):
+ """Encode and stream audio data through a generator"""
+
+ proc = SubProcessPipe(self.command) % source
+
while True:
- __chunk = proc.stdout.read(buffer_size)
+ __chunk = proc.output.read(self.proc.buffer_size)
status = proc.poll()
if status != None and status != 0:
raise ExportProcessError('Command failure:', command, proc)
if len(__chunk) == 0:
break
yield __chunk
- file_out.write(__chunk)
-
- file_out.close()
-
- def post_process(self, item_id, source, metadata, ext,
- cache_dir, options=None):
- """ Post processing : write tags, print infos, etc..."""
- #self.write_tags()
- if not options is None:
- if 'verbose' in self.options and self.options['verbose'] != '0':
- print self.dest
- print self.get_file_info()
+
# External functions
"""Defines methods to decode to FLAC"""
implements(IDecoder)
-
+
def __init__(self):
- self.item_id = ''
- self.source = ''
- self.metadata = {}
- self.options = {}
- self.description = ''
- self.dest = ''
- self.quality_default = '-5'
- self.info = []
- self.buffer_size = 0xFFFF
-
- def get_format(self):
+ self.description = self.description()
+ self.format = self.format()
+ self.mime_type = self.mime_type()
+
+ def format(self):
return 'FLAC'
-
- def get_file_extension(self):
+
+ def file_extension(self):
return 'flac'
- def get_mime_type(self):
+ def mime_type(self):
return 'application/flac'
- def get_description(self):
- return 'FIXME'
+ def description(self):
+ return """
+ Free Lossless Audio Codec (FLAC) is a file format for lossless audio data compression. During compression, FLAC does not lose quality from the audio stream, as lossy compression formats such as MP3, AAC, and Vorbis do. Josh Coalson is the primary author of FLAC.
+ """
def get_file_info(self):
try:
raise IOError('DecoderError: metaflac is not installed or ' + \
'file does not exist.')
- def set_cache_dir(self,path):
- """Set the directory where cached files should be stored. Does nothing
- if the decodeer doesn't support caching.
-
- The driver shouldn't assume that this method will always get called. A
- temporary directory should be used if that's not the case.
- """
- self.cache_dir = path
-
- def decode(self):
- try:
- file_name, ext = get_file_name(self.source)
- dest = self.cache_dir+os.sep+file_name+'.wav'
- os.system('flac -d -o "'+dest+'" "'+self.source+'"')
- self.source = dest
- return dest
- except:
- raise IOError('DecoderError: decoder is not compatible.')
-
- def write_tags(self, file):
- media = FLAC(file)
- for tag in self.metadata:
- name = tag[0]
- value = clean_word(tag[1])
- if name == 'COMMENT':
- media['DESCRIPTION'] = unicode(value)
- else:
- media[name] = unicode(value)
- try:
- media.save()
- except:
- raise IOError('DecoderError: cannot write tags.')
-
- def get_args(self,options=None):
- """Get process options and return arguments for the encoder"""
- args = []
- if not options is None:
- self.options = options
- if not ('verbose' in self.options and self.options['verbose'] != '0'):
- args.append('-s')
- if 'flac_quality' in self.options:
- args.append('-f ' + self.options['flac_quality'])
- else:
- args.append('-f ' + self.quality_default)
- else:
- args.append('-s -f ' + self.quality_default)
-
- #for tag in self.metadata.keys():
- #value = clean_word(self.metadata[tag])
- #args.append('-c %s="%s"' % (tag, value))
- #if tag in self.dub2args_dict.keys():
- #arg = self.dub2args_dict[tag]
- #args.append('-c %s="%s"' % (arg, value))
-
- return args
-
- def process(self, item_id, source, metadata, options=None):
- self.item_id = item_id
- self.source = source
- self.metadata = metadata
- self.args = self.get_args(options)
- self.ext = self.get_file_extension()
- self.args = ' '.join(self.args)
- self.command = 'sox "%s" -s -q -b 16 -r 44100 -t wav -c2 - | flac -c %s - ' % (self.source, self.args)
-
- # Pre-proccessing
- self.dest = self.pre_process(self.item_id,
- self.source,
- self.metadata,
- self.ext,
- self.cache_dir,
- self.options)
-
- # Processing (streaming + cache writing)
- stream = self.core_process(self.command, self.buffer_size, self.dest)
-
- for chunk in stream:
- pass
-
- self.write_tags(self.dest)
- file = open(self.dest,'r')
-
- while True:
- chunk = file.read(self.buffer_size)
- if len(chunk) == 0:
- break
- yield chunk
-
- file.close()
-
- # Post-proccessing
- #self.post_process(self.item_id,
- #self.source,
- #self.metadata,
- #self.ext,
- #self.cache_dir,
- #self.options)
-
from timeside.decode.core import *
from timeside.decode.api import IDecoder
-#from mutagen.id3 import *
+
class Mp3Decoder(DecoderCore):
"""Defines methods to decode to MP3"""
implements(IDecoder)
-
+
def __init__(self):
- self.item_id = ''
- self.metadata = {}
- self.description = ''
- self.info = []
- self.source = ''
- self.dest = ''
- self.options = {}
- self.bitrate_default = '192'
- self.buffer_size = 0xFFFF
- self.dub2id3_dict = {'title': 'TIT2', #title2
- 'creator': 'TCOM', #composer
- 'creator': 'TPE1', #lead
- 'identifier': 'UFID', #Unique ID...
- 'identifier': 'TALB', #album
- 'type': 'TCON', #genre
- 'publisher': 'TPUB', #comment
- #'date': 'TYER', #year
- }
- self.dub2args_dict = {'title': 'tt', #title2
- 'creator': 'ta', #composerS
- 'relation': 'tl', #album
- #'type': 'tg', #genre
- 'publisher': 'tc', #comment
- 'date': 'ty', #year
- }
- def get_format(self):
+ self.description = self.description()
+ self.format = self.format()
+ self.mime_type = self.mime_type()
+
+ def format(self):
return 'MP3'
-
- def get_file_extension(self):
+
+ def file_extension(self):
return 'mp3'
- def get_mime_type(self):
+ def mime_type(self):
return 'audio/mpeg'
- def get_description(self):
- return "FIXME"
-
- def set_cache_dir(self,path):
- self.cache_dir = path
+ def description(self):
+ return """
+ MPEG-1 Audio Layer 3, more commonly referred to as MP3, is a patented digital audio encoding format using a form of lossy data compression. It is a common audio format for consumer audio storage, as well as a de facto standard of digital audio compression for the transfer and playback of music on digital audio players. MP3 is an audio-specific format that was designed by the Moving Picture Experts Group as part of its MPEG-1 standard.
+ """
def get_file_info(self):
try:
except:
raise IOError('DecoderError: file does not exist.')
- def decode(self):
- try:
- os.system('sox "'+self.source+'" -s -q -r 44100 -t wav "' \
- +self.cache_dir+os.sep+self.item_id+'"')
- return self.cache_dir+os.sep+self.item_id+'.wav'
- except:
- raise IOError('DecoderError: decoder is not compatible.')
-
- def write_tags(self):
- """Write all ID3v2.4 tags by mapping dub2id3_dict dictionnary with the
- respect of mutagen classes and methods"""
- from mutagen import id3
- id3 = id3.ID3(self.dest)
- for tag in self.metadata.keys():
- if tag in self.dub2id3_dict.keys():
- frame_text = self.dub2id3_dict[tag]
- value = self.metadata[tag]
- frame = mutagen.id3.Frames[frame_text](3,value)
- try:
- id3.add(frame)
- except:
- raise IOError('DecoderError: cannot tag "'+tag+'"')
- try:
- id3.save()
- except:
- raise IOError('DecoderError: cannot write tags')
-
- def get_args(self, options=None):
- """Get process options and return arguments for the encoder"""
- args = []
- if not options is None:
- self.options = options
- if not ( 'verbose' in self.options and self.options['verbose'] != '0' ):
- args.append('-S')
- if 'mp3_bitrate' in self.options:
- args.append('-b ' + self.options['mp3_bitrate'])
- else:
- args.append('-b '+self.bitrate_default)
- #Copyrights, etc..
- args.append('-c -o')
- else:
- args.append('-S -c -o')
-
- for tag in self.metadata:
- name = tag[0]
- value = clean_word(tag[1])
- if name in self.dub2args_dict.keys():
- arg = self.dub2args_dict[name]
- args.append('--' + arg + ' "' + value + '"')
- return args
-
- def process(self, item_id, source, metadata, options=None):
- self.item_id = item_id
- self.source = source
- self.metadata = metadata
- self.args = self.get_args(options)
- self.ext = self.get_file_extension()
- self.args = ' '.join(self.args)
- self.command = 'sox "%s" -q -b 16 -r 44100 -t wav - | lame %s -' % (self.source, self.args)
- #self.command = 'lame %s "%s" -' % (self.args, self.source)
-
- # Pre-proccessing
- self.dest = self.pre_process(self.item_id,
- self.source,
- self.metadata,
- self.ext,
- self.cache_dir,
- self.options)
-
- # Processing (streaming + cache writing)
- stream = self.core_process(self.command, self.buffer_size, self.dest)
- for chunk in stream:
- yield chunk
-
- # Post-proccessing
- #self.post_process(self.item_id,
- #self.source,
- #self.metadata,
- #self.ext,
- #self.cache_dir,
- #self.options)
-
"""Defines methods to decode to OGG Vorbis"""
implements(IDecoder)
-
+
def __init__(self):
- self.item_id = ''
- self.metadata = {}
- self.description = ''
- self.info = []
- self.source = ''
- self.dest = ''
- self.options = {}
+ self.description = self.description()
+ self.format = self.format()
+ self.mime_type = self.mime_type()
+
self.bitrate_default = '192'
- self.buffer_size = 0xFFFF
self.dub2args_dict = {'creator': 'artist',
'relation': 'album'
}
-
- def get_format(self):
- return 'OGG'
-
- def get_file_extension(self):
+
+ def format(self):
+ return 'OggVorbis'
+
+ def file_extension(self):
return 'ogg'
- def get_mime_type(self):
+ def mime_type(self):
return 'application/ogg'
- def get_description(self):
- return 'FIXME'
+ def description(self):
+ return """
+ Vorbis is a free software / open source project headed by the Xiph.Org Foundation (formerly Xiphophorus company). The project produces an audio format specification and software implementation (codec) for lossy audio compression. Vorbis is most commonly used in conjunction with the Ogg container format and it is therefore often referred to as Ogg Vorbis. (source Wikipedia)
+ """
def get_file_info(self):
try:
except:
raise IOError('DecoderError: file does not exist.')
- def set_cache_dir(self,path):
- self.cache_dir = path
-
- def decode(self):
- try:
- os.system('oggdec -o "'+self.cache_dir+os.sep+self.item_id+
- '.wav" "'+self.source+'"')
- return self.cache_dir+os.sep+self.item_id+'.wav'
- except:
- raise IOError('DecoderError: decoder is not compatible.')
-
- def write_tags(self):
- media = OggVorbis(self.dest)
- for tag in self.metadata.keys():
- media[tag] = str(self.metadata[tag])
- media.save()
-
- def get_args(self,options=None):
- """Get process options and return arguments for the encoder"""
- args = []
- if not options is None:
- self.options = options
- if not ('verbose' in self.options and self.options['verbose'] != '0'):
- args.append('-Q ')
- if 'ogg_bitrate' in self.options:
- args.append('-b '+self.options['ogg_bitrate'])
- elif 'ogg_quality' in self.options:
- args.append('-q '+self.options['ogg_quality'])
- else:
- args.append('-b '+self.bitrate_default)
- else:
- args.append('-Q -b '+self.bitrate_default)
-
- for tag in self.metadata:
- name = tag[0]
- value = clean_word(tag[1])
- args.append('-c %s="%s"' % (name, value))
- if name in self.dub2args_dict.keys():
- arg = self.dub2args_dict[name]
- args.append('-c %s="%s"' % (arg, value))
- return args
-
- def process(self, item_id, source, metadata, options=None):
- self.item_id = item_id
- self.source = source
- self.metadata = metadata
- self.args = self.get_args(options)
- self.ext = self.get_file_extension()
- self.args = ' '.join(self.args)
- self.command = 'sox "%s" -s -q -b 16 -r 44100 -t wav -c2 - | oggenc %s -' % (self.source, self.args)
-
- # Pre-proccessing
- self.dest = self.pre_process(self.item_id,
- self.source,
- self.metadata,
- self.ext,
- self.cache_dir,
- self.options)
-
- # Processing (streaming + cache writing)
- stream = self.core_process(self.command, self.buffer_size, self.dest)
- for chunk in stream:
- yield chunk
-
- # Post-proccessing
- #self.post_process(self.item_id,
- #self.source,
- #self.metadata,
- #self.ext,
- #self.cache_dir,
- #self.options)
-
"""Defines methods to decode to WAV"""
implements(IDecoder)
-
+
def __init__(self):
- self.item_id = ''
- self.metadata = {}
- self.description = ''
- self.info = []
- self.source = ''
- self.dest = ''
- self.options = {}
- self.buffer_size = 0xFFFF
-
- def get_format(self):
+ self.description = self.description()
+ self.format = self.format()
+ self.mime_type = self.mime_type()
+
+ def format(self):
return 'WAV'
-
- def get_file_extension(self):
+
+ def file_extension(self):
return 'wav'
- def get_mime_type(self):
+ def mime_type(self):
return 'audio/x-wav'
- def get_description(self):
- return 'FIXME'
+ def description(self):
+ return """
+ WAV (or WAVE), short for Waveform audio format, also known as Audio for Windows, is a Microsoft and IBM audio file format standard for storing an audio bitstream on PCs. It is an application of the RIFF bitstream format method for storing data in “chunks”, and thus is also close to the 8SVX and the AIFF format used on Amiga and Macintosh computers, respectively. It is the main format used on Windows systems for raw and typically uncompressed audio. The usual bitstream encoding is the Pulse Code Modulation (PCM) format.
+ """
def get_file_info(self):
try:
except:
raise IOError('DecoderError: wavinfo id not installed or file does not exist.')
- def set_cache_dir(self,path):
- self.cache_dir = path
-
- def decode(self):
- try:
- file_name, ext = get_file_name(self.source)
- dest = self.cache_dir+os.sep+file_name+'.wav'
- os.system('sox "'+self.source+'" -s -r 44100 -t wav -c2 "'+ \
- dest+'.wav"')
- self.source = dest
- return dest
- except:
- raise IOError('DecoderError: decoder is not compatible.')
-
- def write_tags(self):
- # Create metadata XML file !
- self.write_metadata_xml(self.dest+'.xml')
-
- def create_md5_key(self):
- """ Create the md5 keys of the dest """
- try:
- os.system('md5sum -b "'+self.dest+'" >"'+self.dest+'.md5"')
- except:
- raise IOError('DecoderError: cannot create the md5 key.')
-
- def create_par_key(self):
- """ Create the par2 keys of the dest """
- args = 'c -n1 '
- if 'verbose' in self.options and self.options['verbose'] != '0':
- args = args
- else:
- args = args + '-q -q '
-
- try:
- os.system('par2 '+args+' "'+self.dest+'"')
- except:
- raise IOError('DecoderError: cannot create the par2 key.')
-
- def process(self, item_id, source, metadata, options=None):
- self.item_id = item_id
- self.source = source
- self.metadata = metadata
- self.options = {}
-
- if not options is None:
- self.options = options
-
- # Pre-proccessing
- self.ext = self.get_file_extension()
- self.dest = self.pre_process(self.item_id,
- self.source,
- self.metadata,
- self.ext,
- self.cache_dir,
- self.options)
-
- # Initializing
- file_in = open(self.source,'rb')
- file_out = open(self.dest,'w')
-
- # Core Processing
- while True:
- chunk = file_in.read(self.buffer_size)
- if len(chunk) == 0:
- break
- yield chunk
- file_out.write(chunk)
-
- file_in.close()
- file_out.close()
-
- # Create the md5 key
- #if 'md5' in self.metadata and self.metadata['md5']:
- self.create_md5_key()
-
- # Create the par2 key
- #if 'par2' in self.metadata and self.metadata['par2']:
- #self.create_par_key()
-
- # Pre-proccessing
- self.post_process(self.item_id,
- self.source,
- self.metadata,
- self.ext,
- self.cache_dir,
- self.options)
-
-
-
- #if self.compare_md5_key():
- #os.system('cp -a "'+self.source+'" "'+ self.dest+'"')
- #print 'COPIED'
-
class IEncoder(Interface):
"""Encoder driver interface"""
- # Remark: the method prototypes do not include any self or cls argument
- # because an interface is meant to show what methods a class must expose
- # from the caller's point of view. However, when implementing the class
+ # Remark: the method prototypes do not include any self or cls argument
+ # because an interface is meant to show what methods a class must expose
+ # from the caller's point of view. However, when implementing the class
# you'll obviously want to include this extra argument.
- def get_format():
- """Return the encode/encoding format as a short string
+ def format():
+ """Return the encode/encoding format as a short string
Example: "MP3", "OGG", "AVI", ...
"""
-
- def get_description():
- """Return a string describing what this encode format provides, is good
- for, etc... The description is meant to help the end user decide what
+
+ def description():
+ """Return a string describing what this encode format provides, is good
+ for, etc... The description is meant to help the end user decide what
format is good for him/her
"""
- def get_file_extension():
+ def file_extension():
"""Return the filename extension corresponding to this encode format"""
- def get_mime_type():
+ def mime_type():
"""Return the mime type corresponding to this encode format"""
def set_cache_dir(path):
"""Set the directory where cached files should be stored. Does nothing
- if the encodeer doesn't support caching.
-
+ if the encodeer doesn't support caching.
+
The driver shouldn't assume that this method will always get called. A
temporary directory should be used if that's not the case.
"""
- def process(item_id, source, metadata, options=None):
+ def process(source, metadata, options=None):
"""Perform the encoding process and stream the result as a generator.
- item_id is the media item id that uniquely identifies this audio/video
- resource
-
- source is a raw audio stream coming from the decoder.
+ source is a raw audio stream coming from a decoder.
metadata is a tuple containing tuples for each descriptor return by
the dc.Ressource of the item, in the model order :
((name1, value1),(name2, value2),(name1, value3), ...)
- The returned file path is not meant to be permanent in any way, it
+ The returned file path is not meant to be permanent in any way, it
should be considered temporary/volatile by the caller.
It is highly recommended that encode drivers implement some sort of
different items, using the same driver instance.
"""
-class ExportProcessError(TimeSideError):
+class EncodeProcessError(TimeSideError):
def __init__(self, message, command, subprocess):
self.message = message
import xml.dom.minidom
import xml.dom.ext
-class EncoderCore(Component):
- """Defines the main parts of the encodeing tools :
- paths, metadata parsing, data streaming thru system command"""
- def __init__(self):
- self.source = ''
- self.collection = ''
- self.verbose = ''
- self.dest = ''
- self.metadata = []
- self.cache_dir = 'cache'
- self.buffer_size = 0xFFFF
+class SubProcessPipe:
+ """Read media and stream data through a generator.
+ Taken from Telemeta (see http://telemeta.org)"""
- def set_cache_dir(self,path):
- self.cache_dir = path
-
- def normalize(self):
- """ Normalize the source and return its path """
- args = ''
- if self.verbose == '0':
- args = '-q'
- try:
- os.system('normalize-audio '+args+' "'+self.source+'"')
- return self.source
- except:
- raise IOError('EncoderError: cannot normalize, path does not exist.')
-
- def check_md5_key(self):
- """ Check if the md5 key is OK and return a boolean """
- try:
- md5_log = os.popen4('md5sum -c "'+self.dest+ \
- '" "'+self.dest+'.md5"')
- return 'OK' in md5_log.split(':')
- except IOError:
- raise IOError('EncoderError: cannot check the md5 key.')
-
- def get_file_info(self):
- """ Return the list of informations of the dest """
- return self.encode.get_file_info()
-
- def get_wav_length_sec(self) :
- """ Return the length of the audio source file in seconds """
- try:
- file1, file2 = os.popen4('wavinfo "'+self.source+ \
- '" | grep wavDataSize')
- for line in file2.readlines():
- line_split = line.split(':')
- value = int(int(line_split[1])/(4*44100))
- return value
- except:
- raise IOError('EncoderError: cannot get the wav length.')
-
- def compare_md5_key(self, source, dest):
- """ Compare source and dest files wih md5 method """
- f_source = open(source).read()
- f_dest = open(dest).read()
- return md5.new(f_source).digest() == md5.new(f_dest).digest()
-
- def write_metadata_xml(self,path):
- doc = xml.dom.minidom.Document()
- root = doc.createElement('timeside')
- doc.appendChild(root)
- for tag in self.metadata.keys() :
- value = self.metadata[tag]
- node = doc.createElement(tag)
- node.setAttribute('value', str(value))
- #node.setAttribute('type', get_type(value))
- root.appendChild(node)
- xml_file = open(path, "w")
- xml.dom.ext.PrettyPrint(doc, xml_file)
- xml_file.close()
-
- def pre_process(self, item_id, source, metadata, ext,
- cache_dir, options=None):
- """ Pre processing : prepare the encode path and return it"""
- self.item_id = str(item_id)
- self.source = source
- file_name = get_file_name(self.source)
- file_name_wo_ext, file_ext = split_file_name(file_name)
- self.cache_dir = cache_dir
- self.metadata = metadata
- #self.collection = self.metadata['Collection']
- #self.artist = self.metadata['Artist']
- #self.title = self.metadata['Title']
-
- # Normalize if demanded
- if not options is None:
- self.options = options
- if 'normalize' in self.options and \
- self.options['normalize'] == True:
- self.normalize()
-
- # Define the encode directory
- self.ext = self.get_file_extension()
- encode_dir = os.path.join(self.cache_dir,self.ext)
-
- if not os.path.exists(encode_dir):
- encode_dir_split = encode_dir.split(os.sep)
- path = os.sep + encode_dir_split[0]
- for _dir in encode_dir_split[1:]:
- path = os.path.join(path,_dir)
- if not os.path.exists(path):
- os.mkdir(path)
- else:
- path = encode_dir
-
- # Set the target file
- target_file = self.item_id+'.'+self.ext
- dest = os.path.join(path,target_file)
- return dest
-
- def core_process(self, command, buffer_size, dest):
- """Encode and stream audio data through a generator"""
-
- __chunk = 0
- file_out = open(dest,'w')
+ def __init__(self, command, stdin=None):
+ self.buffer_size = 0xFFFF
+ if not stdin:
+ stdin = subprocess.PIPE
- proc = subprocess.Popen(command.encode('utf-8'),
+ self.proc = subprocess.Popen(command.encode('utf-8'),
shell = True,
- bufsize = buffer_size,
- stdin = subprocess.PIPE,
+ bufsize = self.buffer_size,
+ stdin = stdin,
stdout = subprocess.PIPE,
close_fds = True)
- # Core processing
+ self.input = self.proc.stdin
+ self.output = self.proc.stdout
+
+
+class EncoderCore(Component):
+ """Defines the main parts of the encoding tools :
+ paths, metadata parsing, data streaming thru system command"""
+
+ def core_process(self, command, stdin):
+ """Encode and stream audio data through a generator"""
+
+ proc = SubProcessPipe(command, stdin)
+
while True:
- __chunk = proc.stdout.read(buffer_size)
+ __chunk = proc.output.read(self.proc.buffer_size)
status = proc.poll()
if status != None and status != 0:
- raise ExportProcessError('Command failure:', command, proc)
+ raise EncodeProcessError('Command failure:', command, proc)
if len(__chunk) == 0:
break
yield __chunk
- file_out.write(__chunk)
-
- file_out.close()
-
- def post_process(self, item_id, source, metadata, ext,
- cache_dir, options=None):
- """ Post processing : write tags, print infos, etc..."""
- #self.write_tags()
- if not options is None:
- if 'verbose' in self.options and self.options['verbose'] != '0':
- print self.dest
- print self.get_file_info()
# External functions
"""Defines methods to encode to FLAC"""
implements(IEncoder)
-
+
def __init__(self):
- self.item_id = ''
- self.source = ''
- self.metadata = {}
- self.options = {}
- self.description = ''
- self.dest = ''
self.quality_default = '-5'
- self.info = []
- self.buffer_size = 0xFFFF
def get_format(self):
return 'FLAC'
-
+
def get_file_extension(self):
return 'flac'
return 'application/flac'
def get_description(self):
- return 'FIXME'
+ return """
+ Free Lossless Audio Codec (FLAC) is a file format for lossless audio data compression. During compression, FLAC does not lose quality from the audio stream, as lossy compression formats such as MP3, AAC, and Vorbis do. Josh Coalson is the primary author of FLAC.
+ """
def get_file_info(self):
try:
raise IOError('EncoderError: metaflac is not installed or ' + \
'file does not exist.')
- def set_cache_dir(self,path):
- """Set the directory where cached files should be stored. Does nothing
- if the encodeer doesn't support caching.
-
- The driver shouldn't assume that this method will always get called. A
- temporary directory should be used if that's not the case.
- """
- self.cache_dir = path
-
- def decode(self):
- try:
- file_name, ext = get_file_name(self.source)
- dest = self.cache_dir+os.sep+file_name+'.wav'
- os.system('flac -d -o "'+dest+'" "'+self.source+'"')
- self.source = dest
- return dest
- except:
- raise IOError('EncoderError: decoder is not compatible.')
-
def write_tags(self, file):
media = FLAC(file)
for tag in self.metadata:
return args
- def process(self, item_id, source, metadata, options=None):
- self.item_id = item_id
- self.source = source
- self.metadata = metadata
- self.args = self.get_args(options)
- self.ext = self.get_file_extension()
- self.args = ' '.join(self.args)
- self.command = 'sox "%s" -s -q -b 16 -r 44100 -t wav -c2 - | flac -c %s - ' % (self.source, self.args)
-
- # Pre-proccessing
- self.dest = self.pre_process(self.item_id,
- self.source,
- self.metadata,
- self.ext,
- self.cache_dir,
- self.options)
-
- # Processing (streaming + cache writing)
- stream = self.core_process(self.command, self.buffer_size, self.dest)
-
- for chunk in stream:
- pass
-
- self.write_tags(self.dest)
- file = open(self.dest,'r')
-
+ def process(self, source, metadata, options=None):
+ buffer_size = 0xFFFF
+ args = self.get_args(options)
+ args = ' '.join(args)
+ ext = self.get_file_extension()
+ command = 'flac -c %s -' % args
+
+ stream = self.core_process(command, source)
+ temp_file = NamedTemporaryFile(delete=False)
+ for __chunk in stream:
+ temp_file.write(__chunk)
+ temp_file.flush()
+
+ self.write_tags(temp_file)
+
while True:
- chunk = file.read(self.buffer_size)
- if len(chunk) == 0:
+ __chunk = temp_file.read(buffer_size)
+ if len(__chunk) == 0:
break
- yield chunk
+ yield __chunk
- file.close()
+ temp_file.close()
- # Post-proccessing
- #self.post_process(self.item_id,
- #self.source,
- #self.metadata,
- #self.ext,
- #self.cache_dir,
- #self.options)
"""Defines methods to encode to MP3"""
implements(IEncoder)
-
+
def __init__(self):
- self.item_id = ''
- self.metadata = {}
- self.description = ''
- self.info = []
- self.source = ''
- self.dest = ''
- self.options = {}
+ self.description = self.description()
+ self.format = self.format()
+ self.mime_type = self.mime_type()
self.bitrate_default = '192'
- self.buffer_size = 0xFFFF
self.dub2id3_dict = {'title': 'TIT2', #title2
'creator': 'TCOM', #composer
'creator': 'TPE1', #lead
}
def get_format(self):
return 'MP3'
-
+
def get_file_extension(self):
return 'mp3'
return 'audio/mpeg'
def get_description(self):
- return "FIXME"
-
- def set_cache_dir(self,path):
- self.cache_dir = path
+ return """
+ MPEG-1 Audio Layer 3, more commonly referred to as MP3, is a patented digital audio encoding format using a form of lossy data compression. It is a common audio format for consumer audio storage, as well as a de facto standard of digital audio compression for the transfer and playback of music on digital audio players. MP3 is an audio-specific format that was designed by the Moving Picture Experts Group as part of its MPEG-1 standard.
+ """
def get_file_info(self):
try:
except:
raise IOError('EncoderError: file does not exist.')
- def decode(self):
- try:
- os.system('sox "'+self.source+'" -s -q -r 44100 -t wav "' \
- +self.cache_dir+os.sep+self.item_id+'"')
- return self.cache_dir+os.sep+self.item_id+'.wav'
- except:
- raise IOError('EncoderError: decoder is not compatible.')
-
def write_tags(self):
"""Write all ID3v2.4 tags by mapping dub2id3_dict dictionnary with the
respect of mutagen classes and methods"""
- from mutagen import id3
+ from mutagen import id3
id3 = id3.ID3(self.dest)
for tag in self.metadata.keys():
if tag in self.dub2id3_dict.keys():
except:
raise IOError('EncoderError: cannot write tags')
- def get_args(self, options=None):
+ def get_args(self):
"""Get process options and return arguments for the encoder"""
args = []
- if not options is None:
- self.options = options
+ if not self.options is None:
if not ( 'verbose' in self.options and self.options['verbose'] != '0' ):
args.append('-S')
if 'mp3_bitrate' in self.options:
args.append('--' + arg + ' "' + value + '"')
return args
- def process(self, item_id, source, metadata, options=None):
- self.item_id = item_id
- self.source = source
+ def process(self, source, metadata, options=None):
self.metadata = metadata
- self.args = self.get_args(options)
- self.ext = self.get_file_extension()
- self.args = ' '.join(self.args)
- self.command = 'sox "%s" -q -b 16 -r 44100 -t wav - | lame %s -' % (self.source, self.args)
- #self.command = 'lame %s "%s" -' % (self.args, self.source)
-
- # Pre-proccessing
- self.dest = self.pre_process(self.item_id,
- self.source,
- self.metadata,
- self.ext,
- self.cache_dir,
- self.options)
-
- # Processing (streaming + cache writing)
- stream = self.core_process(self.command, self.buffer_size, self.dest)
- for chunk in stream:
- yield chunk
-
- # Post-proccessing
- #self.post_process(self.item_id,
- #self.source,
- #self.metadata,
- #self.ext,
- #self.cache_dir,
- #self.options)
+ self.options = options
+ args = get_args(options)
+ args = ' '.join(args)
+ command = 'lame %s - -' % args
+
+ stream = self.core_process(command, source)
+ for __chunk in stream:
+ yield __chunk
from timeside.encode.core import *
from timeside.encode.api import IEncoder
-from mutagen.oggvorbis import OggVorbis
-class OggEncoder(EncoderCore):
+class OggVorbisEncoder(EncoderCore):
"""Defines methods to encode to OGG Vorbis"""
implements(IEncoder)
-
+
def __init__(self):
- self.item_id = ''
- self.metadata = {}
- self.description = ''
- self.info = []
- self.source = ''
- self.dest = ''
- self.options = {}
+ self.description = self.description()
+ self.format = self.format()
+ self.mime_type = self.mime_type()
self.bitrate_default = '192'
- self.buffer_size = 0xFFFF
self.dub2args_dict = {'creator': 'artist',
'relation': 'album'
}
-
- def get_format(self):
- return 'OGG'
-
- def get_file_extension(self):
+
+ def format(self):
+ return 'OggVorbis'
+
+ def file_extension(self):
return 'ogg'
- def get_mime_type(self):
+ def mime_type(self):
return 'application/ogg'
- def get_description(self):
- return 'FIXME'
+ def description(self):
+ return """
+ Vorbis is a free software / open source project headed by the Xiph.Org Foundation (formerly Xiphophorus company). The project produces an audio format specification and software implementation (codec) for lossy audio compression. Vorbis is most commonly used in conjunction with the Ogg container format and it is therefore often referred to as Ogg Vorbis. (source Wikipedia)
+ """
- def get_file_info(self):
+ def get_file_info(self, file):
try:
- file_out1, file_out2 = os.popen4('ogginfo "'+self.dest+'"')
+ file_out1, file_out2 = os.popen4('ogginfo "' + file + '"')
info = []
for line in file_out2.readlines():
info.append(clean_word(line[:-1]))
except:
raise IOError('EncoderError: file does not exist.')
- def set_cache_dir(self,path):
- self.cache_dir = path
-
- def decode(self):
- try:
- os.system('oggdec -o "'+self.cache_dir+os.sep+self.item_id+
- '.wav" "'+self.source+'"')
- return self.cache_dir+os.sep+self.item_id+'.wav'
- except:
- raise IOError('EncoderError: decoder is not compatible.')
-
- def write_tags(self):
- media = OggVorbis(self.dest)
+ def write_tags(self, file):
+ from mutagen.oggvorbis import OggVorbis
+ media = OggVorbis(file)
for tag in self.metadata.keys():
media[tag] = str(self.metadata[tag])
media.save()
- def get_args(self,options=None):
+ def get_args(self):
"""Get process options and return arguments for the encoder"""
args = []
- if not options is None:
- self.options = options
+ if not self.options is None:
if not ('verbose' in self.options and self.options['verbose'] != '0'):
args.append('-Q ')
if 'ogg_bitrate' in self.options:
args.append('-c %s="%s"' % (arg, value))
return args
- def process(self, item_id, source, metadata, options=None):
- self.item_id = item_id
- self.source = source
+ def process(self, source, metadata, options=None):
self.metadata = metadata
- self.args = self.get_args(options)
- self.ext = self.get_file_extension()
- self.args = ' '.join(self.args)
- self.command = 'sox "%s" -s -q -b 16 -r 44100 -t wav -c2 - | oggenc %s -' % (self.source, self.args)
-
- # Pre-proccessing
- self.dest = self.pre_process(self.item_id,
- self.source,
- self.metadata,
- self.ext,
- self.cache_dir,
- self.options)
-
- # Processing (streaming + cache writing)
- stream = self.core_process(self.command, self.buffer_size, self.dest)
- for chunk in stream:
- yield chunk
-
- # Post-proccessing
- #self.post_process(self.item_id,
- #self.source,
- #self.metadata,
- #self.ext,
- #self.cache_dir,
- #self.options)
+ self.options = options
+ args = self.get_args(options)
+ args = ' '.join(args)
+ command = 'oggenc %s -' % args
+
+ stream = self.core_process(command, source)
+ for __chunk in stream:
+ yield __chunk
+
"""Defines methods to encode to WAV"""
implements(IEncoder)
-
+
def __init__(self):
- self.item_id = ''
- self.metadata = {}
- self.description = ''
- self.info = []
- self.source = ''
- self.dest = ''
- self.options = {}
- self.buffer_size = 0xFFFF
+ pass
def get_format(self):
return 'WAV'
-
+
def get_file_extension(self):
return 'wav'
return 'audio/x-wav'
def get_description(self):
- return 'FIXME'
+ return """
+ WAV (or WAVE), short for Waveform audio format, also known as Audio for Windows, is a Microsoft and IBM audio file format standard for storing an audio bitstream on PCs. It is an application of the RIFF bitstream format method for storing data in “chunks”, and thus is also close to the 8SVX and the AIFF format used on Amiga and Macintosh computers, respectively. It is the main format used on Windows systems for raw and typically uncompressed audio. The usual bitstream encoding is the Pulse Code Modulation (PCM) format.
+ """
def get_file_info(self):
try:
except:
raise IOError('EncoderError: wavinfo id not installed or file does not exist.')
- def set_cache_dir(self,path):
- self.cache_dir = path
-
- def decode(self):
- try:
- file_name, ext = get_file_name(self.source)
- dest = self.cache_dir+os.sep+file_name+'.wav'
- os.system('sox "'+self.source+'" -s -r 44100 -t wav -c2 "'+ \
- dest+'.wav"')
- self.source = dest
- return dest
- except:
- raise IOError('EncoderError: decoder is not compatible.')
-
- def write_tags(self):
- # Create metadata XML file !
- self.write_metadata_xml(self.dest+'.xml')
-
- def create_md5_key(self):
- """ Create the md5 keys of the dest """
- try:
- os.system('md5sum -b "'+self.dest+'" >"'+self.dest+'.md5"')
- except:
- raise IOError('EncoderError: cannot create the md5 key.')
-
- def create_par_key(self):
- """ Create the par2 keys of the dest """
- args = 'c -n1 '
- if 'verbose' in self.options and self.options['verbose'] != '0':
- args = args
- else:
- args = args + '-q -q '
-
- try:
- os.system('par2 '+args+' "'+self.dest+'"')
- except:
- raise IOError('EncoderError: cannot create the par2 key.')
-
- def process(self, item_id, source, metadata, options=None):
- self.item_id = item_id
- self.source = source
+ def process(self, source, metadata, options=None):
self.metadata = metadata
- self.options = {}
-
- if not options is None:
- self.options = options
-
- # Pre-proccessing
- self.ext = self.get_file_extension()
- self.dest = self.pre_process(self.item_id,
- self.source,
- self.metadata,
- self.ext,
- self.cache_dir,
- self.options)
-
- # Initializing
- file_in = open(self.source,'rb')
- file_out = open(self.dest,'w')
-
- # Core Processing
- while True:
- chunk = file_in.read(self.buffer_size)
- if len(chunk) == 0:
- break
- yield chunk
- file_out.write(chunk)
-
- file_in.close()
- file_out.close()
-
- # Create the md5 key
- #if 'md5' in self.metadata and self.metadata['md5']:
- self.create_md5_key()
-
- # Create the par2 key
- #if 'par2' in self.metadata and self.metadata['par2']:
- #self.create_par_key()
-
- # Pre-proccessing
- self.post_process(self.item_id,
- self.source,
- self.metadata,
- self.ext,
- self.cache_dir,
- self.options)
-
-
-
- #if self.compare_md5_key():
- #os.system('cp -a "'+self.source+'" "'+ self.dest+'"')
- #print 'COPIED'
+ self.options = options
+ command = 'sox - -s -q -b 16 -r 44100 -t wav -c2 -'
+ stream = self.core_process(command, source)
+ for __chunk in stream:
+ yield __chunk