From: yomguy Date: Fri, 1 May 2009 15:24:54 +0000 (+0000) Subject: add metadata management X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=1af046b0d18eab33fa67a3dcdde32d689ce570fd;p=telemaster.git add metadata management git-svn-id: http://svn.parisson.org/svn/telemaster/trunk@7 353fd7da-fb10-4236-9bec-1a49139083f2 --- diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000..3970657 --- /dev/null +++ b/tools/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +from mp3 import * +from ogg import * +from flac import * +from tools import * diff --git a/tools/flac.py b/tools/flac.py new file mode 100644 index 0000000..8d2ec89 --- /dev/null +++ b/tools/flac.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This software is a computer program whose purpose is to backup, analyse, +# transcode and stream any audio content with its metadata over a web frontend. + +# This software is governed by the CeCILL license under French law and +# abiding by the rules of distribution of free software. You can use, +# modify and/ or redistribute the software under the terms of the CeCILL +# license as circulated by CEA, CNRS and INRIA at the following URL +# "http://www.cecill.info". + +# As a counterpart to the access to the source code and rights to copy, +# modify and redistribute granted by the license, users are provided only +# with a limited warranty and the software's author, the holder of the +# economic rights, and the successive licensors have only limited +# liability. + +# In this respect, the user's attention is drawn to the risks associated +# with loading, using, modifying and/or developing or reproducing the +# software by the user in light of its specific status of free software, +# that may mean that it is complicated to manipulate, and that also +# therefore means that it is reserved for developers and experienced +# professionals having in-depth computer knowledge. Users are therefore +# encouraged to load and test the software's suitability as regards their +# requirements in conditions enabling the security of their systems and/or +# data to be ensured and, more generally, to use and operate it in the +# same conditions as regards security. + +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. + +# Author: Guillaume Pellerin + +import os +import string +import subprocess +from mutagen.flac import FLAC +from tempfile import NamedTemporaryFile + +class Flac: + """Defines methods to export to FLAC""" + + def __init__(self, media): + self.media = media + 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): + media = FLAC(self.media) + for tag in self.metadata.keys(): + if tag == 'COMMENT': + media['DESCRIPTION'] = unicode(self.metadata[tag]) + else: + media[tag] = unicode(self.metadata[tag]) + try: + media.save() + except: + raise IOError('ExporterError: cannot write tags.') + + def get_tags(self): + metadata = {} + audio = FLAC(self.media) + if audio.has_key('title'): + metadata['title'] = audio['title'][0] + if audio.has_key('artist'): + metadata['artist'] = audio['artist'][0] + if audio.has_key('album'): + metadata['album'] = audio['album'][0] + if audio.has_key('year'): + metadata['year'] = audio['year'][0] + if audio.has_key('comment'): + metadata['comment'] = audio['comment'][0] + if audio.has_key('genre'): + metadata['genre'] = audio['genre'][0] + if audio.has_key('tracktotal'): + metadata['tracktot'] = int(audio['tracktotal'][0]) + else: + metadata['tracktot'] = None + if audio.has_key('tracknumber'): + metadata['trackn'] = int(audio['tracknumber'][0]) + if metadata['tracktot'] != None: + metadata['track_str'] = "%s / %s" % (metadata['trackn'], metadata['tracktot']) + else: + metadata['track_str'] = str(metadata['trackn']) + if audio.has_key('mcn'): + metadata['MCN'] = audio['mcn'][0] + if audio.has_key('isrc'): + metadata['ISRC'] = audio['isrc'][0] + + print self.media + print "Sample rate: ", audio.info.sample_rate + print "Nr. of channels: ", audio.info.channels + print "Bits per sample: ", audio.info.bits_per_sample + print "Total nr. of samples:", audio.info.total_samples + print "Length in secs: ", audio.info.length + print "MD5: ", audio.info.md5_signature + + return metadata + + 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/tools/mp3.py b/tools/mp3.py new file mode 100644 index 0000000..ca47c79 --- /dev/null +++ b/tools/mp3.py @@ -0,0 +1,148 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright Guillaume Pellerin (2006-2009) + +# + +# This software is a computer program whose purpose is to stream audio +# and video data through icecast2 servers. + +# This software is governed by the CeCILL license under French law and +# abiding by the rules of distribution of free software. You can use, +# modify and/ or redistribute the software under the terms of the CeCILL +# license as circulated by CEA, CNRS and INRIA at the following URL +# "http://www.cecill.info". + +# As a counterpart to the access to the source code and rights to copy, +# modify and redistribute granted by the license, users are provided only +# with a limited warranty and the software's author, the holder of the +# economic rights, and the successive licensors have only limited +# liability. + +# In this respect, the user's attention is drawn to the risks associated +# with loading, using, modifying and/or developing or reproducing the +# software by the user in light of its specific status of free software, +# that may mean that it is complicated to manipulate, and that also +# therefore means that it is reserved for developers and experienced +# professionals having in-depth computer knowledge. Users are therefore +# encouraged to load and test the software's suitability as regards their +# requirements in conditions enabling the security of their systems and/or +# data to be ensured and, more generally, to use and operate it in the +# same conditions as regards security. + +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. + +# Author: Guillaume Pellerin + +import os +import string +from mutagen.easyid3 import EasyID3 +from mutagen.mp3 import MP3 +from tools import * + +EasyID3.valid_keys["comment"]="COMM::'XXX'" +EasyID3.valid_keys["copyright"]="TCOP::'XXX'" + +class Mp3: + """A MP3 file object""" + + def __init__(self, media): + self.media = media + self.item_id = '' + self.source = self.media + self.options = {} + self.bitrate_default = '192' + self.cache_dir = os.sep + 'tmp' + self.keys2id3 = {'title': 'TIT2', + 'artist': 'TPE1', + 'album': 'TALB', + 'date': 'TDRC', + 'comment': 'COMM', + 'genre': 'TCON', + 'copyright': 'TCOP', + } + self.metadata = self.get_file_metadata() + self.description = self.get_description() + self.mime_type = self.get_mime_type() + self.media_info = get_file_info(self.media) + self.file_name = self.media_info[0] + self.file_title = self.media_info[1] + self.file_ext = self.media_info[2] + self.extension = self.get_file_extension() + self.size = os.path.getsize(media) + #self.args = self.get_args() + + 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 "MPEG audio Layer III" + + def get_file_metadata(self): + m = MP3(self.media, ID3=EasyID3) + metadata = {} + for key in self.keys2id3.keys(): + try: + metadata[key] = m[key][0] + except: + metadata[key] = '' + return metadata + + def decode(self): + try: + os.system('sox "'+self.media+'" -s -q -r 44100 -t wav "' \ + +self.cache_dir+os.sep+self.item_id+'"') + return self.cache_dir+os.sep+self.metadata['title']+'.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""" + id3 = id3.ID3(self.media) + 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.keys(): + if tag in self.dub2args_dict.keys(): + arg = self.dub2args_dict[tag] + value = self.metadata[tag] + args.append('--' + arg) + args.append('"' + value + '"') + + return args diff --git a/tools/ogg.py b/tools/ogg.py new file mode 100644 index 0000000..19ddbe3 --- /dev/null +++ b/tools/ogg.py @@ -0,0 +1,146 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright Guillaume Pellerin (2006-2009) + +# + +# This software is a computer program whose purpose is to stream audio +# and video data through icecast2 servers. + +# This software is governed by the CeCILL license under French law and +# abiding by the rules of distribution of free software. You can use, +# modify and/ or redistribute the software under the terms of the CeCILL +# license as circulated by CEA, CNRS and INRIA at the following URL +# "http://www.cecill.info". + +# As a counterpart to the access to the source code and rights to copy, +# modify and redistribute granted by the license, users are provided only +# with a limited warranty and the software's author, the holder of the +# economic rights, and the successive licensors have only limited +# liability. + +# In this respect, the user's attention is drawn to the risks associated +# with loading, using, modifying and/or developing or reproducing the +# software by the user in light of its specific status of free software, +# that may mean that it is complicated to manipulate, and that also +# therefore means that it is reserved for developers and experienced +# professionals having in-depth computer knowledge. Users are therefore +# encouraged to load and test the software's suitability as regards their +# requirements in conditions enabling the security of their systems and/or +# data to be ensured and, more generally, to use and operate it in the +# same conditions as regards security. + +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. + +# Author: Guillaume Pellerin + +import os +import string +from mutagen.oggvorbis import OggVorbis +from tools import * + + +class Ogg: + """An OGG file object""" + + def __init__(self, media): + self.media = media + self.media_obj = OggVorbis(self.media) + self.item_id = '' + self.source = self.media + self.options = {} + self.bitrate_default = '192' + self.cache_dir = os.sep + 'tmp' + self.keys2ogg = {'title': 'title', + 'artist': 'artist', + 'album': 'album', + 'date': 'date', + 'comment': 'comment', + 'genre': 'genre', + } + self.metadata = self.get_file_metadata() + self.description = self.get_description() + self.mime_type = self.get_mime_type() + self.media_info = get_file_info(self.media) + self.file_name = self.media_info[0] + self.file_title = self.media_info[1] + self.file_ext = self.media_info[2] + self.extension = self.get_file_extension() + self.size = os.path.getsize(media) + #self.args = self.get_args() + + 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 get_file_metadata(self): + metadata = {} + for key in self.keys2ogg.keys(): + try: + text = self.media_obj[key][0].decode() + metadata[key] = text + except: + metadata[key] = '' + return metadata + + 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): + for tag in self.metadata.keys(): + self.media_obj[tag] = str(self.metadata[tag]) + media_obj.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.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 diff --git a/tools/tools.py b/tools/tools.py new file mode 100644 index 0000000..8fb93cd --- /dev/null +++ b/tools/tools.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright Guillaume Pellerin (2006-2009) + +# + +# This software is a computer program whose purpose is to stream audio +# and video data through icecast2 servers. + +# This software is governed by the CeCILL license under French law and +# abiding by the rules of distribution of free software. You can use, +# modify and/ or redistribute the software under the terms of the CeCILL +# license as circulated by CEA, CNRS and INRIA at the following URL +# "http://www.cecill.info". + +# As a counterpart to the access to the source code and rights to copy, +# modify and redistribute granted by the license, users are provided only +# with a limited warranty and the software's author, the holder of the +# economic rights, and the successive licensors have only limited +# liability. + +# In this respect, the user's attention is drawn to the risks associated +# with loading, using, modifying and/or developing or reproducing the +# software by the user in light of its specific status of free software, +# that may mean that it is complicated to manipulate, and that also +# therefore means that it is reserved for developers and experienced +# professionals having in-depth computer knowledge. Users are therefore +# encouraged to load and test the software's suitability as regards their +# requirements in conditions enabling the security of their systems and/or +# data to be ensured and, more generally, to use and operate it in the +# same conditions as regards security. + +# The fact that you are presently reading this means that you have had +# knowledge of the CeCILL license and that you accept its terms. + +# Author: Guillaume Pellerin + +import os + +def get_file_info(media): + file_name = media.split(os.sep)[-1] + file_title = file_name.split('.')[:-1] + file_title = '.'.join(file_title) + file_ext = file_name.split('.')[-1] + return file_name, file_title, file_ext