Source code for coherence.backends.tracker_storage

# Licensed under the MIT license
# http://opensource.org/licenses/mit-license.php

# Copyright 2008, Frank Scholz <coherence@beebits.net>

import os.path

import dbus
import dbus.service
from twisted.internet import defer
from twisted.python import util

import coherence.extern.louie as louie
from coherence.backend import BackendItem, BackendStore
from coherence.upnp.core import DIDLLite

ROOT_CONTAINER_ID = 0
AUDIO_CONTAINER_ID = 100
AUDIO_ALL_CONTAINER_ID = 101
AUDIO_ARTIST_CONTAINER_ID = 102
AUDIO_ALBUM_CONTAINER_ID = 103
AUDIO_PLAYLIST_CONTAINER_ID = 104
AUDIO_GENRE_CONTAINER_ID = 105

VIDEO_CONTAINER_ID = 200
VIDEO_ALL_CONTAINER_ID = 201

IMAGE_CONTAINER_ID = 300
IMAGE_ALL_CONTAINER_ID = 301

BUS_NAME = 'org.freedesktop.Tracker'
OBJECT_PATH = '/org/freedesktop/tracker'

tracks_query = """
<rdfq:Condition>\
<rdfq:equals>\
<rdfq:Property name="Audio:Title" />\
<rdf:String>*</rdf:String>\
</rdfq:equals>\
</rdfq:Condition>\
"""

video_query = """
<rdfq:Condition>\
<rdfq:equals>\
<rdfq:Property name="File:Name" />\
<rdf:String>*</rdf:String>\
</rdfq:equals>\
</rdfq:Condition>\
"""

image_query = """
<rdfq:Condition>\
<rdfq:equals>\
<rdfq:Property name="File:Name" />\
<rdf:String>*</rdf:String>\
</rdfq:equals>\
</rdfq:Condition>\
"""


[docs]class Container(BackendItem): logCategory = 'tracker_store' def __init__(self, id, parent_id, name, store=None, children_callback=None, container_class=DIDLLite.Container): BackendItem.__init__(self) self.id = id self.parent_id = parent_id self.name = name self.mimetype = 'directory' self.item = container_class(id, parent_id, self.name) self.item.childCount = 0 self.update_id = 0 if children_callback is not None: self.children = children_callback else: self.children = util.OrderedDict() self.item.childCount = None # self.get_child_count() if store is not None: self.get_url = lambda: store.urlbase + str(self.id)
[docs] def add_child(self, child): id = child.id if isinstance(child.id, str): _, id = child.id.split('.') self.children[id] = child if self.item.childCount is not None: self.item.childCount += 1
[docs] def get_children(self, start=0, end=0): self.info("container.get_children %r %r", start, end) if callable(self.children): return self.children(start, end - start) else: children = list(self.children.values()) if end == 0: return children[start:] else: return children[start:end]
[docs] def get_child_count(self): if self.item.childCount is not None: return self.item.childCount if callable(self.children): return len(self.children()) else: return len(self.children)
[docs] def get_item(self): return self.item
[docs] def get_name(self): return self.name
[docs] def get_id(self): return self.id
[docs]class Artist(BackendItem): logCategory = 'tracker_store' def __init__(self, store, id, name): BackendItem.__init__(self) self.store = store self.id = 'artist.%d' % int(id) self.name = name self.children = {} self.sorted_children = None
[docs] def add_child(self, child): _, id = child.id.split('.') self.children[id] = child
[docs] def sort_children(self): if self.sorted_children is None: def childs_key_sort(x): return self.children[x].name self.sorted_children = list(self.children.keys()) self.sorted_children = sorted(self.sorted_children, key=childs_key_sort) return self.sorted_children
[docs] def get_artist_all_tracks(self, start=0, request_count=0): children = [] for album in self.sort_children(): children += album.get_children() if request_count == 0: return children[start:] else: return children[start:request_count]
[docs] def get_children(self, start=0, end=0): children = [] for key in self.sort_children(): children.append(self.children[key]) if end == 0: return children[start:] else: return children[start:end]
[docs] def get_child_count(self): return len(self.children)
[docs] def get_item(self, parent_id=AUDIO_ARTIST_CONTAINER_ID): item = DIDLLite.MusicArtist(self.id, parent_id, self.name) return item
[docs] def get_id(self): return self.id
[docs] def get_name(self): return self.name
[docs]class Album(BackendItem): logCategory = 'tracker_store' def __init__(self, store, id, title, artist): BackendItem.__init__(self) self.store = store self.id = 'album.%d' % int(id) self.name = str(title) self.artist = str(artist) self.cover = None self.children = {} self.sorted_children = None
[docs] def add_child(self, child): _, id = child.id.split('.') self.children[id] = child
[docs] def get_children(self, start=0, end=0): children = [] if self.sorted_children is not None: for key in self.sorted_children: children.append(self.children[key]) else: def childs_key_sort(x): return self.children[x].track_nr self.sorted_children = list(self.children.keys()) self.sorted_children = \ sorted(self.sorted_children, key=childs_key_sort) for key in self.sorted_children: children.append(self.children[key]) if end == 0: return children[start:] else: return children[start:end]
[docs] def get_child_count(self): return len(self.children)
[docs] def get_item(self, parent_id=AUDIO_ALBUM_CONTAINER_ID): item = DIDLLite.MusicAlbum(self.id, parent_id, self.name) item.childCount = self.get_child_count() item.artist = self.artist item.albumArtURI = self.cover return item
[docs] def get_id(self): return self.id
[docs] def get_name(self): return self.name
[docs] def get_cover(self): return self.cover
[docs]class Track(BackendItem): logCategory = 'tracker_store' def __init__(self, store, id, parent_id, file, title, artist, album, genre, duration, track_number, size, mimetype): BackendItem.__init__(self) self.store = store self.id = 'song.%d' % int(id) self.parent_id = parent_id self.path = str(file) duration = str(duration).strip() duration = duration.split('.')[0] if len(duration) == 0: duration = 0 seconds = int(duration) hours = seconds / 3600 seconds = seconds - hours * 3600 minutes = seconds / 60 seconds = seconds - minutes * 60 self.duration = ("%d:%02d:%02d") % (hours, minutes, seconds) self.bitrate = 0 self.title = str(title) self.artist = str(artist) self.album = str(album) self.genre = str(genre) track_number = str(track_number).strip() if len(track_number) == 0: track_number = 1 self.track_nr = int(track_number) self.cover = None self.mimetype = str(mimetype) self.size = int(size) self.url = self.store.urlbase + str(self.id)
[docs] def get_children(self, start=0, end=0): return []
[docs] def get_child_count(self): return 0
[docs] def get_item(self, parent_id=None): self.debug("Track get_item %r @ %r", self.id, self.parent_id) # create item item = DIDLLite.MusicTrack(self.id, self.parent_id) item.album = self.album item.artist = self.artist # item.date = item.genre = self.genre item.originalTrackNumber = self.track_nr item.title = self.title item.albumArtURI = self.cover # add http resource res = DIDLLite.Resource(self.url, 'http-get:*:%s:*' % self.mimetype) if self.size > 0: res.size = self.size if self.duration > 0: res.duration = str(self.duration) if self.bitrate > 0: res.bitrate = str(self.bitrate) item.res.append(res) # if self.store.server.coherence.config.get( # 'transcoding', 'no') == 'yes': # if self.mimetype in ('audio/mpeg', # 'application/ogg','audio/ogg', # 'audio/x-m4a', # 'application/x-flac'): # dlna_pn = 'DLNA.ORG_PN=LPCM' # dlna_tags = DIDLLite.simple_dlna_tags[:] # dlna_tags[1] = 'DLNA.ORG_CI=1' # #dlna_tags[2] = 'DLNA.ORG_OP=00' # new_res = DIDLLite.Resource(self.url+'?transcoded=lpcm', # 'http-get:*:%s:%s' % ( # 'audio/L16;rate=44100;channels=2', ';'.join( # [dlna_pn]+dlna_tags))) # new_res.size = None # if self.duration > 0: # new_res.duration = str(self.duration) # item.res.append(new_res) # # if self.mimetype != 'audio/mpeg': # new_res = DIDLLite.Resource(self.url+'?transcoded=mp3', # 'http-get:*:%s:*' % 'audio/mpeg') # new_res.size = None # if self.duration > 0: # new_res.duration = str(self.duration) # item.res.append(new_res) return item
[docs] def get_id(self): return self.id
[docs] def get_name(self): return self.title
[docs] def get_url(self): return self.url
[docs] def get_path(self): return self.path
[docs]class Video(BackendItem): logCategory = 'tracker_store' def __init__(self, store, id, parent_id, file, title, duration, size, mimetype): BackendItem.__init__(self) self.store = store self.id = 'video.%d' % int(id) self.parent_id = parent_id self.path = str(file) duration = str(duration).strip() duration = duration.split('.')[0] if len(duration) == 0: duration = 0 seconds = int(duration) hours = seconds / 3600 seconds = seconds - hours * 3600 minutes = seconds / 60 seconds = seconds - minutes * 60 self.duration = ("%d:%02d:%02d") % (hours, minutes, seconds) self.title = str(title) self.cover = None self.mimetype = str(mimetype) self.size = int(size) self.url = self.store.urlbase + str(self.id)
[docs] def get_children(self, start=0, end=0): return []
[docs] def get_child_count(self): return 0
[docs] def get_item(self, parent_id=None): self.debug("Video get_item %r @ %r", self.id, self.parent_id) # create item item = DIDLLite.VideoItem(self.id, self.parent_id) # item.date = item.title = self.title item.albumArtURI = self.cover # add http resource res = DIDLLite.Resource(self.url, 'http-get:*:%s:*' % self.mimetype) if self.size > 0: res.size = self.size if self.duration > 0: res.duration = str(self.duration) item.res.append(res) return item
[docs] def get_id(self): return self.id
[docs] def get_name(self): return self.title
[docs] def get_url(self): return self.url
[docs] def get_path(self): return self.path
[docs]class Image(BackendItem): logCategory = 'tracker_store' def __init__(self, store, id, parent_id, file, title, album, date, width, height, size, mimetype): BackendItem.__init__(self) self.store = store self.id = 'image.%d' % int(id) self.parent_id = parent_id self.path = str(file) self.title = str(title) self.album = str(album.strip()) self.mimetype = str(mimetype) self.size = int(size) self.url = self.store.urlbase + str(self.id)
[docs] def get_children(self, start=0, end=0): return []
[docs] def get_child_count(self): return 0
[docs] def get_item(self, parent_id=None): self.debug("Image get_item %r @ %r", self.id, self.parent_id) # create item item = DIDLLite.ImageItem(self.id, self.parent_id) # item.date = item.title = self.title # add http resource res = DIDLLite.Resource(self.url, 'http-get:*:%s:*' % self.mimetype) if self.size > 0: res.size = self.size item.res.append(res) return item
[docs] def get_id(self): return self.id
[docs] def get_name(self): return self.title
[docs] def get_url(self): return self.url
[docs] def get_path(self): return self.path
[docs]class TrackerStore(BackendStore): """ this is a backend to Meta Tracker http://www.gnome.org/projects/tracker/index.html """ implements = ['MediaServer'] logCategory = 'tracker_store' def __init__(self, server, **kwargs): if server.coherence.config.get('use_dbus', 'no') != 'yes': raise Exception( 'this backend needs use_dbus enabled in the configuration') BackendStore.__init__(self, server, **kwargs) self.config = kwargs self.name = kwargs.get('name', 'Tracker') self.update_id = 0 self.token = None self.songs = 0 self.albums = 0 self.artists = 0 self.playlists = 0 self.genres = 0 self.videos = 0 self.images = 0 self.bus = dbus.SessionBus() tracker_object = self.bus.get_object(BUS_NAME, OBJECT_PATH) self.tracker_interface = dbus.Interface( tracker_object, 'org.freedesktop.Tracker') self.search_interface = dbus.Interface( tracker_object, 'org.freedesktop.Tracker.Search') self.keywords_interface = dbus.Interface( tracker_object, 'org.freedesktop.Tracker.Keywords') self.metadata_interface = dbus.Interface( tracker_object, 'org.freedesktop.Tracker.Metadata') self.query_id = -1 self.containers = {} self.tracks = {} self.containers[ROOT_CONTAINER_ID] = \ Container(ROOT_CONTAINER_ID, -1, self.name, store=self) def queries_finished(r): louie.send('Coherence.UPnP.Backend.init_completed', None, backend=self) def queries_failed(r): error = '' louie.send('Coherence.UPnP.Backend.init_failed', None, backend=self, msg=error) services = kwargs.get('service', 'Music,Videos,Images') services = [x.strip().lower() for x in services.split(',')] ml = [] mapping = {'music': self.get_tracks, 'videos': self.get_videos, 'images': self.get_images} for service in services: try: ml.append(mapping[service]()) except KeyError: self.warning('Wrong Tracker service definition - %r', service) if len(ml) > 0: dl = defer.DeferredList(ml) dl.addCallback(queries_finished) dl.addErrback( lambda x: louie.send( 'Coherence.UPnP.Backend.init_failed', None, backend=self, msg='Connection to Tracker service(s) failed!')) else: louie.send( 'Coherence.UPnP.Backend.init_failed', None, backend=self, msg='No Tracker service defined!') def __repr__(self): return "TrackerStore"
[docs] def get_by_id(self, id): self.info("looking for id %r", id) if isinstance(id, str): id = id.split('@', 1)[0] elif isinstance(id, bytes): id = id.decode('utf-8').split('@', 1)[0] if isinstance(id, str) and id.startswith('artist_all_tracks_'): try: return self.containers[id] except KeyError: return None item = None try: id = int(id) item = self.containers[id] except (ValueError, KeyError): try: type, id = id.split('.') if type == 'song': return self.containers[AUDIO_ALL_CONTAINER_ID].children[id] if type == 'album': return self.containers[AUDIO_ALBUM_CONTAINER_ID].children[ id] if type == 'artist': return self.containers[AUDIO_ARTIST_CONTAINER_ID].children[ id] if type == 'video': return self.containers[VIDEO_ALL_CONTAINER_ID].children[id] if type == 'image': return self.containers[IMAGE_ALL_CONTAINER_ID].children[id] except (ValueError, KeyError): return None return item
[docs] def get_videos(self): def handle_error(error): print(error) return error def parse_videos_query_result(resultlist): videos = [] for video in resultlist: file, _, title, duration, size, mimetype = video title = title.strip() if len(title) == 0: title = os.path.basename(file) if mimetype == 'video/x-theora+ogg': mimetype = 'video/ogg' video_item = Video(self, self.videos, VIDEO_ALL_CONTAINER_ID, file, title, duration, size, mimetype) self.videos += 1 videos.append(video_item) videos = sorted( videos, key=lambda x, y: (x.get_name().lower() > y.get_name().lower()) - (x.get_name().lower() < y.get_name().lower())) for video_item in videos: self.containers[VIDEO_ALL_CONTAINER_ID].add_child(video_item) self.containers[VIDEO_CONTAINER_ID] = \ Container(VIDEO_CONTAINER_ID, ROOT_CONTAINER_ID, 'Video', store=self) self.containers[ROOT_CONTAINER_ID].add_child( self.containers[VIDEO_CONTAINER_ID]) self.containers[VIDEO_ALL_CONTAINER_ID] = \ Container(VIDEO_ALL_CONTAINER_ID, VIDEO_CONTAINER_ID, 'All Videos', store=self, children_callback=None) self.containers[VIDEO_CONTAINER_ID].add_child( self.containers[VIDEO_ALL_CONTAINER_ID]) fields = ['Video:Title', 'Video:Duration', 'File:Size', 'File:Mime'] d = defer.Deferred() d.addCallback(parse_videos_query_result) d.addErrback(handle_error) self.search_interface.Query( self.query_id, 'Videos', fields, '', '', video_query, False, 0, -1, reply_handler=lambda x: d.callback(x), error_handler=lambda x: d.errback(x)) return d
[docs] def get_images(self): def handle_error(error): return error def parse_images_query_result(resultlist): print("images", resultlist) images = [] for image in resultlist: file, _, title, album, \ date, width, height, \ size, mimetype = image title = title.strip() if len(title) == 0: title = os.path.basename(file) image_item = Image( self, self.images, IMAGE_ALL_CONTAINER_ID, file, title, album, date, width, height, size, mimetype) self.images += 1 images.append(image_item) images = sorted( images, key=lambda x, y: (x.get_name().lower() > y.get_name().lower()) - (x.get_name().lower() < y.get_name().lower())) for image_item in images: self.containers[IMAGE_ALL_CONTAINER_ID].add_child(image_item) self.containers[IMAGE_CONTAINER_ID] = \ Container(IMAGE_CONTAINER_ID, ROOT_CONTAINER_ID, 'Images', store=self) self.containers[ROOT_CONTAINER_ID].add_child( self.containers[IMAGE_CONTAINER_ID]) self.containers[IMAGE_ALL_CONTAINER_ID] = \ Container(IMAGE_ALL_CONTAINER_ID, IMAGE_CONTAINER_ID, 'All Images', store=self, children_callback=None) self.containers[IMAGE_CONTAINER_ID].add_child( self.containers[IMAGE_ALL_CONTAINER_ID]) fields = ['Image:Title', 'Image:Album', 'Image:Date', 'Image:Width', 'Image:Height', 'File:Size', 'File:Mime'] d = defer.Deferred() d.addCallback(parse_images_query_result) d.addErrback(handle_error) self.search_interface.Query( self.query_id, 'Images', fields, '', '', image_query, False, 0, -1, reply_handler=lambda x: d.callback(x), error_handler=lambda x: d.errback(x)) return d
[docs] def get_tracks(self): def handle_error(error): return error def parse_tracks_query_result(resultlist): albums = {} artists = {} tracks = [] for track in resultlist: file, service, title, artist, album, genre, \ duration, album_track_count, \ track_number, codec, \ size, mimetype = track if mimetype == 'video/x-vorbis+ogg': mimetype = 'audio/ogg' track_item = Track( self, self.songs, AUDIO_ALL_CONTAINER_ID, file, title, artist, album, genre, duration, track_number, size, mimetype) self.songs += 1 tracks.append(track_item) tracks = sorted( tracks, key=lambda x, y: (x.get_name() > y.get_name()) - (x.get_name() < y.get_name())) for track_item in tracks: self.containers[AUDIO_ALL_CONTAINER_ID].add_child(track_item) try: album_item = albums[track_item.album] album_item.add_child(track_item) except KeyError: album_item = Album(self, self.albums, track_item.album, track_item.artist) albums[str(track_item.album)] = album_item self.albums += 1 album_item.add_child(track_item) try: artist_item = artists[track_item.artist] artist_item.add_child(album_item) except KeyError: artist_item = Artist(self, self.artists, track_item.artist) artists[str(track_item.artist)] = artist_item self.artists += 1 artist_item.add_child(album_item) sorted_keys = list(albums.keys()) sorted_keys.sort() for key in sorted_keys: self.containers[AUDIO_ALBUM_CONTAINER_ID].add_child( albums[key]) sorted_keys = list(artists.keys()) sorted_keys.sort() for key in sorted_keys: self.containers[AUDIO_ARTIST_CONTAINER_ID].add_child( artists[key]) self.containers[AUDIO_CONTAINER_ID] = \ Container(AUDIO_CONTAINER_ID, ROOT_CONTAINER_ID, 'Audio', store=self) self.containers[ROOT_CONTAINER_ID].add_child( self.containers[AUDIO_CONTAINER_ID]) self.containers[AUDIO_ALL_CONTAINER_ID] = \ Container(AUDIO_ALL_CONTAINER_ID, AUDIO_CONTAINER_ID, 'All Tracks', store=self, children_callback=None) self.containers[AUDIO_CONTAINER_ID].add_child( self.containers[AUDIO_ALL_CONTAINER_ID]) self.containers[AUDIO_ALBUM_CONTAINER_ID] = \ Container(AUDIO_ALBUM_CONTAINER_ID, AUDIO_CONTAINER_ID, 'Albums', store=self, children_callback=None) self.containers[AUDIO_CONTAINER_ID].add_child( self.containers[AUDIO_ALBUM_CONTAINER_ID]) self.containers[AUDIO_ARTIST_CONTAINER_ID] = \ Container(AUDIO_ARTIST_CONTAINER_ID, AUDIO_CONTAINER_ID, 'Artists', store=self, children_callback=None) self.containers[AUDIO_CONTAINER_ID].add_child( self.containers[AUDIO_ARTIST_CONTAINER_ID]) self.containers[AUDIO_PLAYLIST_CONTAINER_ID] = \ Container(AUDIO_PLAYLIST_CONTAINER_ID, AUDIO_CONTAINER_ID, 'Playlists', store=self, children_callback=None, container_class=DIDLLite.PlaylistContainer) self.containers[AUDIO_CONTAINER_ID].add_child( self.containers[AUDIO_PLAYLIST_CONTAINER_ID]) self.containers[AUDIO_GENRE_CONTAINER_ID] = \ Container(AUDIO_GENRE_CONTAINER_ID, AUDIO_CONTAINER_ID, 'Genres', store=self, children_callback=None) self.containers[AUDIO_CONTAINER_ID].add_child( self.containers[AUDIO_GENRE_CONTAINER_ID]) self.wmc_mapping.update( {'4': lambda: self.get_by_id(AUDIO_ALL_CONTAINER_ID), # all tracks '5': lambda: self.get_by_id(AUDIO_GENRE_CONTAINER_ID), # all genres '6': lambda: self.get_by_id(AUDIO_ARTIST_CONTAINER_ID), # all artists '7': lambda: self.get_by_id(AUDIO_ALBUM_CONTAINER_ID), # all albums '13': lambda: self.get_by_id(AUDIO_PLAYLIST_CONTAINER_ID), # all playlists }) fields = ['Audio:Title', 'Audio:Artist', 'Audio:Album', 'Audio:Genre', 'Audio:Duration', 'Audio:AlbumTrackCount', 'Audio:TrackNo', 'Audio:Codec', 'File:Size', 'File:Mime'] d = defer.Deferred() d.addCallback(parse_tracks_query_result) d.addErrback(handle_error) self.search_interface.Query( self.query_id, 'Music', fields, '', '', tracks_query, False, 0, -1, reply_handler=lambda x: d.callback(x), error_handler=lambda x: d.errback(x)) return d
[docs] def upnp_init(self): if self.server: self.server.connection_manager_server.set_variable( 0, 'SourceProtocolInfo', ['http-get:*:audio/mpeg:*', 'internal:%s:audio/mpeg:*' % self.server.coherence.hostname, 'http-get:*:application/ogg:*', 'internal:%s:application/ogg:*' % self.server.coherence.hostname, 'http-get:*:audio/ogg:*', 'internal:%s:audio/ogg:*' % self.server.coherence.hostname, 'http-get:*:video/ogg:*', 'internal:%s:video/ogg:*' % self.server.coherence.hostname, 'http-get:*:video/mpeg:*', 'internal:%s:video/mpeg:*' % self.server.coherence.hostname, 'http-get:*:video/x-msvideo:*', 'internal:%s:video/x-msvideo:*' % self.server.coherence.hostname, 'http-get:*:video/avi:*', 'internal:%s:video/avi:*' % self.server.coherence.hostname, 'http-get:*:video/mp4:*', 'internal:%s:video/mp4:*' % self.server.coherence.hostname, 'http-get:*:video/quicktime:*', 'internal:%s:video/quicktime:*' % self.server.coherence.hostname, 'http-get:*:image/jpg:*', 'internal:%s:image/jpg:*' % self.server.coherence.hostname, 'http-get:*:image/png:*', 'internal:%s:image/png:*' % self.server.coherence.hostname, 'http-get:*:image/gif:*', 'internal:%s:image/gif:*' % self.server.coherence.hostname, ])