]> git.parisson.com Git - telemaster.git/commitdiff
add metadata management
authoryomguy <yomguy@353fd7da-fb10-4236-9bec-1a49139083f2>
Fri, 1 May 2009 15:24:54 +0000 (15:24 +0000)
committeryomguy <yomguy@353fd7da-fb10-4236-9bec-1a49139083f2>
Fri, 1 May 2009 15:24:54 +0000 (15:24 +0000)
git-svn-id: http://svn.parisson.org/svn/telemaster/trunk@7 353fd7da-fb10-4236-9bec-1a49139083f2

tools/__init__.py [new file with mode: 0644]
tools/flac.py [new file with mode: 0644]
tools/mp3.py [new file with mode: 0644]
tools/ogg.py [new file with mode: 0644]
tools/tools.py [new file with mode: 0644]

diff --git a/tools/__init__.py b/tools/__init__.py
new file mode 100644 (file)
index 0000000..3970657
--- /dev/null
@@ -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 (file)
index 0000000..8d2ec89
--- /dev/null
@@ -0,0 +1,216 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2007-2009 Guillaume Pellerin <yomguy@parisson.com>
+
+# 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 <pellerin@parisson.com>
+
+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 (file)
index 0000000..ca47c79
--- /dev/null
@@ -0,0 +1,148 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright Guillaume Pellerin (2006-2009)
+
+# <yomguy@parisson.com>
+
+# 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 <yomguy@parisson.com>
+
+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 (file)
index 0000000..19ddbe3
--- /dev/null
@@ -0,0 +1,146 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright Guillaume Pellerin (2006-2009)
+
+# <yomguy@parisson.com>
+
+# 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 <yomguy@parisson.com>
+
+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 (file)
index 0000000..8fb93cd
--- /dev/null
@@ -0,0 +1,46 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright Guillaume Pellerin (2006-2009)
+
+# <yomguy@parisson.com>
+
+# 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 <yomguy@parisson.com>
+
+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