class ConnectionServer(threading.Thread): """ Connection server listens incoming connections. On incoming connection ConnectionServer spawns a new ClientConnection thread that handles connection. This thread is registered to the MessageBus. This way backend can handle multiple connections simultaneously. """ def __init__(self, port, message_bus): """ Creates a new ConnectionServer object @param port: Port number for this server @param message_bus: Bind connecting client to this MessageBus object """ threading.Thread.__init__(self) self.message_bus = message_bus # Message bus self.logger = Logger().getLogger('backend.core.ConnectionServer') # Is ConnectionServer active (listening incoming connections) self.active = False self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: self.server_socket.bind(('localhost', port)) except socket.error, e: message = e[1] self.logger.error("Socket failed to bind. %s" % message) # socket binding is critical to the backend, so exit sys.exit(1) self.server_socket.listen(2)
class MediaPlayer(gobject.GObject, object): """MediaPlayer uses Gstreamer to play all video and audio files. Entertainer has only one MediaPlayer object at runtime. MediaPlayer can play objects that implement Playable interface.""" __gsignals__ = { "play": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), "pause": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), "stop": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), "skip-forward": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), "skip-backward": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), "volume_changed": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), "position-changed": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), "refresh": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), } # Ratio constants NATIVE = 0 WIDESCREEN = 1 NORMAL = 2 LETTER_BOX = 3 ZOOM = 4 INTELLIGENT = 5 MODE_NONE = 0 MODE_PLAYPAUSE = 1 MODE_SEEK = 2 def __init__(self, stage, width, height): gobject.GObject.__init__(self) self._motion_buffer = MotionBuffer() self._event_mode = self.MODE_NONE self._motion_handler = 0 self.stage = stage # Stage that displays textures # Stage background color when not playing self.bgcolor = stage.get_color() self.stage_width = width # Stage width used for video resizing self.stage_height = height # Stage height used for video resizing self.ratio = MediaPlayer.NATIVE # Video texture ratio self.audio_skip_step = 10 # Audio skip step in seconds self.video_skip_step = 60 # Video skip step in seconds self.playlist = None # Current play list self.media = None # Current media (Playable object) self.shuffle = False # Shuffle mode self.repeat = False # Repeat mode self.is_playing = False # Is media player currently playing self.is_reactive_allowed = False # Is the video_texture reactive self.logger = Logger().getLogger("client.MediaPlayer") self._internal_callback_timeout_key = None self.video_texture = cluttergst.VideoTexture() self.pipeline = self.video_texture.get_pipeline() self.pipeline.set_property("volume", 0.5) self._volume = 10 self.bus = self.pipeline.get_bus() self.bus.add_signal_watch() self.bus.connect("message", self._on_gst_message) self.video_texture.set_reactive(True) self.video_texture.connect("size-change", self._on_size_change) self.video_texture.connect("scroll-event", self._on_scroll_event) self.video_texture.connect("button-press-event", self._on_button_press_event) self.video_texture.connect("button-release-event", self._on_button_release_event) def _on_gst_message(self, bus, message): """ Callback function that is called every time when message occurs on Gstreamer messagebus. """ if message.type == gst.MESSAGE_EOS: if self.media.get_type() == Playable.VIDEO_STREAM or self.playlist is None: self.stop() else: self.next() elif message.type == gst.MESSAGE_ERROR: self.video_texture.set_playing(False) # XXX: laymansterms - I don't know the implications of removing the # position property. # self.video_texture.set_property("position", 0) err, debug = message.parse_error() self.logger.error("Error: %(err)s, %(debug)s" % {"err": err, "debug": debug}) def _get_volume(self): """volume property getter.""" return self._volume def _set_volume(self, volume): """volume property setter.""" self._volume = volume if self._volume > 20: self._volume = 20 if self._volume < 0: self._volume = 0 self.pipeline.set_property("volume", self._volume / 20.0) self.emit("volume-changed") volume = property(_get_volume, _set_volume) def volume_up(self): """Increase player's volume level.""" self.volume = self._volume + 1 def volume_down(self): """Decrease player's volume level.""" self.volume = self._volume - 1 def set_playlist(self, playlist): """Set new playlist to MediaPlayer.""" if len(playlist) == 0: raise Exception("Empty playlist is not allowed!") self.playlist = playlist self.set_media(self.playlist.get_current(), True) def get_playlist(self): """Get current playlist.""" return self.playlist def set_media(self, playable, internal_call=False): """ Set media to media player. Media is an object that implements Playable interface. This media is played back when play() is called. """ # If this function is called from this object we don't set playlist # to None if not internal_call: self.playlist = None # If player is currently playing then we stop it if self.is_playing: self.stop() # Update media information self.media = playable # Set up media player for media if self.media.get_type() == Playable.AUDIO_STREAM or self.media.get_type() == Playable.VIDEO_STREAM: self.video_texture.set_playing(False) self.video_texture.set_uri(playable.get_uri()) # XXX: laymansterms - I don't know the implications of removing the # position property. # self.video_texture.set_property("position", 0) def get_media(self): """Get URI of the current media stream.""" return self.media def has_media(self): """ Has media been set to this player. == has set_media() been called before. """ if self.media is None: return False else: return True def get_media_type(self): """Get the type of the current media.""" return self.media.get_type() def set_shuffle(self, boolean): """ Enable or disable shuffle play. When shuffle is enabled MediaPlayer picks a random Playable from the current playlist. """ self.shuffle = boolean def is_shuffle_enabled(self): """Is shuffle enabled?""" return self.shuffle def set_repeat(self, boolean): """ Enable or disable repeat mode. When repeat is enabled the current playable is repeated forever. """ self.repeat = boolean def is_repeat_enabled(self): """Is repeat enabled?""" return self.repeat def play(self): """Play current media.""" # If current media is an audio file if not self.has_media(): return if self.media.get_type() == Playable.AUDIO_STREAM: self.is_playing = True self.video_texture.set_playing(True) self.emit("play") # If current media is a video file elif self.media.get_type() == Playable.VIDEO_STREAM: if self.video_texture.get_parent() == None: self.stage.add(self.video_texture) self.video_texture.lower_bottom() self.is_playing = True self.stage.set_color((0, 0, 0, 0)) self.video_texture.set_playing(True) self.emit("play") if self._internal_callback_timeout_key is not None: gobject.source_remove(self._internal_callback_timeout_key) self._internal_callback_timeout_key = gobject.timeout_add(200, self._internal_timer_callback) def pause(self): """Pause media player.""" self.is_playing = False self.video_texture.set_playing(False) self.emit("pause") def stop(self): """Stop media player.""" self.is_playing = False if self.media.get_type() == Playable.VIDEO_STREAM: self.stage.set_color(self.bgcolor) self.stage.remove(self.video_texture) self.video_texture.set_playing(False) # XXX: laymansterms - I don't know the implications of removing the # position property. # self.video_texture.set_property("position", 0) self.emit("stop") if self._internal_callback_timeout_key is not None: gobject.source_remove(self._internal_callback_timeout_key) def next(self): """Play next track / video from current playlist.""" if self.playlist is not None: if self.shuffle: self.set_media(self.playlist.get_random(), True) elif self.playlist.has_next(): self.set_media(self.playlist.get_next(), True) self.play() def previous(self): """Play previous track / video from current playlist.""" if self.playlist is not None: if self.shuffle: self.set_media(self.playlist.get_random(), True) elif self.playlist.has_previous(): self.set_media(self.playlist.get_previous(), True) self.play() def skip_forward(self): """Skip media stream forward.""" if (self.media.get_type() == Playable.AUDIO_STREAM) or (self.media.get_type() == Playable.VIDEO_STREAM): pos_int = self.pipeline.query_position(gst.FORMAT_TIME, None)[0] dur = self.pipeline.query_duration(gst.FORMAT_TIME, None)[0] seek_ns = pos_int + (self.audio_skip_step * 1000000000) if seek_ns > dur: seek_ns = dur self.pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, seek_ns) self.emit("skip-forward") def skip_backward(self): """Skip media stream backward.""" if (self.media.get_type() == Playable.AUDIO_STREAM) or (self.media.get_type() == Playable.VIDEO_STREAM): pos_int = self.pipeline.query_position(gst.FORMAT_TIME, None)[0] seek_ns = pos_int - (self.audio_skip_step * 1000000000) if seek_ns < 0: seek_ns = 0 self.pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, seek_ns) self.emit("skip-backward") def get_media_position(self): """Get current position of the play back.""" try: pos = self.pipeline.query_position(gst.FORMAT_TIME, None)[0] dur = self.pipeline.query_duration(gst.FORMAT_TIME, None)[0] except gst.QueryError: # This normally means that the MediaPlayer object is querying # before the media is playing. return 0 dur_sec = dur / 1000000000.0 pos_sec = pos / 1000000000.0 return pos_sec / dur_sec def get_media_position_string(self): """Get current position of the play back as human readable string.""" try: nanoseconds = self.pipeline.query_position(gst.FORMAT_TIME, None)[0] return self._convert_ns_to_human_readable(nanoseconds) except gst.QueryError: # This normally means that the MediaPlayer object is querying # before the media is playing. return "00:00" def set_media_position(self, position): """Set position of the current media.""" if position < 0.0: position = 0.0 if position > 1.0: position = 1.0 if (self.media.get_type() == Playable.AUDIO_STREAM) or (self.media.get_type() == Playable.VIDEO_STREAM): dur = self.pipeline.query_duration(gst.FORMAT_TIME, None)[0] seek_ns = position * dur if seek_ns > dur: seek_ns = dur self.pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, seek_ns) def get_media_duration_string(self): """ Return media duration in string format. Example 04:20 This code is borrowed from gStreamer python tutorial. """ try: nanoseconds = self.pipeline.query_duration(gst.FORMAT_TIME, None)[0] return self._convert_ns_to_human_readable(nanoseconds) except gst.QueryError: # This normally means that the MediaPlayer object is querying # before the media is playing. return "00:00" def get_media_title(self): """Returns the title of the playing media.""" return self.media.get_title() def _convert_ns_to_human_readable(self, time_int): """ Convert nano seconds to human readable time string. This code is borrowed from gStreamer python tutorial. """ time_int = time_int / 1000000000 time_str = "" if time_int >= 3600: _hours = time_int / 3600 time_int = time_int - (_hours * 3600) time_str = str(_hours) + ":" if time_int >= 600: _mins = time_int / 60 time_int = time_int - (_mins * 60) time_str = time_str + str(_mins) + ":" elif time_int >= 60: _mins = time_int / 60 time_int = time_int - (_mins * 60) time_str = time_str + "0" + str(_mins) + ":" else: time_str = time_str + "00:" if time_int > 9: time_str = time_str + str(time_int) else: time_str = time_str + "0" + str(time_int) return time_str def _on_size_change(self, texture, width, height): """ Callback for changing video texture's aspect ratio. This is called when video texture size changes. IMPORTANT NOTE FOR PYLINTers The texture parameter is unused, however it cannot be removed because this method is called as a callback by cluttergst.VideoTexture.connect() """ if self.ratio == MediaPlayer.NATIVE: self.set_native_ratio(width, height) elif self.ratio == MediaPlayer.WIDESCREEN: self.set_widescreen_ratio(width, height) elif self.ratio == MediaPlayer.ZOOM: self.set_zoom_ratio(width, height) elif self.ratio == MediaPlayer.INTELLIGENT: self.set_intelligent_ratio(width, height) def set_native_ratio(self, width=None, height=None): """ Do not stretch video. Use native ratio, but scale video such a way that it fits in the window. """ self.ratio = MediaPlayer.NATIVE if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM: if width is None and height is None: texture_width, texture_height = self.video_texture.get_size() else: texture_width = width texture_height = height x_ratio = self.stage_width / float(texture_width) y_ratio = self.stage_height / float(texture_height) if x_ratio > y_ratio: self.video_texture.set_scale( self.stage_height / float(texture_height), self.stage_height / float(texture_height) ) new_width = int(texture_width * (self.stage_height / float(texture_height))) new_x = int((self.stage_width - new_width) / float(2)) self.video_texture.set_position(new_x, 0) else: self.video_texture.set_scale( self.stage_width / float(texture_width), self.stage_width / float(texture_width) ) new_height = int(texture_height * (self.stage_width / float(texture_width))) new_y = int((self.stage_height - new_height) / float(2)) self.video_texture.set_position(0, new_y) def set_widescreen_ratio(self, width=None, height=None): """'Stretch video to 16:9 ratio.""" self.ratio = MediaPlayer.WIDESCREEN if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM: if width is None and height is None: texture_width, texture_height = self.video_texture.get_size() else: texture_width = width texture_height = height self.video_texture.set_scale( self.stage_width / float(texture_width), self.stage_height / float(texture_height) ) self.video_texture.set_position(0, 0) def set_letter_box_ratio(self, width=None, height=None): """Set video playback into letter box mode.""" self.ratio = MediaPlayer.LETTER_BOX raise Exception("width=", width, "height=", height, "set_letter_box_ratio() is NOT implemented!") def set_zoom_ratio(self, width=None, height=None): """ Stretch video to screen such a way that video covers most of the screen. """ self.ratio = MediaPlayer.ZOOM if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM: if width is None and height is None: texture_width, texture_height = self.video_texture.get_size() else: texture_width = width texture_height = height x_ratio = self.stage_width / float(texture_width) y_ratio = self.stage_height / float(texture_height) if x_ratio < y_ratio: self.video_texture.set_scale( self.stage_height / float(texture_height), self.stage_height / float(texture_height) ) new_width = int(texture_width * (self.stage_height / float(texture_height))) new_x = int((self.stage_width - new_width) / float(2)) self.video_texture.set_position(new_x, 0) else: self.video_texture.set_scale( self.stage_width / float(texture_width), self.stage_width / float(texture_width) ) new_height = int(texture_height * (self.stage_width / float(texture_width))) new_y = int((self.stage_height - new_height) / float(2)) self.video_texture.set_position(0, new_y) def set_intelligent_ratio(self, width=None, height=None): """ This aspect ratio tries to display 4:3 on 16:9 in such a way that it looks good and still uses the whole screen space. It crops some of the image and does some stretching, but not as much as set_widescreen_ratio() method. """ self.ratio = MediaPlayer.INTELLIGENT if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM: ratio = 1.555555555 # 14:9 Aspect ratio if width is None and height is None: texture_width, texture_height = self.video_texture.get_size() else: texture_width = width texture_height = height fake_height = self.stage_width / ratio # Fake stage aspect ratio self.video_texture.set_scale(self.stage_width / float(texture_width), fake_height / float(texture_height)) y_offset = -int((fake_height - self.stage_height) / 2) self.video_texture.set_position(0, y_offset) def get_texture(self): """Get media's texture. This is a video texture or album art texture.""" if self.media.get_type() == Playable.VIDEO_STREAM: return clutter.Clone(self.video_texture) elif self.media.get_type() == Playable.AUDIO_STREAM: url = self.media.get_album_art_url() if url is not None: texture = Texture(url) return texture else: return None def _internal_timer_callback(self): """ A `refresh` event is regulary emited if media is playing. And update of the media's' position if a MODE_SEEK has been started with the pointer. """ if self.is_playing: self.emit("refresh") if self._event_mode == self.MODE_SEEK: position = self.get_media_position() position += self._seek_step self.set_media_position(position) self.emit("position-changed") return True def _on_button_press_event(self, actor, event): """`button-press` event handler.""" if not self.is_reactive_allowed: return clutter.grab_pointer(self.video_texture) if not self.video_texture.handler_is_connected(self._motion_handler): self._motion_handler = self.video_texture.connect("motion-event", self._on_motion_event) self._motion_buffer.start(event) self._event_mode = self.MODE_PLAYPAUSE def _on_button_release_event(self, actor, event): """`button-press` event handler.""" if not self.is_reactive_allowed: return clutter.ungrab_pointer() if self.video_texture.handler_is_connected(self._motion_handler): self.video_texture.disconnect_by_func(self._on_motion_event) if self._event_mode == self.MODE_PLAYPAUSE: if self.is_playing: self.pause() else: self.play() self._event_mode = self.MODE_NONE def _on_motion_event(self, actor, event): """`motion-event` event handler.""" # threshold in pixels = the minimum distance we have to move before we # consider a motion has started motion_threshold = 20 self._motion_buffer.compute_from_start(event) if self._motion_buffer.distance_from_start > motion_threshold: self._motion_buffer.take_new_motion_event(event) self._event_mode = self.MODE_SEEK self._seek_step = float(self._motion_buffer.dx_from_start) self._seek_step /= self.video_texture.get_width() self._seek_step *= 0.01 return False def _on_scroll_event(self, actor, event): """`scroll-event` event handler (mouse's wheel).""" # +/- 2% per scroll event on the position of the media stream. scroll_progress_ratio = 0.02 position = self.get_media_position() if event.direction == clutter.SCROLL_DOWN: position -= scroll_progress_ratio else: position += scroll_progress_ratio self.set_media_position(position) self.emit("position-changed")
class MessageBus: """ MessageBus is the heart of the backend messaging system. Almost all communication between components goes through this MessageBus. Components communicate with Messages, which are delivered to MessageHandlers via MessageBus. MessageBus knows which MessageHandlers are interested in which type of Messages. MessageBus is also aware of MessageHandler priorities and this way can serve high priority components first. When MessageHandler is registered to the MessageBus there is also another parameter besides handler itself. Second parameter is a dictionary that defines MessageTypes that registered handler wants to be notified of and also priorities for those message types.""" # This determines number of message types avaialble. In other words, this # variable tells how many variables is defined in MessageType class. NUMBER_OF_MESSAGE_TYPES = len( [k for k, v in vars(MessageType).items() if type(v) is int] ) def __init__(self): """ Create a new MessageBus object. """ # MessageHandlers - index is MessageType and data is a list of # tuples (priority, MessageHandler object) that is sorted by # priorities. #XXX: rockstar - WTF?! Why is there a list comprehension being used # and still only returning an empty list? # pylint: disable-msg=W0612 self.message_handlers = [ [] for i in range(self.NUMBER_OF_MESSAGE_TYPES) ] self.lock = threading.Lock() self.logger = Logger().getLogger('backend.core.MessageBus') def registerMessageHandler(self, message_handler, message_priority_list): """ Register a new MessageHandler to this MessageBus @param message_handler: MessageHandler object @param message_priority_list: Priority list for this MessageHandler """ if isinstance(message_handler, MessageHandler): for key in message_priority_list: rule = (message_priority_list[key], message_handler) self.message_handlers[key].append(rule) self.message_handlers[key].sort() # Keep priority order else: self.logger.critical( "MessageHandler registration failed. Object " + repr(message_handler) +" is invalid type.") raise TypeError("Only MessageHandlers can be registered!") self.logger.debug("MessageHandler '" + str(message_handler) + "' registered to the message bus.") def unregisterMessageHandler(self, message_handler): """ Unregister MessageHandler form this MessageBus. @param message_handler: MessageHandler object that should be removed from bus """ if isinstance(message_handler, MessageHandler): for i in range(self.NUMBER_OF_MESSAGE_TYPES): if len(self.message_handlers[i]) != 0: rules = self.message_handlers[i] for element in rules: if element[1] is message_handler: del element else: raise TypeError("Only MessageHandlers can be unregistered!") self.logger.debug("MessageHandler '" + str(message_handler) + "' unregistered from the message bus.") def notifyMessage(self, message): """ Emit a new Message to this MessageBus. @param message: Message object """ if isinstance(message, Message): self.lock.acquire() # Lock messagebus self.logger.debug("Message bus locked. Message of type '" + str(message.get_type()) + "' is on the bus.") handler_list = self.message_handlers[message.get_type()] for element in handler_list: element[1].handleMessage(message) self.lock.release() # Release messagebus lock else: message = "TypeError occured when message was notified to the bus." self.logger.error(message) exmessage = "Notified message must be instances of 'Message' type" raise TypeError(exmessage)
class MusicCache(Cache): """ Handles audio file cache. """ # Supported file formats __SUPPORTED_FILE_EXTENSIONS = ['mp3', 'ogg'] # SQLite database stuff __db_conn = None __db_cursor = None # Default values __DEFAULT = { "artist": "Unknown artist", "album": "Unknown album", "title": "Unknown track", "genre": "Unknown" } def __init__(self): """Create a new music database object.""" self.logger = Logger().getLogger( 'backend.components.mediacache.MusicCache') self.config = Configuration() if not os.path.exists(self.config.MUSIC_DB): self.__createMusicCacheDatabase() self.__db_conn = sqlite.connect(self.config.MUSIC_DB) self.__db_cursor = self.__db_conn.cursor() def clearCache(self): """ Clear music cache. Clean cache database and remova all albumart. """ covers = os.listdir(self.config.ALBUM_ART_DIR) for element in covers: os.remove(os.path.join(self.config.ALBUM_ART_DIR, element)) os.remove(self.config.MUSIC_DB) self.__createMusicCacheDatabase() def addFile(self, filename): """Add audio file to the cache.""" filename = filename.encode('utf8') if (not self.isFileInCache(filename) and self.isSupportedFormat(filename)): if self.__getFileExtension(filename) == "mp3": self.__addMP3file(filename) elif self.__getFileExtension(filename) == "ogg": self.__addOGGfile(filename) def removeFile(self, filename): """Remove audio file from the cache.""" if self.isFileInCache(filename): # Check if we should remove albumart self.__db_cursor.execute( """SELECT artist, album FROM track WHERE filename=:fn""", {"fn": filename}) result = self.__db_cursor.fetchall() artist = result[0][0] album = result[0][1] self.__db_cursor.execute( """ SELECT * FROM track WHERE artist=:artist AND album=:album""", { "artist": artist, "album": album }) result = self.__db_cursor.fetchall() # If only one found then it's the file that is going to be removed if (len(result) == 1 and artist != self.__DEFAULT['artist'] and album != self.__DEFAULT['album']): albumart_file = artist + " - " + album + ".jpg" try: os.remove( os.path.join(self.config.ALBUM_ART_DIR, albumart_file)) except OSError: self.logger.error( "Couldn't remove albumart: " + os.path.join(self.config.ALBUM_ART_DIR, albumart_file)) # Remove track from cache self.__db_cursor.execute( """DELETE FROM track WHERE filename=:fn""", {"fn": filename}) self.__db_cursor.execute( """DELETE FROM playlist_relation WHERE filename=:fn""", {"fn": filename}) self.__db_conn.commit() def updateFile(self, filename): """Update audio file that is already in the cache.""" if self.isFileInCache(filename): self.removeFile(filename) self.addFile(filename) def addDirectory(self, path): """Add directory that contains audio files to the cache.""" # pylint: disable-msg=W0612 if not os.path.isdir(path) or not os.path.exists(path): self.logger.error( "Adding a directory to the music cache failed. " + "Path doesn't exist: " + path) else: for root, dirs, files in os.walk(path): for name in files: self.addFile(os.path.join(root, name)) time.sleep(float(self.SLEEP_TIME_BETWEEN_FILES) / 1000) def removeDirectory(self, path): """Remove directory from the cache.""" # Get current artist and albums that are on the removed path self.__db_cursor.execute(""" SELECT DISTINCT artist, album FROM track WHERE filename LIKE ' """ + path + "%'") result = self.__db_cursor.fetchall() # Remove tracks from database self.__db_cursor.execute("DELETE FROM track WHERE filename LIKE '" + path + "%'") self.__db_cursor.execute( "DELETE FROM playlist_relation WHERE filename LIKE '" + path + "%'") self.__db_conn.commit() # Check which album art we should remove for element in result: artist = element[0] album = element[1] self.__db_cursor.execute( """SELECT * FROM track WHERE artist=:artist AND album=:album""", { "artist": artist, "album": album }) found = self.__db_cursor.fetchall() # After delete there is no artist, album combination, so we can # remove album art if (len(found) == 0 and artist != self.__DEFAULT['artist'] and album != self.__DEFAULT['album']): albumart_file = artist + " - " + album + ".jpg" try: os.remove( os.path.join(self.config.ALBUM_ART_DIR, albumart_file)) except OSError: self.logger.error( "Couldn't remove albumart: " + os.path.join(self.config.ALBUM_ART_DIR, albumart_file)) def updateDirectory(self, path): """Update directory that is already in the cache.""" self.removeDirectory(path) self.addDirectory(path) def isDirectoryInCache(self, path): """Check if directory is in cache.""" self.__db_cursor.execute("SELECT * FROM track WHERE filename LIKE '" + path + "%'") result = self.__db_cursor.fetchall() if len(result) == 0: return False else: return True def isFileInCache(self, filename): """Check if file is in cache.""" self.__db_cursor.execute( """SELECT * FROM track WHERE filename=:fn""", {"fn": filename}) result = self.__db_cursor.fetchall() if len(result) == 0: return False else: return True def isSupportedFormat(self, filename): """Check if file is supported.""" if (self.__getFileExtension(filename) in self.__SUPPORTED_FILE_EXTENSIONS): return True else: return False def __createMusicCacheDatabase(self): """Creates a music cache database file.""" db_conn = sqlite.connect(self.config.MUSIC_DB) db_cursor = db_conn.cursor() db_cursor.execute("""CREATE TABLE track( filename TEXT, title VARCHAR(255), artist VARCHAR(255), album VARCHAR(255), tracknumber INTEGER, bitrate INTEGER, year INTEGER, rating INTEGER DEFAULT NULL, length INTEGER, genre VARCHAR(128), comment TEXT, lyrics TEXT DEFAULT "", PRIMARY KEY(filename))""") db_cursor.execute("""CREATE TABLE playlist( title TEXT, PRIMARY KEY(title))""") db_cursor.execute("""CREATE TABLE playlist_relation( title TEXT, filename TEXT, PRIMARY KEY(title, filename))""") db_conn.commit() db_conn.close() self.logger.debug("MusicCache database created successfully") def __getFileExtension(self, filename): """Return lower case file extension""" return filename[filename.rfind('.') + 1:].lower() def __addMP3file(self, filename): """ Add mp3 file to the music cache Process: - Open file - Get tags - Insert data to the music cache database """ try: mp3_file = eyeD3.Mp3AudioFile(filename, eyeD3.ID3_ANY_VERSION) tags = mp3_file.getTag() except ValueError: # Tags are corrupt self.logger.error("Couldn't read ID3tags: " + filename) return if tags is None: self.logger.error("Couldn't read ID3tags: " + filename) return # Get track lenght in seconds length = mp3_file.getPlayTime() # Get avarage bitrate bitrate = mp3_file.getBitRate()[1] # Get artist name artist = tags.getArtist() if artist is None or len(artist) == 0: artist = self.__DEFAULT['artist'] # Get album name album = tags.getAlbum() if album is None or len(album) == 0: album = self.__DEFAULT['album'] # Get track title title = tags.getTitle() if title is None or len(title) == 0: title = self.__DEFAULT['title'] # Get track genre genre = str(tags.getGenre()) if genre is None or len(genre) == 0: genre = self.__DEFAULT['genre'] # Get track number tracknumber = tags.getTrackNum()[0] if tracknumber is None: tracknumber = 0 # Get track comment comment = tags.getComment() if comment is None or len(comment) == 0: comment = "" # Get track release year year = tags.getYear() if year is None or len(year) == 0: year = 0 db_row = (filename, title, artist, album, genre, length, tracknumber, bitrate, comment, year) self.__db_cursor.execute( """INSERT INTO track(filename, title, artist, album, genre, length, tracknumber, bitrate, comment, year) VALUES(?,?,?,?,?,?,?,?,?,?)""", db_row) self.__db_conn.commit() # Get song lyrics lyrics = tags.getLyrics() if len(lyrics) != 0: lyrics = str(lyrics[0]) self.__db_cursor.execute( """UPDATE track SET lyrics=:lyrics WHERE filename=:fn""", { "lyrics": lyrics, "fn": filename }) self.__db_conn.commit() # Get album art self.__searchAlbumArt(artist, album, filename) def __addOGGfile(self, filename): """ Add ogg file to the music cache Process: - Open file - Get tags - Insert data to the music cache database """ ogg_file = ogg.vorbis.VorbisFile(filename) info = ogg_file.comment().as_dict() # Get length length = round(ogg_file.time_total(-1)) # Get avarage bitrate bitrate = round(ogg_file.bitrate(-1) / 1000) # Get album name if info.has_key('ALBUM'): album = info['ALBUM'][0] else: album = self.__DEFAULT['album'] # Get artist name if info.has_key('ARTIST'): artist = info['ARTIST'][0] else: artist = self.__DEFAULT['artist'] # Get track title if info.has_key('TITLE'): if info.has_key('VERSION'): title = (str(info['TITLE'][0]) + " (" + str(info['VERSION'][0]) + ")") else: title = info['TITLE'][0] else: title = self.__DEFAULT['title'] # Get track number if info.has_key('TRACKNUMBER'): track_number = info['TRACKNUMBER'][0] else: track_number = 0 # Get track genre if info.has_key('GENRE'): genre = info['GENRE'][0] else: genre = self.__DEFAULT['genre'] # Get track comment if info.has_key('DESCRIPTION'): comment = info['DESCRIPTION'][0] elif info.has_key('COMMENT'): comment = info['COMMENT'][0] else: comment = "" # Get track year if info.has_key('DATE'): date = info['DATE'][0] year = date[:date.find('-')] else: year = 0 db_row = (filename, title, artist, album, genre, length, track_number, bitrate, comment, year) self.__db_cursor.execute( """INSERT INTO track(filename, title, artist, album, genre, length, tracknumber, bitrate, comment, year) VALUES(?,?,?,?,?,?,?,?,?,?)""", db_row) self.__db_conn.commit() # Get album art self.__searchAlbumArt(artist, album, filename) def __searchAlbumArt(self, artist, album, filename): """Execute album art search thread""" # base64 encode artist and album so there can be a '/' in the artist or # album artist_album = artist + " - " + album artist_album = artist_album.encode("base64") album_art_file = os.path.join(self.config.ALBUM_ART_DIR, artist_album + ".jpg") if not os.path.exists(album_art_file): # Search for local albumart if os.path.exists(filename[:filename.rfind('/') + 1] + "cover.jpg"): shutil.copyfile( filename[:filename.rfind('/') + 1] + "cover.jpg", album_art_file) elif os.path.exists(filename[:filename.rfind('/') + 1] + "folder.jpg"): shutil.copyfile( filename[:filename.rfind('/') + 1] + "folder.jpg", album_art_file) # Local not found -> try internet else: if self.config.download_album_art: if album != "Unknown album" and artist != "Unknown Artist": loader_thread = AlbumArtDownloader( album, artist, self.config.ALBUM_ART_DIR) loader_thread.start()
class MediaPlayer(gobject.GObject, object): '''MediaPlayer uses Gstreamer to play all video and audio files. Entertainer has only one MediaPlayer object at runtime. MediaPlayer can play objects that implement Playable interface.''' __gsignals__ = { 'play': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'pause': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'stop': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'skip-forward': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'skip-backward': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'volume_changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'position-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), 'refresh': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), } # Ratio constants NATIVE = 0 WIDESCREEN = 1 NORMAL = 2 LETTER_BOX = 3 ZOOM = 4 INTELLIGENT = 5 MODE_NONE = 0 MODE_PLAYPAUSE = 1 MODE_SEEK = 2 def __init__(self, stage, width, height): gobject.GObject.__init__(self) self._motion_buffer = MotionBuffer() self._event_mode = self.MODE_NONE self._motion_handler = 0 self.stage = stage # Stage that displays textures # Stage background color when not playing self.bgcolor = stage.get_color() self.stage_width = width # Stage width used for video resizing self.stage_height = height # Stage height used for video resizing self.ratio = MediaPlayer.NATIVE # Video texture ratio self.audio_skip_step = 10 # Audio skip step in seconds self.video_skip_step = 60 # Video skip step in seconds self.playlist = None # Current play list self.media = None # Current media (Playable object) self.shuffle = False # Shuffle mode self.repeat = False # Repeat mode self.is_playing = False # Is media player currently playing self.is_reactive_allowed = False # Is the video_texture reactive self.logger = Logger().getLogger('client.MediaPlayer') self._internal_callback_timeout_key = None self.video_texture = cluttergst.VideoTexture() self.pipeline = self.video_texture.get_pipeline() self.pipeline.set_property("volume", 0.5) self._volume = 10 self.bus = self.pipeline.get_bus() self.bus.add_signal_watch() self.bus.connect('message', self._on_gst_message) self.video_texture.set_reactive(True) self.video_texture.connect('size-change', self._on_size_change) self.video_texture.connect('scroll-event', self._on_scroll_event) self.video_texture.connect('button-press-event', self._on_button_press_event) self.video_texture.connect('button-release-event', self._on_button_release_event) def _on_gst_message(self, bus, message): ''' Callback function that is called every time when message occurs on Gstreamer messagebus. ''' if message.type == gst.MESSAGE_EOS: if self.media.get_type() == Playable.VIDEO_STREAM \ or self.playlist is None: self.stop() else: self.next() elif message.type == gst.MESSAGE_ERROR: self.video_texture.set_playing(False) # XXX: laymansterms - I don't know the implications of removing the # position property. #self.video_texture.set_property("position", 0) err, debug = message.parse_error() self.logger.error("Error: %(err)s, %(debug)s" % \ {'err': err, 'debug': debug}) def _get_volume(self): """volume property getter.""" return self._volume def _set_volume(self, volume): """volume property setter.""" self._volume = volume if self._volume > 20: self._volume = 20 if self._volume < 0: self._volume = 0 self.pipeline.set_property("volume", self._volume / 20.0) self.emit('volume-changed') volume = property(_get_volume, _set_volume) def volume_up(self): """Increase player's volume level.""" self.volume = self._volume + 1 def volume_down(self): """Decrease player's volume level.""" self.volume = self._volume - 1 def set_playlist(self, playlist): '''Set new playlist to MediaPlayer.''' if len(playlist) == 0: raise Exception("Empty playlist is not allowed!") self.playlist = playlist self.set_media(self.playlist.get_current(), True) def get_playlist(self): '''Get current playlist.''' return self.playlist def set_media(self, playable, internal_call=False): ''' Set media to media player. Media is an object that implements Playable interface. This media is played back when play() is called. ''' # If this function is called from this object we don't set playlist # to None if not internal_call: self.playlist = None # If player is currently playing then we stop it if self.is_playing: self.stop() # Update media information self.media = playable # Set up media player for media if self.media.get_type() == Playable.AUDIO_STREAM \ or self.media.get_type() == Playable.VIDEO_STREAM: self.video_texture.set_playing(False) self.video_texture.set_uri(playable.get_uri()) # XXX: laymansterms - I don't know the implications of removing the # position property. #self.video_texture.set_property("position", 0) def get_media(self): '''Get URI of the current media stream.''' return self.media def has_media(self): ''' Has media been set to this player. == has set_media() been called before. ''' if self.media is None: return False else: return True def get_media_type(self): '''Get the type of the current media.''' return self.media.get_type() def set_shuffle(self, boolean): ''' Enable or disable shuffle play. When shuffle is enabled MediaPlayer picks a random Playable from the current playlist. ''' self.shuffle = boolean def is_shuffle_enabled(self): '''Is shuffle enabled?''' return self.shuffle def set_repeat(self, boolean): ''' Enable or disable repeat mode. When repeat is enabled the current playable is repeated forever. ''' self.repeat = boolean def is_repeat_enabled(self): '''Is repeat enabled?''' return self.repeat def play(self): '''Play current media.''' # If current media is an audio file if not self.has_media(): return if self.media.get_type() == Playable.AUDIO_STREAM: self.is_playing = True self.video_texture.set_playing(True) self.emit('play') # If current media is a video file elif self.media.get_type() == Playable.VIDEO_STREAM: if (self.video_texture.get_parent() == None): self.stage.add(self.video_texture) self.video_texture.lower_bottom() self.is_playing = True self.stage.set_color((0, 0, 0, 0)) self.video_texture.set_playing(True) self.emit('play') if self._internal_callback_timeout_key is not None: gobject.source_remove(self._internal_callback_timeout_key) self._internal_callback_timeout_key = \ gobject.timeout_add(200, self._internal_timer_callback) def pause(self): '''Pause media player.''' self.is_playing = False self.video_texture.set_playing(False) self.emit('pause') def stop(self): '''Stop media player.''' self.is_playing = False if self.media.get_type() == Playable.VIDEO_STREAM: self.stage.set_color(self.bgcolor) self.stage.remove(self.video_texture) self.video_texture.set_playing(False) # XXX: laymansterms - I don't know the implications of removing the # position property. #self.video_texture.set_property("position", 0) self.emit('stop') if self._internal_callback_timeout_key is not None: gobject.source_remove(self._internal_callback_timeout_key) def next(self): '''Play next track / video from current playlist.''' if self.playlist is not None: if self.shuffle: self.set_media(self.playlist.get_random(), True) elif self.playlist.has_next(): self.set_media(self.playlist.get_next(), True) self.play() def previous(self): '''Play previous track / video from current playlist.''' if self.playlist is not None: if self.shuffle: self.set_media(self.playlist.get_random(), True) elif self.playlist.has_previous(): self.set_media(self.playlist.get_previous(), True) self.play() def skip_forward(self): '''Skip media stream forward.''' if (self.media.get_type() == Playable.AUDIO_STREAM) or \ (self.media.get_type() == Playable.VIDEO_STREAM): pos_int = self.pipeline.query_position(gst.FORMAT_TIME, None)[0] dur = self.pipeline.query_duration(gst.FORMAT_TIME, None)[0] seek_ns = pos_int + (self.audio_skip_step * 1000000000) if seek_ns > dur: seek_ns = dur self.pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, seek_ns) self.emit('skip-forward') def skip_backward(self): '''Skip media stream backward.''' if (self.media.get_type() == Playable.AUDIO_STREAM) or \ (self.media.get_type() == Playable.VIDEO_STREAM): pos_int = self.pipeline.query_position(gst.FORMAT_TIME, None)[0] seek_ns = pos_int - (self.audio_skip_step * 1000000000) if seek_ns < 0: seek_ns = 0 self.pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, seek_ns) self.emit('skip-backward') def get_media_position(self): '''Get current position of the play back.''' try: pos = self.pipeline.query_position(gst.FORMAT_TIME, None)[0] dur = self.pipeline.query_duration(gst.FORMAT_TIME, None)[0] except gst.QueryError: # This normally means that the MediaPlayer object is querying # before the media is playing. return 0 dur_sec = dur / 1000000000.0 pos_sec = pos / 1000000000.0 return pos_sec / dur_sec def get_media_position_string(self): '''Get current position of the play back as human readable string.''' try: nanoseconds = self.pipeline.query_position(gst.FORMAT_TIME, None)[0] return self._convert_ns_to_human_readable(nanoseconds) except gst.QueryError: # This normally means that the MediaPlayer object is querying # before the media is playing. return "00:00" def set_media_position(self, position): '''Set position of the current media.''' if position < 0.0: position = 0.0 if position > 1.0: position = 1.0 if (self.media.get_type() == Playable.AUDIO_STREAM) or \ (self.media.get_type() == Playable.VIDEO_STREAM): dur = self.pipeline.query_duration(gst.FORMAT_TIME, None)[0] seek_ns = (position * dur) if seek_ns > dur: seek_ns = dur self.pipeline.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, seek_ns) def get_media_duration_string(self): ''' Return media duration in string format. Example 04:20 This code is borrowed from gStreamer python tutorial. ''' try: nanoseconds = self.pipeline.query_duration(gst.FORMAT_TIME, None)[0] return self._convert_ns_to_human_readable(nanoseconds) except gst.QueryError: # This normally means that the MediaPlayer object is querying # before the media is playing. return "00:00" def get_media_title(self): '''Returns the title of the playing media.''' return self.media.get_title() def _convert_ns_to_human_readable(self, time_int): ''' Convert nano seconds to human readable time string. This code is borrowed from gStreamer python tutorial. ''' time_int = time_int / 1000000000 time_str = "" if time_int >= 3600: _hours = time_int / 3600 time_int = time_int - (_hours * 3600) time_str = str(_hours) + ":" if time_int >= 600: _mins = time_int / 60 time_int = time_int - (_mins * 60) time_str = time_str + str(_mins) + ":" elif time_int >= 60: _mins = time_int / 60 time_int = time_int - (_mins * 60) time_str = time_str + "0" + str(_mins) + ":" else: time_str = time_str + "00:" if time_int > 9: time_str = time_str + str(time_int) else: time_str = time_str + "0" + str(time_int) return time_str def _on_size_change(self, texture, width, height): ''' Callback for changing video texture's aspect ratio. This is called when video texture size changes. IMPORTANT NOTE FOR PYLINTers The texture parameter is unused, however it cannot be removed because this method is called as a callback by cluttergst.VideoTexture.connect() ''' if self.ratio == MediaPlayer.NATIVE: self.set_native_ratio(width, height) elif self.ratio == MediaPlayer.WIDESCREEN: self.set_widescreen_ratio(width, height) elif self.ratio == MediaPlayer.ZOOM: self.set_zoom_ratio(width, height) elif self.ratio == MediaPlayer.INTELLIGENT: self.set_intelligent_ratio(width, height) def set_native_ratio(self, width=None, height=None): ''' Do not stretch video. Use native ratio, but scale video such a way that it fits in the window. ''' self.ratio = MediaPlayer.NATIVE if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM: if width is None and height is None: texture_width, texture_height = self.video_texture.get_size() else: texture_width = width texture_height = height x_ratio = self.stage_width / float(texture_width) y_ratio = self.stage_height / float(texture_height) if x_ratio > y_ratio: self.video_texture.set_scale( self.stage_height / float(texture_height), self.stage_height / float(texture_height)) new_width = int(texture_width * \ (self.stage_height / float(texture_height))) new_x = int((self.stage_width - new_width) / float(2)) self.video_texture.set_position(new_x, 0) else: self.video_texture.set_scale( self.stage_width / float(texture_width), self.stage_width / float(texture_width)) new_height = int(texture_height * \ (self.stage_width / float(texture_width))) new_y = int((self.stage_height - new_height) / float(2)) self.video_texture.set_position(0, new_y) def set_widescreen_ratio(self, width=None, height=None): ''''Stretch video to 16:9 ratio.''' self.ratio = MediaPlayer.WIDESCREEN if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM: if width is None and height is None: texture_width, texture_height = self.video_texture.get_size() else: texture_width = width texture_height = height self.video_texture.set_scale( self.stage_width / float(texture_width), self.stage_height / float(texture_height)) self.video_texture.set_position(0, 0) def set_letter_box_ratio(self, width=None, height=None): '''Set video playback into letter box mode.''' self.ratio = MediaPlayer.LETTER_BOX raise Exception("width=", width, "height=", height, "set_letter_box_ratio() is NOT implemented!") def set_zoom_ratio(self, width=None, height=None): ''' Stretch video to screen such a way that video covers most of the screen. ''' self.ratio = MediaPlayer.ZOOM if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM: if width is None and height is None: texture_width, texture_height = self.video_texture.get_size() else: texture_width = width texture_height = height x_ratio = self.stage_width / float(texture_width) y_ratio = self.stage_height / float(texture_height) if x_ratio < y_ratio: self.video_texture.set_scale( self.stage_height / float(texture_height), self.stage_height / float(texture_height)) new_width = int(texture_width * \ (self.stage_height / float(texture_height))) new_x = int((self.stage_width - new_width) / float(2)) self.video_texture.set_position(new_x, 0) else: self.video_texture.set_scale( self.stage_width / float(texture_width), self.stage_width / float(texture_width)) new_height = int(texture_height * \ (self.stage_width / float(texture_width))) new_y = int((self.stage_height - new_height) / float(2)) self.video_texture.set_position(0, new_y) def set_intelligent_ratio(self, width=None, height=None): ''' This aspect ratio tries to display 4:3 on 16:9 in such a way that it looks good and still uses the whole screen space. It crops some of the image and does some stretching, but not as much as set_widescreen_ratio() method. ''' self.ratio = MediaPlayer.INTELLIGENT if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM: ratio = 1.555555555 # 14:9 Aspect ratio if width is None and height is None: texture_width, texture_height = self.video_texture.get_size() else: texture_width = width texture_height = height fake_height = self.stage_width / ratio # Fake stage aspect ratio self.video_texture.set_scale( self.stage_width / float(texture_width), fake_height / float(texture_height)) y_offset = -int((fake_height - self.stage_height) / 2) self.video_texture.set_position(0, y_offset) def get_texture(self): '''Get media's texture. This is a video texture or album art texture.''' if self.media.get_type() == Playable.VIDEO_STREAM: return clutter.Clone(self.video_texture) elif self.media.get_type() == Playable.AUDIO_STREAM: url = self.media.get_album_art_url() if url is not None: texture = Texture(url) return texture else: return None def _internal_timer_callback(self): ''' A `refresh` event is regulary emited if media is playing. And update of the media's' position if a MODE_SEEK has been started with the pointer. ''' if self.is_playing: self.emit('refresh') if self._event_mode == self.MODE_SEEK: position = self.get_media_position() position += self._seek_step self.set_media_position(position) self.emit('position-changed') return True def _on_button_press_event(self, actor, event): """`button-press` event handler.""" if not self.is_reactive_allowed: return clutter.grab_pointer(self.video_texture) if not self.video_texture.handler_is_connected(self._motion_handler): self._motion_handler = self.video_texture.connect( 'motion-event', self._on_motion_event) self._motion_buffer.start(event) self._event_mode = self.MODE_PLAYPAUSE def _on_button_release_event(self, actor, event): """`button-press` event handler.""" if not self.is_reactive_allowed: return clutter.ungrab_pointer() if self.video_texture.handler_is_connected(self._motion_handler): self.video_texture.disconnect_by_func(self._on_motion_event) if self._event_mode == self.MODE_PLAYPAUSE: if self.is_playing: self.pause() else: self.play() self._event_mode = self.MODE_NONE def _on_motion_event(self, actor, event): """`motion-event` event handler.""" # threshold in pixels = the minimum distance we have to move before we # consider a motion has started motion_threshold = 20 self._motion_buffer.compute_from_start(event) if self._motion_buffer.distance_from_start > motion_threshold: self._motion_buffer.take_new_motion_event(event) self._event_mode = self.MODE_SEEK self._seek_step = float(self._motion_buffer.dx_from_start) self._seek_step /= self.video_texture.get_width() self._seek_step *= 0.01 return False def _on_scroll_event(self, actor, event): '''`scroll-event` event handler (mouse's wheel).''' # +/- 2% per scroll event on the position of the media stream. scroll_progress_ratio = 0.02 position = self.get_media_position() if event.direction == clutter.SCROLL_DOWN: position -= scroll_progress_ratio else: position += scroll_progress_ratio self.set_media_position(position) self.emit('position-changed')
class ImageCache(Cache): """ This class is responsible of updating image cache as requested. ImageCache has a public interface that consists of 3 mehtods: addFile, removeFile and updateFile. All these methods get filename as a parameter. When ImageCache is called with filename it checks if the filename is supported format. This is done simply by checking the file extension. Supported file formats are: JPEG """ # Supported file formats SUPPORTED_FILE_EXTENSIONS = ['jpg', 'jpeg', 'png'] def __init__(self): """ Create a new ImageCache. Creates a new database if not already exists and opens a connection to it. """ self.logger = Logger().getLogger( 'backend.components.mediacache.ImageCache') self.config = Configuration() if not os.path.exists(self.config.IMAGE_DB): self._createImageCacheDatabase() self.db_conn = sqlite.connect(self.config.IMAGE_DB) self.db_cursor = self.db_conn.cursor() def clearCache(self): """ Clean image cache completely. Clean cache database and remove all thumbnails. """ thumbnails = os.listdir(self.config.IMAGE_THUMB_DIR) for element in thumbnails: thumb_file = os.path.join(self.config.IMAGE_THUMB_DIR, element) try: os.remove(thumb_file) except OSError: self.logger.error( "Media manager couldn't remove thumbnail : %s" % thumb_file) os.remove(self.config.IMAGE_DB) self._createImageCacheDatabase() def addFile(self, filename): """ Add image file to the cache. Do nothing if file is already cached. """ filename = filename.encode('utf8') if (not self.isFileInCache(filename) and self.isSupportedFormat(filename)): # Do not add album thumbnail to images if (filename[filename.rfind('/') +1:filename.rfind('.')] == ".entertainer_album"): return self._addJPEGfile(filename) def removeFile(self, filename): """ Remove image file from the cache. Do nothing if file is not in cache. """ # Remove image file if self.isFileInCache(filename): self.db_cursor.execute("""SELECT hash FROM image WHERE filename=:fn""", { "fn" : filename}) result = self.db_cursor.fetchall() if len(result) > 0: name = result[0][0] + '.jpg' thumb = os.path.join(self.config.IMAGE_THUMB_DIR, name) try: os.remove(thumb) except OSError: self.logger.error("Couldn't remove thumbnail: " + thumb) self.db_cursor.execute("""DELETE FROM image WHERE filename=:fn""", { "fn" : filename }) self.db_conn.commit() def updateFile(self, filename): """Update image file that is already in the cache.""" if self.isFileInCache(filename): self.removeFile(filename) self.addFile(filename) def addDirectory(self, path): """ Adds a new directory to the cache. Sub directories are added recursively and all files in them. """ # pylint: disable-msg=W0612 if not os.path.isdir(path) or not os.path.exists(path): self.logger.error( "Adding a directory to the image cache failed. " + "Path doesn't exist: " + path) else: for root, dirs, files in os.walk(path): if os.path.split(root)[-1][0] == ".": continue if not self.isDirectoryInCache(root): self._addAlbum(root) for name in files: if os.path.split(name)[-1][0] == ".": continue if self.isSupportedFormat(name): self.addFile(os.path.join(root, name)) time.sleep(float(self.SLEEP_TIME_BETWEEN_FILES) / 1000) def removeDirectory(self, path): """ Removes directory from the cache. Also removes all subdirectories and all files in them. @param path - Absolute path """ # Remove image file thumbnails self.db_cursor.execute("""SELECT hash FROM image WHERE filename LIKE '""" + path + "%'") for row in self.db_cursor: thumb_file = row[0] + ".jpg" os.remove(os.path.join(self.config.IMAGE_THUMB_DIR, thumb_file)) # Remove folder thumbnails self.db_cursor.execute("""SELECT hash FROM album WHERE path LIKE '""" + path + "%'") for row in self.db_cursor: thumb_file = row[0] + ".jpg" os.remove(os.path.join(self.config.IMAGE_THUMB_DIR, thumb_file)) # Clean cache database self.db_cursor.execute( "DELETE FROM album WHERE path LIKE '" + path + "%'") self.db_cursor.execute( "DELETE FROM image WHERE album_path LIKE '" + path + "%'") self.db_conn.commit() def updateDirectory(self, path): """ Update directory. """ self.removeDirectory(path) self.addDirectory(path) def isFileInCache(self, filename): """Check if file is already in cache. Returns boolean value.""" self.db_cursor.execute("""SELECT * FROM image WHERE filename=:fn""", { "fn" : filename }) result = self.db_cursor.fetchall() if len(result) == 0: return False else: return True def isDirectoryInCache(self, path): """Check if album is already in cache. Returns boolean value.""" self.db_cursor.execute("""SELECT * FROM album WHERE path=:p""", { "p" : path}) result = self.db_cursor.fetchall() if len(result) == 0: return False else: return True def isSupportedFormat(self, filename): """Check if file is supported.""" if (filename[filename.rfind('.') + 1 :].lower() in self.SUPPORTED_FILE_EXTENSIONS): return True else: return False def _createImageCacheDatabase(self): """Creates a image cache database file.""" db_conn = sqlite.connect(self.config.IMAGE_DB) db_cursor = db_conn.cursor() db_cursor.execute( """ CREATE TABLE image( filename TEXT, album_path TEXT, title TEXT, description TEXT, date DATE, time TIME, width INTEGER, height INTEGER, filesize LONG, hash VARCHAR(32), PRIMARY KEY(filename))""") db_cursor.execute( """ CREATE TABLE album( path TEXT, title TEXT, description TEXT, hash VARCHAR(32), PRIMARY KEY(path))""") db_conn.commit() db_conn.close() self.logger.debug("ImageCache database created successfully") def _addAlbum(self, path): """ Create a new album into image cache. Folders are handled as albums. Nested folders are not nested in database! All albums are on top level. """ album_info = os.path.join(path, ".entertainer_album.info") album_thumb = os.path.join(path, ".entertainer_album.jpg") # Get album information if os.path.exists(album_info): try: inf_f = open(album_info) a_title = inf_f.readline()[6:] a_description = inf_f.readline()[12:] except IOError: a_title = path[path.rfind('/')+1:].replace('_',' ').title() a_description = "" else: a_title = path[path.rfind('/')+1:].replace('_',' ').title() a_description = "" if os.path.exists(album_thumb): thumbnailer = ImageThumbnailer(album_thumb) thumbnailer.create_thumbnail() a_hash = thumbnailer.get_hash() else: a_hash = "" album_row = (path, a_title, a_description, a_hash) self.db_cursor.execute( """ INSERT INTO album(path, title, description, hash) VALUES(?,?,?,?) """, album_row) self.db_conn.commit() #print "Album added to cache: " + a_title def _addJPEGfile(self, filename): """ Add JPEG image to the image cache. Raises exception if adding fails. Process: - Open file - Get image date and time - Get image title and description - Get image size - Generate thumbnail / get hash from thumbnailer - Insert data to image cache database """ tmp = datetime.datetime.fromtimestamp(os.stat(filename)[-1]) timestamp = [str(tmp.year) + "-" + str(tmp.month) + "-" + str(tmp.day), str(tmp.hour) + ":" + str(tmp.minute) + ":" + str(tmp.second)] # Generate name from filename tmp = filename[filename.rfind('/') + 1 : filename.rfind('.')] title = tmp.replace('_',' ').title() # Make title more attractive description = "" # No description for this image file try: im = Image.open(filename) width, height = im.size except IOError: self.logger.error("Couldn't identify image file: " + filename) return # Create thumbnail and return hash thumbnailer = ImageThumbnailer(filename) thumbnailer.create_thumbnail() thumb_hash = thumbnailer.get_hash() del thumbnailer album_path = filename[:filename.rfind('/')] db_row = (filename, # Filename (full path) title, # Title of the image description, # Description of the image timestamp[0], # Image's taken date timestamp[1], # Image's taken time width, # Image's width height, # Image's height os.path.getsize(filename), # Image file size in bytes thumb_hash, # Thumbnail hash (hash of the filename) album_path) # Path of the album (folder of this image) self.db_cursor.execute( """ INSERT INTO image(filename, title, description, date, time, width, height, filesize, hash, album_path) VALUES(?,?,?,?,?,?,?,?,?,?)""", db_row) self.db_conn.commit()
class VideoCache(Cache): """Handles video file cache.""" # Supported file extensions __SUPPORTED_FILE_EXTENSIONS = [ 'avi', 'mpg', 'mpeg', 'mov', 'wmv', 'ogm', 'mkv', 'mp4', 'm4v' ] # SQLite database stuff __db_conn = None __db_cursor = None # Thread lock for metadata search __metadata_lock = None def __init__(self): self.logger = Logger().getLogger( 'backend.components.mediacache.VideoCache') self.config = Configuration() if not os.path.exists(self.config.VIDEO_DB): self.__createVideoCacheDatabase() self.__db_conn = sqlite.connect(self.config.VIDEO_DB) self.__db_cursor = self.__db_conn.cursor() def clearCache(self): """ Clear video cache. Clean cache database and remova all metadata. """ covers = os.listdir(self.config.MOVIE_ART_DIR) for element in covers: if element[-3:] == "jpg": os.remove(os.path.join(self.config.MOVIE_ART_DIR, element)) os.remove(self.config.VIDEO_DB) self.__createVideoCacheDatabase() def addFile(self, filename): """ This method adds a new file to the cache. """ filename = filename.encode('utf8') if not self.isFileInCache(filename) and \ self.isSupportedFormat(filename): self._addVideoFile(filename) def removeFile(self, filename): """ This method removes file from the cache. """ print "removeFile(): " + filename if self.isFileInCache(filename): self.__db_cursor.execute( """ SELECT title, hash, series_title FROM videofile, metadata WHERE videofile.filename=:fn AND videofile.filename=metadata.filename""", { "fn" : filename }) result = self.__db_cursor.fetchall() title = result[0][0] thash = result[0][1] series = result[0][2] # Series cover art is named by series title (not episode title) if series is not None and len(series) != 0: title = series # Generate absolute path of thumbnail and cover art art = os.path.join(self.config.MOVIE_ART_DIR, str(title) + ".jpg") thumb = os.path.join(self.config.VIDEO_THUMB_DIR, str(thash) + ".jpg") # Remove video from video cache database self.__db_cursor.execute('''DELETE FROM videofile WHERE filename=:fn''', { "fn" : filename }) self.__db_cursor.execute('''DELETE FROM metadata WHERE filename=:fn''', { "fn" : filename }) self.__db_conn.commit() # Remove thumbnail and cover art if os.path.exists(art) and not self.__hasSeriesEpisodes(series): os.remove(art) if os.path.exists(thumb): os.remove(thumb) def updateFile(self, filename): """ This method is executed when a file, that is already in cache, changes. """ if self.isFileInCache(filename): self.removeFile(filename) self.addFile(filename) def addDirectory(self, path): """ This method adds a new directory to the cache. Sub directories are added recursively and all files in them. """ if not os.path.isdir(path) or not os.path.exists(path): self.logger.error( "Adding a directory to the video cache failed. " + "Path doesn't exist: '" + path + "'") else: self.logger.debug( "Adding a directory to the video cache. Path is: '" + path + "'") # pylint: disable-msg=W0612 for root, dirs, files in os.walk(path): for name in files: self.addFile(os.path.join(root, name)) time.sleep(float(self.SLEEP_TIME_BETWEEN_FILES) / 1000) def removeDirectory(self, path): """ This method removes directory from the cache. Also removes all subdirectories and all files in them. """ self.__db_cursor.execute("""SELECT filename FROM videofile WHERE filename LIKE '""" + path + "%'") result = self.__db_cursor.fetchall() for row in result: self.removeFile(row[0]) def updateDirectory(self, path): """ This method is executed when a directory, that is already in cache, changes. """ self.removeDirectory(path) self.addDirectory(path) def isDirectoryInCache(self, path): """ This method returns True if given directory is in cache. Otherwise method returns False. """ self.__db_cursor.execute("""SELECT * FROM videofile WHERE filename LIKE '""" + path + "%'") result = self.__db_cursor.fetchall() if len(result) == 0: return False else: return True def isFileInCache(self, filename): """ This method returns True if given file is in cache. Otherwise method returns False. """ self.__db_cursor.execute("""SELECT * FROM videofile WHERE filename=:fn""", { "fn" : filename}) result = self.__db_cursor.fetchall() if len(result) == 0: return False else: return True def isSupportedFormat(self, filename): """Check if file is supported.""" if (self.__getFileExtension(filename) in self.__SUPPORTED_FILE_EXTENSIONS): return True else: return False def __createVideoCacheDatabase(self): """Creates a video cache database file.""" db_conn = sqlite.connect(self.config.VIDEO_DB) db_cursor = db_conn.cursor() db_cursor.execute("""CREATE TABLE videofile( filename TEXT, hash VARCHAR(32), length INTEGER, resolution VARCHAR(16), PRIMARY KEY(filename))""") db_cursor.execute("""CREATE TABLE metadata( type VARCHAR(16) DEFAULT 'CLIP', filename TEXT, title TEXT, series_title VARCHAR(128), runtime INTEGER, genres VARCHAR(128), rating INTEGER, year VARCHAR(16), plot_outline TEXT, plot TEXT, season INTEGER, episode INTEGER, actor_1 VARCHAR(128), actor_2 VARCHAR(128), actor_3 VARCHAR(128), actor_4 VARCHAR(128), actor_5 VARCHAR(128), writer_1 VARCHAR(128), writer_2 VARCHAR(128), director_1 VARCHAR(128), director_2 VARCHAR(128), PRIMARY KEY(filename))""") db_conn.commit() db_conn.close() self.logger.debug("VideoCache database created successfully") def __getFileExtension(self, filename): """Return lower case file extension""" return filename[filename.rfind('.') + 1 :].lower() def _addVideoFile(self, filename): """Add video file to the video cache.""" # Generate thumbnail thumbnailer = VideoThumbnailer(filename) thumbnailer.create_thumbnail() thash = thumbnailer.get_hash() del thumbnailer self.__db_cursor.execute("""INSERT INTO videofile(filename, hash) VALUES (:fn, :hash)""", { 'fn': filename, 'hash': thash, } ) self.__db_cursor.execute("""INSERT INTO metadata(filename) VALUES (:fn)""", { "fn" : filename } ) self.__db_conn.commit() if self.config.download_metadata: self.__searchMetadata(filename) def __searchMetadata(self, filename): """Search metadata for video file from the Internet.""" search_thread = None search_thread = VideoMetadataSearch(filename) if search_thread is not None: search_thread.start() def __hasSeriesEpisodes(self, series_title): """ Return True if there are episodes for given series, otherwise False. This is used when removing file from cache. """ if len(series_title) == 0: return False else: self.__db_cursor.execute("""SELECT * FROM metadata WHERE series_title=:sn""", { "sn" : series_title} ) result = self.__db_cursor.fetchall() if len(result) == 0: return False else: return True
class ImageCache(Cache): """ This class is responsible of updating image cache as requested. ImageCache has a public interface that consists of 3 mehtods: addFile, removeFile and updateFile. All these methods get filename as a parameter. When ImageCache is called with filename it checks if the filename is supported format. This is done simply by checking the file extension. Supported file formats are: JPEG """ # Supported file formats SUPPORTED_FILE_EXTENSIONS = ['jpg', 'jpeg', 'png'] def __init__(self): """ Create a new ImageCache. Creates a new database if not already exists and opens a connection to it. """ self.logger = Logger().getLogger( 'backend.components.mediacache.ImageCache') self.config = Configuration() if not os.path.exists(self.config.IMAGE_DB): self._createImageCacheDatabase() self.db_conn = sqlite.connect(self.config.IMAGE_DB) self.db_cursor = self.db_conn.cursor() def clearCache(self): """ Clean image cache completely. Clean cache database and remove all thumbnails. """ thumbnails = os.listdir(self.config.IMAGE_THUMB_DIR) for element in thumbnails: thumb_file = os.path.join(self.config.IMAGE_THUMB_DIR, element) try: os.remove(thumb_file) except OSError: self.logger.error( "Media manager couldn't remove thumbnail : %s" % thumb_file) os.remove(self.config.IMAGE_DB) self._createImageCacheDatabase() def addFile(self, filename): """ Add image file to the cache. Do nothing if file is already cached. """ filename = filename.encode('utf8') if (not self.isFileInCache(filename) and self.isSupportedFormat(filename)): # Do not add album thumbnail to images if (filename[filename.rfind('/') + 1:filename.rfind('.')] == ".entertainer_album"): return self._addJPEGfile(filename) def removeFile(self, filename): """ Remove image file from the cache. Do nothing if file is not in cache. """ # Remove image file if self.isFileInCache(filename): self.db_cursor.execute( """SELECT hash FROM image WHERE filename=:fn""", {"fn": filename}) result = self.db_cursor.fetchall() if len(result) > 0: name = result[0][0] + '.jpg' thumb = os.path.join(self.config.IMAGE_THUMB_DIR, name) try: os.remove(thumb) except OSError: self.logger.error("Couldn't remove thumbnail: " + thumb) self.db_cursor.execute( """DELETE FROM image WHERE filename=:fn""", {"fn": filename}) self.db_conn.commit() def updateFile(self, filename): """Update image file that is already in the cache.""" if self.isFileInCache(filename): self.removeFile(filename) self.addFile(filename) def addDirectory(self, path): """ Adds a new directory to the cache. Sub directories are added recursively and all files in them. """ # pylint: disable-msg=W0612 if not os.path.isdir(path) or not os.path.exists(path): self.logger.error( "Adding a directory to the image cache failed. " + "Path doesn't exist: " + path) else: for root, dirs, files in os.walk(path): if os.path.split(root)[-1][0] == ".": continue if not self.isDirectoryInCache(root): self._addAlbum(root) for name in files: if os.path.split(name)[-1][0] == ".": continue if self.isSupportedFormat(name): self.addFile(os.path.join(root, name)) time.sleep(float(self.SLEEP_TIME_BETWEEN_FILES) / 1000) def removeDirectory(self, path): """ Removes directory from the cache. Also removes all subdirectories and all files in them. @param path - Absolute path """ # Remove image file thumbnails self.db_cursor.execute("""SELECT hash FROM image WHERE filename LIKE '""" + path + "%'") for row in self.db_cursor: thumb_file = row[0] + ".jpg" os.remove(os.path.join(self.config.IMAGE_THUMB_DIR, thumb_file)) # Remove folder thumbnails self.db_cursor.execute("""SELECT hash FROM album WHERE path LIKE '""" + path + "%'") for row in self.db_cursor: thumb_file = row[0] + ".jpg" os.remove(os.path.join(self.config.IMAGE_THUMB_DIR, thumb_file)) # Clean cache database self.db_cursor.execute("DELETE FROM album WHERE path LIKE '" + path + "%'") self.db_cursor.execute("DELETE FROM image WHERE album_path LIKE '" + path + "%'") self.db_conn.commit() def updateDirectory(self, path): """ Update directory. """ self.removeDirectory(path) self.addDirectory(path) def isFileInCache(self, filename): """Check if file is already in cache. Returns boolean value.""" self.db_cursor.execute( """SELECT * FROM image WHERE filename=:fn""", {"fn": filename}) result = self.db_cursor.fetchall() if len(result) == 0: return False else: return True def isDirectoryInCache(self, path): """Check if album is already in cache. Returns boolean value.""" self.db_cursor.execute( """SELECT * FROM album WHERE path=:p""", {"p": path}) result = self.db_cursor.fetchall() if len(result) == 0: return False else: return True def isSupportedFormat(self, filename): """Check if file is supported.""" if (filename[filename.rfind('.') + 1:].lower() in self.SUPPORTED_FILE_EXTENSIONS): return True else: return False def _createImageCacheDatabase(self): """Creates a image cache database file.""" db_conn = sqlite.connect(self.config.IMAGE_DB) db_cursor = db_conn.cursor() db_cursor.execute(""" CREATE TABLE image( filename TEXT, album_path TEXT, title TEXT, description TEXT, date DATE, time TIME, width INTEGER, height INTEGER, filesize LONG, hash VARCHAR(32), PRIMARY KEY(filename))""") db_cursor.execute(""" CREATE TABLE album( path TEXT, title TEXT, description TEXT, hash VARCHAR(32), PRIMARY KEY(path))""") db_conn.commit() db_conn.close() self.logger.debug("ImageCache database created successfully") def _addAlbum(self, path): """ Create a new album into image cache. Folders are handled as albums. Nested folders are not nested in database! All albums are on top level. """ album_info = os.path.join(path, ".entertainer_album.info") album_thumb = os.path.join(path, ".entertainer_album.jpg") # Get album information if os.path.exists(album_info): try: inf_f = open(album_info) a_title = inf_f.readline()[6:] a_description = inf_f.readline()[12:] except IOError: a_title = path[path.rfind('/') + 1:].replace('_', ' ').title() a_description = "" else: a_title = path[path.rfind('/') + 1:].replace('_', ' ').title() a_description = "" if os.path.exists(album_thumb): thumbnailer = ImageThumbnailer(album_thumb) thumbnailer.create_thumbnail() a_hash = thumbnailer.get_hash() else: a_hash = "" album_row = (path, a_title, a_description, a_hash) self.db_cursor.execute( """ INSERT INTO album(path, title, description, hash) VALUES(?,?,?,?) """, album_row) self.db_conn.commit() #print "Album added to cache: " + a_title def _addJPEGfile(self, filename): """ Add JPEG image to the image cache. Raises exception if adding fails. Process: - Open file - Get image date and time - Get image title and description - Get image size - Generate thumbnail / get hash from thumbnailer - Insert data to image cache database """ tmp = datetime.datetime.fromtimestamp(os.stat(filename)[-1]) timestamp = [ str(tmp.year) + "-" + str(tmp.month) + "-" + str(tmp.day), str(tmp.hour) + ":" + str(tmp.minute) + ":" + str(tmp.second) ] # Generate name from filename tmp = filename[filename.rfind('/') + 1:filename.rfind('.')] title = tmp.replace('_', ' ').title() # Make title more attractive description = "" # No description for this image file try: im = Image.open(filename) width, height = im.size except IOError: self.logger.error("Couldn't identify image file: " + filename) return # Create thumbnail and return hash thumbnailer = ImageThumbnailer(filename) thumbnailer.create_thumbnail() thumb_hash = thumbnailer.get_hash() del thumbnailer album_path = filename[:filename.rfind('/')] db_row = ( filename, # Filename (full path) title, # Title of the image description, # Description of the image timestamp[0], # Image's taken date timestamp[1], # Image's taken time width, # Image's width height, # Image's height os.path.getsize(filename), # Image file size in bytes thumb_hash, # Thumbnail hash (hash of the filename) album_path) # Path of the album (folder of this image) self.db_cursor.execute( """ INSERT INTO image(filename, title, description, date, time, width, height, filesize, hash, album_path) VALUES(?,?,?,?,?,?,?,?,?,?)""", db_row) self.db_conn.commit()
class MusicCache(Cache): """ Handles audio file cache. """ # Supported file formats __SUPPORTED_FILE_EXTENSIONS = ['mp3', 'ogg'] # SQLite database stuff __db_conn = None __db_cursor = None # Default values __DEFAULT = { "artist" : "Unknown artist", "album" : "Unknown album", "title" : "Unknown track", "genre" : "Unknown" } def __init__(self): """Create a new music database object.""" self.logger = Logger().getLogger( 'backend.components.mediacache.MusicCache') self.config = Configuration() if not os.path.exists(self.config.MUSIC_DB): self.__createMusicCacheDatabase() self.__db_conn = sqlite.connect(self.config.MUSIC_DB) self.__db_cursor = self.__db_conn.cursor() def clearCache(self): """ Clear music cache. Clean cache database and remova all albumart. """ covers = os.listdir(self.config.ALBUM_ART_DIR) for element in covers: os.remove(os.path.join(self.config.ALBUM_ART_DIR, element)) os.remove(self.config.MUSIC_DB) self.__createMusicCacheDatabase() def addFile(self, filename): """Add audio file to the cache.""" filename = filename.encode('utf8') if (not self.isFileInCache(filename) and self.isSupportedFormat(filename)): if self.__getFileExtension(filename) == "mp3": self.__addMP3file(filename) elif self.__getFileExtension(filename) == "ogg": self.__addOGGfile(filename) def removeFile(self, filename): """Remove audio file from the cache.""" if self.isFileInCache(filename): # Check if we should remove albumart self.__db_cursor.execute("""SELECT artist, album FROM track WHERE filename=:fn""", { "fn" : filename }) result = self.__db_cursor.fetchall() artist = result[0][0] album = result[0][1] self.__db_cursor.execute( """ SELECT * FROM track WHERE artist=:artist AND album=:album""", { "artist" : artist, "album" : album}) result = self.__db_cursor.fetchall() # If only one found then it's the file that is going to be removed if (len(result) == 1 and artist != self.__DEFAULT['artist'] and album != self.__DEFAULT['album']): albumart_file = artist + " - " + album + ".jpg" try: os.remove(os.path.join(self.config.ALBUM_ART_DIR, albumart_file)) except OSError: self.logger.error("Couldn't remove albumart: " + os.path.join(self.config.ALBUM_ART_DIR, albumart_file)) # Remove track from cache self.__db_cursor.execute("""DELETE FROM track WHERE filename=:fn""", { "fn" : filename}) self.__db_cursor.execute("""DELETE FROM playlist_relation WHERE filename=:fn""", { "fn" : filename}) self.__db_conn.commit() def updateFile(self, filename): """Update audio file that is already in the cache.""" if self.isFileInCache(filename): self.removeFile(filename) self.addFile(filename) def addDirectory(self, path): """Add directory that contains audio files to the cache.""" # pylint: disable-msg=W0612 if not os.path.isdir(path) or not os.path.exists(path): self.logger.error( "Adding a directory to the music cache failed. " + "Path doesn't exist: " + path) else: for root, dirs, files in os.walk(path): for name in files: self.addFile(os.path.join(root, name)) time.sleep(float(self.SLEEP_TIME_BETWEEN_FILES) / 1000) def removeDirectory(self, path): """Remove directory from the cache.""" # Get current artist and albums that are on the removed path self.__db_cursor.execute( """ SELECT DISTINCT artist, album FROM track WHERE filename LIKE ' """ + path + "%'") result = self.__db_cursor.fetchall() # Remove tracks from database self.__db_cursor.execute( "DELETE FROM track WHERE filename LIKE '" + path + "%'") self.__db_cursor.execute( "DELETE FROM playlist_relation WHERE filename LIKE '" + path + "%'") self.__db_conn.commit() # Check which album art we should remove for element in result: artist = element[0] album = element[1] self.__db_cursor.execute("""SELECT * FROM track WHERE artist=:artist AND album=:album""", { "artist" : artist, "album" : album}) found = self.__db_cursor.fetchall() # After delete there is no artist, album combination, so we can # remove album art if (len(found) == 0 and artist != self.__DEFAULT['artist'] and album != self.__DEFAULT['album']): albumart_file = artist + " - " + album + ".jpg" try: os.remove(os.path.join(self.config.ALBUM_ART_DIR, albumart_file)) except OSError: self.logger.error( "Couldn't remove albumart: " + os.path.join(self.config.ALBUM_ART_DIR, albumart_file)) def updateDirectory(self, path): """Update directory that is already in the cache.""" self.removeDirectory(path) self.addDirectory(path) def isDirectoryInCache(self, path): """Check if directory is in cache.""" self.__db_cursor.execute( "SELECT * FROM track WHERE filename LIKE '" + path + "%'") result = self.__db_cursor.fetchall() if len(result) == 0: return False else: return True def isFileInCache(self, filename): """Check if file is in cache.""" self.__db_cursor.execute("""SELECT * FROM track WHERE filename=:fn""", {"fn":filename} ) result = self.__db_cursor.fetchall() if len(result) == 0: return False else: return True def isSupportedFormat(self, filename): """Check if file is supported.""" if (self.__getFileExtension(filename) in self.__SUPPORTED_FILE_EXTENSIONS): return True else: return False def __createMusicCacheDatabase(self): """Creates a music cache database file.""" db_conn = sqlite.connect(self.config.MUSIC_DB) db_cursor = db_conn.cursor() db_cursor.execute("""CREATE TABLE track( filename TEXT, title VARCHAR(255), artist VARCHAR(255), album VARCHAR(255), tracknumber INTEGER, bitrate INTEGER, year INTEGER, rating INTEGER DEFAULT NULL, length INTEGER, genre VARCHAR(128), comment TEXT, lyrics TEXT DEFAULT "", PRIMARY KEY(filename))""") db_cursor.execute("""CREATE TABLE playlist( title TEXT, PRIMARY KEY(title))""") db_cursor.execute("""CREATE TABLE playlist_relation( title TEXT, filename TEXT, PRIMARY KEY(title, filename))""") db_conn.commit() db_conn.close() self.logger.debug("MusicCache database created successfully") def __getFileExtension(self, filename): """Return lower case file extension""" return filename[filename.rfind('.') + 1 :].lower() def __addMP3file(self, filename): """ Add mp3 file to the music cache Process: - Open file - Get tags - Insert data to the music cache database """ try: mp3_file = eyeD3.Mp3AudioFile(filename, eyeD3.ID3_ANY_VERSION) tags = mp3_file.getTag() except ValueError: # Tags are corrupt self.logger.error("Couldn't read ID3tags: " + filename) return if tags is None: self.logger.error("Couldn't read ID3tags: " + filename) return # Get track lenght in seconds length = mp3_file.getPlayTime() # Get avarage bitrate bitrate = mp3_file.getBitRate()[1] # Get artist name artist = tags.getArtist() if artist is None or len(artist) == 0: artist = self.__DEFAULT['artist'] # Get album name album = tags.getAlbum() if album is None or len(album) == 0: album = self.__DEFAULT['album'] # Get track title title = tags.getTitle() if title is None or len(title) == 0: title = self.__DEFAULT['title'] # Get track genre genre = str(tags.getGenre()) if genre is None or len(genre) == 0: genre = self.__DEFAULT['genre'] # Get track number tracknumber = tags.getTrackNum()[0] if tracknumber is None: tracknumber = 0 # Get track comment comment = tags.getComment() if comment is None or len(comment) == 0: comment = "" # Get track release year year = tags.getYear() if year is None or len(year) == 0: year = 0 db_row = (filename, title, artist, album, genre, length, tracknumber, bitrate, comment, year) self.__db_cursor.execute("""INSERT INTO track(filename, title, artist, album, genre, length, tracknumber, bitrate, comment, year) VALUES(?,?,?,?,?,?,?,?,?,?)""", db_row) self.__db_conn.commit() # Get song lyrics lyrics = tags.getLyrics() if len(lyrics) != 0: lyrics = str(lyrics[0]) self.__db_cursor.execute("""UPDATE track SET lyrics=:lyrics WHERE filename=:fn""", { "lyrics" : lyrics, "fn" : filename }) self.__db_conn.commit() # Get album art self.__searchAlbumArt(artist, album, filename) def __addOGGfile(self, filename): """ Add ogg file to the music cache Process: - Open file - Get tags - Insert data to the music cache database """ ogg_file = ogg.vorbis.VorbisFile(filename) info = ogg_file.comment().as_dict() # Get length length = round(ogg_file.time_total(-1)) # Get avarage bitrate bitrate = round(ogg_file.bitrate(-1) / 1000) # Get album name if info.has_key('ALBUM'): album = info['ALBUM'][0] else: album = self.__DEFAULT['album'] # Get artist name if info.has_key('ARTIST'): artist = info['ARTIST'][0] else: artist = self.__DEFAULT['artist'] # Get track title if info.has_key('TITLE'): if info.has_key('VERSION'): title = (str(info['TITLE'][0]) + " (" + str(info['VERSION'][0]) + ")") else: title = info['TITLE'][0] else: title = self.__DEFAULT['title'] # Get track number if info.has_key('TRACKNUMBER'): track_number = info['TRACKNUMBER'][0] else: track_number = 0 # Get track genre if info.has_key('GENRE'): genre = info['GENRE'][0] else: genre = self.__DEFAULT['genre'] # Get track comment if info.has_key('DESCRIPTION'): comment = info['DESCRIPTION'][0] elif info.has_key('COMMENT'): comment = info['COMMENT'][0] else: comment = "" # Get track year if info.has_key('DATE'): date = info['DATE'][0] year = date[:date.find('-')] else: year = 0 db_row = (filename, title, artist, album, genre, length, track_number, bitrate, comment, year) self.__db_cursor.execute("""INSERT INTO track(filename, title, artist, album, genre, length, tracknumber, bitrate, comment, year) VALUES(?,?,?,?,?,?,?,?,?,?)""", db_row) self.__db_conn.commit() # Get album art self.__searchAlbumArt(artist, album, filename) def __searchAlbumArt(self, artist, album, filename): """Execute album art search thread""" # base64 encode artist and album so there can be a '/' in the artist or # album artist_album = artist + " - " + album artist_album = artist_album.encode("base64") album_art_file = os.path.join( self.config.ALBUM_ART_DIR, artist_album + ".jpg") if not os.path.exists(album_art_file): # Search for local albumart if os.path.exists(filename[:filename.rfind('/')+1]+"cover.jpg"): shutil.copyfile(filename[:filename.rfind('/')+1]+"cover.jpg", album_art_file) elif os.path.exists(filename[:filename.rfind('/')+1]+"folder.jpg"): shutil.copyfile(filename[:filename.rfind('/')+1]+"folder.jpg", album_art_file) # Local not found -> try internet else: if self.config.download_album_art: if album != "Unknown album" and artist != "Unknown Artist": loader_thread = AlbumArtDownloader(album, artist, self.config.ALBUM_ART_DIR) loader_thread.start()