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
@@ -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): | |||
@@ -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 | |||
@@ -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()) |
@@ -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: | |||