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

Source Code for Module elisa.core.media_db

  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  Media database, keeping references of elisa medias and their metadata 
 19  """ 
 20   
 21   
 22  __maintainer__ = 'Philippe Normand <philippe@fluendo.com>' 
 23   
 24  from elisa.core import log, common, db_backend 
 25  from elisa.extern import natural_sort 
 26  from elisa.core.media_uri import MediaUri, quote, unquote 
 27  from elisa.base_components.media_provider import NotifyEvent 
 28  from elisa.core.observers.dict import DictObservable 
 29   
 30   
 31  CURRENT_DB_VERSION=1 
 32   
 33  # FIXME : schema is incomplete 
 34  SQL_SCHEMA="""\ 
 35  CREATE TABLE core_meta ( 
 36      version INTEGER UNIQUE 
 37  ); 
 38   
 39  CREATE TABLE core_source ( 
 40      id INTEGER PRIMARY KEY AUTOINCREMENT, 
 41      uri TEXT NOT NULL, 
 42      short_name TEXT, 
 43      available INT DEFAULT 1 
 44  ); 
 45   
 46  CREATE TABLE core_media ( 
 47      id INTEGER PRIMARY KEY AUTOINCREMENT, 
 48      uri TEXT UNIQUE NOT NULL, 
 49      short_name TEXT, 
 50      media_node_id INT DEFAULT 0, 
 51      source_id INT DEFAULT 0, 
 52      format TEXT DEFAULT '', 
 53      typ TEXT DEFAULT '', 
 54      deleted INT DEFAULT 0, 
 55      rating INT DEFAULT 50, 
 56      date_added DATETIME, 
 57      last_played DATETIME DEFAULT NULL, 
 58      fs_mtime DATETIME DEFAULT NULL, 
 59      updated INT DEFAULT 0 
 60  ); 
 61   
 62  CREATE TABLE core_audio_media ( 
 63      media_id INT UNIQUE, 
 64      artist TEXT DEFAULT 'unknown artist', 
 65      album TEXT DEFAULT 'unknown album', 
 66      song TEXT DEFAULT '', 
 67      track INT DEFAULT 0, 
 68      cover_uri TEXT DEFAULT '' 
 69  ); 
 70   
 71  CREATE TABLE core_video_media ( 
 72      media_id INT UNIQUE 
 73  ); 
 74   
 75  CREATE TABLE core_image_media ( 
 76      media_id INT UNIQUE 
 77  ); 
 78   
 79  CREATE UNIQUE INDEX INDX_SOURCE_ID ON core_source(id); 
 80   
 81  CREATE UNIQUE INDEX INDX_MEDIA_URI ON core_media(uri); 
 82  CREATE INDEX INDX_MEDIA_SOURCEID ON core_media(source_id); 
 83  CREATE INDEX INDX_MEDIA_MD_ID ON core_media(media_node_id); 
 84  CREATE INDEX INDX_MEDIA_ARTIST ON core_audio_media(artist); 
 85   
 86  CREATE INDEX INDX_AUDIO_MEDIA_ID ON core_audio_media(media_id); 
 87  CREATE INDEX INDX_VIDEO_MEDIA_ID ON core_video_media(media_id); 
 88  CREATE INDEX INDX_IMAGE_MEDIA_ID ON core_image_media(media_id); 
 89  """ 
 90   
 91   
92 -class MediaDB(log.Loggable):
93 """ Elisa Media database store 94 95 I'm keeping a cache of media source hierarchies in a database. I 96 use the L{elisa.core.db_backend} to communicate with supported 97 database backends. 98 99 Media sources are basically roots of media locations, like ~/Music 100 folder for audio content for instance. Media sources are referenced in 101 the "source" db table. 102 103 Medias can be both files and directories. Each Media has a parent 104 media id and a source id. Content-type specific information are 105 stored in diferent tables for audio, video and images. 106 107 """ 108 109 # available metadata: key = name used in URI, value = db field 110 available_meta = { 111 'artists' : 'artist', 112 'albums' : 'album', 113 'files' : 'uri' 114 } 115
116 - def __init__(self, backend=None, first_load=False):
117 """ 118 Initialize our _backend instance variable. If backend is None 119 we retrieve one in the Application. 120 121 @keyword backend: the database backend to use 122 @type backend: L{elisa.core.db_backend.DBBackend} or 123 None (to use application's) 124 @keyword first_load: is it the first time the db is loaded? 125 @type first_load: bool 126 """ 127 self.log_category = "media_db" 128 log.Loggable.__init__(self) 129 130 if not backend: 131 backend = common.application.db_backend 132 133 self._backend = backend 134 135 if first_load: 136 self._check_schema()
137
138 - def close(self):
139 """ 140 Disconnect the backend 141 """ 142 self._backend.disconnect()
143
144 - def _check_schema(self):
145 version = 0 146 query = "select version from core_meta" 147 version_row = self._backend.sql_execute(query, quiet=True) 148 if version_row: 149 version = version_row[0].version 150 151 if version < CURRENT_DB_VERSION: 152 # FIXME: handle future db schema upgrades 153 self._reset()
154
155 - def _reset(self):
156 self.debug("Resetting the database") 157 158 # drop existing tables 159 for table_name in db_backend.table_names(SQL_SCHEMA): 160 self._backend.sql_execute("drop table %s" % table_name, quiet=True) 161 162 # create tables 163 for sql in SQL_SCHEMA.split(';'): 164 if sql: 165 self._backend.sql_execute(sql) 166 167 168 specific_sql = db_backend.BACKEND_SPECIFIC_SQL.get(self._backend.name) 169 if specific_sql: 170 for query in specific_sql.split(';'): 171 self._backend.sql_execute(query) 172 173 self._backend.sql_execute("insert into core_meta(version) values(?)", 174 CURRENT_DB_VERSION) 175 self._backend.save_changes() 176 self._backend.reconnect()
177
178 - def get_files_count_for_source_uri(self, source_uri):
179 """ 180 DOCME 181 """ 182 request = "select count(*) as c from core_media media, " \ 183 "core_source source where source.uri = '%s' and " \ 184 "media.source_id = source.id" % source_uri 185 count = self._backend.sql_execute(request)[0].c 186 return count
187 188
189 - def prepare_source_for_update(self, source):
190 """ 191 DOCME 192 """ 193 # TODO: move this to media_db 194 self.info('Preparing %s' % source.uri) 195 req = 'update core_media set updated=0 where source_id=%s and deleted=0' % source.id 196 self._backend.sql_execute(req)
197
198 - def hide_un_updated_medias_for_source(self, source):
199 """ 200 DOCME 201 """ 202 # TODO: move this to media_db 203 req = 'select uri, typ, format from core_media where updated=0 and source_id=%s' % source.id 204 rows = self._backend.sql_execute(req) 205 count = len(rows) 206 if count: 207 self.info("Deleting %s medias from db" % count) 208 req = 'update core_media set updated=1, deleted=1 where source_id=%s and updated=0' % source.id 209 self._backend.sql_execute(req) 210 return rows
211
212 - def add_source(self, uri, short_name):
213 """ Add a new media source in the database 214 215 Add a new row in the "source" table. Source scanning is not handled 216 by this method. See the L{elisa.core.media_scanner.MediaScanner} 217 for that. 218 219 @param uri: the URI identifying the media source 220 @type uri: L{elisa.core.media_uri.MediaUri} 221 @type short_name: display friendly name of the source 222 @param short_name: string 223 @returns: the newly created source. 224 @rtype: L{elisa.extern.db_row.DBRow} 225 """ 226 self.info("Adding source %s" % uri) 227 request = "insert into core_source(uri, short_name) values(?, ?)" 228 self._backend.sql_execute(request, uri, short_name) 229 source = self.get_source_for_uri(uri) 230 now = 'current_timestamp' 231 request = "insert into core_media(uri, updated, date_added, media_node_id, short_name, source_id) values (?, 1, %s, -1, ?, ?)" % now 232 args = (uri, short_name, source.id) 233 #self._backend.sql_execute(request, *args) 234 self._backend.save_changes() 235 #REVIEW: is the cost of saving changes high ? if we add a lot of sources 236 #in a row, it might be better to call one time the saving. Could be a 237 #boolean parameter 238 return source
239
240 - def hide_source(self, source):
241 """ Mark a source as unavailable in the database. 242 243 Update the "available" flag of the given source record in the "source" 244 table. Return True if source was correctly hidden 245 246 @param source: the source to mark as unavailable 247 @type source: L{elisa.extern.db_row.DBRow} 248 @rtype: bool 249 """ 250 request = "update core_source set available=0 where id=%s" % source.id 251 self._backend.sql_execute(request) 252 return True
253
254 - def show_source(self, source):
255 """ Mark a source as available in the database. 256 257 Update the "available" flag of the given source record in the "source" 258 table. Return True if source was correctly shown 259 260 @param source: the source to mark as available 261 @type source: L{elisa.extern.db_row.DBRow} 262 @rtype: bool 263 """ 264 request = "update core_source set available=1 where id=%s" % source.id 265 self._backend.sql_execute(request) 266 return True
267
268 - def is_source(self, row):
269 """ 270 DOCME 271 """ 272 return row.has_key('available')
273
274 - def add_media(self, uri, short_name, source_id, content_type, **extra):
275 """ Add a new media in the "media" table and in specialized tables 276 277 There's one specialized table for each content-type (audio, 278 video, picture). The Media can be either a file or a directory. 279 280 If the media is already on database but marked as unavailable 281 or deleted it will be marked as available and undeleted. In 282 that case not further insert will be done. 283 284 @param uri: the URI identifying the media 285 @type uri: L{elisa.core.media_uri.MediaUri} 286 @param short_name: display-friendly media name 287 @type short_name: string 288 @param parent: the source or media to register the media in 289 @type parent: L{elisa.extern.db_row.DBRow} 290 @param content_type: the media content-type ('directory', 'audio', etc) 291 @type content_type: string 292 @param extra: extra row attributes 293 @type extra: dict 294 295 @returns: True if inserted, False if updated 296 @rtype: bool 297 298 @todo: complete keywords list 299 """ 300 301 """ 302 if not force_insert: 303 node = self.get_media_information(uri) 304 if node: 305 if node.deleted and node.artist != 'unknown artist': 306 do_update = True 307 else: 308 do_insert = True 309 """ 310 311 media_exist = self.media_exists(uri) 312 313 extra['source_id'] = source_id 314 metadata = extra.get('metadata',{}) 315 if 'metadata' in extra: 316 del extra['metadata'] 317 318 if not media_exist: 319 320 extras_spc, extras_val, extras_marks = self._kw_extract(extra) 321 if extras_spc: 322 req = "insert or replace into core_media(uri, updated, date_added, short_name,typ, %s) values (?,1, current_timestamp, ?,?,%s)" 323 req = req % (extras_spc, extras_marks) 324 else: 325 req = "insert or replace into core_media(uri, updated, date_added, short_name,typ) values (?, 1, current_timestamp, ?,?)" 326 327 id = self._backend.insert(req, uri, short_name, content_type, 328 *extras_val) 329 330 # add new row in specialized table 331 format = extra.get('format') 332 if format: 333 spec, values, marks = self._kw_extract(metadata) 334 if spec and values: 335 req = "insert or replace into core_%s_media(media_id,%s) values (?,%s)" 336 req = req % (format, spec, marks) 337 args = (id,) + tuple(values) 338 else: 339 req = "insert or replace into core_%s_media(media_id) values (?)" % format 340 args = (id,) 341 self._backend.insert(req, *args) 342 return True 343 344 else: 345 pass 346 #FIXME update is not implemented 347 #extra.update(deleted=0, updated=1) 348 #self.update_media(node, **extra) 349 350 return False
351
352 - def del_media_node(self, media, force_drop=False):
353 """ Mark a media as deleted in database. 354 355 FIXME: document force_drop 356 357 @param media: the media to mark as deleted. 358 @type media: L{elisa.extern.db_row.DBRow} 359 @rtype: bool 360 """ 361 deleted = False 362 if not media.deleted: 363 if force_drop: 364 self.info("Dropping Media %r from DB", media.uri) 365 if media.format: 366 req = "delete from core_%s_media where media_node_id=?" 367 self._backend.sql_execute(req, media.id) 368 req = "delete from core_media where id=?" 369 self._backend.sql_execute(req, media.id) 370 else: 371 self.info("Marking the Media %r as unavailable in DB", media.uri) 372 req = "update core_media set deleted=1 where id=?" 373 self._backend.sql_execute(req, media.id) 374 deleted = True 375 return deleted
376
377 - def get_source_for_uri(self, uri):
378 """ Find in which media source the given uri is registered with. 379 380 The URI has to be referenced in the "source" table. 381 382 @param uri: the URI to search in the "source" table 383 @type uri: L{elisa.core.media_uri.MediaUri} 384 @rtype: L{elisa.extern.db_row.DBRow} 385 """ 386 row = None 387 request = "select * from core_source where uri=?" 388 rows = self._backend.sql_execute(request, uri) 389 if rows: 390 row = rows[0] 391 return row
392 393
394 - def media_exists(self, uri):
395 return False 396 request = u"select count(*) from core_media where uri=?" 397 rows = self._backend.sql_execute(request, uri) 398 399 print rows 400 return False
401 402
403 - def get_media_information(self, uri, extended=True, media_type=None):
404 """ Find in database the media corresponding with the given URI. 405 406 The URI has to be referenced in the "media" table. 407 408 @param uri: the URI to search in the "media" table 409 @type uri: L{elisa.core.media_uri.MediaUri} 410 @rtype: L{elisa.extern.db_row.DBRow} 411 """ 412 media = None 413 if extended: 414 if not media_type: 415 request = u"select * from core_media where uri=?" 416 rows = self._backend.sql_execute(request, uri) 417 if rows: 418 media_row = rows[0] 419 if media_row.format: 420 media_type = media_row.format 421 else: 422 media = media_row 423 if media_type and not media: 424 req = "select m.*, s.* from core_media m, core_%s_media s where uri=? and id=media_id" 425 req = req % media_type #, media_row.id) 426 result = self._backend.sql_execute(req, uri) 427 if result: 428 media = result[0] 429 if not media: 430 request = u"select * from core_media where uri=?" 431 rows = self._backend.sql_execute(request, uri) 432 if rows: 433 media = rows[0] 434 435 return media
436
437 - def get_medias(self, source=None, media_type=None):
438 req = 'select * from core_media where' 439 args = () 440 if source: 441 where = 'source_id=%s' % source.id 442 elif media_type: 443 where = 'format=?' 444 args = (media_type,) 445 else: 446 where = 'media_node_id=-1' 447 req = "%s %s and deleted=0" % (req, where) 448 return self._backend.sql_execute(req, *args)
449 450
451 - def get_media_with_id(self, media_id):
452 """ Fetch the media with given id in the database 453 454 @param media_id: the identifier of the Media i have to dig in the db 455 @type media_id: int 456 @rtype: L{elisa.extern.db_row.DBRow} 457 """ 458 media = None 459 request = "select * from core_media where id=%s limit 1" % media_id 460 result = self._backend.sql_execute(request) 461 if result: 462 media = result[0] 463 return media
464 465
466 - def update_media(self, media, **new_values):
467 """ Update some attributes in database of the given media 468 469 @todo: document valid keys of new_values dict 470 471 @param media: the media I'm checking 472 @type media: L{elisa.extern.db_row.DBRow} 473 @param new_values: attributes to update. Keys have to match "media" 474 table column names. 475 @type new_values: dict 476 """ 477 table = "core_media" 478 parsed = {} 479 for k,v in new_values.iteritems(): 480 if type(v) == long: 481 v = int(v) 482 if v is not None: 483 parsed[k] = v 484 values = ', '.join(["%s=?" % k for k in parsed.keys()]) 485 if values: 486 req = "update %s set %s where id=?" % (table, values) 487 args = parsed.values() + [media.id,] 488 self._backend.sql_execute(req, *args)
489
490 - def update_media_metadata(self, media, **metadata):
491 """ 492 DOCME 493 """ 494 format = media.format 495 if not format: 496 format = self._guess_format_from_metadata(metadata) 497 if format: 498 table = "core_%s_media" % format 499 500 req = "select count(media_id) as c, %s.* from %s, core_media where media_id=%s and id=media_id" 501 result = self._backend.sql_execute(req % (table, table, media.id)) 502 if not result or result[0].c == 0: 503 spec, values, marks = self._kw_extract(metadata) 504 if spec: 505 req = "insert into %s(media_id,%s) values(?,%s)" 506 req = req % (table, spec, marks) 507 args = (media.id, ) + tuple(values) 508 else: 509 req = "insert into %s(media_id) values(?)" % table 510 args = (media.id,) 511 self._backend.sql_execute(req, *args) 512 else: 513 media = result[0] 514 parsed = {} 515 for k,v in metadata.iteritems(): 516 if type(v) == long: 517 v = int(v) 518 if v is not None: 519 parsed[k] = v 520 values = ', '.join(["%s=?" % k for k in parsed.keys()]) 521 if self._node_changed(media, parsed) and values: 522 req = "update %s set %s where media_id=?" % (table, values) 523 args = tuple(parsed.values()) + (media.media_id,) 524 self._backend.sql_execute(req, *args)
525 ## for k,v in parsed.iteritems(): 526 ## setattr(parent, k, v) 527 528
529 - def get_next_location(self, uri, root_uri):
530 # FIXME : bahhhhhhhhhhhhhhhh 531 # to many resources used for this 532 533 select_attribute, path_values = self._parse_uri(root_uri) 534 query, args = self._build_request('uri', path_values) 535 rows = self._backend.sql_execute(query, *args) 536 return_next = False 537 538 if rows and str(uri) == str(root_uri): 539 return MediaUri(rows[0][0]) 540 541 for row in rows: 542 if return_next == True: 543 return MediaUri(row[0]) 544 545 if row[0] == str(uri): 546 return_next = True 547 548 return None
549
550 - def _parse_uri(self, uri):
551 552 current_attribute = '' 553 select_attribute = '' 554 path_values = {} 555 for element in uri.path.split('/'): 556 if element != '': 557 if current_attribute == '' and self.available_meta.has_key(element): 558 current_attribute = element 559 select_attribute = current_attribute 560 elif current_attribute != '': 561 path_values[current_attribute] = element 562 current_attribute = '' 563 select_attribute = '' 564 else: 565 self.warning("uri %s is invalid, metadata '%s' not supported" % (uri, element) ) 566 return (False, False) 567 568 return (select_attribute, path_values)
569 570
571 - def _build_request(self, select_clause, path_values, start=0, item_count=-1):
572 #start to build request 573 args = [] 574 where_clause = '' 575 for key in path_values: 576 if path_values[key] != '': 577 val = path_values[key] 578 if self.available_meta[key] != 'uri': 579 val = unquote(path_values[key]) 580 581 where_clause += " %s=? and" % \ 582 (self.available_meta[key]) 583 args.append(val) 584 585 limit_clause = '' 586 if start > 0 and item_count == -1: 587 limit_clause = ' limit %i' % start 588 elif start > 0 and item_count > 1: 589 limit_clause += ' limit %i,%i' % start,item_count 590 591 #remove last 'and' in where_clause 592 where_clause += ' C.id=A.media_id' 593 # FIXME artist != '' due to a biug in metadata : must be filled with unknown_artist 594 where_clause += " and A.artist != '' and A.album != ''" 595 query = "select distinct %s from core_media C, core_audio_media A where%s" % (select_clause, where_clause) 596 query += limit_clause 597 query += ' order by 1' 598 599 return query, args
600 601
602 - def get_uris_by_meta_uri(self, uri, children, start=0, item_count=-1):
603 """ 604 This function can handle an URI with the elisa:// scheme. 605 It returns a list of uris matching the request defined 606 in uri's path 607 608 @param uri: uri representing an elisa:// scheme 609 @type uri: L{elisa.core.media_uri.MediaUri} 610 @param children: uri representing an elisa:// scheme 611 @type children: list of tuple (uri, info) 612 @rtype: list of tuple (string, L{elisa.core.media_uri.MediaUri}, int) 613 """ 614 615 if not self.has_children(uri): 616 return children 617 618 select_attribute, path_values = self._parse_uri(uri) 619 620 if select_attribute == False: 621 return children 622 623 uri_source = unicode(uri) 624 #ADD / at the end of the URI 625 if uri_source[-1] != '/': 626 uri_source += '/' 627 628 # FIXME theses rules mut be automatic with the new database scheme 629 if select_attribute == '': 630 if path_values.has_key('albums'): 631 select_attribute = 'files' 632 elif path_values.has_key('artists') and not path_values.has_key('albums'): 633 select_attribute = 'albums' 634 uri_source += 'albums/' 635 636 #Case A : I know the attribute to return 637 if select_attribute == '': 638 for key in self.available_meta: 639 if key not in path_values.keys(): 640 child_uri = uri_source + key 641 metadata = DictObservable() 642 children.append( (MediaUri(unicode(child_uri)), metadata) ) 643 else: 644 645 if select_attribute == 'files': 646 select_clause = 'track, song, uri, cover_uri' 647 else: 648 select_clause = self.available_meta[select_attribute] 649 650 query, args = self._build_request(select_clause, path_values, start, item_count) 651 rows = self._backend.sql_execute(query, *args) 652 653 for row in rows: 654 child_label = None 655 if select_attribute == 'files': 656 child_uri = row[2] 657 track = row[0] 658 if track > 0: 659 child_label = "%s - %s" % (str(track).zfill(2), 660 row[1]) 661 else: 662 child_label = row[1] 663 else: 664 child_uri = uri_source + quote(row[0]) 665 666 metadata = DictObservable() 667 if select_attribute == 'albums': 668 metadata['album'] = row[0] 669 metadata['artist'] = self.get_artist_from_album(row[0]) 670 elif select_attribute == 'files': 671 default_image = row[3] 672 if default_image == u'': 673 default_image = None 674 else: 675 default_image = MediaUri(default_image) 676 metadata['default_image'] = default_image 677 678 new_uri = MediaUri(unicode(child_uri)) 679 if child_label: 680 new_uri.label = child_label 681 682 children.append( (new_uri, metadata) ) 683 684 return children
685 686
687 - def has_children(self, uri):
688 if 'files' not in uri.path.split('/'): 689 return True 690 691 no_slash_uri = str(uri) 692 if no_slash_uri[-1] == '/': 693 no_slash_uri = uri[0:-1] 694 695 if no_slash_uri.endswith('files'): 696 return True 697 698 return False
699
700 - def get_artist_from_album(self, album):
701 """ 702 DOCME 703 """ 704 # FIXME temporary function : can be removed when de DB will re change 705 # FIXME artist != '' due to a biug in metadata : must be filled with unknown_artist 706 request = "select artist from core_audio_media where album=? and artist != '' limit 1" 707 return self._backend.sql_execute(request, unquote(album))[0][0]
708
709 - def _guess_format_from_metadata(self, metadata):
710 formats = {'audio': self._backend.table_columns('core_audio_media'), 711 'video': self._backend.table_columns('core_video_media'), 712 'picture': self._backend.table_columns('core_image_media') 713 } 714 format = '' 715 keys = set(metadata.keys()) 716 for fmt, col_names in formats.iteritems(): 717 if keys.issubset(col_names): 718 format = fmt 719 break 720 return format
721
722 - def _node_changed(self, node, values):
723 changed = False 724 for key, value in values.iteritems(): 725 stored_value = getattr(node, key) 726 if stored_value != value: 727 changed = True 728 break 729 730 return changed
731 732
733 - def _kw_extract(self, kw):
734 values = [] 735 spec = [] 736 737 for col_name, value in kw.iteritems(): 738 if value == None: 739 continue 740 spec.append(col_name) 741 if type(value) == long: 742 value = int(value) 743 if type(value) not in (int, str, unicode): 744 value = repr(value) 745 values.append(value) 746 747 spec = ','.join(spec) 748 #values = ','.join(values) 749 marks = ','.join('?' * len(values)) 750 if marks.endswith(','): 751 marks = marks[:-1] 752 return (spec, values, marks)
753