コード例 #1
0
ファイル: test_logger.py プロジェクト: tiwilliam/entertainer
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'''
コード例 #2
0
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)
コード例 #3
0
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)
コード例 #4
0
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'''
コード例 #5
0
    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()
コード例 #6
0
 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)
コード例 #7
0
    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()
コード例 #8
0
ファイル: message_bus.py プロジェクト: tiwilliam/entertainer
 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')
コード例 #9
0
ファイル: video_cache.py プロジェクト: tiwilliam/entertainer
    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()
コード例 #10
0
    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))
コード例 #11
0
 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)
コード例 #12
0
ファイル: music_cache.py プロジェクト: tiwilliam/entertainer
    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()
コード例 #13
0
ファイル: media_player.py プロジェクト: tiwilliam/entertainer
    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)
コード例 #14
0
    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()
コード例 #15
0
 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)
コード例 #16
0
 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
コード例 #17
0
ファイル: image_cache.py プロジェクト: tiwilliam/entertainer
    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()
コード例 #18
0
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")
コード例 #19
0
    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!")
コード例 #20
0
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")
コード例 #21
0
 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)
コード例 #22
0
    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()
コード例 #23
0
ファイル: media_player.py プロジェクト: tiwilliam/entertainer
    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)
コード例 #24
0
ファイル: media_player.py プロジェクト: tiwilliam/entertainer
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")
コード例 #25
0
ファイル: media_player.py プロジェクト: tiwilliam/entertainer
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')
コード例 #26
0
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))
コード例 #27
0
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()
コード例 #28
0
    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")
コード例 #29
0
ファイル: message_bus.py プロジェクト: tiwilliam/entertainer
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)
コード例 #30
0
ファイル: music_cache.py プロジェクト: tiwilliam/entertainer
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()
コード例 #31
0
ファイル: image_cache.py プロジェクト: tiwilliam/entertainer
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()
コード例 #32
0
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))
コード例 #33
0
ファイル: video_cache.py プロジェクト: tiwilliam/entertainer
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
コード例 #34
0
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()
コード例 #35
0
    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()
コード例 #36
0
    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")
コード例 #37
0
ファイル: test_logger.py プロジェクト: tiwilliam/entertainer
    def setUp(self):
        '''See unittest.TestCase'''
        EntertainerTest.setUp(self)

        self.logger = Logger()
コード例 #38
0
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()
コード例 #39
0
ファイル: indexer.py プロジェクト: tiwilliam/entertainer
 def __init__(self):
     self.configuration = Configuration()
     self.logger = Logger().getLogger(
         'entertainerlib.indexer.indexing.Indexer')
コード例 #40
0
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()
コード例 #41
0
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()
コード例 #42
0
    def setUp(self):
        '''See unittest.TestCase'''
        EntertainerTest.setUp(self)

        self.logger = Logger()