Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

# -*- coding: utf-8 -*- 

 

# Licensed under the MIT license 

# http://opensource.org/licenses/mit-license.php 

 

# Copyright 2008, Benjamin Kampmann <ben.kampmann@googlemail.com> 

 

""" 

This is a Media Backend that allows you to access the cool and cute pictures 

from lolcats.com. This is mainly meant as a Sample Media Backend to learn 

how to write a Media Backend. 

 

So. You are still reading which allows me to assume that you want to learn how 

to write a Media Backend for Coherence. NICE :) . 

 

Once again: This is a SIMPLE Media Backend. It does not contain any big 

requests, searches or even transcoding. The only thing we want to do in this 

simple example, is to fetch a rss link on startup, parse it, save it and 

restart the process one hour later again. Well, on top of this, we also want 

to provide these informations as a Media Server in the UPnP/DLNA 

Network of course ;) . 

 

Wow. You are still reading. You must be really interested. Then let's go. 

""" 

 

# NOTE: 

# Please don't complain about the coding style of this document - I know. 

# It is just this way to make it easier to document and to understand. 

 

# THE IMPORTS 

# And to parse the RSS-Data (which is XML), we use lxml.etree.fromstring 

from lxml.etree import fromstring 

 

# And we also import the reactor, 

# that allows us to specify an action to happen later 

from twisted.internet import reactor 

 

# The data itself is stored in BackendItems. 

# They are also the first things we are going to create. 

from coherence.backend import BackendItem 

 

# The entry point for each kind of Backend is a 'BackendStore'. 

# The BackendStore is the instance that does everything Usually. 

#  In this Example it can be understood as the 'Server', the object retrieving 

#  and serving the data. 

from coherence.backend import BackendStore 

 

# To make the data 'renderable' we need to define the DIDLite-Class 

# of the Media we are providing. For that we have a bunch of helpers that we 

# also want to import 

from coherence.upnp.core import DIDLLite 

 

# Coherence relies on the Twisted backend. I hope you are familiar with the 

# concept of deferreds. If not please read: 

#   http://twistedmatrix.com/projects/core/documentation/howto/async.html 

# 

# It is a basic concept that you need to understand the following 

# code. But why am I talking about it? Oh, right, because we use a http-client 

# based on the twisted.web.client module to do our requests. 

from coherence.upnp.core.utils import getPage 

 

 

# THE MODELS 

# After the download and parsing of the data is done, we want to save it. In 

# this case, we want to fetch the images and store their URL and the title of 

# the image. That is the LolcatsImage class: 

 

class LolcatsImage(BackendItem): 

    # We inherit from BackendItem as it already contains a lot of 

    # helper methods and implementations. For this simple example, 

    #  we only have to fill the item with data. 

 

    def __init__(self, parent_id, id, title, url): 

        BackendItem.__init__(self) 

        self.parentid = parent_id  # used to be able to 'go back' 

 

        self.update_id = 0 

 

        self.id = id  # each item has its own and unique id 

 

        self.location = url  # the url of the picture 

 

        self.name = title  # the title of the picture. Inside 

        # coherence this is called 'name' 

 

        # Item.item is a special thing. This is used to explain the client what 

        # kind of data this is. For e.g. A VideoItem or a MusicTrack. In our 

        # case, we have an image. 

        self.item = DIDLLite.ImageItem(id, parent_id, self.name) 

 

        # each Item.item has to have one or more Resource objects these hold 

        # detailed information about the media data and can represent variants 

        #  of it (different sizes, transcoded formats) 

        res = DIDLLite.Resource(self.location, 'http-get:*:image/jpeg:*') 

        res.size = None  # FIXME: we should have a size here 

        #       and a resolution entry would be nice too 

        self.item.res.append(res) 

 

 

class LolcatsContainer(BackendItem): 

    # The LolcatsContainer will hold the reference to all our LolcatsImages. 

    # This kind of BackenedItem is a bit different from the normal BackendItem, 

    # because it has 'children' (the lolcatsimages). Because of that we have 

    # some more stuff to do in here. 

 

    def __init__(self, parent_id, id): 

        BackendItem.__init__(self) 

        # the ids as above 

        self.parent_id = parent_id 

        self.id = id 

 

        # we never have a different name anyway 

        self.name = 'LOLCats' 

 

        # but we need to set it to a certain mimetype to explain it, that we 

        # contain 'children'. 

        self.mimetype = 'directory' 

 

        # As we are updating our data periodically, we increase this value so 

        # that our clients can check easier if something has changed 

        # since their last request. 

        self.update_id = 0 

 

        # that is where we hold the children 

        self.children = [] 

 

        # and we need to give a DIDLLite again. This time we want to be 

        # understood as 'Container'. 

        self.item = DIDLLite.Container(id, parent_id, self.name) 

 

        self.item.childCount = None  # will be set as soon as we have images 

 

    def get_children(self, start=0, end=0): 

        # This is the only important implementation thing: 

        #     we have to return our list of children 

        if end != 0: 

            return self.children[start:end] 

        return self.children[start:] 

 

    # there is nothing special in here 

    # FIXME: move it to a base BackendContainer class 

    def get_child_count(self): 

        return len(self.children) 

 

    def get_item(self): 

        return self.item 

 

    def get_name(self): 

        return self.name 

 

    def get_id(self): 

        return self.id 

 

 

# THE SERVER 

# As already said before the implementation of the server is done in an 

# inheritance of a BackendStore. This is where the real code happens (usually). 

# In our case this would be: downloading the page, parsing the content, saving 

# it in the models and returning them on request. 

 

class LolcatsStore(BackendStore): 

    # this *must* be set. Because the (most used) MediaServer Coherence also 

    # allows other kind of Backends (like remote lights). 

    implements = ['MediaServer'] 

 

    # This is only for this implementation: the http link to the lolcats rss 

    # feed that we want to read and parse: 

    rss_url = "http://feeds.feedburner.com/ICanHasCheezburger?format=xml" 

 

    # As we are going to build a (very small) tree with the items, we need to 

    # define the first (the root) item: 

    ROOT_ID = 0 

 

    def __init__(self, server, *args, **kwargs): 

        # First we initialize our heritage 

        BackendStore.__init__(self, server, **kwargs) 

 

        # When a Backend is initialized, the configuration is given as keyword 

        # arguments to the initialization. We receive it here as a dictionary 

        # and allow some values to be set: 

        #       the name of the MediaServer as it appears in the network 

        self.name = kwargs.get('name', 'Lolcats') 

 

        # timeout between updates in hours: 

        self.refresh = int(kwargs.get('refresh', 1)) * (60 * 60) 

 

        # the UPnP device that's hosting that backend, that's already done 

        # in the BackendStore.__init__, just left here the sake of completeness 

        self.server = server 

 

        # internally used to have a new id for each item 

        self.next_id = 1000 

 

        # we store the last update from the rss feed so that we know 

        # if we have to parse again, or not: 

        self.last_updated = None 

 

        # initialize our lolcats container (no parent, this is the root) 

        self.container = LolcatsContainer(None, self.ROOT_ID) 

 

        # but as we also have to return them on 'get_by_id', we have our local 

        # store of images per id: 

        self.images = {} 

 

        # we tell that if an XBox sends a request for images we'll 

        # map the WMC id of that request to our local one 

        self.wmc_mapping = {'16': 0} 

 

        # and trigger an update of the data 

        dfr = self.update_data() 

 

        # So, even though the initialize is kind of done, 

        # Coherence does not yet announce our Media Server. 

        # Coherence does wait for signal send by us that we are ready now. 

        # And we don't want that to happen as long as we don't have succeeded 

        # in fetching some first data, so we delay this signaling after 

        # the update is done: 

        dfr.addCallback(self.init_completed) 

        dfr.addCallback(self.queue_update) 

 

    def get_by_id(self, id): 

        print("asked for", id, type(id)) 

        # what ever we are asked for, 

        #  we want to return the container only 

        if isinstance(id, str): 

            id = id.split('@', 1)[0] 

        elif isinstance(id, bytes): 

            id = id.decode('utf-8').split('@', 1)[0] 

        if int(id) == self.ROOT_ID: 

            return self.container 

        return self.images.get(int(id), None) 

 

    def upnp_init(self): 

        # After the signal was triggered, 

        # this method is called by coherence and 

        # from now on self.server is existing and we can do the 

        # necessary setup here that allows us to specify our server 

        # options in more detail. 

 

        # Here we define what kind of media content we do provide 

        # mostly needed to make some naughty DLNA devices behave 

        # will probably move into Coherence internals one day 

        self.server.connection_manager_server.set_variable( 

            0, 'SourceProtocolInfo', 

            ['http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN;' 

             'DLNA.ORG_OP=01;DLNA.ORG_FLAGS=00f00000000000000000000000000000', 

             'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;' 

             'DLNA.ORG_OP=01;DLNA.ORG_FLAGS=00f00000000000000000000000000000', 

             'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED;' 

             'DLNA.ORG_OP=01;DLNA.ORG_FLAGS=00f00000000000000000000000000000', 

             'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG;' 

             'DLNA.ORG_OP=01;DLNA.ORG_FLAGS=00f00000000000000000000000000000', 

             'http-get:*:image/jpeg:*']) 

 

        # and as it was done after we fetched the data the first time 

        # we want to take care about the server wide updates as well 

        self._update_container() 

 

    def _update_container(self, result=None): 

        # we need to inform Coherence about these changes 

        # again this is something that will probably move 

        # into Coherence internals one day 

        if self.server: 

            self.server.content_directory_server.set_variable( 

                0, 'SystemUpdateID', self.update_id) 

            value = (self.ROOT_ID, self.container.update_id) 

            self.server.content_directory_server.set_variable( 

                0, 'ContainerUpdateIDs', value) 

        return result 

 

    def update_loop(self): 

        # in the loop we want to call update_data 

        dfr = self.update_data() 

        # aftert it was done we want to take care about updating 

        # the container 

        dfr.addCallback(self._update_container) 

        # in ANY case queue an update of the data 

        dfr.addBoth(self.queue_update) 

 

    def update_data(self): 

        # trigger an update of the data 

 

        # fetch the rss 

        dfr = getPage(self.rss_url) 

 

        # push it through our xml parser 

        dfr.addCallback(fromstring) 

 

        # then parse the data into our models 

        dfr.addCallback(self.parse_data) 

 

        return dfr 

 

    def parse_data(self, root): 

        # from there, we look for the newest update and compare it with the one 

        # we have saved. If they are the same, we don't need to go on: 

        pub_date = root.find('./channel/lastBuildDate').text 

 

        if pub_date == self.last_updated: 

            return 

 

        # not the case, set this as the last update and continue 

        self.last_updated = pub_date 

 

        # and reset the children list of the container and the local storage 

        self.container.children = [] 

        self.images = {} 

 

        # Attention, as this is an example, this code is meant to be as simple 

        # as possible and not as efficient as possible. IMHO the following code 

        # pretty much sucks, because it is totally blocking 

        # (even though we have 'only' 20 elements) 

 

        # we go through our entries and do something specific to the 

        # lolcats-rss-feed to fetch the data out of it 

        url_item = './{http://search.yahoo.com/mrss/}content' 

        for item in root.findall('./channel/item'): 

            title = item.find('./title').text 

            try: 

                url = item.findall(url_item)[1].get('url', None) 

            except IndexError: 

                continue 

 

            if url is None: 

                continue 

 

            image = LolcatsImage(self.ROOT_ID, self.next_id, title, url) 

            self.container.children.append(image) 

            self.images[self.next_id] = image 

 

            # increase the next_id entry every time 

            self.next_id += 1 

 

        # and increase the container update id and the system update id 

        # so that the clients can refresh with the new data 

        self.container.update_id += 1 

        self.update_id += 1 

 

    def queue_update(self, error_or_failure): 

        # We use the reactor to queue another updating of our data 

        print(error_or_failure) 

        reactor.callLater(self.refresh, self.update_loop)