Browse Source

improve the DIDLLite to match ConectentDirectory v2.. Most of the

optional items are available at the Object level now... so we can
make these a lot more generic...  not so much specific code anymore..
(though some clients, such as MediaControler, will only use these
elements if ContentDirectory v1 listed them, such as
originalTrackNumber on a MusicTrack)...

add cdrtoc.py that parses toc files that include title and other
information..

add an attribute to flac that has the tags...  This makes it
more generic...

use the new toc parser and the tags element to set the title on
tracks...

also set the original track number..

[git-p4: depot-paths = "//depot/": change = 1577]
main
John-Mark Gurney 14 years ago
parent
commit
669aed0f46
4 changed files with 276 additions and 142 deletions
  1. +103
    -127
      DIDLLite.py
  2. +36
    -15
      audioraw.py
  3. +134
    -0
      cdrtoc.py
  4. +3
    -0
      flac.py

+ 103
- 127
DIDLLite.py View File

@@ -109,9 +109,79 @@ class Object(object):
"""The root class of the entire content directory class heirachy."""

klass = 'object'
creator = None

_optionattrs = {
'creator': 'dc',
'writeStatus': 'upnp',
'artist': 'upnp',
'actor': 'upnp',
'author': 'upnp',
'producer': 'upnp',
'director': 'upnp',
'publisher': 'dc',
'contributor': 'dc',
'genre': 'upnp',
'album': 'upnp',
'playlist': 'upnp',
'albumArtURI': 'upnp',
'artistDiscographyURI': 'upnp',
'lyricsURI': 'upnp',
'relation': 'dc',
'storageMedium': 'upnp',
'description': 'dc',
'longDescription': 'upnp',
'icon': 'upnp',
'region': 'upnp',
'rights': 'dc',
'date': 'dc',
'language': 'dc',
'playbackCount': 'upnp',
'lastPlaybackTime': 'upnp',
'lastPlaybackPosition': 'upnp',
'recordedStartDateTime': 'upnp',
'recordedDuration': 'upnp',
'recordedDayOfWeek': 'upnp',
'srsRecordScheduleID': 'upnp',
'srsRecordTaskID': 'upnp',
'recordable': 'upnp',
'programTitle': 'upnp',
'seriesTitle': 'upnp',
'programID': 'upnp',
'seriesID': 'upnp',
'channelID': 'upnp',
'episodeCount': 'upnp',
'episodeNumber': 'upnp',
'programCode': 'upnp',
'rating': 'upnp',
'channelGroupName': 'upnp',
'callSign': 'upnp',
'networkAffiliation': 'upnp',
'serviceProvider': 'upnp',
'price': 'upnp',
'payPerView': 'upnp',
'epgProviderName': 'upnp',
'dateTimeRange': 'upnp',
'radioCallSign': 'upnp',
'radioStationID': 'upnp',
'radioBand': 'upnp',
'channelNr': 'upnp',
'channelName': 'upnp',
'scheduledStartTime': 'upnp',
'scheduledEndTime': 'upnp',
'signalStrength': 'upnp',
'signalLocked': 'upnp',
'tuned': 'upnp',
'neverPlayable': 'upnp',
'bookmarkID': 'upnp',
'bookmarkedObjectID': 'upnp',
'deviceUDN': 'upnp',
'stateVariableCollection': 'upnp',
'DVDRegionCode': 'upnp',
'originalTrackNumber': 'upnp',
'toc': 'upnp',
'userAnnoation': 'upnp',
}
res = None
writeStatus = None
content = property(lambda x: x._content)
needupdate = None # do we update before sending? (for res)

@@ -130,7 +200,12 @@ class Object(object):
self.restricted = '0'

if kwargs.has_key('content'):
self._content = kwargs['content']
self._content = kwargs.pop('content')

for i in kwargs:
if i not in self._optionattrs:
raise TypeError('invalid keyword arg: %s' % `i`)
setattr(self, i, kwargs[i])

def __cmp__(self, other):
if not isinstance(other, self.__class__):
@@ -160,8 +235,12 @@ class Object(object):

root.attrib['restricted'] = self.restricted

if self.creator is not None:
SubElement(root, 'dc:creator').text = self.creator
for i in (x for x in self.__dict__ if x in self._optionattrs):
obj = getattr(self, i)
if obj is None:
continue
SubElement(root, '%s:%s' % (self._optionattrs[i],
i)).text = unicode(getattr(self, i))

if self.res is not None:
try:
@@ -171,9 +250,6 @@ class Object(object):
for res in resiter:
root.append(res.toElement())

if self.writeStatus is not None:
SubElement(root, 'upnp:writeStatus').text = self.writeStatus

return root

def toString(self):
@@ -213,83 +289,11 @@ class AudioItem(Item):

klass = Item.klass + '.audioItem'

genre = None
description = None
longDescription = None
publisher = None
language = None
relation = None
rights = None

def toElement(self):

root = Item.toElement(self)

if self.genre is not None:
SubElement(root, 'upnp:genre').text = self.genre

if self.description is not None:
SubElement(root, 'dc:description').text = self.description

if self.longDescription is not None:
SubElement(root, 'upnp:longDescription').text = \
self.longDescription

if self.publisher is not None:
SubElement(root, 'dc:publisher').text = self.publisher

if self.language is not None:
SubElement(root, 'dc:language').text = self.language

if self.relation is not None:
SubElement(root, 'dc:relation').text = self.relation

if self.rights is not None:
SubElement(root, 'dc:rights').text = self.rights

return root

class MusicTrack(AudioItem):
"""A discrete piece of audio that should be interpreted as music."""

klass = AudioItem.klass + '.musicTrack'

artist = None
album = None
originalTrackNumber = None
playlist = None
storageMedium = None
contributor = None
date = None

def toElement(self):

root = AudioItem.toElement(self)

if self.artist is not None:
SubElement(root, 'upnp:artist').text = self.artist

if self.album is not None:
SubElement(root, 'upnp:album').text = self.album

if self.originalTrackNumber is not None:
SubElement(root, 'upnp:originalTrackNumber').text = \
self.originalTrackNumber

if self.playlist is not None:
SubElement(root, 'upnp:playlist').text = self.playlist

if self.storageMedium is not None:
SubElement(root, 'upnp:storageMedium').text = self.storageMedium

if self.contributor is not None:
SubElement(root, 'dc:contributor').text = self.contributor

if self.date is not None:
SubElement(root, 'dc:date').text = self.date

return root

class AudioBroadcast(AudioItem):
klass = AudioItem.klass + '.audioBroadcast'

@@ -321,12 +325,19 @@ class Container(Object, list):

elementName = 'container'
childCount = property(lambda x: len(x))
createClass = None
searchClass = None
searchable = None
updateID = 0
needupdate = False

_optionattrs = Object._optionattrs.copy()
_optionattrs.update({
'searchClass': 'upnp',
'createClass': 'upnp',
'storageTotal': 'upnp',
'storageUsed': 'upnp',
'storageFree': 'upnp',
'storageMaxPartition': 'upnp',
})
def __init__(self, cd, id, parentID, title, **kwargs):
Object.__init__(self, cd, id, parentID, title, **kwargs)
list.__init__(self)
@@ -444,9 +455,6 @@ class Container(Object, list):
if self.childCount:
root.attrib['childCount'] = str(self.childCount)

self._addSet('upnp:createclass', self.createClass)
self._addSet('upnp:searchclass', self.searchClass)

if self.searchable is not None:
root.attrib['searchable'] = str(self.searchable)

@@ -488,56 +496,24 @@ class MovieGenre(Genre):
class StorageSystem(Container):
klass = Container.klass + '.storageSystem'

total = -1
used = -1
free = -1
maxpartition = -1
medium = 'UNKNOWN'

def toElement(self):

root = Container.toElement(self)

SubElement(root, 'upnp:storageTotal').text = str(self.total)
SubElement(root, 'upnp:storageUsed').text = str(self.used)
SubElement(root, 'upnp:storageFree').text = str(self.free)
SubElement(root, 'upnp:storageMaxPartition').text = str(self.maxpartition)
SubElement(root, 'upnp:storageMedium').text = self.medium

return root
storageTotal = -1
storageUsed = -1
storageFree = -1
storageMaxParition = -1
storageMedium = 'UNKNOWN'

class StorageVolume(Container):
klass = Container.klass + '.storageVolume'

total = -1
used = -1
free = -1
medium = 'UNKNOWN'

def toElement(self):

root = Container.toElement(self)

SubElement(root, 'upnp:storageTotal').text = str(self.total)
SubElement(root, 'upnp:storageUsed').text = str(self.used)
SubElement(root, 'upnp:storageFree').text = str(self.free)
SubElement(root, 'upnp:storageMedium').text = self.medium

return root
storageTotal = -1
storageUsed = -1
storageFree = -1
storageMedium = 'UNKNOWN'

class StorageFolder(Container):
klass = Container.klass + '.storageFolder'

used = -1

def toElement(self):

root = Container.toElement(self)

if self.used is not None:
SubElement(root, 'upnp:storageUsed').text = str(self.used)

return root
storageUsed = -1

class DIDLElement(_ElementInterface):
def __init__(self):


+ 36
- 15
audioraw.py View File

@@ -3,7 +3,9 @@

'''Audio Raw Converter'''

from DIDLLite import AudioItem, Album, Resource, ResourceList
import cdrtoc

from DIDLLite import MusicTrack, AudioItem, MusicAlbum, Resource, ResourceList
from FSStorage import FSObject, registerklassfun

from twisted.web import resource, server
@@ -148,26 +150,26 @@ class AudioResource(resource.Resource):
# and make sure the connection doesn't get closed
return server.NOT_DONE_YET

# XXX - maybe should be MusicAlbum, but needs to change AudioRaw
class AudioDisc(FSObject, Album):
class AudioDisc(FSObject, MusicAlbum):
def __init__(self, *args, **kwargs):
self.cuesheet = kwargs.pop('cuesheet')
self.toc = kwargs.pop('toc', {})
self.kwargs = kwargs.copy()

self.file = kwargs.pop('file')
nchan = kwargs['channels']
samprate = kwargs['samplerate']
bitsps = kwargs['bitspersample']
samples = kwargs['samples']
nchan = kwargs.pop('channels')
samprate = kwargs.pop('samplerate')
bitsps = kwargs.pop('bitspersample')
samples = kwargs.pop('samples')
totalbytes = nchan * samples * bitsps / 8

FSObject.__init__(self, kwargs['path'])
FSObject.__init__(self, kwargs.pop('path'))

# XXX - exclude track 1 pre-gap?
kwargs['content'] = AudioResource(self.file,
kwargs.pop('decoder'), 0, kwargs['samples'])
kwargs.pop('decoder'), 0, samples)
#print 'doing construction'
Album.__init__(self, *args, **kwargs)
MusicAlbum.__init__(self, *args, **kwargs)

#print 'adding resource'
self.url = '%s/%s' % (self.cd.urlbase, self.id)
@@ -183,7 +185,7 @@ class AudioDisc(FSObject, Album):
self.res.append(r)
#print 'completed'

def sort(self, fun=lambda x, y: cmp(int(x.title), int(y.title))):
def sort(self, fun=lambda x, y: cmp(int(x.originalTrackNumber), int(y.originalTrackNumber))):
return list.sort(self, fun)

def genChildren(self):
@@ -229,10 +231,18 @@ class AudioDisc(FSObject, Album):
kwargs['start'] = start
kwargs['samples'] = trkarray[trkidx + 1]['offset'] - start
#print 'track: %d, kwargs: %s' % (i, `kwargs`)
kwargs['originalTrackNumber'] = i
try:
oi = self.toc['tracks'][i]['TITLE']
pass
except KeyError:
pass

return AudioRaw, oi, (), kwargs
return AudioRawTrack, oi, (), kwargs

class AudioRaw(AudioItem, FSObject):
# XXX - figure out how to make custom mix-ins w/ other than AudioItem
# metaclass?
class AudioRawBase(FSObject):
def __init__(self, *args, **kwargs):
file = kwargs.pop('file')
nchan = kwargs.pop('channels')
@@ -242,12 +252,12 @@ class AudioRaw(AudioItem, FSObject):
startsamp = kwargs.pop('start', 0)
totalbytes = nchan * samples * bitsps / 8

FSObject.__init__(self, kwargs['path'])
FSObject.__init__(self, kwargs.pop('path'))

#print 'AudioRaw:', `startsamp`, `samples`
kwargs['content'] = AudioResource(file,
kwargs.pop('decoder'), startsamp, samples)
AudioItem.__init__(self, *args, **kwargs)
self.baseObject.__init__(self, *args, **kwargs)

self.url = '%s/%s' % (self.cd.urlbase, self.id)
self.res = ResourceList()
@@ -261,6 +271,12 @@ class AudioRaw(AudioItem, FSObject):
r.nrAudioChannels = nchan
self.res.append(r)

class AudioRaw(AudioRawBase, AudioItem):
baseObject = AudioItem

class AudioRawTrack(AudioRawBase, MusicTrack):
baseObject = MusicTrack

def detectaudioraw(origpath, fobj):
for i in decoders.itervalues():
try:
@@ -280,6 +296,11 @@ def detectaudioraw(origpath, fobj):
}

if obj.cuesheet is not None:
print 'tags:', `obj.tags`
if 'jmg_toc' in obj.tags:
args['toc'] = cdrtoc.parsetoc(
obj.tags['jmg_toc'][0])

args['cuesheet'] = obj.cuesheet
return AudioDisc, args



+ 134
- 0
cdrtoc.py View File

@@ -0,0 +1,134 @@
#!/usr/bin/env python

import string

types = frozenset([ 'CD_DA', 'CD_ROM', 'CD_ROMXA'])

def decodestr(i, pos):
return decodestrend(i, pos)[0]

def decodestrend(i, pos):
r = []
bspos = None
dqpos = None
while True:
if bspos is None or bspos == -1 or bspos < pos:
bspos = i.find('\\', pos)
if dqpos is None or dqpos < pos:
dqpos = i.index('"', pos)
if bspos >= 0 and bspos < dqpos:
r.append(i[pos:bspos])
c = i[bspos + 1]
if c == '"':
r.append('"')
pos = bspos + 2
elif c in string.digits:
r.append(unichr(int(i[bspos + 1:bspos + 4], 8)))
pos = bspos + 4
else:
raise ValueError('unknown escape char')
else:
r.append(i[pos:dqpos])
break

return ''.join(r), dqpos

def parsetoc(toc):
# state machine info:
# 0: header
# 1: in CD_TEXT
# 2: in LANGUAGE_MAP
# 3: in LANGUAGE

r = { 'tracks': {} }
langmap = {}
state = 0
curlang = None
textobj = None
langobj = None
track = 0
for i in toc.split('\n'):
i = i.strip()
if not i:
continue

items = i.split()
key = items[0]

if state == 0:
if i in types:
r['type'] = i
elif key == 'CD_TEXT':
state = 1
if track == 0:
textobj = r
elif key == 'TRACK':
track += 1
textobj = { 'track': track }
r['tracks'][track] = textobj
elif key == 'TWO_CHANNEL_AUDIO':
textobj['channels'] = 2
elif key == 'FOUR_CHANNEL_AUDIO':
textobj['channels'] = 4
elif key == 'ISRC':
textobj['isrc'] = decodestr(i, i.index('"') + 1)
elif key == 'COPY':
textobj['copy'] = True
elif items[0] == 'NO' and items[1] == 'COPY':
textobj['copy'] = False
elif key == 'PRE_EMPHASIS':
textobj['preemphasis'] = True
elif items[0] == 'NO' and items[1] == 'PRE_EMPHASIS':
textobj['preemphasis'] = False
elif key == 'FILE':
pass # XXX
elif key == 'START':
pass # XXX
elif key == '//':
pass
else:
raise ValueError('unknown line: %s' % `i`)
elif state == 1:
if key == 'LANGUAGE_MAP':
state = 2
elif key == 'LANGUAGE':
state = 3
langobj = textobj
# XXX - don't try to use more than one!
#lang = items[1].strip()
#textobj[langmap[lang]] = langobj
elif key == '}':
textobj = None
state = 0
elif state == 2:
if key == '}':
state = 1
else:
key, value = (x.strip() for x in i.split(':'))
value = int(value)
langmap[key] = value
elif state == 3:
if key == '}':
langobj = None
state = 1
else:
curl = i.find('{')
dquo = i.find('"')
if curl != -1 and curl < dquo:
val = i[i.index('{') + 1:i.index('}')]
val = ''.join(chr(int(x)) for x in
val.split(','))
else:
if dquo == -1:
raise ValueError('no dquote')
val = decodestr(i, dquo + 1)
langobj[key] = val

return r

if __name__ == '__main__':
import sys

for i in sys.argv[1:]:
print 'file:', `i`
print parsetoc(open(i).read())

+ 3
- 0
flac.py View File

@@ -469,6 +469,7 @@ class FLACDec(object):
bitspersample = property(lambda x: x._bitspersample)
totalsamples = property(lambda x: x._totalsamples)
bytespersample = property(lambda x: x._bytespersample)
tags = property(lambda x: x._tags)
vorbis = property(lambda x: x._vorbis)
cuesheet = property(lambda x: x._cuesheet)

@@ -508,6 +509,7 @@ class FLACDec(object):
raise ValueError(
FLAC__StreamDecoderInitStatusString[status])

self._tags = {}
self._vorbis = None
self._cuesheet = None

@@ -586,6 +588,7 @@ class FLACDec(object):
#print `si`
elif md.type == FLAC__METADATA_TYPE_VORBIS_COMMENT:
self._vorbis = md.data.vorbis_comment.asobj()
self._tags = self.vorbis[1]
#print 'vc:', `md.data.vorbis_comment`
#print 'v:', `self.vorbis`
elif md.type == FLAC__METADATA_TYPE_CUESHEET:


Loading…
Cancel
Save