Package elisa :: Package core :: Module media_manager
[hide private]
[frames] | no frames]

Source Code for Module elisa.core.media_manager

  1  # -*- coding: utf-8 -*- 
  2  # Elisa - Home multimedia server 
  3  # Copyright (C) 2006-2008 Fluendo Embedded S.L. (www.fluendo.com). 
  4  # All rights reserved. 
  5  # 
  6  # This file is available under one of two license agreements. 
  7  # 
  8  # This file is licensed under the GPL version 3. 
  9  # See "LICENSE.GPL" in the root of this distribution including a special 
 10  # exception to use Elisa with Fluendo's plugins. 
 11  # 
 12  # The GPL part of Elisa is also available under a commercial licensing 
 13  # agreement from Fluendo. 
 14  # See "LICENSE.Elisa" in the root directory of this distribution package 
 15  # for details on that license. 
 16   
 17   
 18   
 19  __maintainer__ = 'Philippe Normand <philippe@fluendo.com>' 
 20   
 21   
 22  """ 
 23  MediaManager & MediaProvider notes and ideas 
 24   
 25   
 26  - The MediaManager can ask the MediaProvider component if a URI is cacheable. 
 27    The MediaManager should have a totally abstract view on the type of data it 
 28    is managing, the MediaProvider implements all data specific cases. 
 29   
 30  - When a media access is requested through the MediaManager, it is up to the 
 31    MediaProvider to implement this operation as a singleton or simultaneous 
 32    data access. i.e : you could open two files on two samba servers at the 
 33    same time. 
 34   
 35  - That media request needs to provide non-blocking and blocking calls. 
 36    Blocking I/O operations called from the main thread should raise an 
 37    exception, in an effort to keep the entire program to block (the 
 38    intention is to have Elisa as plugin-weakness safe as possible) 
 39   
 40  - The MediaProviders should have functions to provide meta data to the 
 41    MediaManager. The data structure of the meta data should be 
 42    predefined. **NOTE**: This isn't specific to MediaProviders, 
 43    IMHO. Metadata retrieval should be implemented using GStreamer 
 44    and/or a third-party library (like TagLib). MediaProviders provide 
 45    URI data support, but they shouldn't be aware of the kind of 
 46    multimedia files they provide access to. This is the job of the 
 47    underlying multimedia framework. -- Philippe 
 48    - We do it now with the usage of a dictionary containing metadata 
 49   
 50  - The MediaManager is portable by design. The MediaProviders can be 
 51    system-dependent or requiring external applications running. 
 52   
 53  - The MediaManager's caching system could have parallel lists of contents 
 54    to cache depending on the resource type. MediaProvider could provide 
 55    a way for the MediaManager to know if the resource is likely to take 
 56    long to retrieve or not. For example, if there is only one to-cache list, 
 57    if the first 10 elements are HTTP resources and the next are from the 
 58    local filesystem, the local filesystem resources could be processed in 
 59    parralel to provide faster access to them. **NOTE**: How do you define 
 60    "long to retrieve"? For me every task should be long by 
 61    definition. -- Philippe 
 62    **NOTE2**: then no parralelism. Although you can be pretty sure a local 
 63    disk access will really be faster than a HTTP request, so it could come 
 64    handy - although it's some kind of specials case - cause it might give the 
 65    user a better responsivity. 
 66   
 67  - The caching system should be priority-list oriented. Priorities should 
 68    be assigned to Medias to cache, and a request to MediaManager cached 
 69    media should be able to interrupt current caching and process the 
 70    request first. 
 71   
 72  - The MediaManager could count accesses to data. Those statistics could be 
 73    considered as metadata, and could be retrieved by Components. 
 74   
 75  - The Components could add metadata to the Medias (like tags). 
 76    However it will be needed first to ensure that the data given by the 
 77    Component cannot cause a database corruption. **NOTE**: This 
 78    metadata would be stored both on db and in files? -- Philippe 
 79    **NOTE2**: DB for sure, and maybe in files if the MediaProvider 
 80    supports it ? 
 81   
 82  - It should be possible to place a restriction on the Cache database 
 83    file size.  That would be necessary in the case of Elisa running on 
 84    a system more aimed at retrieving data from networks, that has a 
 85    low-capacity hard drive. **NOTE**: What would we do when db file 
 86    size exceeds the limit? drop some db records? never add anything 
 87    more in the db? :) The box we have in mind will have a CF card, we 
 88    found out that even with huge media libraries the db size doesn't 
 89    exceeds 20mb. This also depends on how the db is designed. -- Philippe 
 90   
 91    **NOTE2**: This is in a way related to fault-tolerancy (CF card or not). 
 92    The behaviour should be IMHO to stop caching -- Colin 
 93   
 94  - The MediaManager should have different monitor location list 
 95    depending on the time the user wants it to check for updates of 
 96    these locations (one time, 5 minutes, 30min, hourly, daily, ...) 
 97   
 98    There should also be an unmonitored location list so that the user can 
 99    explicitly tell the MediaManager not to do caching on certain locations. 
100   
101    The MediaManager will also ask the MediaProvider components if it can do 
102    monitoring on the medias that are relevant to a certain MediaProvider 
103    (like INotify for Local FS support). 
104   
105    The MediaProviders can indeed add locations to monitor (HAL plugin 
106    for example). **NOTE**: I don't understand that example. The HAL 
107    plugin doesn't provide a media_provider, does it? -- Philippe 
108    **NOTE2** I was thinking about the case MediaProvider get signaled 
109    by the HAL plugin (which is not a MP) that a new location is available 
110    -- Colin 
111   
112  - MediaProviders should be able to provide source elements for 
113    GStreamer. This can become handy for URIs not supported (yet) by 
114    GStreamer but supported by MediaProviders who have access to the 
115    data (example: DAAP uris) 
116   
117  """ 
118   
119  import threading, os 
120  from twisted.internet import reactor, defer 
121   
122  from elisa.core import common, manager 
123  from elisa.core.utils import classinit 
124  from elisa.base_components import media_provider 
125  from elisa.core import media_scanner 
126  from elisa.core import media_uri 
127  from elisa.core import db_backend 
128  from elisa.core.media_db import MediaDB 
129   
130  DB_CONNECTIONS = {} 
131   
132 -class MediaProviderNotFound(Exception):
133
134 - def __init__(self, uri):
135 Exception.__init__(self) 136 self.uri = uri
137
138 - def __str__(self):
139 return "No MediaProvider found for %r URI" % self.uri
140
141 -class MediaManager(manager.Manager):
142 """ 143 Provides access to files through the use of MediaProvider 144 components which allows to handle different file I/O protocols 145 using URIs. It also handles caching in a database and the 146 monitoring of files via the L{elisa.core.media_scanner.MediaScanner}. 147 148 Database caching can be disabled: 149 150 - explicitely in the application's config 151 - if any dependency (pysqlite for instance) is unmet 152 153 In a such case, the media_scanner instance variable is set to None. 154 155 @ivar media_scanner: the component which scans media sources and keep 156 the database up to date 157 @type media_scanner: L{elisa.core.media_scanner.MediaScanner} 158 """ 159
160 - def __init__(self, metadata_manager):
161 """ Initialize media_providers instance variable and try to 162 load the media_scanner. 163 164 @param metadata_manager: The MetadataManager to use to create the MediaScanner 165 @type metadata_manager: L{elisa.core.metadata_manager.MetadataManager} 166 """ 167 manager.Manager.__init__(self) 168 self._metadata_manager = metadata_manager 169 self._media_scanner = media_scanner.MediaScanner(self, metadata_manager) 170 self._scanner_enabled = self._media_scanner.enabled 171 172 self._db_ok = True 173 self._db_lock = threading.Lock() 174 self._providers_by_scheme = {} 175 self._delayed_start = None 176 177 # disable media_scanner if db support not available 178 if not self.media_db: 179 self._scanner_enabled = False
180
181 - def media_db__get(self):
182 """ MediaDB instance accessor. 183 """ 184 db = None 185 186 global DB_CONNECTIONS 187 try: 188 self._db_lock.acquire() 189 current_thread = threading.currentThread() 190 thread_id = id(current_thread) 191 backend = DB_CONNECTIONS.get(thread_id) 192 first_load = False 193 if not backend: 194 first_load = True 195 opts = self._get_db_options() 196 try: 197 backend = db_backend.DBBackend(**opts) 198 except db_backend.DBBackendError, error: 199 self.warning(error) 200 self._db_ok = False 201 else: 202 DB_CONNECTIONS[thread_id] = backend 203 if backend: 204 db = MediaDB(backend, first_load) 205 finally: 206 self._db_lock.release() 207 208 return db
209
210 - def _get_db_options(self):
211 config = self._media_scanner._config 212 config_dir = common.application.config.get_config_dir() 213 214 opts = dict([(k,v) for k,v in config.iteritems()]) 215 216 # SQLite db should be in same directory as the elisa config file. 217 if opts['db_backend'] == 'sqlite': 218 opts['database'] = os.path.join(config_dir, opts['database']) 219 220 return opts
221
222 - def enabled__get(self):
223 return self._db_ok or self._scanner_enabled
224
225 - def start(self, seconds=0, resume_scan=False):
226 """ 227 Load all enabled MediaProvider components using the 228 PluginRegistry and eventually start the media_scanner 229 230 @keyword seconds: time in seconds to wait before starting the scanner 231 @type seconds: int 232 @keyword resume_scan: should the media_scanner resume interrupted 233 scan at startup? 234 @type resume_scan: bool 235 """ 236 237 return self._media_scanner.start(seconds)
238
239 - def stop(self):
240 """ 241 Stop the media_scanner if it's running and clean all 242 registered media_providers. 243 """ 244 self.info('Stopping') 245 246 self._media_scanner.stop() 247 return manager.Manager.stop(self)
248
249 - def register_component(self, component):
250 try: 251 manager.Manager.register_component(self, component) 252 schemes = component.supported_uri_schemes 253 for scheme, index in schemes.iteritems(): 254 if scheme not in self._providers_by_scheme: 255 self._providers_by_scheme[scheme] = [component,] 256 else: 257 self._providers_by_scheme[scheme].insert(index, component) 258 except manager.AlreadyRegistered: 259 pass
260 #FIXME: needs to be raised? 261
262 - def unregister_component(self, component):
263 for scheme in self._providers_by_scheme.keys(): 264 m_providers = self._providers_by_scheme[scheme] 265 if component in m_providers: 266 m_providers.remove(component) 267 self._providers_by_scheme[scheme] = m_providers 268 try: 269 manager.Manager.unregister_component(self, component) 270 except CannotUnregister: 271 pass
272 #FIXME: needs to be raised? 273
274 - def get_metadata(self, metadata, low_priority=False):
275 """just a proxy to the metadata_manager.get_metadata""" 276 return self._metadata_manager.get_metadata(metadata, low_priority=False)
277
278 - def add_source(self, source_uri, media_types=None):
279 """ 280 Add a new source in DB. If it's already there, just mark it as 281 available again (and start monitoring it). 282 283 @param source_uri: The location of the source 284 @type source_uri: L{elisa.core.media_uri.MediaUri} 285 @param media_types: a sequence of media types to scan on this source eg: 286 ('audio', 'image') or None 287 @type media_types: sequence 288 """ 289 source = None 290 source = self.get_source_for_uri(source_uri) 291 if not source: 292 short_name = source_uri.label 293 source = self.media_db.add_source(source_uri, short_name) 294 295 return self._media_scanner.add_source(source_uri, media_types)
296
297 - def remove_source(self, source_uri):
298 """ 299 Mark a source as unavailable in the db and stop monitoring it 300 301 @param source_uri: The location of the source 302 @type source_uri: L{elisa.core.media_uri.MediaUri} 303 """ 304 if self.enabled: 305 source = self.get_source_for_uri(source_uri) 306 if source: 307 # mark the source as unavailable in the db 308 self.media_db.hide_source(source) 309 310 self._media_scanner.remove_source(source_uri) 311 312 # FIXME: return the proper result when we make the db asynchronous 313 return defer.succeed(None)
314
315 - def update_source(self, source_uri):
316 """ 317 Schedule a new scan of the source located at given uri. The 318 scan may not start just after calling this method. 319 320 @param source_uri: The location of the source 321 @type source_uri: L{elisa.core.media_uri.MediaUri} 322 """ 323 return self._media_scanner.update_source(source_uri)
324
325 - def get_source_for_uri(self, uri):
326 """ 327 Fetch the source located at given uri, by retrieving its 328 information in the media database. 329 330 @param uri: The uri of the source to look for 331 @type uri: L{elisa.core.media_uri.MediaUri} 332 @rtype: L{elisa.extern.db_row.DBRow} 333 """ 334 if self.enabled: 335 return self.media_db.get_source_for_uri(uri)
336
337 - def handle_notify_event(self, uri, event, media_types):
338 if self.enabled: 339 parent_uri = media_uri.MediaUri(uri.parent[:-1]) 340 source = self.media_db.get_source_for_uri(parent_uri) 341 342 if source: 343 if event == media_provider.NotifyEvent.ADDED: 344 self._media_scanner.add_media(source.source_id, uri) 345 elif event == media_provider.NotifyEvent.REMOVED: 346 self._media_scanner.remove_media(source.source_id, uri)
347
348 - def get_media_information(self, uri, extended=True, media_type=None):
349 """ 350 Fetch the media located at given uri, by retrieving its 351 information in the media database. 352 353 @param uri: The uri of the media to look for 354 @type uri: L{elisa.core.media_uri.MediaUri} 355 @rtype: L{elisa.extern.db_row.DBRow} 356 """ 357 if self.enabled: 358 return self.media_db.get_media_information(uri, 359 media_type=media_type, 360 extended=extended)
361
362 - def _get_media_provider(self, uri):
363 """ 364 Retrieve the MediaProvider supporting the scheme of the given 365 URI. If multiple MediaProviders support it, the first one is 366 chosen. 367 368 @param uri: The location of the source 369 @type uri: L{elisa.core.media_uri.MediaUri} 370 @rtype: L{elisa.base_components.media_provider.MediaProvider} 371 """ 372 provider = None 373 providers = self._providers_by_scheme.get(uri.scheme) 374 375 if providers: 376 provider = providers[0] 377 self.debug("Using %r media_provider to access %s://", provider.name, 378 uri.scheme) 379 else: 380 raise MediaProviderNotFound(uri) 381 return provider
382
383 - def _proxy(self, method_name, fallback_result, uri, *args, **kw):
384 result = fallback_result 385 provider = self._get_media_provider(uri) 386 if provider: 387 real_args = (uri,) + args + (kw,) 388 self.debug("Calling %s.%s%r", provider.name, method_name, 389 real_args) 390 result = getattr(provider, method_name)(uri, *args, **kw) 391 return result
392
393 - def is_scannable(self, uri):
394 provider = self._get_media_provider(uri) 395 scannable = provider and uri.scheme in provider.scannable_uri_schemes 396 return scannable
397
398 - def _guess_source_for_uri(self, uri):
399 source = None 400 source_uri = uri 401 402 while True: 403 u_uri = unicode(source_uri) 404 if u_uri[-1] == '/': 405 u_uri = u_uri[:-1] 406 407 source = self.get_source_for_uri(u_uri) 408 if source: 409 break 410 411 source = None 412 parent = source_uri.parent 413 if parent == source_uri: 414 break 415 416 source_uri = parent 417 418 return source
419
420 - def get_media_type(self, uri):
421 """ 422 Try to guess the maximum information from the media located 423 at given uri by looking at eventual file extension. Will 424 return something like:: 425 426 {'file_type': string (values: one of media_provider.media_types, 427 'mime_type': string (example: 'audio/mpeg' for .mp3 uris. can be 428 empty string if unguessable) 429 } 430 431 432 @param uri: the URI to analyze 433 @type uri: L{elisa.core.media_uri.MediaUri} 434 @rtype: L{twisted.internet.defer.Deferred} 435 """ 436 437 if self.media_db: 438 infos = self.media_db.get_media_information(uri, extended=False) 439 if infos: 440 self.debug('returning cached media type %s', infos.typ) 441 file_type = infos.format 442 if infos.typ: 443 mime_type = infos.typ 444 else: 445 mime_type = None 446 return defer.succeed({'file_type': file_type, 447 'mime_type': mime_type}) 448 449 def get_media_type_done(media_type, uri): 450 # cache media_type in the db 451 # FIXME: currently this is a bit hackish because we can't tell the 452 # source of an URI from a MediaUri 453 454 if media_type['file_type'] not in ('video', 'audio', 'image'): 455 return media_type 456 457 source = self._guess_source_for_uri(uri) 458 if source: 459 file_type = media_type['file_type'] 460 mime_type = media_type['mime_type'] 461 462 self.media_db.add_media(uri, uri.label, source.id, 'file', 463 format=file_type, typ=mime_type) 464 465 return media_type
466 467 dfr = self._proxy('get_media_type', {}, uri) 468 if self.media_db: 469 dfr.addCallback(get_media_type_done, uri) 470 471 return dfr
472
473 - def blocking_get_media_type(self, uri):
474 return self._proxy('_blocking_get_media_type', {}, uri)
475
476 - def is_directory(self, uri):
477 """ 478 return True if a directory 479 480 @param uri: the URI to analyze 481 @type uri: L{elisa.core.media_uri.MediaUri} 482 @rtype: bool 483 """ 484 return self._proxy('is_directory', False, uri)
485
486 - def blocking_is_directory(self, uri):
487 return self._proxy('_blocking_is_directory', False, uri)
488
489 - def has_children_with_types(self, uri, media_types):
490 """ 491 Detect whether the given uri has children for given media 492 types which can be one of media_provider.media_types. 493 Implies the URI is a directory as well. 494 495 @param uri: the URI to scan 496 @type uri: L{elisa.core.media_uri.MediaUri} 497 @param media_types: the media_types to look for on the directory 498 @type media_types: list of strings 499 @rtype: L{twisted.internet.defer.Deferred} 500 """ 501 return self._proxy('has_children_with_types', False, uri, media_types)
502
503 - def blocking_has_children_with_types(self, uri, media_types):
504 return self._proxy('_blocking_has_children_with_types', 505 False, uri, media_types)
506
507 - def get_direct_children(self, uri, children_with_info):
508 """ 509 Scan the data located at given uri and return a deferred. 510 Fills children_with_info. Defferred is called when the 511 gathering is finished with children_with_info as parameter 512 513 Typemap of filled result: 514 515 [ 516 (uri : media_uri.MediaUri, 517 additional info: dict), 518 ... 519 ] 520 521 @param uri: the URI to analyze 522 @type uri: L{elisa.core.media_uri.MediaUri} 523 @param children_with_info: List where the children will be appended 524 @type children_with_info: list 525 @rtype: twisted.internet.deferred 526 """ 527 return self._proxy('get_direct_children', None, uri, children_with_info)
528
529 - def blocking_get_direct_children(self, children_with_info):
530 return self._proxy('_blocking_get_direct_children', None, uri, 531 children_with_info)
532
533 - def open(self, uri, mode='r'):
534 """ 535 Open an uri and return MediaFile file if the block keyword 536 parameter is True. Else we return a deferred which will be 537 trigerred when the media_file has been successfully opened. 538 539 @param uri: the URI to open 540 @type uri: L{elisa.core.media_uri.MediaUri} 541 @param mode: how to open the file -- see manual of builtin open() 542 @type mode: string or None 543 @rtype: L{elisa.core.media_file.MediaFile} 544 """ 545 return self._proxy('open', None, uri, mode=mode)
546 547
548 - def blocking_open(self, uri, mode='r'):
549 return self._proxy('_blocking_open', None, uri, mode=mode)
550
551 - def next_location(self, uri, root=None):
552 """ 553 Return the uri just next to given uri and record it to history 554 555 @param uri: the URI representing the file or directory from 556 where to move on 557 @type uri: L{elisa.core.media_uri.MediaUri} 558 @param root: root URI 559 @type root: L{elisa.core.media_uri.MediaUri} 560 @rtype: L{elisa.core.media_uri.MediaUri} 561 """ 562 result = None 563 method_name = 'next_location' 564 if root: 565 provider = self._get_media_provider(root) 566 else: 567 provider = self._get_media_provider(uri) 568 if provider: 569 result = getattr(provider, method_name)(uri, root=root) 570 571 def got_location(location): 572 self.debug("Next of %r: %r", uri, location) 573 return location
574 575 result.addCallback(got_location) 576 return result 577
578 - def blocking_next_location(self, uri, root=None):
579 location = None 580 method_name = '_blocking_next_location' 581 if root: 582 provider = self._get_media_provider(root) 583 else: 584 provider = self._get_media_provider(uri) 585 if provider: 586 location = getattr(provider, method_name)(uri, root=root) 587 self.debug("Next of %r: %r", uri, location) 588 return location
589
590 - def previous_location(self, uri):
591 """ 592 Return the uri found before given uri 593 594 @param uri: the URI representing the file or directory prior 595 to uri 596 @type uri: L{elisa.core.media_uri.MediaUri} 597 @rtype: L{elisa.core.media_uri.MediaUri} 598 """ 599 return self._proxy('previous_location', None, uri)
600
601 - def blocking_previous_location(self, uri):
602 return self._proxy('_blocking_previous_location', None, uri)
603
604 - def monitor_uri(self, uri, callback, *extra_args):
605 """ 606 Start monitoring given uri for modification and call a 607 function in case of any change happening on `uri` 608 Raises UriNotMonitorable(uri) if uri can't be monitored 609 610 @param uri: URI representing the file or directory to monitor 611 @type uri: L{elisa.core.media_uri.MediaUri} 612 @param extra_args: extra positional arguments to pass to the callback 613 @type extra_args: tuple 614 @param callback: a callable taking the event that occured and the uri 615 of the file on which the event applies to 616 prototype: callable(uri, event) 617 type uri: L{elisa.core.media_uri.MediaUri} 618 type event: L{elisa.base_components.media_provider.NotifyEvent} 619 """ 620 621 return self._proxy('monitor_uri', None, uri, callback, *extra_args)
622
623 - def unmonitor_uri(self, uri):
624 """ 625 Stop monitoring given uri. 626 627 @param uri: the URI representing the file or directory to monitor 628 @type uri: L{elisa.core.media_uri.MediaUri} 629 """ 630 return self._proxy('unmonitor_uri', None, uri)
631
632 - def uri_is_monitorable(self, uri):
633 """ 634 Check if the uri is monitorable for modification 635 636 @param uri: the URI representing the file or directory for 637 which we would like to know if it is monitorable or not 638 @type uri: L{elisa.core.media_uri.MediaUri} 639 @rtype: bool 640 """ 641 return self._proxy('uri_is_monitorable', False, uri)
642
643 - def uri_is_monitored(self, uri):
644 """ 645 Check if the uri is currently monitored for modification 646 647 @param uri: the URI representing the file or directory for 648 which we would like to know if it is currently 649 monitored or not 650 @type uri: L{elisa.core.media_uri.MediaUri} 651 @rtype: bool 652 """ 653 return self._proxy('uri_is_monitored', False, uri)
654
655 - def copy(self, orig_uri, dest_uri, recursive=False):
656 """ 657 Copy one location to another. If both URIs represent a 658 directory and recursive flag is set to True I will recursively 659 copy the directory to the destination URI. 660 661 @param orig_uri: the URI to copy, can represent either a directory or 662 a file 663 @type orig_uri: L{elisa.core.media_uri.MediaUri} 664 @param dest_uri: the destination URI, can represent either a directory 665 or a file 666 @type dest_uri: L{elisa.core.media_uri.MediaUri} 667 @param recursive: if orig_uri represents a directory, should I copy it 668 recursively to dest_uri? 669 @type recursive: bool 670 @rtype: bool 671 """ 672 return self._proxy('copy', False, orig_uri, dest_uri, 673 recursive=recursive)
674
675 - def move(self, orig_uri, dest_uri):
676 """ 677 Move data located at given URI to another URI. If orig_uri 678 represents a directory it will recusively be moved to 679 dest_uri. In the case where orig_uri is a directory, dest_uri 680 can't be a file. 681 682 @param orig_uri: the URI to move, can represent either a directory or 683 a file 684 @type orig_uri: L{elisa.core.media_uri.MediaUri} 685 @param dest_uri: the destination URI, can represent either a directory 686 or a file 687 @type dest_uri: L{elisa.core.media_uri.MediaUri} 688 @rtype: bool 689 """ 690 return self._proxy('move', False, orig_uri, dest_uri)
691
692 - def delete(self, uri, recursive=False):
693 """ 694 Delete a resource located at given URI. If that URI represents 695 a directory and recursive flag is set to True I will 696 recursively remove the directory. 697 698 @param uri: the URI representing the file or directory for 699 which we would like to know if it is currently 700 monitored or not 701 @type uri: L{elisa.core.media_uri.MediaUri} 702 @param recursive: if orig_uri represents a directory, should I copy it 703 recursively to dest_uri? 704 @type recursive: bool 705 @rtype: bool 706 """ 707 return self._proxy('delete', False, uri, recursive=recursive)
708
709 - def get_real_uri(self, uri):
710 """ 711 Returns the original uri (reachable) from a virtual 712 uri representation. 713 714 @param uri: the URI to validate 715 @type uri: L{elisa.core.media_uri.MediaUri} 716 @rtype: L{elisa.core.media_uri.MediaUri} 717 """ 718 return self._proxy('get_real_uri', None, uri)
719