Esempio n. 1
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)
Esempio n. 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)
Esempio n. 3
0
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")
Esempio n. 4
0
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)
Esempio n. 5
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()
Esempio n. 6
0
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')
Esempio n. 7
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()
Esempio n. 8
0
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
Esempio n. 9
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()
Esempio n. 10
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()