class TestLogger(EntertainerTest): '''Logger test case''' def setUp(self): '''See unittest.TestCase''' EntertainerTest.setUp(self) self.logger = Logger() def tearDown(self): '''See unittest.TestCase''' EntertainerTest.tearDown(self) def testGetLogger(self): '''Tests logger.getLogger without a name''' self.lowlogger = self.logger.getLogger() self.assertTrue(isinstance(self.lowlogger, logging.Logger)) def testNamedLogger(self): '''Tests logger.getLogger with a name''' self.lowlogger = self.logger.getLogger('test') self.lowlogger.debug('Logger test for named logger test') # test this log for its name def testNoParams(self): '''Tests multiple logging mechanism'''
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)
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 __init__(self): """ Create a new MediaCacheManager object """ MessageHandler.__init__(self) self.logger = Logger().getLogger( 'backend.components.mediacache.MediaCacheManager') self.config = Configuration() self.video_folders = self.config.media_folders self._index_videos(self.video_folders) self.music_folders = self.config.media_folders self._index_music(self.music_folders) self.image_folders = self.config.media_folders self._index_images(self.image_folders)
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 __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 __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 __init__(self, filename, x_pos_percent=0, y_pos_percent=0): """Initialize the Texture object""" Base.__init__(self) clutter.Texture.__init__(self, filename) self.logger = Logger().getLogger('gui.widgets.Texture') self._position = None self._set_position((x_pos_percent, y_pos_percent))
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)
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 __init__(self): gobject.threads_init() self.config = Configuration() self.logger = Logger().getLogger('backend.BackendServer') self.message_bus = MessageBus() self._port = self.config.port # Connection server - Thread that listens incoming socket connections self.connection_server = None self.scheduler = None self.media_manager = None # The order of the initialize method calls is significant! Don't change # the order unless you know what you are doing! self.initialize_configuration() self.initialize_media_cache_manager() self.initialize_connection_server() self.initialize_scheduler()
def __init__(self, socket, message_bus): """ Create a new client connection @param socket: Socket object @param message_bus: MessageBus object """ threading.Thread.__init__(self) MessageHandler.__init__(self) self.message_bus = message_bus self.logger = Logger().getLogger('backend.core.ClientConnection') self.client_out = socket self.client = socket.makefile() self.client_name = "Unknown" # Client name self.message_bus_connected = False # Is connected to the message bus
class BackendServer: '''Backend is responsible for things like updating media library cache.''' def __init__(self): gobject.threads_init() self.config = Configuration() self.logger = Logger().getLogger('backend.BackendServer') self.message_bus = MessageBus() self._port = self.config.port # Connection server - Thread that listens incoming socket connections self.connection_server = None self.scheduler = None self.media_manager = None # The order of the initialize method calls is significant! Don't change # the order unless you know what you are doing! self.initialize_configuration() self.initialize_media_cache_manager() self.initialize_connection_server() self.initialize_scheduler() def initialize_configuration(self): """Initialize configuration""" cfg_dict = { MessageType.CONTENT_CONF_UPDATED : MessagePriority.VERY_HIGH, } self.message_bus.registerMessageHandler(self.config, cfg_dict) self.logger.debug("Configuration intialized successfully") def initialize_connection_server(self): """Initialize connection server.""" self.connection_server = ConnectionServer(self._port, self.message_bus) # Start listening incoming connections self.connection_server.start() def initialize_scheduler(self): """Initialize message scheduler.""" self.scheduler = MessageScheduler(self.message_bus) self.logger.debug("Message scheduler intialized successfully") def initialize_media_cache_manager(self): '''Initialize the media cache manager''' self.media_manager = MediaCacheManager() media_dict = { MessageType.CONTENT_CONF_UPDATED : MessagePriority.VERY_LOW, MessageType.REBUILD_IMAGE_CACHE : MessagePriority.HIGH, MessageType.REBUILD_MUSIC_CACHE : MessagePriority.HIGH, MessageType.REBUILD_VIDEO_CACHE : MessagePriority.HIGH } self.message_bus.registerMessageHandler(self.media_manager, media_dict) self.logger.debug("Media Manager intialized successfully")
def __init__(self, filename): """ Initialize metadata search thread. @param filename: Filename as string (find metadata for this file) """ threading.Thread.__init__(self) self.setName("Video metadata search thread") self.logger = Logger().getLogger( 'backend.components.mediacache.VideoMetadataSearch') self.config = Configuration() self.filename = filename self.title, self.season, self.episode = self._parse_filename(filename) try: self.IMDb = imdb.IMDb() except imdb.IMDbError: raise IOError("Couldn't connect to IMDB server!")
class BackendServer: '''Backend is responsible for things like updating media library cache.''' def __init__(self): gobject.threads_init() self.config = Configuration() self.logger = Logger().getLogger('backend.BackendServer') self.message_bus = MessageBus() self._port = self.config.port # Connection server - Thread that listens incoming socket connections self.connection_server = None self.scheduler = None self.media_manager = None # The order of the initialize method calls is significant! Don't change # the order unless you know what you are doing! self.initialize_configuration() self.initialize_media_cache_manager() self.initialize_connection_server() self.initialize_scheduler() def initialize_configuration(self): """Initialize configuration""" cfg_dict = { MessageType.CONTENT_CONF_UPDATED: MessagePriority.VERY_HIGH, } self.message_bus.registerMessageHandler(self.config, cfg_dict) self.logger.debug("Configuration intialized successfully") def initialize_connection_server(self): """Initialize connection server.""" self.connection_server = ConnectionServer(self._port, self.message_bus) # Start listening incoming connections self.connection_server.start() def initialize_scheduler(self): """Initialize message scheduler.""" self.scheduler = MessageScheduler(self.message_bus) self.logger.debug("Message scheduler intialized successfully") def initialize_media_cache_manager(self): '''Initialize the media cache manager''' self.media_manager = MediaCacheManager() media_dict = { MessageType.CONTENT_CONF_UPDATED: MessagePriority.VERY_LOW, MessageType.REBUILD_IMAGE_CACHE: MessagePriority.HIGH, MessageType.REBUILD_MUSIC_CACHE: MessagePriority.HIGH, MessageType.REBUILD_VIDEO_CACHE: MessagePriority.HIGH } self.message_bus.registerMessageHandler(self.media_manager, media_dict) self.logger.debug("Media Manager intialized successfully")
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)
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 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 MediaCacheManager(MessageHandler): """Makes sure that client has all the data available.""" def __init__(self): """ Create a new MediaCacheManager object """ MessageHandler.__init__(self) self.logger = Logger().getLogger( 'backend.components.mediacache.MediaCacheManager') self.config = Configuration() self.video_folders = self.config.media_folders self._index_videos(self.video_folders) self.music_folders = self.config.media_folders self._index_music(self.music_folders) self.image_folders = self.config.media_folders self._index_images(self.image_folders) # Should we rebuild to detect files that were removed while backend was # not running?! THERE HAS TO BE A BETTER SOLUTION FOR THIS PROBLEM #self.rebuildAllMediaCaches() def rebuildAllMediaCaches(self): """Rebuilds all media caches.""" self.rebuildImageCache() self.rebuildMusicCache() self.rebuildVideoCache() def rebuildVideoCache(self): """Destroy all current data and index everything from the scratch.""" self.logger.info("Video cache rebuilding requested") video_cache = VideoCache() video_cache.clearCache() self._index_videos(self.video_folders) def rebuildMusicCache(self): """Destroy all current data and index everything from the scratch.""" self.logger.info("Music cache rebuilding requested") music_cache = MusicCache() music_cache.clearCache() self._index_music(self.music_folders) def rebuildImageCache(self): """Destroy all current data and index everything from the scratch.""" self.logger.info("Image cache rebuilding requested") image_cache = ImageCache() image_cache.clearCache() self._index_images(self.image_folders) # Implements MessageHandler interface def handleMessage(self, message): '''Handles messages''' if message.get_type() == MessageType.CONTENT_CONF_UPDATED: self._update_content_folders() elif message.get_type() == MessageType.REBUILD_VIDEO_CACHE: self.rebuildVideoCache() elif message.get_type() == MessageType.REBUILD_MUSIC_CACHE: self.rebuildMusicCache() elif message.get_type() == MessageType.REBUILD_IMAGE_CACHE: self.rebuildImageCache() def _index_images(self, folders): """Index images from the given folders and their subfolders""" if len(folders) > 0: indexer = IndexerThread() indexer.setCacheType("image") indexer.setFolders(folders) indexer.start() def _index_music(self, folders): """Index music from the given folders and their subfolders""" if len(folders) > 0: indexer = IndexerThread() indexer.setCacheType("music") indexer.setFolders(folders) indexer.start() def _index_videos(self, folders): """Index videos from the given folders and their subfolders""" if len(folders) > 0: indexer = IndexerThread() indexer.setCacheType("video") indexer.setFolders(folders) indexer.start() def _update_content_folders(self): """ This updates media manager's content folders. This method is executed when content.conf has been updated. If folders are added we need to index them. If folders are removed, we need to remove them from the cache and also from FileSystemObeserver. """ updated_video_folders = self.config.media_folders updated_music_folders = self.config.media_folders updated_image_folders = self.config.media_folders # Handle image folder changes current_images = set(self.image_folders) updated_images = set(updated_image_folders) removed_images = current_images - updated_images new_images = updated_images - current_images self.image_folders = updated_image_folders image_cache = ImageCache() for element in removed_images: image_cache.removeDirectory(element) self._index_images(list(new_images)) # Handle music folder changes current_music = set(self.music_folders) updated_music = set(updated_music_folders) removed_music = current_music - updated_music new_music = updated_music - current_music self.music_folders = updated_music_folders music_cache = MusicCache() for element in removed_music: music_cache.removeDirectory(element) self._index_music(list(new_music)) # Handle video folder changes current_videos = set(self.video_folders) updated_videos = set(updated_video_folders) removed_videos = current_videos - updated_videos new_videos = updated_videos - current_videos self.video_folders = updated_video_folders video_cache = VideoCache() for element in removed_videos: video_cache.removeDirectory(element) self._index_videos(list(new_videos))
class LogViewer: """ Implements dialog that allows user to see logged events. This dialog is used to check Entertainer logfiles. It reads all data from selected file and saves rows to self.log_rows. Then it filters unwanted rows away by calling self.filterMessages(). This method adds rows to ListStore, which is the model of TreeView object. Combobox and refresh -button actions read files again Checkbox actions just filter current rows again """ UI_DIR = os.path.join(os.path.dirname(__file__), "uis") # Is this dialog running as a stand alone process __STAND_ALONE = None widgets = None dialog = None log_store = None log_rows = [] def __init__(self, stand_alone): self.logfile_entertainer = Configuration().LOG self.logger = Logger().getLogger('utils.log_viewer') self.__STAND_ALONE = stand_alone try: uifile = os.path.join(self.UI_DIR, "log_dialog.ui") self.builder = gtk.Builder() self.builder.set_translation_domain('entertainer') self.builder.add_from_file(uifile) except RuntimeError: self.logger.critical("Couldn't open ui file: " + uifile) sys.exit(1) callback_dic = { "on_close_log_button_clicked" : self.on_close_log_button_clicked, "on_log_refresh_button_clicked" : self.update_log_rows, "on_checkbutton_debug_toggled" : self.filter_messages, "on_checkbutton_critical_toggled" : self.filter_messages, "on_checkbutton_error_toggled" : self.filter_messages, "on_checkbutton_warning_toggled" : self.filter_messages, "on_checkbutton_info_toggled" : self.filter_messages } self.builder.connect_signals(callback_dic) # Create log treeview treeview = self.builder.get_object("treeview_log") cell_renderer1 = gtk.CellRendererText() cell_renderer2 = gtk.CellRendererText() cell_renderer3 = gtk.CellRendererText() cell_renderer4 = gtk.CellRendererText() column1 = gtk.TreeViewColumn("Date") column1.pack_start(cell_renderer1, True) column1.set_attributes(cell_renderer1, text = 0) column2 = gtk.TreeViewColumn("Time") column2.pack_start(cell_renderer2, True) column2.set_attributes(cell_renderer2, text = 1) column3 = gtk.TreeViewColumn("Type") column3.pack_start(cell_renderer3, True) column3.set_attributes(cell_renderer3, text = 2) column4 = gtk.TreeViewColumn("Message") column4.pack_end(cell_renderer4, True) column4.set_attributes(cell_renderer4, text = 3) treeview.append_column(column1) treeview.append_column(column2) treeview.append_column(column3) treeview.append_column(column4) treeview.set_headers_visible(True) # Set model to view and read data from logfile self.log_store = gtk.ListStore(str, str, str, str) treeview.set_model(self.log_store) self.update_log_rows() # Show Log viewer dialog self.dialog = self.builder.get_object("LogDialog") self.dialog.resize(750, 500) self.dialog.connect("destroy", self.on_close_log_button_clicked) self.dialog.show() def update_log_rows(self, widget=None): """Read logfile and udpate treeview""" self.log_rows[:] = [] try: for line in open(self.logfile_entertainer, 'r'): try: line_table = line.split() message = ' '.join(line_table[3:]) row = line_table[:3] + [message] parsed_row = parse_row(row) self.log_rows.append(parsed_row) except IndexError: print "Cannot parse log line: ", line except IOError: print "Cannot find logfile: ", self.logfile_entertainer # Reverse so that the latest message is at top self.log_rows.reverse() # Filter unwated message types self.filter_messages() def filter_messages(self, widget = None): """Checks which message types should be displayed on treeview""" if self.log_store: self.log_store.clear() debug = self.builder.get_object("checkbutton_debug").get_active() critical = self.builder.get_object("checkbutton_critical").get_active() error = self.builder.get_object("checkbutton_error").get_active() warning = self.builder.get_object("checkbutton_warning").get_active() info = self.builder.get_object("checkbutton_info").get_active() for element in self.log_rows: if element[2] == "DEBUG" and debug: self.log_store.append(element) elif element[2] == "CRITICAL" and critical: self.log_store.append(element) elif element[2] == "ERROR" and error: self.log_store.append(element) elif element[2] == "WARNING" and warning: self.log_store.append(element) elif element[2] == "INFO" and info: self.log_store.append(element) # Signal handlers def on_close_log_button_clicked(self, widget): """ If running as a stand alone process, quit. Otherwise only destroy dialog. """ self.dialog.hide() self.dialog.destroy() if(self.__STAND_ALONE): gtk.main_quit()
def __init__(self, image_library, music_library, video_library, quit_client_callback): self.quit_client_callback = quit_client_callback self.config = Configuration() # Store the dimensions in case users want to return to window mode self.old_width = self.config.stage_width self.old_height = self.config.stage_height self.logger = Logger().getLogger("client.gui.UserInterface") self.window = gtk.Window() self.window.connect("destroy", self.destroy_callback) self.window.set_title("Entertainer") # Set the window icon icon_theme = gtk.icon_theme_get_default() try: icon = icon_theme.load_icon("entertainer", 48, 0) self.window.set_icon(icon) except gobject.GError: # Must not be installed from a package, get icon from the branch file_dir = os.path.dirname(__file__) icon_path = os.path.join(file_dir, "..", "..", "icons", "hicolor", "48x48", "apps", "entertainer.png") icon = gtk.gdk.pixbuf_new_from_file(icon_path) self.window.set_icon(icon) # cluttergtk.Embed contains the stage that is the canvas for the GUI embed = cluttergtk.Embed() # Enforce a minimum size to prevent weird widget bugs embed.set_size_request(self.config.stage_width, self.config.stage_height) self.window.add(embed) # The embed widget must be realized before you can get the stage. embed.realize() self.stage = embed.get_stage() self._hide_cursor_timeout_key = None self.stage.connect("key-press-event", self.handle_keyboard_event) self.stage.connect("motion-event", self._handle_motion_event) self.stage.set_color(self.config.theme.get_color("background")) self.stage.set_size(self.config.stage_width, self.config.stage_height) self.stage.set_title("Entertainer") if self.config.start_in_fullscreen: self._fullscreen() self.is_fullscreen = True else: self.is_fullscreen = False # Initialize Screen history (allows user to navigate "back") self.history = ScreenHistory(self._remove_from_stage) self.player = MediaPlayer(self.stage, self.config.stage_width, self.config.stage_height) self.player.connect("volume-changed", self._on_volume_changed) # Initialize menu overlay texture self.is_overlay = False self.menu_overlay = MenuOverlay(self.config.theme) self.menu_overlay.set_opacity(0) self.menu_overlay.set_size(self.config.stage_width, self.config.stage_height) self.stage.add(self.menu_overlay) self.volume_indicator = VolumeIndicator() self.stage.add(self.volume_indicator) self.volume_indicator.connect("hiding", self._on_volume_indicator_hiding) self.fade_screen_timeline = clutter.Timeline(200) alpha = clutter.Alpha(self.fade_screen_timeline, clutter.EASE_IN_OUT_SINE) self.fade_screen_behaviour = clutter.BehaviourOpacity(255, 0, alpha) # Transition object. Handles effects between screen changes. transition_factory = TransitionFactory(self._remove_from_stage) self.transition = transition_factory.generate_transition() # Screen factory to create new screens self.screen_factory = ScreenFactory( image_library, music_library, video_library, self.player, self.move_to_new_screen, self.move_to_previous_screen, ) def default_key_to_user_event(): """Return the default user event provided by an unmapped keyboard event.""" return UserEvent.DEFAULT_EVENT # Dictionary for keyboard event handling self.key_to_user_event = defaultdict( default_key_to_user_event, { clutter.keysyms.Return: UserEvent.NAVIGATE_SELECT, clutter.keysyms.Up: UserEvent.NAVIGATE_UP, clutter.keysyms.Down: UserEvent.NAVIGATE_DOWN, clutter.keysyms.Left: UserEvent.NAVIGATE_LEFT, clutter.keysyms.Right: UserEvent.NAVIGATE_RIGHT, clutter.keysyms.BackSpace: UserEvent.NAVIGATE_BACK, clutter.keysyms.h: UserEvent.NAVIGATE_HOME, clutter.keysyms.w: UserEvent.NAVIGATE_FIRST_PAGE, clutter.keysyms.e: UserEvent.NAVIGATE_PREVIOUS_PAGE, clutter.keysyms.r: UserEvent.NAVIGATE_NEXT_PAGE, clutter.keysyms.t: UserEvent.NAVIGATE_LAST_PAGE, clutter.keysyms.f: UserEvent.TOGGLE_FULLSCREEN, clutter.keysyms.p: UserEvent.PLAYER_PLAY_PAUSE, clutter.keysyms.s: UserEvent.PLAYER_STOP, clutter.keysyms._1: UserEvent.USE_ASPECT_RATIO_1, clutter.keysyms._2: UserEvent.USE_ASPECT_RATIO_2, clutter.keysyms._3: UserEvent.USE_ASPECT_RATIO_3, clutter.keysyms._4: UserEvent.USE_ASPECT_RATIO_4, clutter.keysyms.x: UserEvent.PLAYER_SKIP_BACKWARD, clutter.keysyms.c: UserEvent.PLAYER_SKIP_FORWARD, clutter.keysyms.z: UserEvent.PLAYER_PREVIOUS, clutter.keysyms.v: UserEvent.PLAYER_NEXT, clutter.keysyms.m: UserEvent.PLAYER_VOLUME_UP, clutter.keysyms.l: UserEvent.PLAYER_VOLUME_DOWN, clutter.keysyms.q: UserEvent.QUIT, clutter.keysyms.Escape: UserEvent.QUIT, }, ) self.event_handlers = { UserEvent.DEFAULT_EVENT: self._handle_default, UserEvent.NAVIGATE_SELECT: self._handle_default, UserEvent.NAVIGATE_UP: self._handle_default, UserEvent.NAVIGATE_DOWN: self._handle_default, UserEvent.NAVIGATE_LEFT: self._handle_default, UserEvent.NAVIGATE_RIGHT: self._handle_default, UserEvent.NAVIGATE_BACK: self._handle_navigate_back, UserEvent.NAVIGATE_HOME: self._handle_navigate_home, UserEvent.NAVIGATE_FIRST_PAGE: self._handle_default, UserEvent.NAVIGATE_PREVIOUS_PAGE: self._handle_default, UserEvent.NAVIGATE_NEXT_PAGE: self._handle_default, UserEvent.NAVIGATE_LAST_PAGE: self._handle_default, UserEvent.TOGGLE_FULLSCREEN: self._handle_toggle_fullscreen, UserEvent.PLAYER_PLAY_PAUSE: self._handle_player_play_pause, UserEvent.PLAYER_STOP: self._handle_player_stop, UserEvent.USE_ASPECT_RATIO_1: self._handle_aspect_ratio, UserEvent.USE_ASPECT_RATIO_2: self._handle_aspect_ratio, UserEvent.USE_ASPECT_RATIO_3: self._handle_aspect_ratio, UserEvent.USE_ASPECT_RATIO_4: self._handle_aspect_ratio, UserEvent.PLAYER_SKIP_BACKWARD: self._handle_player_skip_backward, UserEvent.PLAYER_SKIP_FORWARD: self._handle_player_skip_forward, UserEvent.PLAYER_PREVIOUS: self._handle_player_previous, UserEvent.PLAYER_NEXT: self._handle_player_next, UserEvent.PLAYER_VOLUME_UP: self._handle_player_volume_up, UserEvent.PLAYER_VOLUME_DOWN: self._handle_player_volume_down, UserEvent.QUIT: self._handle_quit_client, } self.logger.debug("Frontend GUI initialized succesfully")
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 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 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()
def __init__(self, stand_alone): self.logfile_entertainer = Configuration().LOG self.logger = Logger().getLogger('utils.log_viewer') self.__STAND_ALONE = stand_alone try: uifile = os.path.join(self.UI_DIR, "log_dialog.ui") self.builder = gtk.Builder() self.builder.set_translation_domain('entertainer') self.builder.add_from_file(uifile) except RuntimeError: self.logger.critical("Couldn't open ui file: " + uifile) sys.exit(1) callback_dic = { "on_close_log_button_clicked" : self.on_close_log_button_clicked, "on_log_refresh_button_clicked" : self.update_log_rows, "on_checkbutton_debug_toggled" : self.filter_messages, "on_checkbutton_critical_toggled" : self.filter_messages, "on_checkbutton_error_toggled" : self.filter_messages, "on_checkbutton_warning_toggled" : self.filter_messages, "on_checkbutton_info_toggled" : self.filter_messages } self.builder.connect_signals(callback_dic) # Create log treeview treeview = self.builder.get_object("treeview_log") cell_renderer1 = gtk.CellRendererText() cell_renderer2 = gtk.CellRendererText() cell_renderer3 = gtk.CellRendererText() cell_renderer4 = gtk.CellRendererText() column1 = gtk.TreeViewColumn("Date") column1.pack_start(cell_renderer1, True) column1.set_attributes(cell_renderer1, text = 0) column2 = gtk.TreeViewColumn("Time") column2.pack_start(cell_renderer2, True) column2.set_attributes(cell_renderer2, text = 1) column3 = gtk.TreeViewColumn("Type") column3.pack_start(cell_renderer3, True) column3.set_attributes(cell_renderer3, text = 2) column4 = gtk.TreeViewColumn("Message") column4.pack_end(cell_renderer4, True) column4.set_attributes(cell_renderer4, text = 3) treeview.append_column(column1) treeview.append_column(column2) treeview.append_column(column3) treeview.append_column(column4) treeview.set_headers_visible(True) # Set model to view and read data from logfile self.log_store = gtk.ListStore(str, str, str, str) treeview.set_model(self.log_store) self.update_log_rows() # Show Log viewer dialog self.dialog = self.builder.get_object("LogDialog") self.dialog.resize(750, 500) self.dialog.connect("destroy", self.on_close_log_button_clicked) self.dialog.show()
def __init__(self, image_library, music_library, video_library, quit_client_callback): self.quit_client_callback = quit_client_callback self.config = Configuration() # Store the dimensions in case users want to return to window mode self.old_width = self.config.stage_width self.old_height = self.config.stage_height self.logger = Logger().getLogger('client.gui.UserInterface') self.window = gtk.Window() self.window.connect('destroy', self.destroy_callback) self.window.set_title('Entertainer') # Set the window icon icon_theme = gtk.icon_theme_get_default() try: icon = icon_theme.load_icon('entertainer', 48, 0) self.window.set_icon(icon) except gobject.GError: # Must not be installed from a package, get icon from the branch file_dir = os.path.dirname(__file__) icon_path = os.path.join(file_dir, '..', '..', 'icons', 'hicolor', '48x48', 'apps', 'entertainer.png') icon = gtk.gdk.pixbuf_new_from_file(icon_path) self.window.set_icon(icon) # cluttergtk.Embed contains the stage that is the canvas for the GUI embed = cluttergtk.Embed() # Enforce a minimum size to prevent weird widget bugs embed.set_size_request( self.config.stage_width, self.config.stage_height) self.window.add(embed) # The embed widget must be realized before you can get the stage. embed.realize() self.stage = embed.get_stage() self._hide_cursor_timeout_key = None self.stage.connect('key-press-event', self.handle_keyboard_event) self.stage.connect('motion-event', self._handle_motion_event) self.stage.set_color(self.config.theme.get_color("background")) self.stage.set_size(self.config.stage_width, self.config.stage_height) self.stage.set_title("Entertainer") if self.config.start_in_fullscreen: self._fullscreen() self.is_fullscreen = True else: self.is_fullscreen = False # Initialize Screen history (allows user to navigate "back") self.history = ScreenHistory(self._remove_from_stage) self.player = MediaPlayer(self.stage, self.config.stage_width, self.config.stage_height) self.player.connect('volume-changed', self._on_volume_changed) # Initialize menu overlay texture self.is_overlay = False self.menu_overlay = MenuOverlay(self.config.theme) self.menu_overlay.set_opacity(0) self.menu_overlay.set_size( self.config.stage_width, self.config.stage_height) self.stage.add(self.menu_overlay) self.volume_indicator = VolumeIndicator() self.stage.add(self.volume_indicator) self.volume_indicator.connect('hiding', self._on_volume_indicator_hiding) self.fade_screen_timeline = clutter.Timeline(200) alpha = clutter.Alpha(self.fade_screen_timeline, clutter.EASE_IN_OUT_SINE) self.fade_screen_behaviour = clutter.BehaviourOpacity(255, 0, alpha) # Transition object. Handles effects between screen changes. transition_factory = TransitionFactory(self._remove_from_stage) self.transition = transition_factory.generate_transition() # Screen factory to create new screens self.screen_factory = ScreenFactory( image_library, music_library, video_library, self.player, self.move_to_new_screen, self.move_to_previous_screen) def default_key_to_user_event(): '''Return the default user event provided by an unmapped keyboard event.''' return UserEvent.DEFAULT_EVENT # Dictionary for keyboard event handling self.key_to_user_event = defaultdict(default_key_to_user_event, { clutter.keysyms.Return : UserEvent.NAVIGATE_SELECT, clutter.keysyms.Up : UserEvent.NAVIGATE_UP, clutter.keysyms.Down : UserEvent.NAVIGATE_DOWN, clutter.keysyms.Left : UserEvent.NAVIGATE_LEFT, clutter.keysyms.Right : UserEvent.NAVIGATE_RIGHT, clutter.keysyms.BackSpace : UserEvent.NAVIGATE_BACK, clutter.keysyms.h : UserEvent.NAVIGATE_HOME, clutter.keysyms.w : UserEvent.NAVIGATE_FIRST_PAGE, clutter.keysyms.e : UserEvent.NAVIGATE_PREVIOUS_PAGE, clutter.keysyms.r : UserEvent.NAVIGATE_NEXT_PAGE, clutter.keysyms.t : UserEvent.NAVIGATE_LAST_PAGE, clutter.keysyms.f : UserEvent.TOGGLE_FULLSCREEN, clutter.keysyms.p : UserEvent.PLAYER_PLAY_PAUSE, clutter.keysyms.s : UserEvent.PLAYER_STOP, clutter.keysyms._1 : UserEvent.USE_ASPECT_RATIO_1, clutter.keysyms._2 : UserEvent.USE_ASPECT_RATIO_2, clutter.keysyms._3 : UserEvent.USE_ASPECT_RATIO_3, clutter.keysyms._4 : UserEvent.USE_ASPECT_RATIO_4, clutter.keysyms.x : UserEvent.PLAYER_SKIP_BACKWARD, clutter.keysyms.c : UserEvent.PLAYER_SKIP_FORWARD, clutter.keysyms.z : UserEvent.PLAYER_PREVIOUS, clutter.keysyms.v : UserEvent.PLAYER_NEXT, clutter.keysyms.m : UserEvent.PLAYER_VOLUME_UP, clutter.keysyms.l : UserEvent.PLAYER_VOLUME_DOWN, clutter.keysyms.q : UserEvent.QUIT, clutter.keysyms.Escape : UserEvent.QUIT }) self.event_handlers = { UserEvent.DEFAULT_EVENT : self._handle_default, UserEvent.NAVIGATE_SELECT : self._handle_default, UserEvent.NAVIGATE_UP : self._handle_default, UserEvent.NAVIGATE_DOWN : self._handle_default, UserEvent.NAVIGATE_LEFT : self._handle_default, UserEvent.NAVIGATE_RIGHT : self._handle_default, UserEvent.NAVIGATE_BACK : self._handle_navigate_back, UserEvent.NAVIGATE_HOME : self._handle_navigate_home, UserEvent.NAVIGATE_FIRST_PAGE : self._handle_default, UserEvent.NAVIGATE_PREVIOUS_PAGE : self._handle_default, UserEvent.NAVIGATE_NEXT_PAGE : self._handle_default, UserEvent.NAVIGATE_LAST_PAGE : self._handle_default, UserEvent.TOGGLE_FULLSCREEN : self._handle_toggle_fullscreen, UserEvent.PLAYER_PLAY_PAUSE : self._handle_player_play_pause, UserEvent.PLAYER_STOP : self._handle_player_stop, UserEvent.USE_ASPECT_RATIO_1 : self._handle_aspect_ratio, UserEvent.USE_ASPECT_RATIO_2 : self._handle_aspect_ratio, UserEvent.USE_ASPECT_RATIO_3 : self._handle_aspect_ratio, UserEvent.USE_ASPECT_RATIO_4 : self._handle_aspect_ratio, UserEvent.PLAYER_SKIP_BACKWARD : self._handle_player_skip_backward, UserEvent.PLAYER_SKIP_FORWARD : self._handle_player_skip_forward, UserEvent.PLAYER_PREVIOUS : self._handle_player_previous, UserEvent.PLAYER_NEXT : self._handle_player_next, UserEvent.PLAYER_VOLUME_UP : self._handle_player_volume_up, UserEvent.PLAYER_VOLUME_DOWN : self._handle_player_volume_down, UserEvent.QUIT : self._handle_quit_client } self.logger.debug("Frontend GUI initialized succesfully")
def setUp(self): '''See unittest.TestCase''' EntertainerTest.setUp(self) self.logger = Logger()
class UserInterface: """A main GUI window of the Entertainer client.""" def __init__(self, image_library, music_library, video_library, quit_client_callback): self.quit_client_callback = quit_client_callback self.config = Configuration() # Store the dimensions in case users want to return to window mode self.old_width = self.config.stage_width self.old_height = self.config.stage_height self.logger = Logger().getLogger("client.gui.UserInterface") self.window = gtk.Window() self.window.connect("destroy", self.destroy_callback) self.window.set_title("Entertainer") # Set the window icon icon_theme = gtk.icon_theme_get_default() try: icon = icon_theme.load_icon("entertainer", 48, 0) self.window.set_icon(icon) except gobject.GError: # Must not be installed from a package, get icon from the branch file_dir = os.path.dirname(__file__) icon_path = os.path.join(file_dir, "..", "..", "icons", "hicolor", "48x48", "apps", "entertainer.png") icon = gtk.gdk.pixbuf_new_from_file(icon_path) self.window.set_icon(icon) # cluttergtk.Embed contains the stage that is the canvas for the GUI embed = cluttergtk.Embed() # Enforce a minimum size to prevent weird widget bugs embed.set_size_request(self.config.stage_width, self.config.stage_height) self.window.add(embed) # The embed widget must be realized before you can get the stage. embed.realize() self.stage = embed.get_stage() self._hide_cursor_timeout_key = None self.stage.connect("key-press-event", self.handle_keyboard_event) self.stage.connect("motion-event", self._handle_motion_event) self.stage.set_color(self.config.theme.get_color("background")) self.stage.set_size(self.config.stage_width, self.config.stage_height) self.stage.set_title("Entertainer") if self.config.start_in_fullscreen: self._fullscreen() self.is_fullscreen = True else: self.is_fullscreen = False # Initialize Screen history (allows user to navigate "back") self.history = ScreenHistory(self._remove_from_stage) self.player = MediaPlayer(self.stage, self.config.stage_width, self.config.stage_height) self.player.connect("volume-changed", self._on_volume_changed) # Initialize menu overlay texture self.is_overlay = False self.menu_overlay = MenuOverlay(self.config.theme) self.menu_overlay.set_opacity(0) self.menu_overlay.set_size(self.config.stage_width, self.config.stage_height) self.stage.add(self.menu_overlay) self.volume_indicator = VolumeIndicator() self.stage.add(self.volume_indicator) self.volume_indicator.connect("hiding", self._on_volume_indicator_hiding) self.fade_screen_timeline = clutter.Timeline(200) alpha = clutter.Alpha(self.fade_screen_timeline, clutter.EASE_IN_OUT_SINE) self.fade_screen_behaviour = clutter.BehaviourOpacity(255, 0, alpha) # Transition object. Handles effects between screen changes. transition_factory = TransitionFactory(self._remove_from_stage) self.transition = transition_factory.generate_transition() # Screen factory to create new screens self.screen_factory = ScreenFactory( image_library, music_library, video_library, self.player, self.move_to_new_screen, self.move_to_previous_screen, ) def default_key_to_user_event(): """Return the default user event provided by an unmapped keyboard event.""" return UserEvent.DEFAULT_EVENT # Dictionary for keyboard event handling self.key_to_user_event = defaultdict( default_key_to_user_event, { clutter.keysyms.Return: UserEvent.NAVIGATE_SELECT, clutter.keysyms.Up: UserEvent.NAVIGATE_UP, clutter.keysyms.Down: UserEvent.NAVIGATE_DOWN, clutter.keysyms.Left: UserEvent.NAVIGATE_LEFT, clutter.keysyms.Right: UserEvent.NAVIGATE_RIGHT, clutter.keysyms.BackSpace: UserEvent.NAVIGATE_BACK, clutter.keysyms.h: UserEvent.NAVIGATE_HOME, clutter.keysyms.w: UserEvent.NAVIGATE_FIRST_PAGE, clutter.keysyms.e: UserEvent.NAVIGATE_PREVIOUS_PAGE, clutter.keysyms.r: UserEvent.NAVIGATE_NEXT_PAGE, clutter.keysyms.t: UserEvent.NAVIGATE_LAST_PAGE, clutter.keysyms.f: UserEvent.TOGGLE_FULLSCREEN, clutter.keysyms.p: UserEvent.PLAYER_PLAY_PAUSE, clutter.keysyms.s: UserEvent.PLAYER_STOP, clutter.keysyms._1: UserEvent.USE_ASPECT_RATIO_1, clutter.keysyms._2: UserEvent.USE_ASPECT_RATIO_2, clutter.keysyms._3: UserEvent.USE_ASPECT_RATIO_3, clutter.keysyms._4: UserEvent.USE_ASPECT_RATIO_4, clutter.keysyms.x: UserEvent.PLAYER_SKIP_BACKWARD, clutter.keysyms.c: UserEvent.PLAYER_SKIP_FORWARD, clutter.keysyms.z: UserEvent.PLAYER_PREVIOUS, clutter.keysyms.v: UserEvent.PLAYER_NEXT, clutter.keysyms.m: UserEvent.PLAYER_VOLUME_UP, clutter.keysyms.l: UserEvent.PLAYER_VOLUME_DOWN, clutter.keysyms.q: UserEvent.QUIT, clutter.keysyms.Escape: UserEvent.QUIT, }, ) self.event_handlers = { UserEvent.DEFAULT_EVENT: self._handle_default, UserEvent.NAVIGATE_SELECT: self._handle_default, UserEvent.NAVIGATE_UP: self._handle_default, UserEvent.NAVIGATE_DOWN: self._handle_default, UserEvent.NAVIGATE_LEFT: self._handle_default, UserEvent.NAVIGATE_RIGHT: self._handle_default, UserEvent.NAVIGATE_BACK: self._handle_navigate_back, UserEvent.NAVIGATE_HOME: self._handle_navigate_home, UserEvent.NAVIGATE_FIRST_PAGE: self._handle_default, UserEvent.NAVIGATE_PREVIOUS_PAGE: self._handle_default, UserEvent.NAVIGATE_NEXT_PAGE: self._handle_default, UserEvent.NAVIGATE_LAST_PAGE: self._handle_default, UserEvent.TOGGLE_FULLSCREEN: self._handle_toggle_fullscreen, UserEvent.PLAYER_PLAY_PAUSE: self._handle_player_play_pause, UserEvent.PLAYER_STOP: self._handle_player_stop, UserEvent.USE_ASPECT_RATIO_1: self._handle_aspect_ratio, UserEvent.USE_ASPECT_RATIO_2: self._handle_aspect_ratio, UserEvent.USE_ASPECT_RATIO_3: self._handle_aspect_ratio, UserEvent.USE_ASPECT_RATIO_4: self._handle_aspect_ratio, UserEvent.PLAYER_SKIP_BACKWARD: self._handle_player_skip_backward, UserEvent.PLAYER_SKIP_FORWARD: self._handle_player_skip_forward, UserEvent.PLAYER_PREVIOUS: self._handle_player_previous, UserEvent.PLAYER_NEXT: self._handle_player_next, UserEvent.PLAYER_VOLUME_UP: self._handle_player_volume_up, UserEvent.PLAYER_VOLUME_DOWN: self._handle_player_volume_down, UserEvent.QUIT: self._handle_quit_client, } self.logger.debug("Frontend GUI initialized succesfully") def _fullscreen(self): """Set the window, stage, and config to fullscreen dimensions.""" self.window.fullscreen() self.stage.set_fullscreen(True) self.config.stage_width = int(gtk.gdk.screen_width()) self.config.stage_height = int(gtk.gdk.screen_height()) def destroy_callback(self, widget): """Handle the GTK destroy signal and close gracefully.""" self.shutdown() def confirm_exit(self): """Confirm that the user wants to shut down.""" if self.current.name == "Question": # Confirmation dialog is already displayed. return kwargs = { "question": _("Are you sure you want to exit Entertainer?"), "answers": (_("Yes"), _("No")), "callbacks": (self.shutdown, None), } self.move_to_new_screen("question", kwargs) def start_up(self): """Start the user interface and make it visible.""" self.show() self.stage.hide_cursor() self.current = self.create_screen("main") self.transition.forward_effect(None, self.current) self.enable_menu_overlay() def shutdown(self): """Shut down the user interface.""" self.quit_client_callback() def _toggle_fullscreen(self): """Set the User Interface to fullscreen mode or back to window mode.""" if self.is_fullscreen: self.stage.set_fullscreen(False) self.window.unfullscreen() self.config.stage_width = self.old_width self.config.stage_height = self.old_height self.is_fullscreen = False else: self._fullscreen() self.is_fullscreen = True def create_screen(self, screen_type, data=None): """Delegate to the screen factory to generate a screen.""" screen = self.screen_factory.generate_screen(screen_type, data) self.stage.add(screen) return screen def move_to_new_screen(self, screen_type, kwargs=None, transition=Transition.FORWARD): """Callback method for screens and tabs to ask for new screens""" screen = self.create_screen(screen_type, kwargs) self.change_screen(screen, transition) def move_to_previous_screen(self): """Callback method to return to the previous screen in history.""" screen = self.history.get_screen() screen.update() self.change_screen(screen, Transition.BACKWARD) def show(self): """Show the user interface.""" self.window.show_all() def hide(self): """Hide the user interface.""" self.window.hide_all() def _remove_from_stage(self, group): """Remove the listed group from the stage""" self.stage.remove(group) def enable_menu_overlay(self): """ Enable menu overlay. Overlay should be enabled always when there is a video playing and menu showing at the same time. Overlay is not part of any specific screen. It is used for all screens when neccesary. """ if not self.is_overlay: self.is_overlay = True self.menu_overlay.fade_in() self.player.is_reactive_allowed = False def disable_menu_overlay(self): """ Disable menu overlay. Overlay should be disabled when current screen is a type of Screen.OSD. """ if self.is_overlay: self.is_overlay = False self.menu_overlay.fade_out() self.player.is_reactive_allowed = True def change_screen(self, screen, direction): """Transition the given screen in the direction provided.""" # Enable/Disable menu overlay if screen.kind == Screen.OSD: self.disable_menu_overlay() else: self.enable_menu_overlay() # Add current screen to screen history if direction == Transition.FORWARD: self.history.add_screen(self.current) # Change screen (Logical). Graphics is changed via animation from_screen = self.current self.current = screen # Animate screen change if direction == Transition.FORWARD: self.transition.forward_effect(from_screen, screen) elif direction == Transition.BACKWARD: self.transition.backward_effect(from_screen, screen) def _hide_cursor_timeout_callback(self): """Hide the cursor""" self.stage.hide_cursor() return True def _handle_motion_event(self, stage, clutter_event): """Show the cursor and start a timeout to hide it after 4 seconds.""" self.stage.show_cursor() if self._hide_cursor_timeout_key is not None: gobject.source_remove(self._hide_cursor_timeout_key) self._hide_cursor_timeout_key = gobject.timeout_add(4000, self._hide_cursor_timeout_callback) def handle_keyboard_event(self, stage, clutter_event, event_handler=None): """Translate all received keyboard events to UserEvents.""" if event_handler is None: event_handler = self.handle_user_event user_event = self.key_to_user_event[clutter_event.keyval] event_handler(UserEvent(user_event)) def handle_user_event(self, event): """Delegate the user event to its proper handler method.""" kind = event.get_type() self.event_handlers[kind](event) def _handle_aspect_ratio(self, event): """Handle UserEvent.USE_ASPECT_RATIO_*.""" kind = event.get_type() set_methods = { UserEvent.USE_ASPECT_RATIO_1: self.player.set_native_ratio, UserEvent.USE_ASPECT_RATIO_2: self.player.set_widescreen_ratio, UserEvent.USE_ASPECT_RATIO_3: self.player.set_zoom_ratio, UserEvent.USE_ASPECT_RATIO_4: self.player.set_intelligent_ratio, } set_methods[kind]() self.current.handle_user_event(event) def _handle_default(self, event): """Handle the most basic case where the event is passed to the current screen.""" self.current.handle_user_event(event) def _handle_navigate_back(self, event): """Handle UserEvent.NAVIGATE_BACK.""" if not self.history.is_empty: self.move_to_previous_screen() def _handle_navigate_home(self, event): """Handle UserEvent.NAVIGATE_HOME.""" self.move_to_new_screen("main") def _handle_player_next(self, event): """Handle UserEvent.PLAYER_NEXT.""" self.player.next() def _handle_player_play_pause(self, event): """Handle UserEvent.PLAYER_PLAY_PAUSE.""" if self.current.is_interested_in_play_action(): self.current.execute_play_action() else: if self.player.is_playing: self.player.pause() self.current.handle_user_event(event) else: self.player.play() self.current.handle_user_event(event) def _handle_player_previous(self, event): """Handle UserEvent.PLAYER_PREVIOUS.""" self.player.previous() def _handle_player_skip_backward(self, event): """Handle UserEvent.PLAYER_SKIP_BACKWARD.""" self.player.skip_backward() self.current.handle_user_event(event) def _handle_player_skip_forward(self, event): """Handle UserEvent.PLAYER_SKIP_FORWARD.""" self.player.skip_forward() self.current.handle_user_event(event) def _handle_player_stop(self, event): """Handle UserEvent.PLAYER_STOP.""" if self.player.is_playing: self.player.stop() self.current.handle_user_event(event) def _handle_player_volume_up(self, event): """Handle UserEvent.PLAYER_VOLUME_UP.""" self.player.volume_up() def _handle_player_volume_down(self, event): """Handle UserEvent.PLAYER_VOLUME_DOWN.""" self.player.volume_down() def _handle_toggle_fullscreen(self, event): """Handle UserEvent.TOGGLE_FULLSCREEN.""" self._toggle_fullscreen() def _handle_quit_client(self, event): """Handle UserEvent.QUIT.""" self.confirm_exit() def _on_volume_changed(self, event): """Show volume indicator and fade out the screen (if needed).""" if not self.volume_indicator.visible: if not self.fade_screen_behaviour.is_applied(self.current): self.fade_screen_behaviour.apply(self.current) self.fade_screen_behaviour.set_bounds(255, 50) self.fade_screen_timeline.start() self.volume_indicator.show_volume(self.player.volume) def _on_volume_indicator_hiding(self, event): """Restore previous screen opacity.""" self.fade_screen_behaviour.set_bounds(50, 255) self.fade_screen_timeline.start()
def __init__(self): self.configuration = Configuration() self.logger = Logger().getLogger( 'entertainerlib.indexer.indexing.Indexer')
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 UserInterface: '''A main GUI window of the Entertainer client.''' def __init__(self, image_library, music_library, video_library, quit_client_callback): self.quit_client_callback = quit_client_callback self.config = Configuration() # Store the dimensions in case users want to return to window mode self.old_width = self.config.stage_width self.old_height = self.config.stage_height self.logger = Logger().getLogger('client.gui.UserInterface') self.window = gtk.Window() self.window.connect('destroy', self.destroy_callback) self.window.set_title('Entertainer') # Set the window icon icon_theme = gtk.icon_theme_get_default() try: icon = icon_theme.load_icon('entertainer', 48, 0) self.window.set_icon(icon) except gobject.GError: # Must not be installed from a package, get icon from the branch file_dir = os.path.dirname(__file__) icon_path = os.path.join(file_dir, '..', '..', 'icons', 'hicolor', '48x48', 'apps', 'entertainer.png') icon = gtk.gdk.pixbuf_new_from_file(icon_path) self.window.set_icon(icon) # cluttergtk.Embed contains the stage that is the canvas for the GUI embed = cluttergtk.Embed() # Enforce a minimum size to prevent weird widget bugs embed.set_size_request( self.config.stage_width, self.config.stage_height) self.window.add(embed) # The embed widget must be realized before you can get the stage. embed.realize() self.stage = embed.get_stage() self._hide_cursor_timeout_key = None self.stage.connect('key-press-event', self.handle_keyboard_event) self.stage.connect('motion-event', self._handle_motion_event) self.stage.set_color(self.config.theme.get_color("background")) self.stage.set_size(self.config.stage_width, self.config.stage_height) self.stage.set_title("Entertainer") if self.config.start_in_fullscreen: self._fullscreen() self.is_fullscreen = True else: self.is_fullscreen = False # Initialize Screen history (allows user to navigate "back") self.history = ScreenHistory(self._remove_from_stage) self.player = MediaPlayer(self.stage, self.config.stage_width, self.config.stage_height) self.player.connect('volume-changed', self._on_volume_changed) # Initialize menu overlay texture self.is_overlay = False self.menu_overlay = MenuOverlay(self.config.theme) self.menu_overlay.set_opacity(0) self.menu_overlay.set_size( self.config.stage_width, self.config.stage_height) self.stage.add(self.menu_overlay) self.volume_indicator = VolumeIndicator() self.stage.add(self.volume_indicator) self.volume_indicator.connect('hiding', self._on_volume_indicator_hiding) self.fade_screen_timeline = clutter.Timeline(200) alpha = clutter.Alpha(self.fade_screen_timeline, clutter.EASE_IN_OUT_SINE) self.fade_screen_behaviour = clutter.BehaviourOpacity(255, 0, alpha) # Transition object. Handles effects between screen changes. transition_factory = TransitionFactory(self._remove_from_stage) self.transition = transition_factory.generate_transition() # Screen factory to create new screens self.screen_factory = ScreenFactory( image_library, music_library, video_library, self.player, self.move_to_new_screen, self.move_to_previous_screen) def default_key_to_user_event(): '''Return the default user event provided by an unmapped keyboard event.''' return UserEvent.DEFAULT_EVENT # Dictionary for keyboard event handling self.key_to_user_event = defaultdict(default_key_to_user_event, { clutter.keysyms.Return : UserEvent.NAVIGATE_SELECT, clutter.keysyms.Up : UserEvent.NAVIGATE_UP, clutter.keysyms.Down : UserEvent.NAVIGATE_DOWN, clutter.keysyms.Left : UserEvent.NAVIGATE_LEFT, clutter.keysyms.Right : UserEvent.NAVIGATE_RIGHT, clutter.keysyms.BackSpace : UserEvent.NAVIGATE_BACK, clutter.keysyms.h : UserEvent.NAVIGATE_HOME, clutter.keysyms.w : UserEvent.NAVIGATE_FIRST_PAGE, clutter.keysyms.e : UserEvent.NAVIGATE_PREVIOUS_PAGE, clutter.keysyms.r : UserEvent.NAVIGATE_NEXT_PAGE, clutter.keysyms.t : UserEvent.NAVIGATE_LAST_PAGE, clutter.keysyms.f : UserEvent.TOGGLE_FULLSCREEN, clutter.keysyms.p : UserEvent.PLAYER_PLAY_PAUSE, clutter.keysyms.s : UserEvent.PLAYER_STOP, clutter.keysyms._1 : UserEvent.USE_ASPECT_RATIO_1, clutter.keysyms._2 : UserEvent.USE_ASPECT_RATIO_2, clutter.keysyms._3 : UserEvent.USE_ASPECT_RATIO_3, clutter.keysyms._4 : UserEvent.USE_ASPECT_RATIO_4, clutter.keysyms.x : UserEvent.PLAYER_SKIP_BACKWARD, clutter.keysyms.c : UserEvent.PLAYER_SKIP_FORWARD, clutter.keysyms.z : UserEvent.PLAYER_PREVIOUS, clutter.keysyms.v : UserEvent.PLAYER_NEXT, clutter.keysyms.m : UserEvent.PLAYER_VOLUME_UP, clutter.keysyms.l : UserEvent.PLAYER_VOLUME_DOWN, clutter.keysyms.q : UserEvent.QUIT, clutter.keysyms.Escape : UserEvent.QUIT }) self.event_handlers = { UserEvent.DEFAULT_EVENT : self._handle_default, UserEvent.NAVIGATE_SELECT : self._handle_default, UserEvent.NAVIGATE_UP : self._handle_default, UserEvent.NAVIGATE_DOWN : self._handle_default, UserEvent.NAVIGATE_LEFT : self._handle_default, UserEvent.NAVIGATE_RIGHT : self._handle_default, UserEvent.NAVIGATE_BACK : self._handle_navigate_back, UserEvent.NAVIGATE_HOME : self._handle_navigate_home, UserEvent.NAVIGATE_FIRST_PAGE : self._handle_default, UserEvent.NAVIGATE_PREVIOUS_PAGE : self._handle_default, UserEvent.NAVIGATE_NEXT_PAGE : self._handle_default, UserEvent.NAVIGATE_LAST_PAGE : self._handle_default, UserEvent.TOGGLE_FULLSCREEN : self._handle_toggle_fullscreen, UserEvent.PLAYER_PLAY_PAUSE : self._handle_player_play_pause, UserEvent.PLAYER_STOP : self._handle_player_stop, UserEvent.USE_ASPECT_RATIO_1 : self._handle_aspect_ratio, UserEvent.USE_ASPECT_RATIO_2 : self._handle_aspect_ratio, UserEvent.USE_ASPECT_RATIO_3 : self._handle_aspect_ratio, UserEvent.USE_ASPECT_RATIO_4 : self._handle_aspect_ratio, UserEvent.PLAYER_SKIP_BACKWARD : self._handle_player_skip_backward, UserEvent.PLAYER_SKIP_FORWARD : self._handle_player_skip_forward, UserEvent.PLAYER_PREVIOUS : self._handle_player_previous, UserEvent.PLAYER_NEXT : self._handle_player_next, UserEvent.PLAYER_VOLUME_UP : self._handle_player_volume_up, UserEvent.PLAYER_VOLUME_DOWN : self._handle_player_volume_down, UserEvent.QUIT : self._handle_quit_client } self.logger.debug("Frontend GUI initialized succesfully") def _fullscreen(self): '''Set the window, stage, and config to fullscreen dimensions.''' self.window.fullscreen() self.stage.set_fullscreen(True) self.config.stage_width = int(gtk.gdk.screen_width()) self.config.stage_height = int(gtk.gdk.screen_height()) def destroy_callback(self, widget): '''Handle the GTK destroy signal and close gracefully.''' self.shutdown() def confirm_exit(self): '''Confirm that the user wants to shut down.''' if self.current.name == "Question": # Confirmation dialog is already displayed. return kwargs = { 'question' : _('Are you sure you want to exit Entertainer?'), 'answers' : (_('Yes'), _('No')), 'callbacks' : (self.shutdown, None) } self.move_to_new_screen("question", kwargs) def start_up(self): '''Start the user interface and make it visible.''' self.show() self.stage.hide_cursor() self.current = self.create_screen("main") self.transition.forward_effect(None, self.current) self.enable_menu_overlay() def shutdown(self): '''Shut down the user interface.''' self.quit_client_callback() def _toggle_fullscreen(self): '''Set the User Interface to fullscreen mode or back to window mode.''' if self.is_fullscreen: self.stage.set_fullscreen(False) self.window.unfullscreen() self.config.stage_width = self.old_width self.config.stage_height = self.old_height self.is_fullscreen = False else: self._fullscreen() self.is_fullscreen = True def create_screen(self, screen_type, data=None): '''Delegate to the screen factory to generate a screen.''' screen = self.screen_factory.generate_screen(screen_type, data) self.stage.add(screen) return screen def move_to_new_screen(self, screen_type, kwargs=None, transition=Transition.FORWARD): '''Callback method for screens and tabs to ask for new screens''' screen = self.create_screen(screen_type, kwargs) self.change_screen(screen, transition) def move_to_previous_screen(self): '''Callback method to return to the previous screen in history.''' screen = self.history.get_screen() screen.update() self.change_screen(screen, Transition.BACKWARD) def show(self): '''Show the user interface.''' self.window.show_all() def hide(self): '''Hide the user interface.''' self.window.hide_all() def _remove_from_stage(self, group): '''Remove the listed group from the stage''' self.stage.remove(group) def enable_menu_overlay(self): """ Enable menu overlay. Overlay should be enabled always when there is a video playing and menu showing at the same time. Overlay is not part of any specific screen. It is used for all screens when neccesary. """ if not self.is_overlay: self.is_overlay = True self.menu_overlay.fade_in() self.player.is_reactive_allowed = False def disable_menu_overlay(self): """ Disable menu overlay. Overlay should be disabled when current screen is a type of Screen.OSD. """ if self.is_overlay: self.is_overlay = False self.menu_overlay.fade_out() self.player.is_reactive_allowed = True def change_screen(self, screen, direction): '''Transition the given screen in the direction provided.''' # Enable/Disable menu overlay if screen.kind == Screen.OSD: self.disable_menu_overlay() else: self.enable_menu_overlay() # Add current screen to screen history if direction == Transition.FORWARD: self.history.add_screen(self.current) # Change screen (Logical). Graphics is changed via animation from_screen = self.current self.current = screen # Animate screen change if direction == Transition.FORWARD: self.transition.forward_effect(from_screen, screen) elif direction == Transition.BACKWARD: self.transition.backward_effect(from_screen, screen) def _hide_cursor_timeout_callback(self): '''Hide the cursor''' self.stage.hide_cursor() return True def _handle_motion_event(self, stage, clutter_event): '''Show the cursor and start a timeout to hide it after 4 seconds.''' self.stage.show_cursor() if self._hide_cursor_timeout_key is not None: gobject.source_remove(self._hide_cursor_timeout_key) self._hide_cursor_timeout_key = gobject.timeout_add(4000, self._hide_cursor_timeout_callback) def handle_keyboard_event(self, stage, clutter_event, event_handler=None): '''Translate all received keyboard events to UserEvents.''' if event_handler is None: event_handler = self.handle_user_event user_event = self.key_to_user_event[clutter_event.keyval] event_handler(UserEvent(user_event)) def handle_user_event(self, event): '''Delegate the user event to its proper handler method.''' kind = event.get_type() self.event_handlers[kind](event) def _handle_aspect_ratio(self, event): '''Handle UserEvent.USE_ASPECT_RATIO_*.''' kind = event.get_type() set_methods = { UserEvent.USE_ASPECT_RATIO_1 : self.player.set_native_ratio, UserEvent.USE_ASPECT_RATIO_2 : self.player.set_widescreen_ratio, UserEvent.USE_ASPECT_RATIO_3 : self.player.set_zoom_ratio, UserEvent.USE_ASPECT_RATIO_4 : self.player.set_intelligent_ratio } set_methods[kind]() self.current.handle_user_event(event) def _handle_default(self, event): '''Handle the most basic case where the event is passed to the current screen.''' self.current.handle_user_event(event) def _handle_navigate_back(self, event): '''Handle UserEvent.NAVIGATE_BACK.''' if not self.history.is_empty: self.move_to_previous_screen() def _handle_navigate_home(self, event): '''Handle UserEvent.NAVIGATE_HOME.''' self.move_to_new_screen('main') def _handle_player_next(self, event): '''Handle UserEvent.PLAYER_NEXT.''' self.player.next() def _handle_player_play_pause(self, event): '''Handle UserEvent.PLAYER_PLAY_PAUSE.''' if self.current.is_interested_in_play_action(): self.current.execute_play_action() else: if self.player.is_playing: self.player.pause() self.current.handle_user_event(event) else: self.player.play() self.current.handle_user_event(event) def _handle_player_previous(self, event): '''Handle UserEvent.PLAYER_PREVIOUS.''' self.player.previous() def _handle_player_skip_backward(self, event): '''Handle UserEvent.PLAYER_SKIP_BACKWARD.''' self.player.skip_backward() self.current.handle_user_event(event) def _handle_player_skip_forward(self, event): '''Handle UserEvent.PLAYER_SKIP_FORWARD.''' self.player.skip_forward() self.current.handle_user_event(event) def _handle_player_stop(self, event): '''Handle UserEvent.PLAYER_STOP.''' if self.player.is_playing: self.player.stop() self.current.handle_user_event(event) def _handle_player_volume_up(self, event): '''Handle UserEvent.PLAYER_VOLUME_UP.''' self.player.volume_up() def _handle_player_volume_down(self, event): '''Handle UserEvent.PLAYER_VOLUME_DOWN.''' self.player.volume_down() def _handle_toggle_fullscreen(self, event): '''Handle UserEvent.TOGGLE_FULLSCREEN.''' self._toggle_fullscreen() def _handle_quit_client(self, event): '''Handle UserEvent.QUIT.''' self.confirm_exit() def _on_volume_changed(self, event): '''Show volume indicator and fade out the screen (if needed).''' if not self.volume_indicator.visible: if not self.fade_screen_behaviour.is_applied(self.current): self.fade_screen_behaviour.apply(self.current) self.fade_screen_behaviour.set_bounds(255, 50) self.fade_screen_timeline.start() self.volume_indicator.show_volume(self.player.volume) def _on_volume_indicator_hiding(self, event): '''Restore previous screen opacity.''' self.fade_screen_behaviour.set_bounds(50, 255) self.fade_screen_timeline.start()