Source code for pytomo.kaa_metadata.audio.m4a

# -*- coding: iso-8859-1 -*-
# -----------------------------------------------------------------------------
# m4a.py - m4a file parser
# -----------------------------------------------------------------------------
# $Id: m4a.py 3996 2009-05-16 14:12:36Z tack $
#
# -----------------------------------------------------------------------------
# kaa-Metadata - Media Metadata for Python
# Copyright (C) 2003-2006 Thomas Schueppel, Dirk Meyer
#
# First Edition: Aubin Paul <aubin@outlyer.org>
# Maintainer:    Dirk Meyer <dischi@freevo.org>
#
# Please see the file AUTHORS for a complete list of authors.
#
# Based on a sample implementation posted to daap-dev mailing list by
# Bob Ippolito <bob@redivi.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 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 __future__ import absolute_import

__all__ = ['Parser']

# python imports
import logging

# import kaa_metadata.audio core
from . import core

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

import struct

FLAGS= CONTAINER, SKIPPER, TAGITEM, IGNORE= [2**_ for _ in xrange(4)]

# CONTAINER: datum contains other boxes
# SKIPPER: ignore first 4 bytes of datum
# TAGITEM: "official" tag item
CALLBACK= TAGITEM
FLAGS.append(CALLBACK)

TAGTYPES= (
    ('ftyp', TAGITEM),
    ('mvhd', 0),
    ('moov', CONTAINER),
    ('mdat', 0),
    ('udta', CONTAINER),
    ('meta', CONTAINER|SKIPPER),
    ('ilst', CONTAINER),
    ('\xa9ART', TAGITEM),
    ('\xa9nam', TAGITEM),
    ('\xa9too', TAGITEM),
    ('\xa9alb', TAGITEM),
    ('\xa9day', TAGITEM),
    ('\xa9gen', TAGITEM),
    ('\xa9wrt', TAGITEM),
    ('\xa9cmt', TAGITEM),
    ('trkn', TAGITEM),
    ('trak', CONTAINER),
    ('mdia', CONTAINER),
    ('mdhd', TAGITEM),
    ('minf', CONTAINER),
    ('dinf', CONTAINER),
    ('stbl', CONTAINER),
)

flagged= {}
for flag in FLAGS:
    flagged[flag]= frozenset(_[0] for _ in TAGTYPES if _[1] & flag)

def _analyse(fp, offset0, offset1):
    "Walk the atom tree in a mp4 file"
    offset= offset0
    while offset < offset1:
        fp.seek(offset)
        atomsize= struct.unpack("!i", fp.read(4))[0]
        atomtype= fp.read(4)
        if atomsize < 9:
            # This logic is not likely correct, but at least avoids
            # an exception from fp.read() below.
            break
        if atomtype in flagged[CONTAINER]:
            data= ''
            for reply in _analyse(fp, offset+(atomtype in flagged[SKIPPER] and 12 or 8),
                offset+atomsize):
                yield reply
        else:
            fp.seek(offset+8)
            if atomtype in flagged[TAGITEM]:
                data=fp.read(atomsize-8)
            else:
                data= fp.read(min(atomsize-8, 32))
            #print `atomtype`, `data`
        if not atomtype in flagged[IGNORE]: yield atomtype, atomsize, data
        offset+= atomsize

def mp4_atoms(fp):
    fp.seek(0,2)
    size=fp.tell()
    for atom in _analyse(fp, 0, size):
        yield atom

class M4ATags(dict):
    "An class reading .m4a tags"
    convtag= {
        'ftyp': 'FileType',
        'trkn': 'Track',
        'length': 'Length',
        'samplerate': 'SampleRate',
        '\xa9ART': 'Artist',
        '\xa9nam': 'Title',
        '\xa9alb': 'Album',
        '\xa9day': 'Year',
        '\xa9gen': 'Genre',
        '\xa9cmt': 'Comment',
        '\xa9wrt': 'Writer',
        '\xa9too': 'Tool',
    }
    def __init__(self, fp):
        super(dict, self).__init__()
        self['FileType'] = 'unknown'
        fp.seek(0,0)
        try:
            size= struct.unpack("!i", fp.read(4))[0]
        except struct.error:
            # EOF.
            return
        type= fp.read(4)
        #check for ftyp identification
        if type == 'ftyp':
            for atomtype, atomsize, atomdata in mp4_atoms(fp):
                self.atom2tag(atomtype, atomdata)

    def atom2tag(self, atomtype, atomdata):
        "Insert items using descriptive key instead of atomtype"
        if atomtype.find('\xa9',0,4) != -1:
            key= self.convtag[atomtype]
            self[key]= atomdata[16:].decode("utf-8")
        elif atomtype == 'mdhd':
            if ord(atomdata[0]) == 1:
                #if version is 1 then date and duration values are 8 bytes in length
                timescale= struct.unpack("!Q",atomdata[20:24])[0]
                duration= struct.unpack("!Q",atomdata[24:30])[0]
            else:
                timescale= struct.unpack("!i",atomdata[12:16])[0]
                duration= struct.unpack("!i",atomdata[16:20])[0]
            self[self.convtag['length']]= duration/timescale
            self[self.convtag['samplerate']]= timescale
        elif atomtype == 'trkn':
            self[self.convtag[atomtype]]= struct.unpack("!i",atomdata[16:20])[0]
        elif atomtype == 'ftyp':
            self[self.convtag[atomtype]]= atomdata[8:12].decode("utf-8")


class Mpeg4Audio(core.Music):

    def __init__(self, file):
        core.Music.__init__(self)
        self.valid = 0
        returnval = 0
        tags = M4ATags(file)
        if tags['FileType'] == 'M4A ':
            try:
                self.title = tags['Title']
                self.artist = tags['Artist']
                self.album = tags['Album']
                self.trackno = tags['Track']
                self.year = tags['Year']
                self.encoder = tags['Tool']
                self.length = tags['Length']
                self.samplerate = tags['SampleRate']
                self.valid = 1
                self.mime = 'audio/mp4'
                self.filename = file.name
            except ValueError:
                returnval = 1
        if not self.valid:
            raise core.ParseError()

Parser = Mpeg4Audio