Source code for eyed3.core

# -*- coding: utf-8 -*-
################################################################################
#  Copyright (C) 2012  Travis Shirk <travis@pobox.com>
#
#  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
#  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 this program; if not, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
################################################################################
'''Basic core types and utilities.'''
import os, time
from . import Exception, LOCAL_FS_ENCODING
from .utils import guessMimetype

import logging
log = logging.getLogger(__name__)

AUDIO_NONE = 0
'''Audio type selecter for no audio.'''
AUDIO_MP3 =  1
'''Audio type selecter for mpeg (mp3) audio.'''

AUDIO_TYPES = (AUDIO_NONE, AUDIO_MP3)


[docs]def load(path, tag_version=None): '''Loads the file identified by ``path`` and returns a concrete type of :class:`eyed3.core.AudioFile`. If ``path`` is not a file an ``IOError`` is raised. ``None`` is returned when the file type (i.e. mime-type) is not recognized. The following AudioFile types are supported: * :class:`eyed3.mp3.Mp3AudioFile` - For mp3 audio files. * :class:`eyed3.id3.TagFile` - For raw ID3 data files. If ``tag_version`` is not None (the default) only a specific version of metadata is loaded. This value must be a version constant specific to the eventual format of the metadata. ''' from . import mp3, id3 log.info("Loading file: %s" % path) if os.path.exists(path): if not os.path.isfile(path): raise IOError("not a file: %s" % path) else: raise IOError("file not found: %s" % path) mtype = guessMimetype(path) if mtype in mp3.MIME_TYPES: return mp3.Mp3AudioFile(path, tag_version) elif mtype == "application/x-id3": return id3.TagFile(path, tag_version) else: return None
[docs]class AudioInfo(object): '''A base container for common audio details.''' time_secs = 0 '''The number of seconds of audio data (i.e., the playtime)''' size_bytes = 0 '''The number of bytes of audio data.'''
[docs]class Tag(object): '''An abstract interface for audio tag (meta) data (e.g. artist, title, etc.)''' def _setArtist(self, val): raise NotImplementedError def _getArtist(self): raise NotImplementedError def _setAlbum(self, val): raise NotImplementedError def _getAlbum(self): raise NotImplementedError def _setTitle(self, val): raise NotImplementedError def _getTitle(self): raise NotImplementedError def _setTrackNum(self, val): raise NotImplementedError def _getTrackNum(self): raise NotImplementedError @property def artist(self): return self._getArtist() @artist.setter
[docs] def artist(self, v): self._setArtist(v)
@property def album(self): return self._getAlbum() @album.setter
[docs] def album(self, v): self._setAlbum(v)
@property def title(self): return self._getTitle() @title.setter
[docs] def title(self, v): self._setTitle(v)
@property def track_num(self): '''Track number property. Must return a 2-tuple of (track-number, total-number-of-tracks). Either tuple value may be ``None``. ''' return self._getTrackNum() @track_num.setter
[docs] def track_num(self, v): self._setTrackNum(v)
[docs]class AudioFile(object): '''Abstract base class for audio file types (AudioInfo + Tag)''' def _read(self): '''Subclasses MUST override this method and set ``self._info``, ``self._tag`` and ``self.type``. ''' raise NotImplementedError()
[docs] def rename(self, name, fsencoding=LOCAL_FS_ENCODING): '''Rename the file to ``name``. The encoding used for the file name is :attr:`eyed3.LOCAL_FS_ENCODING` unless overridden by ``fsencoding``. Note, if the target file already exists, or the full path contains non-existent directories the operation will fail with :class:`eyed3.Exception`.''' import os base = os.path.basename(self.path) base_ext = os.path.splitext(base)[1] dir = os.path.dirname(self.path) if not dir: dir = '.' new_name = "%s%s" % (os.path.join(dir.encode(fsencoding), name.encode(fsencoding)), base_ext) if os.path.exists(new_name): raise Exception("File '%s' exists, will not overwrite" % new_name) elif not os.path.exists(os.path.dirname(new_name)): raise Exception("Target directory '%s' does not exists, will not " "create" % os.path.dirname(new_name)) os.rename(self.path, new_name) self.path = new_name
@property def path(self): '''The absolute path of this file.''' return self._path @path.setter
[docs] def path(self, t): '''Set the path''' from os.path import abspath, realpath, normpath self._path = normpath(realpath(abspath(t)))
@property
[docs] def info(self): '''Returns a concrete implemenation of :class:`eyed3.core.AudioFile`''' return self._info
@property def tag(self): '''Returns a concrete implemenation of :class:`eyed3.core.Tag`''' return self._tag @tag.setter
[docs] def tag(self, t): self._tag = t
def __init__(self, path): '''Construct with a path and invoke ``_read``. All other members are set to None.''' self.path = path self.type = None self._info = None self._tag = None self._read()
[docs]class Date(object): TIME_STAMP_FORMATS = ["%Y", "%Y-%m", "%Y-%m-%d", "%Y-%m-%dT%H", "%Y-%m-%dT%H:%M", "%Y-%m-%dT%H:%M:%S", # The following end with 'Z' signally time is UTC "%Y-%m-%dT%HZ", "%Y-%m-%dT%H:%MZ", "%Y-%m-%dT%H:%M:%SZ", # The following are wrong per the specs, but ... "%Y-%m-%d %H:%M:%S", "%Y-00-00", ] '''Valid time stamp formats per ISO 8601 and used by \c strptime.''' def __init__(self, year, month=None, day=None, hour=None, minute=None, second=None): # Validate with datetime from datetime import datetime _ = datetime(year, month if month is not None else 1, day if day is not None else 1, hour if hour is not None else 0, minute if minute is not None else 0, second if second is not None else 0) self._year = year self._month = month self._day = day self._hour = hour self._minute = minute self._second = second # Python's date classes do a lot more date validation than does not # need to be duplicated here. Validate it _ = Date._validateFormat(str(self)) @property
[docs] def year(self): return self._year
@property
[docs] def month(self): return self._month
@property
[docs] def day(self): return self._day
@property
[docs] def hour(self): return self._hour
@property
[docs] def minute(self): return self._minute
@property
[docs] def second(self): return self._second
def __eq__(self, rhs): return (self.year == rhs.year and self.month == rhs.month and self.day == rhs.day and self.hour == rhs.hour and self.minute == rhs.minute and self.second == rhs.second) @staticmethod def _validateFormat(s): pdate = None for fmt in Date.TIME_STAMP_FORMATS: try: pdate = time.strptime(s, fmt) break except ValueError: # date string did not match format. continue if pdate is None: raise ValueError("Invalid date string: %s" % s) assert(pdate) return pdate, fmt @staticmethod
[docs] def parse(s): s = s.strip('\x00') pdate, fmt = Date._validateFormat(s) # Here is the difference with Python date/datetime objects, some # of the members can be None kwargs = {} if "%m" in fmt: kwargs["month"] = pdate.tm_mon if "%d" in fmt: kwargs["day"] = pdate.tm_mday if "%H" in fmt: kwargs["hour"] = pdate.tm_hour if "%M" in fmt: kwargs["minute"] = pdate.tm_min if "%S" in fmt: kwargs["second"] = pdate.tm_sec return Date(pdate.tm_year, **kwargs)
def __str__(self): s = "%d" % self.year if self.month: s += "-%s" % str(self.month).rjust(2, '0') if self.day: s += "-%s" % str(self.day).rjust(2, '0') if self.hour is not None: s += "T%s" % str(self.hour).rjust(2, '0') if self.minute is not None: s += ":%s" % str(self.minute).rjust(2, '0') if self.second is not None: s += ":%s" % str(self.second).rjust(2, '0') return s def __unicode__(self): return unicode(str(self), "latin1")
[docs]def parseError(ex): log.warning(ex)