Home | Trees | Indices | Help |
---|
|
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 140142 """ 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 """ 159472 475161 """ 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 = False180182 """ 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 db209211 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 opts221 224226 """ 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)238240 """ 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)248250 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 pass260 #FIXME: needs to be raised? 261263 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 pass272 #FIXME: needs to be raised? 273275 """just a proxy to the metadata_manager.get_metadata""" 276 return self._metadata_manager.get_metadata(metadata, low_priority=False)277279 """ 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)296298 """ 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)314316 """ 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)324326 """ 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)336338 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)347349 """ 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)361363 """ 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 provider382384 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 result392394 provider = self._get_media_provider(uri) 395 scannable = provider and uri.scheme in provider.scannable_uri_schemes 396 return scannable397399 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 source419421 """ 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_type466 467 dfr = self._proxy('get_media_type', {}, uri) 468 if self.media_db: 469 dfr.addCallback(get_media_type_done, uri) 470 471 return dfr477 """ 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 488490 """ 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 506508 """ 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 532534 """ 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 550552 """ 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 location574 575 result.addCallback(got_location) 576 return result 577579 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 location589591 """ 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 603605 """ 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)622624 """ 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)631633 """ 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)642644 """ 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)654656 """ 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)674676 """ 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)691693 """ 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 719
Home | Trees | Indices | Help |
---|
Generated by Epydoc 3.0beta1 on Wed Jan 16 19:10:59 2008 | http://epydoc.sourceforge.net |