Ticket #240: daap_storage.py

File daap_storage.py, 8.6 kB (added by flows, 3 years ago)

First release of a backend supporting DAAP

Line 
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