Update of /cvsroot/freevo/kaa/metadata/src
In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv14035/src

Added Files:
        .cvsignore __init__.py factory.py mediainfo.py table.py 
        version.py 
Log Message:
move current mmpython cvs to kaa.metadata

--- NEW FILE: .cvsignore ---
*.pyc *.pyo

--- NEW FILE: version.py ---
# the intention of this file is to make it possible to check
# for the program using this lib if it's an up-to-date version

# "offical" version of kaa-metadata
VERSION = '0.5.0.cvs'

# latest major change (date as integer)
CHANGED = 20040629


--- NEW FILE: mediainfo.py ---
# -*- coding: iso-8859-1 -*-
# -----------------------------------------------------------------------------
# mediainfo.py
# -----------------------------------------------------------------------------
# $Id: mediainfo.py,v 1.1 2005/07/02 16:33:10 dischi Exp $
#
# TODO: move some code from mediainfo to media/core.py
# See image/core.py was example.
#
# -----------------------------------------------------------------------------
# kaa-Metadata - Media Metadata for Python
# Copyright (C) 2003-2005 Thomas Schueppel, Dirk Meyer
#
# First Edition: Thomas Schueppel <[EMAIL PROTECTED]>
# Maintainer:    Dirk Meyer <[EMAIL PROTECTED]>
#
# Please see the file doc/CREDITS for a complete list of authors.
#
# This program 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.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MER-
# CHANTABILITY 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 this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# -----------------------------------------------------------------------------

# python imports
import os
import logging
import copy
import locale

# kaa imports
import table

LOCAL_ENCODING = locale.getpreferredencoding();
if not LOCAL_ENCODING or LOCAL_ENCODING == "ANSI_X3.4-1968":
    LOCAL_ENCODING = 'latin1';

# type definitions
TYPE_NONE      = 0
TYPE_AUDIO     = 1
TYPE_VIDEO     = 2
TYPE_IMAGE     = 4
TYPE_AV        = 5
TYPE_MUSIC     = 6
TYPE_HYPERTEXT = 8
TYPE_MISC      = 10

MEDIACORE = ['title', 'caption', 'comment', 'artist', 'size', 'type',
             'subtype', 'date', 'keywords', 'country', 'language', 'url']

AUDIOCORE = ['channels', 'samplerate', 'length', 'encoder', 'codec',
             'samplebits', 'bitrate', 'language']

VIDEOCORE = ['length', 'encoder', 'bitrate', 'samplerate', 'codec',
             'samplebits', 'width', 'height', 'fps', 'aspect']

MUSICCORE = ['trackno', 'trackof', 'album', 'genre','discs']

AVCORE    = ['length', 'encoder', 'trackno', 'trackof', 'copyright', 'product',
             'genre', 'secondary genre', 'subject', 'writer', 'producer',
             'cinematographer', 'production designer', 'edited by',
             'costume designer', 'music by', 'studio', 'distributed by',
             'rating', 'starring', 'ripped by', 'digitizing date',
             'internet address', 'source form', 'medium', 'source',
             'archival location', 'commisioned by', 'engineer', 'cropped',
             'sharpness', 'dimensions', 'lightness', 'dots per inch',
             'palette setting', 'default audio stream', 'logo url',
             'watermark url', 'info url', 'banner image', 'banner url',
             'infotext', 'delay']


UNPRINTABLE_KEYS = [ 'thumbnail', ]

EXTENSION_DEVICE    = 'device'
EXTENSION_DIRECTORY = 'directory'
EXTENSION_STREAM    = 'stream'

# get logging object
log = logging.getLogger('metadata')


class KaaMetadataParseError:
    pass

class MediaInfo:
    """
    MediaInfo is the base class to all Media Metadata Containers. It defines
    the basic structures that handle metadata. MediaInfo and its derivates
    contain a common set of metadata attributes that is listed in keys.
    Specific derivates contain additional keys to the dublin core set that is
    defined in MediaInfo.
    MediaInfo also contains tables of addional metadata. These tables are maps
    of keys to values. The keys themselves should remain in the format that is
    defined by the metadata (I.E. Hex-Numbers, FOURCC, ...) and will be
    translated to more readable and i18nified values by an external entity.
    """
    def __init__(self):
        self.keys = []
        self._tables = {}
        for k in MEDIACORE:
            setattr(self,k,None)
            self.keys.append(k)
        # get media type by parsing the __class__ information
        media = str(self.__class__)
        media = media[media.find('kaa.metadata.') + 13:]
        self.media = media[:media.find('.')]
        self.keys.append('media')

        
    def __unicode__(self):
        keys = copy.copy(self.keys)

        for k in UNPRINTABLE_KEYS:
            if k in keys:
                keys.remove(k)

        result = reduce( lambda a,b: self[b] and b != u'url' and \
                         u'%s\n        %s: %s' % \
                         (a, unicode(b), unicode(self[b])) or a, keys, u'' )
        if log.level < 30:
            try:
                for i in self._tables.keys():
                    try:
                        result += unicode(self._tables[i])
                    except AttributeError:
                        pass
            except AttributeError:
                pass
        return result


    def appendtable(self, name, hashmap, language='en'):
        """
        Appends a tables of additional metadata to the Object.
        If such a table already exists, the given tables items are
        added to the existing one.
        """
        if not self._tables.has_key((name, language)):
            self._tables[(name, language)] = \
                                table.Table(hashmap, name, language)
        else:
            # Append to the already existing table
            for k in hashmap.keys():
                self._tables[(name, language)][k] = hashmap[k]


    def correct_data(self):
        """
        Correct same data based on specific rules
        """
        # make sure all strings are unicode
        for key in self.keys:
            value = getattr(self, key)
            if isinstance(value, str):
                setattr(self, key, unicode(value, LOCAL_ENCODING, 'replace'))


    def gettable(self, name, language='en'):
        """
        returns a table of the given name and language
        """
        return self._tables.get((name, language), {})


    def setitem(self, item, dict, key, convert_to_str=False):
        """
        set item to a specific value for the dict
        """
        try:
            if self.__dict__.has_key(item):
                if isinstance(dict[key], str):
                    self.__dict__[item] = unicode(dict[key])
                elif convert_to_str:
                    self.__dict__[item] = unicode(dict[key])
                else:
                    self.__dict__[item] = dict[key]
            else:
                _debug("Unknown key: %s" % item)
        except:
            pass


    def __getitem__(self,key):
        """
        get the value of 'key'
        """
        if self.__dict__.has_key(key):
            if isinstance(self.__dict__[key], str):
                return self.__dict__[key].strip().rstrip().replace('\0', '')
            return self.__dict__[key]
        elif hasattr(self, key):
            return getattr(self, key)
        return None


    def __setitem__(self, key, val):
        """
        set the value of 'key' to 'val'
        """
        self.__dict__[key] = val


    def has_key(self, key):
        """
        check if the object has a key 'key'
        """
        return self.__dict__.has_key(key) or hasattr(self, key)


    def __delitem__(self, key):
        """
        delete informations about 'key'
        """
        try:
            del self.__dict__[key]
        except:
            pass
        if hasattr(self, key):
            setattr(self, key, None)


class AudioInfo(MediaInfo):
    """
    Audio Tracks in a Multiplexed Container.
    """
    def __init__(self):
        self.keys = []
        for k in AUDIOCORE:
            setattr(self,k,None)
            self.keys.append(k)


class MusicInfo(AudioInfo):
    """
    Digital Music.
    """
    def __init__(self):
        MediaInfo.__init__(self)
        for k in AUDIOCORE+MUSICCORE:
            setattr(self,k,None)
            self.keys.append(k)


    def correct_data(self):
        """
        correct trackof to be two digest
        """
        AudioInfo.correct_data(self)
        if self['trackof']:
            try:
                # XXX Why is this needed anyway?
                if int(self['trackno']) < 10:
                    self['trackno'] = '0%s' % int(self['trackno'])
            except:
                pass


class VideoInfo(MediaInfo):
    """
    Video Tracks in a Multiplexed Container.
    """
    def __init__(self):
        self.keys = []
        for k in VIDEOCORE:
            setattr(self,k,None)
            self.keys.append(k)


class ChapterInfo(MediaInfo):
    """
    Chapter in a Multiplexed Container.
    """
    def __init__(self, name, pos=0):
        self.keys = ['name', 'pos']
        setattr(self,'name', name)
        setattr(self,'pos', pos)


class AVInfo(MediaInfo):
    """
    Container for Audio and Video streams. This is the Container Type for
    all media, that contain more than one stream.
    """
    def __init__(self):
        MediaInfo.__init__(self)
        for k in AVCORE:
            setattr(self,k,None)
            self.keys.append(k)
        self.audio = []
        self.video = []
        self.subtitles = []
        self.chapters  = []


    def correct_data(self):
        """
        correct length to be an int
        """
        MediaInfo.correct_data(self)
        if not self['length'] and len(self.video) and self.video[0]['length']:
            self['length'] = self.video[0]['length']
        for container in [ self ] + self.video + self.audio:
            if container['length']:
                container['length'] = int(container['length'])


    def find_subtitles(self, filename):
        """
        Search for subtitle files. Right now only VobSub is supported
        """
        base = os.path.splitext(filename)[0]
        if os.path.isfile(base+'.idx') and \
               (os.path.isfile(base+'.sub') or os.path.isfile(base+'.rar')):
            file = open(base+'.idx')
            if file.readline().find('VobSub index file') > 0:
                line = file.readline()
                while (line):
                    if line.find('id') == 0:
                        self.subtitles.append(line[4:6])
                    line = file.readline()
            file.close()


    def __unicode__(self):
        result = u'Attributes:'
        result += MediaInfo.__unicode__(self)
        if len(self.video) + len(self.audio) + len(self.subtitles) > 0:
            result += "\n Stream list:"
            if len(self.video):
                result += reduce( lambda a,b: a + u'  \n   Video Stream:' + \
                                  unicode(b), self.video, u'' )
            if len(self.audio):
                result += reduce( lambda a,b: a + u'  \n   Audio Stream:' + \
                                  unicode(b), self.audio, u'' )
            if len(self.subtitles):
                result += reduce( lambda a,b: a + u'  \n   Subtitle Stream:' +\
                                  unicode(b), self.subtitles, u'' )
        if not isinstance(self.chapters, int) and len(self.chapters) > 0:
            result += u'\n Chapter list:'
            for i in range(len(self.chapters)):
                result += u'\n   %2s: "%s" %s' % \
                          (i+1, unicode(self.chapters[i]['name']),
                           self.chapters[i]['pos'])
        return result


class CollectionInfo(MediaInfo):
    """
    Collection of Digial Media like CD, DVD, Directory, Playlist
    """
    def __init__(self):
        MediaInfo.__init__(self)
        self.tracks = []
        self.keys.append('id')
        self.id = None

    def __unicode__(self):
        result = MediaInfo.__unicode__(self)
        result += u'\nTrack list:'
        for counter in range(0,len(self.tracks)):
             result += u' \nTrack %d:\n%s' % \
                       (counter+1, unicode(self.tracks[counter]))
        return result

    def appendtrack(self, track):
        self.tracks.append(track)

--- NEW FILE: table.py ---
# -*- coding: iso-8859-1 -*-
# -----------------------------------------------------------------------------
# table.py - table handling
# -----------------------------------------------------------------------------
# $Id: table.py,v 1.1 2005/07/02 16:33:10 dischi Exp $
#
# -----------------------------------------------------------------------------
# kaa-Metadata - Media Metadata for Python
# Copyright (C) 2003-2005 Thomas Schueppel, Dirk Meyer
#
# First Edition: Thomas Schueppel <[EMAIL PROTECTED]>
# Maintainer:    Dirk Meyer <[EMAIL PROTECTED]>
#
# Please see the file doc/CREDITS for a complete list of authors.
#
# This program 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.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MER-
# CHANTABILITY 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 this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# -----------------------------------------------------------------------------

from gettext import GNUTranslations
import os

LOCALEDIR = 'i18n'

class Table:
    def __init__(self, hashmap, name, language='en'):
        self.dict = hashmap
        self.name = name
        self.language = language
        self.translations = {}
        self.languages = []
        self.i18ndir = os.path.join(LOCALEDIR, name.lower())
        try:
            self.read_translations()
        except:
            pass

    def read_translations(self):
        for filename in [x for x in os.listdir(self.i18ndir) \
                         if x.endswith('.mo')]:
            lang = filename[:-3]
            filename = os.path.join(self.i18ndir, filename)
            f = open(filename, 'rb')
            self.translations[lang] = GNUTranslations(f)
            f.close()
        self.languages = self.translations.keys()


    def gettext(self, message, language = None):
        try:
            return self.translations[language].gettext(unicode(message))
        except KeyError:
            return unicode(message)


    def __setitem__(self,key,value):
        self.dict[key] = value


    def __getitem__(self,key):
        try:
            return self.dict[key]
        except KeyError:
            return None


    def getstr(self,key):
        s = self[key]
        try:
            if s and len(unicode(s)) < 100:
                return s
            else:
                return "Not Displayable"
        except UnicodeDecodeError:
            return "Not Displayable"


    def has_key(self, key):
        return self.dict.has_key(key)


    def getEntry(self, key, language = 'en'):
        pair = (self.gettext(key), unicode(self.dict[key]))


    def __unicode__(self):
        header = "\nTable %s (%s):" % (self.name, self.language)
        result = reduce( lambda a,b: self[b] and "%s\n        %s: %s" % \
                         (a, self.gettext(b,'en'), self.getstr(b)) or \
                         a, self.dict.keys(), header )
        return result


    def accept(self,mi):
        pass

--- NEW FILE: __init__.py ---
# -*- coding: iso-8859-1 -*-
# -----------------------------------------------------------------------------
# kaa.metadata.__init__.py
# -----------------------------------------------------------------------------
# $Id: __init__.py,v 1.1 2005/07/02 16:33:10 dischi Exp $
#
# -----------------------------------------------------------------------------
# kaa-Metadata - Media Metadata for Python
# Copyright (C) 2003-2005 Thomas Schueppel, Dirk Meyer
#
# First Edition: Thomas Schueppel <[EMAIL PROTECTED]>
# Maintainer:    Dirk Meyer <[EMAIL PROTECTED]>
#
# Please see the file doc/CREDITS for a complete list of authors.
#
# Please see the file freevo/Docs/CREDITS for a complete list of authors.
#
# This program 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.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MER-
# CHANTABILITY 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 this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# -----------------------------------------------------------------------------

# logging support
import logging
logger = logging.getLogger('metadata')
if not logger.level:
    # level not set for kaa.metadata, set it to WARNING
    logger.setLevel(logging.WARNING)

# import factory code for kaa.metadata access
from factory import *

# use network functions
USE_NETWORK     = 1

--- NEW FILE: factory.py ---
# -*- coding: iso-8859-1 -*-
# -----------------------------------------------------------------------------
# factory.py
# -----------------------------------------------------------------------------
# $Id: factory.py,v 1.1 2005/07/02 16:33:10 dischi Exp $
#
# -----------------------------------------------------------------------------
# kaa-Metadata - Media Metadata for Python
# Copyright (C) 2003-2005 Thomas Schueppel, Dirk Meyer
#
# First Edition: Thomas Schueppel <[EMAIL PROTECTED]>
# Maintainer:    Dirk Meyer <[EMAIL PROTECTED]>
#
# Please see the file doc/CREDITS for a complete list of authors.
#
# This program 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.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MER-
# CHANTABILITY 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 this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# -----------------------------------------------------------------------------

__all__ = [ 'Factory', 'register', 'gettype', 'parse' ]

# python imports
import stat
import os
import urlparse
import traceback
import urllib
import logging

# kaa imports
import mediainfo

# get logging object
log = logging.getLogger('metadata')


_factory = None

def Factory():
    """
    Create or return global unique factory object.
    """
    global _factory

    # One-time init
    if _factory == None:
        _factory = _Factory()
        _factory.import_parser()
    return _factory


def register(mimetype, extensions, type, c):
    """
    Register a parser to the factory.
    """
    return Factory().register(mimetype,extensions,type,c)    


def gettype(mimetype, extensions):
    """
    Return parser for mimetype / extensions
    """
    return Factory().get(mimetype,extensions)    
    

def parse(filename):
    """
    parse a file
    """
    return Factory().create(filename)


class _Factory:
    """
    Abstract Factory for the creation of MediaInfo instances. The different
    Methods create MediaInfo objects by parsing the given medium.
    """
    def __init__(self):
        self.extmap = {}
        self.mimemap = {}
        self.types = []
        self.device_types = []
        self.directory_types = []
        self.stream_types = []


    def import_parser(self):
        """
        This functions imports all known parser.
        """
        import mediainfo
        import audio.ogginfo
        import audio.pcminfo
        import audio.m4ainfo
        import audio.ac3info
        import video.riffinfo
        import video.mpeginfo
        import video.asfinfo
        import video.movinfo
        import image.jpginfo
        import image.pnginfo
        import image.tiffinfo
        import image.pilinfo
        import video.vcdinfo
        import video.realinfo
        import video.ogminfo
        import video.mkvinfo
        import misc.xmlinfo
        
        # import some disc modules (may fail)
        try:
            import disc.discinfo
            import disc.vcdinfo
            import disc.audioinfo
        except ImportError:
            pass
        
        # find the best working DVD module
        try:
            import disc.lsdvd
        except ImportError, e:
            if log.level < 30:
                log.error(e)
        
        try:
            import disc.dvdinfo
        except ImportError, e:
            if log.level < 30:
                log.error(e)
        
        # use fallback disc module
        try:
            import disc.datainfo
        except ImportError, e:
            if log.level < 30:
                log.error(e)
        
        import audio.eyed3info
        import audio.webradioinfo
        import audio.flacinfo
        import misc.dirinfo
        
        
    def create_from_file(self, file):
        """
        create based on the file stream 'file
        """
        # Check extension as a hint
        e = os.path.splitext(file.name)[1].lower()
        if e and e.startswith('.') and e[1:] in self.extmap:
            log.debug("trying ext %s" % e[1:])
            file.seek(0,0)
            try:
                return self.extmap[e[1:]][3](file)
            except mediainfo.KaaMetadataParseError:
                pass
            except:
                log.exception('parse error')

        log.info('No Type found by Extension. Trying all')

        for e in self.types:
            log.debug('trying %s' % e[0])
            file.seek(0,0)
            try:
                return e[3](file)
            except:
                pass
        return None


    def create_from_url(self,url):
        """
        Create information for urls. This includes file:// and cd://
        """
        split  = urlparse.urlsplit(url)
        scheme = split[0]

        if scheme == 'file':
            (scheme, location, path, query, fragment) = split
            return self.create_from_filename(location+path)

        elif scheme == 'cdda':
            r = self.create_from_filename(split[4])
            if r:
                r.url = url
            return r

        elif scheme == 'http':
            # Quick Hack for webradio support
            # We will need some more soffisticated and generic construction
            # method for this. Perhaps move file.open stuff into __init__
            # instead of doing it here...
            for e in self.stream_types:
                log.debug('Trying %s' % e[0])
                try:
                    return e[3](url)
                except mediainfo.KaaMetadataParseError:
                    pass
        else:
            (scheme, location, path, query, fragment) = split
            uhandle = urllib.urlopen(url)
            mime = uhandle.info().gettype()
            log.debug("Trying %s" % mime)
            if self.mimemap.has_key(mime):
                try:
                    return self.mimemap[mime][3](file)
                except mediainfo.KaaMetadataParseError:
                    pass
            # XXX Todo: Try other types
        pass


    def create_from_filename(self, filename):
        """
        Create information for the given filename
        """
        if os.path.isdir(filename):
            return None
        if os.path.isfile(filename):
            try:
                f = open(filename,'rb')
            except IOError:
                log.error('IOError reading %s' % filename)
                return None
            r = self.create_from_file(f)
            f.close()
            if r:
                r.correct_data()
                r.url = 'file://%s' % os.path.abspath(filename)
                return r
        return None


    def create_from_device(self,devicename):
        """
        Create information from the device. Currently only rom drives
        are supported.
        """
        for e in self.device_types:
            log.debug('Trying %s' % e[0])
            try:
                t = e[3](devicename)
                t.url = 'file://%s' % os.path.abspath(devicename)
                return t
            except mediainfo.KaaMetadataParseError:
                pass
        return None


    def create_from_directory(self, dirname):
        """
        Create information from the directory.
        """
        for e in self.directory_types:
            log.debug('Trying %s' % e[0])
            try:
                return e[3](dirname)
            except mediainfo.KaaMetadataParseError:
                pass
        return None


    def create(self, name):
        """
        Global 'create' function. This function calls the different
        'create_from_'-functions.
        """
        try:
            if name.find('://') > 0:
                return self.create_from_url(name)
            if not os.path.exists(name):
                return None
            try:
                if (os.uname()[0] == 'FreeBSD' and \
                    stat.S_ISCHR(os.stat(name)[stat.ST_MODE])) \
                    or stat.S_ISBLK(os.stat(name)[stat.ST_MODE]):
                    return self.create_from_device(name)
            except AttributeError:
                pass
            if os.path.isdir(name):
                return self.create_from_directory(name)
            return self.create_from_filename(name)
        except:
            log.exception('kaa.metadata.create error')
            log.warning('Please report this bug to the Freevo mailing list')
            return None



    def register(self,mimetype,extensions,type,c):
        """
        register the parser to kaa.metadata
        """
        log.debug('%s registered' % mimetype)
        tuple = (mimetype,extensions,type,c)

        if extensions == mediainfo.EXTENSION_DEVICE:
            self.device_types.append(tuple)
        elif extensions == mediainfo.EXTENSION_DIRECTORY:
            self.directory_types.append(tuple)
        elif extensions == mediainfo.EXTENSION_STREAM:
            self.stream_types.append(tuple)
        else:
            self.types.append(tuple)
            for e in extensions:
                self.extmap[e.lower()] = tuple
            self.mimemap[mimetype] = tuple


    def get(self, mimetype, extensions):
        """
        return the object for mimetype/extensions or None
        """
        if extensions == mediainfo.EXTENSION_DEVICE:
            l = self.device_types
        elif extensions == mediainfo.EXTENSION_DIRECTORY:
            l = self.directory_types
        elif extensions == mediainfo.EXTENSION_STREAM:
            l = self.stream_types
        else:
            l = self.types

        for info in l:
            if info[0] == mimetype and info[1] == extensions:
                return info[3]

        return None



-------------------------------------------------------
SF.Net email is sponsored by: Discover Easy Linux Migration Strategies
from IBM. Find simple to follow Roadmaps, straightforward articles,
informative Webcasts and more! Get everything you need to get up to
speed, fast. http://ads.osdn.com/?ad_id=7477&alloc_id=16492&op=click
_______________________________________________
Freevo-cvslog mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/freevo-cvslog

Reply via email to