| 1 |
# -*- coding: utf-8 -*- |
|---|
| 2 |
|
|---|
| 3 |
# Licensed under the MIT license |
|---|
| 4 |
# http://opensource.org/licenses/mit-license.php |
|---|
| 5 |
|
|---|
| 6 |
# Copyright 2009, Florian Wiesweg <Flo@Wiesweg-net.de> |
|---|
| 7 |
|
|---|
| 8 |
# DEPENDS ON: |
|---|
| 9 |
# - PythonDaap (http://jerakeen.org/code/pythondaap/) by Tom Insam |
|---|
| 10 |
# (tom@jerakeen.org) |
|---|
| 11 |
# |
|---|
| 12 |
# TODO: |
|---|
| 13 |
# - add support for more mimetypes (e.g. flac, videos, images?) |
|---|
| 14 |
# concerning images: I heard of Apple's iPhoto using a similar protocol to |
|---|
| 15 |
# share images, is this also possible with python-daap? it's a pity I |
|---|
| 16 |
# don't have a Mac to test it |
|---|
| 17 |
# - cleanup: DaapSession.logout() when coherence shuts down |
|---|
| 18 |
# - seperate containers: genres, artists, albums like in ampache_storage? |
|---|
| 19 |
# would require additional sorting since they are only passed as |
|---|
| 20 |
# attributes of the music tracks and not in seperate lists, so it would be |
|---|
| 21 |
# quite a piece of work |
|---|
| 22 |
# |
|---|
| 23 |
# UNTESTED: |
|---|
| 24 |
# - playlists - seem to work (tested with gupnp-universal-cp), but I've got no |
|---|
| 25 |
# application capable of really using them (neither GNOME Totem, Rhythmbox) |
|---|
| 26 |
# |
|---|
| 27 |
# CONFIGURATION: |
|---|
| 28 |
# - remoteServer: name or address of the DAAP-Server hosting the share |
|---|
| 29 |
# default: 'localhost' |
|---|
| 30 |
# - remotePort: listening port of the DAAP-Server |
|---|
| 31 |
# default: 3689 (default port of DAAP, AFAIK) |
|---|
| 32 |
# - databaseName: name of the database to read the music from |
|---|
| 33 |
# note: normally a DAAP share hosts only a single database, |
|---|
| 34 |
# so this can normally be left out, daap_storage chooses |
|---|
| 35 |
# it automatically |
|---|
| 36 |
# default: None |
|---|
| 37 |
# - password: password securing the share, only needed if one is set |
|---|
| 38 |
# default: None |
|---|
| 39 |
# - refresh: time between regular updates of the UPnP data (in minutes) |
|---|
| 40 |
# default: 5 |
|---|
| 41 |
# |
|---|
| 42 |
|
|---|
| 43 |
from coherence.backend import BackendStore |
|---|
| 44 |
from coherence.backend import BackendItem |
|---|
| 45 |
from coherence.upnp.core import DIDLLite |
|---|
| 46 |
import coherence.extern.louie as louie |
|---|
| 47 |
from twisted.internet import reactor |
|---|
| 48 |
|
|---|
| 49 |
from daap import DAAPClient |
|---|
| 50 |
|
|---|
| 51 |
class DaapMusicTrack(BackendItem): |
|---|
| 52 |
|
|---|
| 53 |
# translate dmap.itemkind to MIME type for UPnP |
|---|
| 54 |
typeMap = {'mp3':'audio/mpeg3', |
|---|
| 55 |
'ogg':'audio/ogg' # unsure if this is right for ogg on the DAAP side |
|---|
| 56 |
} |
|---|
| 57 |
|
|---|
| 58 |
def __init__(self, parent_id, track, location): |
|---|
| 59 |
self.daapTrack = track |
|---|
| 60 |
self.update_id = 0 |
|---|
| 61 |
self.id = self.daapTrack.id |
|---|
| 62 |
self.parent_id = parent_id |
|---|
| 63 |
self.name = self.daapTrack.name |
|---|
| 64 |
self.location = location |
|---|
| 65 |
|
|---|
| 66 |
self.item = DIDLLite.MusicTrack(id, parent_id, self.name) |
|---|
| 67 |
self.item.album = self.daapTrack.album |
|---|
| 68 |
self.item.contributor = self.daapTrack.artist |
|---|
| 69 |
try: |
|---|
| 70 |
self.item.genre = self.daapTrack.asgn |
|---|
| 71 |
except (AttributeError): |
|---|
| 72 |
pass |
|---|
| 73 |
|
|---|
| 74 |
type = 'http-get:*:' + self.getMimeType(self.daapTrack) + ':*' |
|---|
| 75 |
res = DIDLLite.Resource(self.location, type) |
|---|
| 76 |
res.size = self.daapTrack.size |
|---|
| 77 |
self.item.res.append(res) |
|---|
| 78 |
|
|---|
| 79 |
def getMimeType(self, daapTrack): |
|---|
| 80 |
if DaapMusicTrack.typeMap.has_key(daapTrack.type): |
|---|
| 81 |
return DaapMusicTrack.typeMap[daapTrack.type] |
|---|
| 82 |
else: |
|---|
| 83 |
return 'application/octet-stream' |
|---|
| 84 |
|
|---|
| 85 |
def get_id(self): |
|---|
| 86 |
return self.id |
|---|
| 87 |
|
|---|
| 88 |
def get_name(self): |
|---|
| 89 |
return self.name |
|---|
| 90 |
|
|---|
| 91 |
class DaapContainer(BackendItem): |
|---|
| 92 |
def __init__(self, id, parent_id, name): |
|---|
| 93 |
BackendItem.__init__(self) |
|---|
| 94 |
self.parent_id = parent_id |
|---|
| 95 |
self.id = id |
|---|
| 96 |
self.name = name |
|---|
| 97 |
self.mimetype = 'directory' |
|---|
| 98 |
self.update_id = 0 |
|---|
| 99 |
self.item = DIDLLite.Container(id, parent_id, self.name) |
|---|
| 100 |
self.children = [] |
|---|
| 101 |
|
|---|
| 102 |
def get_children(self, start=0, end=0): |
|---|
| 103 |
if end != 0: |
|---|
| 104 |
return self.children[start:end] |
|---|
| 105 |
return self.children[start:] |
|---|
| 106 |
|
|---|
| 107 |
def get_child_count(self): |
|---|
| 108 |
return len(self.children) |
|---|
| 109 |
|
|---|
| 110 |
def get_id(self): |
|---|
| 111 |
return self.id |
|---|
| 112 |
|
|---|
| 113 |
def get_item(self): |
|---|
| 114 |
return self.item |
|---|
| 115 |
|
|---|
| 116 |
class DaapPlaylist(DaapContainer): |
|---|
| 117 |
def __init__(self, id, parent_id, name, musicTracks): |
|---|
| 118 |
self.id = id |
|---|
| 119 |
self.parent_id = parent_id |
|---|
| 120 |
self.name = name |
|---|
| 121 |
self.children = musicTracks |
|---|
| 122 |
self.item = DIDLLite.PlaylistItem(self.id, parent_id, self.name) |
|---|
| 123 |
self.item.childCount = len(musicTracks) |
|---|
| 124 |
|
|---|
| 125 |
def get_name(self): |
|---|
| 126 |
return self.name |
|---|
| 127 |
|
|---|
| 128 |
class DaapStore(BackendStore): |
|---|
| 129 |
|
|---|
| 130 |
implements = ["MediaServer"] |
|---|
| 131 |
logCategory = 'daap_store' |
|---|
| 132 |
|
|---|
| 133 |
ROOT_CONTAINER_ID = 0 |
|---|
| 134 |
next_id = 1000 |
|---|
| 135 |
|
|---|
| 136 |
def __init__(self, server, **kwargs): |
|---|
| 137 |
BackendStore.__init__(self, server, **kwargs) |
|---|
| 138 |
|
|---|
| 139 |
self.name = kwargs.get('name', 'Daap' ) |
|---|
| 140 |
self.remoteServer = kwargs.get('remoteServer', 'localhost') |
|---|
| 141 |
self.remotePort = int(kwargs.get('remotePort', 3689)) |
|---|
| 142 |
self.databaseName = kwargs.get('database', None) |
|---|
| 143 |
self.password = kwargs.get('password', None) |
|---|
| 144 |
self.refresh = int(kwargs.get('refresh', 5)) |
|---|
| 145 |
|
|---|
| 146 |
self.daapItems = {} |
|---|
| 147 |
self.container= DaapContainer(self.ROOT_CONTAINER_ID, None, 'root' ) |
|---|
| 148 |
self.wmc_mapping = {'16': 0} |
|---|
| 149 |
|
|---|
| 150 |
self.update_loop() |
|---|
| 151 |
|
|---|
| 152 |
def upnp_init(self): |
|---|
| 153 |
self.server.connection_manager_server.set_variable( |
|---|
| 154 |
0, 'SourceProtocolInfo', |
|---|
| 155 |
['http-get:*:audio/mpeg:*', |
|---|
| 156 |
'http-get:*:application/ogg:*',]) |
|---|
| 157 |
|
|---|
| 158 |
def _update_container(self, result=None): |
|---|
| 159 |
if self.server: |
|---|
| 160 |
self.server.content_directory_server.set_variable(0, |
|---|
| 161 |
'SystemUpdateID', self.update_id) |
|---|
| 162 |
value = (self.ROOT_CONTAINER_ID,self.container.update_id) |
|---|
| 163 |
self.server.content_directory_server.set_variable(0, |
|---|
| 164 |
'ContainerUpdateIDs', value) |
|---|
| 165 |
return result |
|---|
| 166 |
|
|---|
| 167 |
def update_loop(self): |
|---|
| 168 |
self.connect() |
|---|
| 169 |
self.update_data() |
|---|
| 170 |
reactor.callLater(self.refresh * 60, self.update_loop) |
|---|
| 171 |
|
|---|
| 172 |
def connect(self): |
|---|
| 173 |
self.client = DAAPClient() |
|---|
| 174 |
self.client.connect(self.remoteServer, self.remotePort, self.password) |
|---|
| 175 |
self.session = self.client.login() |
|---|
| 176 |
|
|---|
| 177 |
if self.session is None: |
|---|
| 178 |
self.error('could not connect to daap server %s, port %d', |
|---|
| 179 |
self.server, self.port) |
|---|
| 180 |
return |
|---|
| 181 |
|
|---|
| 182 |
databases = self.session.databases() |
|---|
| 183 |
if self.databaseName is None: |
|---|
| 184 |
self.databaseName = self.session.library().name |
|---|
| 185 |
|
|---|
| 186 |
self.database=None |
|---|
| 187 |
for d in databases: |
|---|
| 188 |
if d.name == self.databaseName: |
|---|
| 189 |
self.database = d |
|---|
| 190 |
|
|---|
| 191 |
def update_data(self): |
|---|
| 192 |
remoteMusicTracks = self.database.tracks() |
|---|
| 193 |
self.daapItems = {} |
|---|
| 194 |
self.container= DaapContainer(None, self.ROOT_CONTAINER_ID, 'root' ) |
|---|
| 195 |
|
|---|
| 196 |
for t in remoteMusicTracks: |
|---|
| 197 |
location = self.get_url_by_daap_track(t) |
|---|
| 198 |
musicTrack = DaapMusicTrack( |
|---|
| 199 |
self.ROOT_CONTAINER_ID, t, location) |
|---|
| 200 |
self.container.children.append(musicTrack) |
|---|
| 201 |
self.daapItems[t.id] = musicTrack |
|---|
| 202 |
|
|---|
| 203 |
remotePlaylists = self.database.playlists() |
|---|
| 204 |
|
|---|
| 205 |
for p in remotePlaylists: |
|---|
| 206 |
if p.id == 1: |
|---|
| 207 |
continue |
|---|
| 208 |
playlistTracks = [] |
|---|
| 209 |
remotePlaylistTracks = p.tracks() |
|---|
| 210 |
for t in remotePlaylistTracks: |
|---|
| 211 |
playlistTracks.append(self.get_by_id(t.id)) |
|---|
| 212 |
|
|---|
| 213 |
playlist = DaapPlaylist(p.id, self.ROOT_CONTAINER_ID, |
|---|
| 214 |
p.name, playlistTracks) |
|---|
| 215 |
self.container.children.append(playlist) |
|---|
| 216 |
self.daapItems[p.id] = playlist |
|---|
| 217 |
|
|---|
| 218 |
self.container.update_id += 1 |
|---|
| 219 |
self.update_id += 1 |
|---|
| 220 |
louie.send('Coherence.UPnP.Backend.init_completed', None, backend=self) |
|---|
| 221 |
|
|---|
| 222 |
def get_by_id(self, id): |
|---|
| 223 |
if isinstance(id, basestring): |
|---|
| 224 |
id = id.split('@',1) |
|---|
| 225 |
id = id[0] |
|---|
| 226 |
if int(id) == self.ROOT_CONTAINER_ID: |
|---|
| 227 |
return self.container |
|---|
| 228 |
return self.daapItems.get(int(id), None) |
|---|
| 229 |
|
|---|
| 230 |
def get_url_by_daap_track(self, daapTrack): |
|---|
| 231 |
self.connect() |
|---|
| 232 |
return 'http://' + str(self.remoteServer) + ':' + str(self.remotePort) \ |
|---|
| 233 |
+ '/databases/' + str(self.database.id) \ |
|---|
| 234 |
+ '/items/' + str(daapTrack.id) \ |
|---|
| 235 |
+ '.mp3?session-id=' + str(self.session.sessionid) |
|---|
| 236 |
|
|---|
| 237 |
if __name__ == '__main__': |
|---|
| 238 |
|
|---|
| 239 |
from coherence.base import Coherence |
|---|
| 240 |
from twisted.internet import reactor |
|---|
| 241 |
|
|---|
| 242 |
def main(): |
|---|
| 243 |
def got_result(result): |
|---|
| 244 |
print "got_result" |
|---|
| 245 |
|
|---|
| 246 |
|
|---|
| 247 |
config = {} |
|---|
| 248 |
config['logmode'] = 'debug' |
|---|
| 249 |
c = Coherence(config) |
|---|
| 250 |
f = c.add_plugin('DaapStore', |
|---|
| 251 |
remoteServer='homeServer.local', |
|---|
| 252 |
name="DAAP Test Store" |
|---|
| 253 |
) |
|---|
| 254 |
|
|---|
| 255 |
#store = DaapStore(None, remoteServer='homeServer.local', |
|---|
| 256 |
# name='DAAP Test Store 2') |
|---|
| 257 |
reactor.callWhenRunning(main) |
|---|
| 258 |
reactor.run() |
|---|
| 259 |
|
|---|