From: yomguy Date: Wed, 7 Oct 2009 13:17:00 +0000 (+0000) Subject: change export to decode/encode X-Git-Tag: 0.3.2~250 X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=bdc121febf3b3bfb5823770480bb157340fcae46;p=timeside.git change export to decode/encode --- diff --git a/decode/__init__.py b/decode/__init__.py new file mode 100644 index 0000000..06cb43a --- /dev/null +++ b/decode/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +from timeside.export.api import * +from timeside.export.core import * +from timeside.export.ogg import * +from timeside.export.flac import * +from timeside.export.wav import * +from timeside.export.mp3 import * \ No newline at end of file diff --git a/decode/api.py b/decode/api.py new file mode 100644 index 0000000..7ee861c --- /dev/null +++ b/decode/api.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007-2009 Parisson +# Copyright (c) 2007 Olivier Guilyardi +# Copyright (c) 2007-2009 Guillaume Pellerin +# +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +from timeside.core import Interface, TimeSideError + +class IExporter(Interface): + """Export 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 + # you'll obviously want to include this extra argument. + + def get_format(): + """Return the export/encoding format as a short string + Example: "MP3", "OGG", "AVI", ... + """ + + def get_description(): + """Return a string describing what this export 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(): + """Return the filename extension corresponding to this export format""" + + def get_mime_type(): + """Return the mime type corresponding to this export format""" + + def set_cache_dir(path): + """Set the directory where cached files should be stored. Does nothing + if the exporter 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): + """Perform the exporting process and return the absolute path + to the resulting file. + + item_id is the media item id that uniquely identifies this audio/video + resource + + source is the audio/video source file absolute path. For audio that + should be a WAV file + + 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 export drivers implement some sort of + cache instead of re-encoding each time process() is called. + + It should be possible to make subsequent calls to process() with + different items, using the same driver instance. + """ + +class ExportProcessError(TimeSideError): + + def __init__(self, message, command, subprocess): + self.message = message + self.command = str(command) + self.subprocess = subprocess + + def __str__(self): + if self.subprocess.stderr != None: + error = self.subprocess.stderr.read() + else: + error = '' + return "%s ; command: %s; error: %s" % (self.message, + self.command, + error) diff --git a/decode/core.py b/decode/core.py new file mode 100644 index 0000000..6f83356 --- /dev/null +++ b/decode/core.py @@ -0,0 +1,277 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +import os +import re +import md5 +import string +import subprocess +import mutagen + +from timeside.export import * +from timeside.core import * +import xml.dom.minidom +import xml.dom.ext + +class ExporterCore(Component): + """Defines the main parts of the exporting 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 + + 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('ExporterError: 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('ExporterError: cannot check the md5 key.') + + def get_file_info(self): + """ Return the list of informations of the dest """ + return self.export.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('ExporterError: 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 export 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 export directory + self.ext = self.get_file_extension() + export_dir = os.path.join(self.cache_dir,self.ext) + + if not os.path.exists(export_dir): + export_dir_split = export_dir.split(os.sep) + path = os.sep + export_dir_split[0] + for _dir in export_dir_split[1:]: + path = os.path.join(path,_dir) + if not os.path.exists(path): + os.mkdir(path) + else: + path = export_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') + + proc = subprocess.Popen(command.encode('utf-8'), + shell = True, + bufsize = buffer_size, + stdin = subprocess.PIPE, + stdout = subprocess.PIPE, + close_fds = True) + + # Core processing + while True: + __chunk = proc.stdout.read(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 + +def get_type(value): + """ Return a String with the type of value """ + types = {bool : 'bool', int : 'int', str : 'str'} + # 'bool' type must be placed *before* 'int' type, otherwise booleans are + # detected as integers + for type in types.keys(): + if isinstance(value, type) : + return types[type] + raise TypeError(str(value) + ' has an unsupported type') + +def get_cast(value, type) : + """ Return value, casted into type """ + if type == 'bool' : + if value == 'True' : + return True + return False + elif type == 'int' : + return int(value) + elif type == 'str' : + return str(value) + raise TypeError(type + ' is an unsupported type') + +def get_file_mime_type(path): + """ Return the mime type of a file """ + try: + file_out1, file_out2 = os.popen4('file -i "'+path+'"') + for line in file_out2.readlines(): + line_split = line.split(': ') + mime = line_split[len(line_split)-1] + return mime[:len(mime)-1] + except: + raise IOError('ExporterError: path does not exist.') + +def get_file_type_desc(path): + """ Return the type of a file given by the 'file' command """ + try: + file_out1, file_out2 = os.popen4('file "'+path+'"') + for line in file_out2.readlines(): + description = line.split(': ') + description = description[1].split(', ') + return description + except: + raise IOError('ExporterError: path does not exist.') + +def iswav(path): + """ Tell if path is a WAV """ + try: + mime = get_file_mime_type(path) + return mime == 'audio/x-wav' + except: + raise IOError('ExporterError: path does not exist.') + +def iswav16(path): + """ Tell if path is a 16 bit WAV """ + try: + file_type_desc = get_file_type_desc(path) + return iswav(path) and '16 bit' in file_type_desc + except: + raise IOError('ExporterError: path does not exist.') + +def get_file_name(path): + """ Return the file name targeted in the path """ + return os.path.split(path)[1] + +def split_file_name(file): + """ Return main file name and its extension """ + try: + return os.path.splitext(file) + except: + raise IOError('ExporterError: path does not exist.') + +def clean_word(word) : + """ Return the word without excessive blank spaces, underscores and + characters causing problem to exporters""" + word = re.sub("^[^\w]+","",word) #trim the beginning + word = re.sub("[^\w]+$","",word) #trim the end + word = re.sub("_+","_",word) #squeeze continuous _ to one _ + word = re.sub("^[^\w]+","",word) #trim the beginning _ + #word = string.replace(word,' ','_') + #word = string.capitalize(word) + dict = '&[];"*:,' + for letter in dict: + word = string.replace(word,letter,'_') + return word + +def recover_par_key(path): + """ Recover a file with par2 key """ + os.system('par2 r "'+path+'"') + +def verify_par_key(path): + """ Verify a par2 key """ + os.system('par2 v "'+path+'.par2"') + + diff --git a/decode/flac.py b/decode/flac.py new file mode 100644 index 0000000..60f2f92 --- /dev/null +++ b/decode/flac.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +import os +import string +import subprocess + +from timeside.export.core import * +from timeside.export.api import IExporter +from mutagen.flac import FLAC +from tempfile import NamedTemporaryFile + +class FlacExporter(ExporterCore): + """Defines methods to export to FLAC""" + + implements(IExporter) + + 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' + + def get_mime_type(self): + return 'application/flac' + + def get_description(self): + return 'FIXME' + + def get_file_info(self): + try: + file1, file2 = os.popen4('metaflac --list "'+self.dest+'"') + info = [] + for line in file2.readlines(): + info.append(clean_word(line[:-1])) + self.info = info + return self.info + except: + raise IOError('ExporterError: 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 exporter 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('ExporterError: 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('ExporterError: 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 new file mode 100644 index 0000000..e75f15f --- /dev/null +++ b/decode/mp3.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Parisson SARL +# Copyright (c) 2006-2007 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +import os +import string +import subprocess + +from timeside.export.core import * +from timeside.export.api import IExporter +#from mutagen.id3 import * + +class Mp3Exporter(ExporterCore): + """Defines methods to export to MP3""" + + implements(IExporter) + + 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): + return 'MP3' + + def get_file_extension(self): + return 'mp3' + + def get_mime_type(self): + return 'audio/mpeg' + + def get_description(self): + return "FIXME" + + def set_cache_dir(self,path): + self.cache_dir = path + + def get_file_info(self): + try: + file_out1, file_out2 = os.popen4('mp3info "'+self.dest+'"') + info = [] + for line in file_out2.readlines(): + info.append(clean_word(line[:-1])) + self.info = info + return self.info + except: + raise IOError('ExporterError: 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('ExporterError: 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('ExporterError: cannot tag "'+tag+'"') + try: + id3.save() + except: + raise IOError('ExporterError: 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 new file mode 100644 index 0000000..270449a --- /dev/null +++ b/decode/ogg.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +import os +import string +import subprocess + +from timeside.export.core import * +from timeside.export.api import IExporter +from mutagen.oggvorbis import OggVorbis + +class OggExporter(ExporterCore): + """Defines methods to export to OGG Vorbis""" + + implements(IExporter) + + 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.dub2args_dict = {'creator': 'artist', + 'relation': 'album' + } + + def get_format(self): + return 'OGG' + + def get_file_extension(self): + return 'ogg' + + def get_mime_type(self): + return 'application/ogg' + + def get_description(self): + return 'FIXME' + + def get_file_info(self): + try: + file_out1, file_out2 = os.popen4('ogginfo "'+self.dest+'"') + info = [] + for line in file_out2.readlines(): + info.append(clean_word(line[:-1])) + self.info = info + return self.info + except: + raise IOError('ExporterError: 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('ExporterError: 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 new file mode 100644 index 0000000..c98a172 --- /dev/null +++ b/decode/wav.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +import os +import string + +from timeside.export.core import * +from timeside.export.api import IExporter + +class WavExporter(ExporterCore): + """Defines methods to export to WAV""" + + implements(IExporter) + + 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): + return 'WAV' + + def get_file_extension(self): + return 'wav' + + def get_mime_type(self): + return 'audio/x-wav' + + def get_description(self): + return 'FIXME' + + def get_file_info(self): + try: + file1, file2 = os.popen4('wavinfo "'+self.dest+'"') + info = [] + for line in file2.readlines(): + info.append(clean_word(line[:-1])) + self.info = info + return self.info + except: + raise IOError('ExporterError: 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('ExporterError: 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('ExporterError: 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('ExporterError: 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/__init__.py b/encode/__init__.py new file mode 100644 index 0000000..06cb43a --- /dev/null +++ b/encode/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +from timeside.export.api import * +from timeside.export.core import * +from timeside.export.ogg import * +from timeside.export.flac import * +from timeside.export.wav import * +from timeside.export.mp3 import * \ No newline at end of file diff --git a/encode/api.py b/encode/api.py new file mode 100644 index 0000000..7ee861c --- /dev/null +++ b/encode/api.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007-2009 Parisson +# Copyright (c) 2007 Olivier Guilyardi +# Copyright (c) 2007-2009 Guillaume Pellerin +# +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +from timeside.core import Interface, TimeSideError + +class IExporter(Interface): + """Export 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 + # you'll obviously want to include this extra argument. + + def get_format(): + """Return the export/encoding format as a short string + Example: "MP3", "OGG", "AVI", ... + """ + + def get_description(): + """Return a string describing what this export 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(): + """Return the filename extension corresponding to this export format""" + + def get_mime_type(): + """Return the mime type corresponding to this export format""" + + def set_cache_dir(path): + """Set the directory where cached files should be stored. Does nothing + if the exporter 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): + """Perform the exporting process and return the absolute path + to the resulting file. + + item_id is the media item id that uniquely identifies this audio/video + resource + + source is the audio/video source file absolute path. For audio that + should be a WAV file + + 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 export drivers implement some sort of + cache instead of re-encoding each time process() is called. + + It should be possible to make subsequent calls to process() with + different items, using the same driver instance. + """ + +class ExportProcessError(TimeSideError): + + def __init__(self, message, command, subprocess): + self.message = message + self.command = str(command) + self.subprocess = subprocess + + def __str__(self): + if self.subprocess.stderr != None: + error = self.subprocess.stderr.read() + else: + error = '' + return "%s ; command: %s; error: %s" % (self.message, + self.command, + error) diff --git a/encode/core.py b/encode/core.py new file mode 100644 index 0000000..6f83356 --- /dev/null +++ b/encode/core.py @@ -0,0 +1,277 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +import os +import re +import md5 +import string +import subprocess +import mutagen + +from timeside.export import * +from timeside.core import * +import xml.dom.minidom +import xml.dom.ext + +class ExporterCore(Component): + """Defines the main parts of the exporting 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 + + 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('ExporterError: 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('ExporterError: cannot check the md5 key.') + + def get_file_info(self): + """ Return the list of informations of the dest """ + return self.export.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('ExporterError: 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 export 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 export directory + self.ext = self.get_file_extension() + export_dir = os.path.join(self.cache_dir,self.ext) + + if not os.path.exists(export_dir): + export_dir_split = export_dir.split(os.sep) + path = os.sep + export_dir_split[0] + for _dir in export_dir_split[1:]: + path = os.path.join(path,_dir) + if not os.path.exists(path): + os.mkdir(path) + else: + path = export_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') + + proc = subprocess.Popen(command.encode('utf-8'), + shell = True, + bufsize = buffer_size, + stdin = subprocess.PIPE, + stdout = subprocess.PIPE, + close_fds = True) + + # Core processing + while True: + __chunk = proc.stdout.read(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 + +def get_type(value): + """ Return a String with the type of value """ + types = {bool : 'bool', int : 'int', str : 'str'} + # 'bool' type must be placed *before* 'int' type, otherwise booleans are + # detected as integers + for type in types.keys(): + if isinstance(value, type) : + return types[type] + raise TypeError(str(value) + ' has an unsupported type') + +def get_cast(value, type) : + """ Return value, casted into type """ + if type == 'bool' : + if value == 'True' : + return True + return False + elif type == 'int' : + return int(value) + elif type == 'str' : + return str(value) + raise TypeError(type + ' is an unsupported type') + +def get_file_mime_type(path): + """ Return the mime type of a file """ + try: + file_out1, file_out2 = os.popen4('file -i "'+path+'"') + for line in file_out2.readlines(): + line_split = line.split(': ') + mime = line_split[len(line_split)-1] + return mime[:len(mime)-1] + except: + raise IOError('ExporterError: path does not exist.') + +def get_file_type_desc(path): + """ Return the type of a file given by the 'file' command """ + try: + file_out1, file_out2 = os.popen4('file "'+path+'"') + for line in file_out2.readlines(): + description = line.split(': ') + description = description[1].split(', ') + return description + except: + raise IOError('ExporterError: path does not exist.') + +def iswav(path): + """ Tell if path is a WAV """ + try: + mime = get_file_mime_type(path) + return mime == 'audio/x-wav' + except: + raise IOError('ExporterError: path does not exist.') + +def iswav16(path): + """ Tell if path is a 16 bit WAV """ + try: + file_type_desc = get_file_type_desc(path) + return iswav(path) and '16 bit' in file_type_desc + except: + raise IOError('ExporterError: path does not exist.') + +def get_file_name(path): + """ Return the file name targeted in the path """ + return os.path.split(path)[1] + +def split_file_name(file): + """ Return main file name and its extension """ + try: + return os.path.splitext(file) + except: + raise IOError('ExporterError: path does not exist.') + +def clean_word(word) : + """ Return the word without excessive blank spaces, underscores and + characters causing problem to exporters""" + word = re.sub("^[^\w]+","",word) #trim the beginning + word = re.sub("[^\w]+$","",word) #trim the end + word = re.sub("_+","_",word) #squeeze continuous _ to one _ + word = re.sub("^[^\w]+","",word) #trim the beginning _ + #word = string.replace(word,' ','_') + #word = string.capitalize(word) + dict = '&[];"*:,' + for letter in dict: + word = string.replace(word,letter,'_') + return word + +def recover_par_key(path): + """ Recover a file with par2 key """ + os.system('par2 r "'+path+'"') + +def verify_par_key(path): + """ Verify a par2 key """ + os.system('par2 v "'+path+'.par2"') + + diff --git a/encode/flac.py b/encode/flac.py new file mode 100644 index 0000000..60f2f92 --- /dev/null +++ b/encode/flac.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +import os +import string +import subprocess + +from timeside.export.core import * +from timeside.export.api import IExporter +from mutagen.flac import FLAC +from tempfile import NamedTemporaryFile + +class FlacExporter(ExporterCore): + """Defines methods to export to FLAC""" + + implements(IExporter) + + 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' + + def get_mime_type(self): + return 'application/flac' + + def get_description(self): + return 'FIXME' + + def get_file_info(self): + try: + file1, file2 = os.popen4('metaflac --list "'+self.dest+'"') + info = [] + for line in file2.readlines(): + info.append(clean_word(line[:-1])) + self.info = info + return self.info + except: + raise IOError('ExporterError: 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 exporter 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('ExporterError: 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('ExporterError: 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/encode/mp3.py b/encode/mp3.py new file mode 100644 index 0000000..e75f15f --- /dev/null +++ b/encode/mp3.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Parisson SARL +# Copyright (c) 2006-2007 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +import os +import string +import subprocess + +from timeside.export.core import * +from timeside.export.api import IExporter +#from mutagen.id3 import * + +class Mp3Exporter(ExporterCore): + """Defines methods to export to MP3""" + + implements(IExporter) + + 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): + return 'MP3' + + def get_file_extension(self): + return 'mp3' + + def get_mime_type(self): + return 'audio/mpeg' + + def get_description(self): + return "FIXME" + + def set_cache_dir(self,path): + self.cache_dir = path + + def get_file_info(self): + try: + file_out1, file_out2 = os.popen4('mp3info "'+self.dest+'"') + info = [] + for line in file_out2.readlines(): + info.append(clean_word(line[:-1])) + self.info = info + return self.info + except: + raise IOError('ExporterError: 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('ExporterError: 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('ExporterError: cannot tag "'+tag+'"') + try: + id3.save() + except: + raise IOError('ExporterError: 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/encode/ogg.py b/encode/ogg.py new file mode 100644 index 0000000..270449a --- /dev/null +++ b/encode/ogg.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +import os +import string +import subprocess + +from timeside.export.core import * +from timeside.export.api import IExporter +from mutagen.oggvorbis import OggVorbis + +class OggExporter(ExporterCore): + """Defines methods to export to OGG Vorbis""" + + implements(IExporter) + + 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.dub2args_dict = {'creator': 'artist', + 'relation': 'album' + } + + def get_format(self): + return 'OGG' + + def get_file_extension(self): + return 'ogg' + + def get_mime_type(self): + return 'application/ogg' + + def get_description(self): + return 'FIXME' + + def get_file_info(self): + try: + file_out1, file_out2 = os.popen4('ogginfo "'+self.dest+'"') + info = [] + for line in file_out2.readlines(): + info.append(clean_word(line[:-1])) + self.info = info + return self.info + except: + raise IOError('ExporterError: 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('ExporterError: 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/encode/wav.py b/encode/wav.py new file mode 100644 index 0000000..c98a172 --- /dev/null +++ b/encode/wav.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +import os +import string + +from timeside.export.core import * +from timeside.export.api import IExporter + +class WavExporter(ExporterCore): + """Defines methods to export to WAV""" + + implements(IExporter) + + 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): + return 'WAV' + + def get_file_extension(self): + return 'wav' + + def get_mime_type(self): + return 'audio/x-wav' + + def get_description(self): + return 'FIXME' + + def get_file_info(self): + try: + file1, file2 = os.popen4('wavinfo "'+self.dest+'"') + info = [] + for line in file2.readlines(): + info.append(clean_word(line[:-1])) + self.info = info + return self.info + except: + raise IOError('ExporterError: 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('ExporterError: 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('ExporterError: 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('ExporterError: 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/export/__init__.py b/export/__init__.py deleted file mode 100644 index 06cb43a..0000000 --- a/export/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- - -from timeside.export.api import * -from timeside.export.core import * -from timeside.export.ogg import * -from timeside.export.flac import * -from timeside.export.wav import * -from timeside.export.mp3 import * \ No newline at end of file diff --git a/export/api.py b/export/api.py deleted file mode 100644 index 7ee861c..0000000 --- a/export/api.py +++ /dev/null @@ -1,95 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2007-2009 Parisson -# Copyright (c) 2007 Olivier Guilyardi -# Copyright (c) 2007-2009 Guillaume Pellerin -# -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -from timeside.core import Interface, TimeSideError - -class IExporter(Interface): - """Export 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 - # you'll obviously want to include this extra argument. - - def get_format(): - """Return the export/encoding format as a short string - Example: "MP3", "OGG", "AVI", ... - """ - - def get_description(): - """Return a string describing what this export 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(): - """Return the filename extension corresponding to this export format""" - - def get_mime_type(): - """Return the mime type corresponding to this export format""" - - def set_cache_dir(path): - """Set the directory where cached files should be stored. Does nothing - if the exporter 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): - """Perform the exporting process and return the absolute path - to the resulting file. - - item_id is the media item id that uniquely identifies this audio/video - resource - - source is the audio/video source file absolute path. For audio that - should be a WAV file - - 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 export drivers implement some sort of - cache instead of re-encoding each time process() is called. - - It should be possible to make subsequent calls to process() with - different items, using the same driver instance. - """ - -class ExportProcessError(TimeSideError): - - def __init__(self, message, command, subprocess): - self.message = message - self.command = str(command) - self.subprocess = subprocess - - def __str__(self): - if self.subprocess.stderr != None: - error = self.subprocess.stderr.read() - else: - error = '' - return "%s ; command: %s; error: %s" % (self.message, - self.command, - error) diff --git a/export/core.py b/export/core.py deleted file mode 100644 index 6f83356..0000000 --- a/export/core.py +++ /dev/null @@ -1,277 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 Guillaume Pellerin - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Guillaume Pellerin - -import os -import re -import md5 -import string -import subprocess -import mutagen - -from timeside.export import * -from timeside.core import * -import xml.dom.minidom -import xml.dom.ext - -class ExporterCore(Component): - """Defines the main parts of the exporting 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 - - 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('ExporterError: 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('ExporterError: cannot check the md5 key.') - - def get_file_info(self): - """ Return the list of informations of the dest """ - return self.export.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('ExporterError: 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 export 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 export directory - self.ext = self.get_file_extension() - export_dir = os.path.join(self.cache_dir,self.ext) - - if not os.path.exists(export_dir): - export_dir_split = export_dir.split(os.sep) - path = os.sep + export_dir_split[0] - for _dir in export_dir_split[1:]: - path = os.path.join(path,_dir) - if not os.path.exists(path): - os.mkdir(path) - else: - path = export_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') - - proc = subprocess.Popen(command.encode('utf-8'), - shell = True, - bufsize = buffer_size, - stdin = subprocess.PIPE, - stdout = subprocess.PIPE, - close_fds = True) - - # Core processing - while True: - __chunk = proc.stdout.read(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 - -def get_type(value): - """ Return a String with the type of value """ - types = {bool : 'bool', int : 'int', str : 'str'} - # 'bool' type must be placed *before* 'int' type, otherwise booleans are - # detected as integers - for type in types.keys(): - if isinstance(value, type) : - return types[type] - raise TypeError(str(value) + ' has an unsupported type') - -def get_cast(value, type) : - """ Return value, casted into type """ - if type == 'bool' : - if value == 'True' : - return True - return False - elif type == 'int' : - return int(value) - elif type == 'str' : - return str(value) - raise TypeError(type + ' is an unsupported type') - -def get_file_mime_type(path): - """ Return the mime type of a file """ - try: - file_out1, file_out2 = os.popen4('file -i "'+path+'"') - for line in file_out2.readlines(): - line_split = line.split(': ') - mime = line_split[len(line_split)-1] - return mime[:len(mime)-1] - except: - raise IOError('ExporterError: path does not exist.') - -def get_file_type_desc(path): - """ Return the type of a file given by the 'file' command """ - try: - file_out1, file_out2 = os.popen4('file "'+path+'"') - for line in file_out2.readlines(): - description = line.split(': ') - description = description[1].split(', ') - return description - except: - raise IOError('ExporterError: path does not exist.') - -def iswav(path): - """ Tell if path is a WAV """ - try: - mime = get_file_mime_type(path) - return mime == 'audio/x-wav' - except: - raise IOError('ExporterError: path does not exist.') - -def iswav16(path): - """ Tell if path is a 16 bit WAV """ - try: - file_type_desc = get_file_type_desc(path) - return iswav(path) and '16 bit' in file_type_desc - except: - raise IOError('ExporterError: path does not exist.') - -def get_file_name(path): - """ Return the file name targeted in the path """ - return os.path.split(path)[1] - -def split_file_name(file): - """ Return main file name and its extension """ - try: - return os.path.splitext(file) - except: - raise IOError('ExporterError: path does not exist.') - -def clean_word(word) : - """ Return the word without excessive blank spaces, underscores and - characters causing problem to exporters""" - word = re.sub("^[^\w]+","",word) #trim the beginning - word = re.sub("[^\w]+$","",word) #trim the end - word = re.sub("_+","_",word) #squeeze continuous _ to one _ - word = re.sub("^[^\w]+","",word) #trim the beginning _ - #word = string.replace(word,' ','_') - #word = string.capitalize(word) - dict = '&[];"*:,' - for letter in dict: - word = string.replace(word,letter,'_') - return word - -def recover_par_key(path): - """ Recover a file with par2 key """ - os.system('par2 r "'+path+'"') - -def verify_par_key(path): - """ Verify a par2 key """ - os.system('par2 v "'+path+'.par2"') - - diff --git a/export/flac.py b/export/flac.py deleted file mode 100644 index 60f2f92..0000000 --- a/export/flac.py +++ /dev/null @@ -1,168 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 Guillaume Pellerin - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Guillaume Pellerin - -import os -import string -import subprocess - -from timeside.export.core import * -from timeside.export.api import IExporter -from mutagen.flac import FLAC -from tempfile import NamedTemporaryFile - -class FlacExporter(ExporterCore): - """Defines methods to export to FLAC""" - - implements(IExporter) - - 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' - - def get_mime_type(self): - return 'application/flac' - - def get_description(self): - return 'FIXME' - - def get_file_info(self): - try: - file1, file2 = os.popen4('metaflac --list "'+self.dest+'"') - info = [] - for line in file2.readlines(): - info.append(clean_word(line[:-1])) - self.info = info - return self.info - except: - raise IOError('ExporterError: 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 exporter 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('ExporterError: 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('ExporterError: 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/export/mp3.py b/export/mp3.py deleted file mode 100644 index e75f15f..0000000 --- a/export/mp3.py +++ /dev/null @@ -1,169 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2007 Parisson SARL -# Copyright (c) 2006-2007 Guillaume Pellerin - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Guillaume Pellerin - -import os -import string -import subprocess - -from timeside.export.core import * -from timeside.export.api import IExporter -#from mutagen.id3 import * - -class Mp3Exporter(ExporterCore): - """Defines methods to export to MP3""" - - implements(IExporter) - - 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): - return 'MP3' - - def get_file_extension(self): - return 'mp3' - - def get_mime_type(self): - return 'audio/mpeg' - - def get_description(self): - return "FIXME" - - def set_cache_dir(self,path): - self.cache_dir = path - - def get_file_info(self): - try: - file_out1, file_out2 = os.popen4('mp3info "'+self.dest+'"') - info = [] - for line in file_out2.readlines(): - info.append(clean_word(line[:-1])) - self.info = info - return self.info - except: - raise IOError('ExporterError: 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('ExporterError: 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('ExporterError: cannot tag "'+tag+'"') - try: - id3.save() - except: - raise IOError('ExporterError: 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/export/ogg.py b/export/ogg.py deleted file mode 100644 index 270449a..0000000 --- a/export/ogg.py +++ /dev/null @@ -1,143 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 Guillaume Pellerin - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Guillaume Pellerin - -import os -import string -import subprocess - -from timeside.export.core import * -from timeside.export.api import IExporter -from mutagen.oggvorbis import OggVorbis - -class OggExporter(ExporterCore): - """Defines methods to export to OGG Vorbis""" - - implements(IExporter) - - 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.dub2args_dict = {'creator': 'artist', - 'relation': 'album' - } - - def get_format(self): - return 'OGG' - - def get_file_extension(self): - return 'ogg' - - def get_mime_type(self): - return 'application/ogg' - - def get_description(self): - return 'FIXME' - - def get_file_info(self): - try: - file_out1, file_out2 = os.popen4('ogginfo "'+self.dest+'"') - info = [] - for line in file_out2.readlines(): - info.append(clean_word(line[:-1])) - self.info = info - return self.info - except: - raise IOError('ExporterError: 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('ExporterError: 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/export/wav.py b/export/wav.py deleted file mode 100644 index c98a172..0000000 --- a/export/wav.py +++ /dev/null @@ -1,158 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 Guillaume Pellerin - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Guillaume Pellerin - -import os -import string - -from timeside.export.core import * -from timeside.export.api import IExporter - -class WavExporter(ExporterCore): - """Defines methods to export to WAV""" - - implements(IExporter) - - 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): - return 'WAV' - - def get_file_extension(self): - return 'wav' - - def get_mime_type(self): - return 'audio/x-wav' - - def get_description(self): - return 'FIXME' - - def get_file_info(self): - try: - file1, file2 = os.popen4('wavinfo "'+self.dest+'"') - info = [] - for line in file2.readlines(): - info.append(clean_word(line[:-1])) - self.info = info - return self.info - except: - raise IOError('ExporterError: 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('ExporterError: 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('ExporterError: 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('ExporterError: 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' -