From 71bf0cc4079178621b2f27c6fb8ab81a88c54fe5 Mon Sep 17 00:00:00 2001 From: yomguy Date: Thu, 8 Oct 2009 14:08:23 +0000 Subject: [PATCH] modify encode and decode apis, adapt each plugins --- decode/api.py | 29 ++------- decode/core.py | 159 +++++++++---------------------------------------- decode/flac.py | 128 ++++----------------------------------- decode/mp3.py | 132 +++++----------------------------------- decode/ogg.py | 104 +++++--------------------------- decode/wav.py | 122 ++++--------------------------------- encode/api.py | 37 ++++++------ encode/core.py | 157 ++++++++---------------------------------------- encode/flac.py | 91 ++++++++-------------------- encode/mp3.py | 75 +++++++---------------- encode/ogg.py | 97 ++++++++++-------------------- encode/wav.py | 114 ++++------------------------------- 12 files changed, 220 insertions(+), 1025 deletions(-) diff --git a/decode/api.py b/decode/api.py index 61f5f87..7963848 100644 --- a/decode/api.py +++ b/decode/api.py @@ -29,46 +29,28 @@ class IDecoder(Interface): # 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. @@ -76,6 +58,7 @@ class IDecoder(Interface): different items, using the same driver instance. """ + class DecodeProcessError(TimeSideError): def __init__(self, message, command, subprocess): diff --git a/decode/core.py b/decode/core.py index 8e6874b..da895f4 100644 --- a/decode/core.py +++ b/decode/core.py @@ -32,151 +32,50 @@ from timeside.core import * 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 diff --git a/decode/flac.py b/decode/flac.py index 37b956e..466b6e8 100644 --- a/decode/flac.py +++ b/decode/flac.py @@ -32,29 +32,25 @@ class FlacDecoder(DecoderCore): """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: @@ -68,101 +64,3 @@ class FlacDecoder(DecoderCore): 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) - diff --git a/decode/mp3.py b/decode/mp3.py index b89a924..81e4851 100644 --- a/decode/mp3.py +++ b/decode/mp3.py @@ -26,53 +26,31 @@ import subprocess 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: @@ -85,85 +63,3 @@ class Mp3Decoder(DecoderCore): 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) - diff --git a/decode/ogg.py b/decode/ogg.py index 278e089..b4fa46a 100644 --- a/decode/ogg.py +++ b/decode/ogg.py @@ -31,32 +31,30 @@ class OggDecoder(DecoderCore): """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: @@ -69,75 +67,3 @@ class OggDecoder(DecoderCore): 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) - diff --git a/decode/wav.py b/decode/wav.py index 87a64ad..1a27014 100644 --- a/decode/wav.py +++ b/decode/wav.py @@ -29,28 +29,25 @@ class WavDecoder(DecoderCore): """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: @@ -63,96 +60,3 @@ class WavDecoder(DecoderCore): 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' - diff --git a/encode/api.py b/encode/api.py index b97edca..4187037 100644 --- a/encode/api.py +++ b/encode/api.py @@ -24,49 +24,46 @@ from timeside.core import Interface, TimeSideError 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 @@ -76,7 +73,7 @@ class IEncoder(Interface): different items, using the same driver instance. """ -class ExportProcessError(TimeSideError): +class EncodeProcessError(TimeSideError): def __init__(self, message, command, subprocess): self.message = message diff --git a/encode/core.py b/encode/core.py index 00f428a..7988279 100644 --- a/encode/core.py +++ b/encode/core.py @@ -32,151 +32,44 @@ from timeside.core import * 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 diff --git a/encode/flac.py b/encode/flac.py index e655390..8e41a8b 100644 --- a/encode/flac.py +++ b/encode/flac.py @@ -32,21 +32,13 @@ class FlacEncoder(EncoderCore): """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' @@ -54,7 +46,9 @@ class FlacEncoder(EncoderCore): 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: @@ -68,25 +62,6 @@ class FlacEncoder(EncoderCore): 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: @@ -124,45 +99,27 @@ class FlacEncoder(EncoderCore): 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) diff --git a/encode/mp3.py b/encode/mp3.py index 0b4fa38..72136a1 100644 --- a/encode/mp3.py +++ b/encode/mp3.py @@ -32,17 +32,12 @@ class Mp3Encoder(EencoderCore): """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 @@ -61,7 +56,7 @@ class Mp3Encoder(EencoderCore): } def get_format(self): return 'MP3' - + def get_file_extension(self): return 'mp3' @@ -69,10 +64,9 @@ class Mp3Encoder(EencoderCore): 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: @@ -85,18 +79,10 @@ class Mp3Encoder(EencoderCore): 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(): @@ -112,11 +98,10 @@ class Mp3Encoder(EencoderCore): 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: @@ -136,34 +121,14 @@ class Mp3Encoder(EencoderCore): 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 diff --git a/encode/ogg.py b/encode/ogg.py index 7437ac7..e4a2d8c 100644 --- a/encode/ogg.py +++ b/encode/ogg.py @@ -25,42 +25,38 @@ import subprocess 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])) @@ -69,28 +65,17 @@ class OggEncoder(EncoderCore): 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: @@ -111,33 +96,15 @@ class OggEncoder(EncoderCore): 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 + diff --git a/encode/wav.py b/encode/wav.py index b1e822c..482a532 100644 --- a/encode/wav.py +++ b/encode/wav.py @@ -29,20 +29,13 @@ class WavEncoder(EncoderCore): """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' @@ -50,7 +43,9 @@ class WavEncoder(EncoderCore): 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: @@ -63,96 +58,11 @@ class WavEncoder(EncoderCore): 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 -- 2.39.5