class BackendServer: '''Backend is responsible for things like updating media library cache.''' def __init__(self): gobject.threads_init() self.config = Configuration() self.logger = Logger().getLogger('backend.BackendServer') self.message_bus = MessageBus() self._port = self.config.port # Connection server - Thread that listens incoming socket connections self.connection_server = None self.scheduler = None self.media_manager = None # The order of the initialize method calls is significant! Don't change # the order unless you know what you are doing! self.initialize_configuration() self.initialize_media_cache_manager() self.initialize_connection_server() self.initialize_scheduler() def initialize_configuration(self): """Initialize configuration""" cfg_dict = { MessageType.CONTENT_CONF_UPDATED : MessagePriority.VERY_HIGH, } self.message_bus.registerMessageHandler(self.config, cfg_dict) self.logger.debug("Configuration intialized successfully") def initialize_connection_server(self): """Initialize connection server.""" self.connection_server = ConnectionServer(self._port, self.message_bus) # Start listening incoming connections self.connection_server.start() def initialize_scheduler(self): """Initialize message scheduler.""" self.scheduler = MessageScheduler(self.message_bus) self.logger.debug("Message scheduler intialized successfully") def initialize_media_cache_manager(self): '''Initialize the media cache manager''' self.media_manager = MediaCacheManager() media_dict = { MessageType.CONTENT_CONF_UPDATED : MessagePriority.VERY_LOW, MessageType.REBUILD_IMAGE_CACHE : MessagePriority.HIGH, MessageType.REBUILD_MUSIC_CACHE : MessagePriority.HIGH, MessageType.REBUILD_VIDEO_CACHE : MessagePriority.HIGH } self.message_bus.registerMessageHandler(self.media_manager, media_dict) self.logger.debug("Media Manager intialized successfully")
class BackendServer: '''Backend is responsible for things like updating media library cache.''' def __init__(self): gobject.threads_init() self.config = Configuration() self.logger = Logger().getLogger('backend.BackendServer') self.message_bus = MessageBus() self._port = self.config.port # Connection server - Thread that listens incoming socket connections self.connection_server = None self.scheduler = None self.media_manager = None # The order of the initialize method calls is significant! Don't change # the order unless you know what you are doing! self.initialize_configuration() self.initialize_media_cache_manager() self.initialize_connection_server() self.initialize_scheduler() def initialize_configuration(self): """Initialize configuration""" cfg_dict = { MessageType.CONTENT_CONF_UPDATED: MessagePriority.VERY_HIGH, } self.message_bus.registerMessageHandler(self.config, cfg_dict) self.logger.debug("Configuration intialized successfully") def initialize_connection_server(self): """Initialize connection server.""" self.connection_server = ConnectionServer(self._port, self.message_bus) # Start listening incoming connections self.connection_server.start() def initialize_scheduler(self): """Initialize message scheduler.""" self.scheduler = MessageScheduler(self.message_bus) self.logger.debug("Message scheduler intialized successfully") def initialize_media_cache_manager(self): '''Initialize the media cache manager''' self.media_manager = MediaCacheManager() media_dict = { MessageType.CONTENT_CONF_UPDATED: MessagePriority.VERY_LOW, MessageType.REBUILD_IMAGE_CACHE: MessagePriority.HIGH, MessageType.REBUILD_MUSIC_CACHE: MessagePriority.HIGH, MessageType.REBUILD_VIDEO_CACHE: MessagePriority.HIGH } self.message_bus.registerMessageHandler(self.media_manager, media_dict) self.logger.debug("Media Manager intialized successfully")
class UserInterface: """A main GUI window of the Entertainer client.""" def __init__(self, image_library, music_library, video_library, quit_client_callback): self.quit_client_callback = quit_client_callback self.config = Configuration() # Store the dimensions in case users want to return to window mode self.old_width = self.config.stage_width self.old_height = self.config.stage_height self.logger = Logger().getLogger("client.gui.UserInterface") self.window = gtk.Window() self.window.connect("destroy", self.destroy_callback) self.window.set_title("Entertainer") # Set the window icon icon_theme = gtk.icon_theme_get_default() try: icon = icon_theme.load_icon("entertainer", 48, 0) self.window.set_icon(icon) except gobject.GError: # Must not be installed from a package, get icon from the branch file_dir = os.path.dirname(__file__) icon_path = os.path.join(file_dir, "..", "..", "icons", "hicolor", "48x48", "apps", "entertainer.png") icon = gtk.gdk.pixbuf_new_from_file(icon_path) self.window.set_icon(icon) # cluttergtk.Embed contains the stage that is the canvas for the GUI embed = cluttergtk.Embed() # Enforce a minimum size to prevent weird widget bugs embed.set_size_request(self.config.stage_width, self.config.stage_height) self.window.add(embed) # The embed widget must be realized before you can get the stage. embed.realize() self.stage = embed.get_stage() self._hide_cursor_timeout_key = None self.stage.connect("key-press-event", self.handle_keyboard_event) self.stage.connect("motion-event", self._handle_motion_event) self.stage.set_color(self.config.theme.get_color("background")) self.stage.set_size(self.config.stage_width, self.config.stage_height) self.stage.set_title("Entertainer") if self.config.start_in_fullscreen: self._fullscreen() self.is_fullscreen = True else: self.is_fullscreen = False # Initialize Screen history (allows user to navigate "back") self.history = ScreenHistory(self._remove_from_stage) self.player = MediaPlayer(self.stage, self.config.stage_width, self.config.stage_height) self.player.connect("volume-changed", self._on_volume_changed) # Initialize menu overlay texture self.is_overlay = False self.menu_overlay = MenuOverlay(self.config.theme) self.menu_overlay.set_opacity(0) self.menu_overlay.set_size(self.config.stage_width, self.config.stage_height) self.stage.add(self.menu_overlay) self.volume_indicator = VolumeIndicator() self.stage.add(self.volume_indicator) self.volume_indicator.connect("hiding", self._on_volume_indicator_hiding) self.fade_screen_timeline = clutter.Timeline(200) alpha = clutter.Alpha(self.fade_screen_timeline, clutter.EASE_IN_OUT_SINE) self.fade_screen_behaviour = clutter.BehaviourOpacity(255, 0, alpha) # Transition object. Handles effects between screen changes. transition_factory = TransitionFactory(self._remove_from_stage) self.transition = transition_factory.generate_transition() # Screen factory to create new screens self.screen_factory = ScreenFactory( image_library, music_library, video_library, self.player, self.move_to_new_screen, self.move_to_previous_screen, ) def default_key_to_user_event(): """Return the default user event provided by an unmapped keyboard event.""" return UserEvent.DEFAULT_EVENT # Dictionary for keyboard event handling self.key_to_user_event = defaultdict( default_key_to_user_event, { clutter.keysyms.Return: UserEvent.NAVIGATE_SELECT, clutter.keysyms.Up: UserEvent.NAVIGATE_UP, clutter.keysyms.Down: UserEvent.NAVIGATE_DOWN, clutter.keysyms.Left: UserEvent.NAVIGATE_LEFT, clutter.keysyms.Right: UserEvent.NAVIGATE_RIGHT, clutter.keysyms.BackSpace: UserEvent.NAVIGATE_BACK, clutter.keysyms.h: UserEvent.NAVIGATE_HOME, clutter.keysyms.w: UserEvent.NAVIGATE_FIRST_PAGE, clutter.keysyms.e: UserEvent.NAVIGATE_PREVIOUS_PAGE, clutter.keysyms.r: UserEvent.NAVIGATE_NEXT_PAGE, clutter.keysyms.t: UserEvent.NAVIGATE_LAST_PAGE, clutter.keysyms.f: UserEvent.TOGGLE_FULLSCREEN, clutter.keysyms.p: UserEvent.PLAYER_PLAY_PAUSE, clutter.keysyms.s: UserEvent.PLAYER_STOP, clutter.keysyms._1: UserEvent.USE_ASPECT_RATIO_1, clutter.keysyms._2: UserEvent.USE_ASPECT_RATIO_2, clutter.keysyms._3: UserEvent.USE_ASPECT_RATIO_3, clutter.keysyms._4: UserEvent.USE_ASPECT_RATIO_4, clutter.keysyms.x: UserEvent.PLAYER_SKIP_BACKWARD, clutter.keysyms.c: UserEvent.PLAYER_SKIP_FORWARD, clutter.keysyms.z: UserEvent.PLAYER_PREVIOUS, clutter.keysyms.v: UserEvent.PLAYER_NEXT, clutter.keysyms.m: UserEvent.PLAYER_VOLUME_UP, clutter.keysyms.l: UserEvent.PLAYER_VOLUME_DOWN, clutter.keysyms.q: UserEvent.QUIT, clutter.keysyms.Escape: UserEvent.QUIT, }, ) self.event_handlers = { UserEvent.DEFAULT_EVENT: self._handle_default, UserEvent.NAVIGATE_SELECT: self._handle_default, UserEvent.NAVIGATE_UP: self._handle_default, UserEvent.NAVIGATE_DOWN: self._handle_default, UserEvent.NAVIGATE_LEFT: self._handle_default, UserEvent.NAVIGATE_RIGHT: self._handle_default, UserEvent.NAVIGATE_BACK: self._handle_navigate_back, UserEvent.NAVIGATE_HOME: self._handle_navigate_home, UserEvent.NAVIGATE_FIRST_PAGE: self._handle_default, UserEvent.NAVIGATE_PREVIOUS_PAGE: self._handle_default, UserEvent.NAVIGATE_NEXT_PAGE: self._handle_default, UserEvent.NAVIGATE_LAST_PAGE: self._handle_default, UserEvent.TOGGLE_FULLSCREEN: self._handle_toggle_fullscreen, UserEvent.PLAYER_PLAY_PAUSE: self._handle_player_play_pause, UserEvent.PLAYER_STOP: self._handle_player_stop, UserEvent.USE_ASPECT_RATIO_1: self._handle_aspect_ratio, UserEvent.USE_ASPECT_RATIO_2: self._handle_aspect_ratio, UserEvent.USE_ASPECT_RATIO_3: self._handle_aspect_ratio, UserEvent.USE_ASPECT_RATIO_4: self._handle_aspect_ratio, UserEvent.PLAYER_SKIP_BACKWARD: self._handle_player_skip_backward, UserEvent.PLAYER_SKIP_FORWARD: self._handle_player_skip_forward, UserEvent.PLAYER_PREVIOUS: self._handle_player_previous, UserEvent.PLAYER_NEXT: self._handle_player_next, UserEvent.PLAYER_VOLUME_UP: self._handle_player_volume_up, UserEvent.PLAYER_VOLUME_DOWN: self._handle_player_volume_down, UserEvent.QUIT: self._handle_quit_client, } self.logger.debug("Frontend GUI initialized succesfully") def _fullscreen(self): """Set the window, stage, and config to fullscreen dimensions.""" self.window.fullscreen() self.stage.set_fullscreen(True) self.config.stage_width = int(gtk.gdk.screen_width()) self.config.stage_height = int(gtk.gdk.screen_height()) def destroy_callback(self, widget): """Handle the GTK destroy signal and close gracefully.""" self.shutdown() def confirm_exit(self): """Confirm that the user wants to shut down.""" if self.current.name == "Question": # Confirmation dialog is already displayed. return kwargs = { "question": _("Are you sure you want to exit Entertainer?"), "answers": (_("Yes"), _("No")), "callbacks": (self.shutdown, None), } self.move_to_new_screen("question", kwargs) def start_up(self): """Start the user interface and make it visible.""" self.show() self.stage.hide_cursor() self.current = self.create_screen("main") self.transition.forward_effect(None, self.current) self.enable_menu_overlay() def shutdown(self): """Shut down the user interface.""" self.quit_client_callback() def _toggle_fullscreen(self): """Set the User Interface to fullscreen mode or back to window mode.""" if self.is_fullscreen: self.stage.set_fullscreen(False) self.window.unfullscreen() self.config.stage_width = self.old_width self.config.stage_height = self.old_height self.is_fullscreen = False else: self._fullscreen() self.is_fullscreen = True def create_screen(self, screen_type, data=None): """Delegate to the screen factory to generate a screen.""" screen = self.screen_factory.generate_screen(screen_type, data) self.stage.add(screen) return screen def move_to_new_screen(self, screen_type, kwargs=None, transition=Transition.FORWARD): """Callback method for screens and tabs to ask for new screens""" screen = self.create_screen(screen_type, kwargs) self.change_screen(screen, transition) def move_to_previous_screen(self): """Callback method to return to the previous screen in history.""" screen = self.history.get_screen() screen.update() self.change_screen(screen, Transition.BACKWARD) def show(self): """Show the user interface.""" self.window.show_all() def hide(self): """Hide the user interface.""" self.window.hide_all() def _remove_from_stage(self, group): """Remove the listed group from the stage""" self.stage.remove(group) def enable_menu_overlay(self): """ Enable menu overlay. Overlay should be enabled always when there is a video playing and menu showing at the same time. Overlay is not part of any specific screen. It is used for all screens when neccesary. """ if not self.is_overlay: self.is_overlay = True self.menu_overlay.fade_in() self.player.is_reactive_allowed = False def disable_menu_overlay(self): """ Disable menu overlay. Overlay should be disabled when current screen is a type of Screen.OSD. """ if self.is_overlay: self.is_overlay = False self.menu_overlay.fade_out() self.player.is_reactive_allowed = True def change_screen(self, screen, direction): """Transition the given screen in the direction provided.""" # Enable/Disable menu overlay if screen.kind == Screen.OSD: self.disable_menu_overlay() else: self.enable_menu_overlay() # Add current screen to screen history if direction == Transition.FORWARD: self.history.add_screen(self.current) # Change screen (Logical). Graphics is changed via animation from_screen = self.current self.current = screen # Animate screen change if direction == Transition.FORWARD: self.transition.forward_effect(from_screen, screen) elif direction == Transition.BACKWARD: self.transition.backward_effect(from_screen, screen) def _hide_cursor_timeout_callback(self): """Hide the cursor""" self.stage.hide_cursor() return True def _handle_motion_event(self, stage, clutter_event): """Show the cursor and start a timeout to hide it after 4 seconds.""" self.stage.show_cursor() if self._hide_cursor_timeout_key is not None: gobject.source_remove(self._hide_cursor_timeout_key) self._hide_cursor_timeout_key = gobject.timeout_add(4000, self._hide_cursor_timeout_callback) def handle_keyboard_event(self, stage, clutter_event, event_handler=None): """Translate all received keyboard events to UserEvents.""" if event_handler is None: event_handler = self.handle_user_event user_event = self.key_to_user_event[clutter_event.keyval] event_handler(UserEvent(user_event)) def handle_user_event(self, event): """Delegate the user event to its proper handler method.""" kind = event.get_type() self.event_handlers[kind](event) def _handle_aspect_ratio(self, event): """Handle UserEvent.USE_ASPECT_RATIO_*.""" kind = event.get_type() set_methods = { UserEvent.USE_ASPECT_RATIO_1: self.player.set_native_ratio, UserEvent.USE_ASPECT_RATIO_2: self.player.set_widescreen_ratio, UserEvent.USE_ASPECT_RATIO_3: self.player.set_zoom_ratio, UserEvent.USE_ASPECT_RATIO_4: self.player.set_intelligent_ratio, } set_methods[kind]() self.current.handle_user_event(event) def _handle_default(self, event): """Handle the most basic case where the event is passed to the current screen.""" self.current.handle_user_event(event) def _handle_navigate_back(self, event): """Handle UserEvent.NAVIGATE_BACK.""" if not self.history.is_empty: self.move_to_previous_screen() def _handle_navigate_home(self, event): """Handle UserEvent.NAVIGATE_HOME.""" self.move_to_new_screen("main") def _handle_player_next(self, event): """Handle UserEvent.PLAYER_NEXT.""" self.player.next() def _handle_player_play_pause(self, event): """Handle UserEvent.PLAYER_PLAY_PAUSE.""" if self.current.is_interested_in_play_action(): self.current.execute_play_action() else: if self.player.is_playing: self.player.pause() self.current.handle_user_event(event) else: self.player.play() self.current.handle_user_event(event) def _handle_player_previous(self, event): """Handle UserEvent.PLAYER_PREVIOUS.""" self.player.previous() def _handle_player_skip_backward(self, event): """Handle UserEvent.PLAYER_SKIP_BACKWARD.""" self.player.skip_backward() self.current.handle_user_event(event) def _handle_player_skip_forward(self, event): """Handle UserEvent.PLAYER_SKIP_FORWARD.""" self.player.skip_forward() self.current.handle_user_event(event) def _handle_player_stop(self, event): """Handle UserEvent.PLAYER_STOP.""" if self.player.is_playing: self.player.stop() self.current.handle_user_event(event) def _handle_player_volume_up(self, event): """Handle UserEvent.PLAYER_VOLUME_UP.""" self.player.volume_up() def _handle_player_volume_down(self, event): """Handle UserEvent.PLAYER_VOLUME_DOWN.""" self.player.volume_down() def _handle_toggle_fullscreen(self, event): """Handle UserEvent.TOGGLE_FULLSCREEN.""" self._toggle_fullscreen() def _handle_quit_client(self, event): """Handle UserEvent.QUIT.""" self.confirm_exit() def _on_volume_changed(self, event): """Show volume indicator and fade out the screen (if needed).""" if not self.volume_indicator.visible: if not self.fade_screen_behaviour.is_applied(self.current): self.fade_screen_behaviour.apply(self.current) self.fade_screen_behaviour.set_bounds(255, 50) self.fade_screen_timeline.start() self.volume_indicator.show_volume(self.player.volume) def _on_volume_indicator_hiding(self, event): """Restore previous screen opacity.""" self.fade_screen_behaviour.set_bounds(50, 255) self.fade_screen_timeline.start()
class UserInterface: '''A main GUI window of the Entertainer client.''' def __init__(self, image_library, music_library, video_library, quit_client_callback): self.quit_client_callback = quit_client_callback self.config = Configuration() # Store the dimensions in case users want to return to window mode self.old_width = self.config.stage_width self.old_height = self.config.stage_height self.logger = Logger().getLogger('client.gui.UserInterface') self.window = gtk.Window() self.window.connect('destroy', self.destroy_callback) self.window.set_title('Entertainer') # Set the window icon icon_theme = gtk.icon_theme_get_default() try: icon = icon_theme.load_icon('entertainer', 48, 0) self.window.set_icon(icon) except gobject.GError: # Must not be installed from a package, get icon from the branch file_dir = os.path.dirname(__file__) icon_path = os.path.join(file_dir, '..', '..', 'icons', 'hicolor', '48x48', 'apps', 'entertainer.png') icon = gtk.gdk.pixbuf_new_from_file(icon_path) self.window.set_icon(icon) # cluttergtk.Embed contains the stage that is the canvas for the GUI embed = cluttergtk.Embed() # Enforce a minimum size to prevent weird widget bugs embed.set_size_request( self.config.stage_width, self.config.stage_height) self.window.add(embed) # The embed widget must be realized before you can get the stage. embed.realize() self.stage = embed.get_stage() self._hide_cursor_timeout_key = None self.stage.connect('key-press-event', self.handle_keyboard_event) self.stage.connect('motion-event', self._handle_motion_event) self.stage.set_color(self.config.theme.get_color("background")) self.stage.set_size(self.config.stage_width, self.config.stage_height) self.stage.set_title("Entertainer") if self.config.start_in_fullscreen: self._fullscreen() self.is_fullscreen = True else: self.is_fullscreen = False # Initialize Screen history (allows user to navigate "back") self.history = ScreenHistory(self._remove_from_stage) self.player = MediaPlayer(self.stage, self.config.stage_width, self.config.stage_height) self.player.connect('volume-changed', self._on_volume_changed) # Initialize menu overlay texture self.is_overlay = False self.menu_overlay = MenuOverlay(self.config.theme) self.menu_overlay.set_opacity(0) self.menu_overlay.set_size( self.config.stage_width, self.config.stage_height) self.stage.add(self.menu_overlay) self.volume_indicator = VolumeIndicator() self.stage.add(self.volume_indicator) self.volume_indicator.connect('hiding', self._on_volume_indicator_hiding) self.fade_screen_timeline = clutter.Timeline(200) alpha = clutter.Alpha(self.fade_screen_timeline, clutter.EASE_IN_OUT_SINE) self.fade_screen_behaviour = clutter.BehaviourOpacity(255, 0, alpha) # Transition object. Handles effects between screen changes. transition_factory = TransitionFactory(self._remove_from_stage) self.transition = transition_factory.generate_transition() # Screen factory to create new screens self.screen_factory = ScreenFactory( image_library, music_library, video_library, self.player, self.move_to_new_screen, self.move_to_previous_screen) def default_key_to_user_event(): '''Return the default user event provided by an unmapped keyboard event.''' return UserEvent.DEFAULT_EVENT # Dictionary for keyboard event handling self.key_to_user_event = defaultdict(default_key_to_user_event, { clutter.keysyms.Return : UserEvent.NAVIGATE_SELECT, clutter.keysyms.Up : UserEvent.NAVIGATE_UP, clutter.keysyms.Down : UserEvent.NAVIGATE_DOWN, clutter.keysyms.Left : UserEvent.NAVIGATE_LEFT, clutter.keysyms.Right : UserEvent.NAVIGATE_RIGHT, clutter.keysyms.BackSpace : UserEvent.NAVIGATE_BACK, clutter.keysyms.h : UserEvent.NAVIGATE_HOME, clutter.keysyms.w : UserEvent.NAVIGATE_FIRST_PAGE, clutter.keysyms.e : UserEvent.NAVIGATE_PREVIOUS_PAGE, clutter.keysyms.r : UserEvent.NAVIGATE_NEXT_PAGE, clutter.keysyms.t : UserEvent.NAVIGATE_LAST_PAGE, clutter.keysyms.f : UserEvent.TOGGLE_FULLSCREEN, clutter.keysyms.p : UserEvent.PLAYER_PLAY_PAUSE, clutter.keysyms.s : UserEvent.PLAYER_STOP, clutter.keysyms._1 : UserEvent.USE_ASPECT_RATIO_1, clutter.keysyms._2 : UserEvent.USE_ASPECT_RATIO_2, clutter.keysyms._3 : UserEvent.USE_ASPECT_RATIO_3, clutter.keysyms._4 : UserEvent.USE_ASPECT_RATIO_4, clutter.keysyms.x : UserEvent.PLAYER_SKIP_BACKWARD, clutter.keysyms.c : UserEvent.PLAYER_SKIP_FORWARD, clutter.keysyms.z : UserEvent.PLAYER_PREVIOUS, clutter.keysyms.v : UserEvent.PLAYER_NEXT, clutter.keysyms.m : UserEvent.PLAYER_VOLUME_UP, clutter.keysyms.l : UserEvent.PLAYER_VOLUME_DOWN, clutter.keysyms.q : UserEvent.QUIT, clutter.keysyms.Escape : UserEvent.QUIT }) self.event_handlers = { UserEvent.DEFAULT_EVENT : self._handle_default, UserEvent.NAVIGATE_SELECT : self._handle_default, UserEvent.NAVIGATE_UP : self._handle_default, UserEvent.NAVIGATE_DOWN : self._handle_default, UserEvent.NAVIGATE_LEFT : self._handle_default, UserEvent.NAVIGATE_RIGHT : self._handle_default, UserEvent.NAVIGATE_BACK : self._handle_navigate_back, UserEvent.NAVIGATE_HOME : self._handle_navigate_home, UserEvent.NAVIGATE_FIRST_PAGE : self._handle_default, UserEvent.NAVIGATE_PREVIOUS_PAGE : self._handle_default, UserEvent.NAVIGATE_NEXT_PAGE : self._handle_default, UserEvent.NAVIGATE_LAST_PAGE : self._handle_default, UserEvent.TOGGLE_FULLSCREEN : self._handle_toggle_fullscreen, UserEvent.PLAYER_PLAY_PAUSE : self._handle_player_play_pause, UserEvent.PLAYER_STOP : self._handle_player_stop, UserEvent.USE_ASPECT_RATIO_1 : self._handle_aspect_ratio, UserEvent.USE_ASPECT_RATIO_2 : self._handle_aspect_ratio, UserEvent.USE_ASPECT_RATIO_3 : self._handle_aspect_ratio, UserEvent.USE_ASPECT_RATIO_4 : self._handle_aspect_ratio, UserEvent.PLAYER_SKIP_BACKWARD : self._handle_player_skip_backward, UserEvent.PLAYER_SKIP_FORWARD : self._handle_player_skip_forward, UserEvent.PLAYER_PREVIOUS : self._handle_player_previous, UserEvent.PLAYER_NEXT : self._handle_player_next, UserEvent.PLAYER_VOLUME_UP : self._handle_player_volume_up, UserEvent.PLAYER_VOLUME_DOWN : self._handle_player_volume_down, UserEvent.QUIT : self._handle_quit_client } self.logger.debug("Frontend GUI initialized succesfully") def _fullscreen(self): '''Set the window, stage, and config to fullscreen dimensions.''' self.window.fullscreen() self.stage.set_fullscreen(True) self.config.stage_width = int(gtk.gdk.screen_width()) self.config.stage_height = int(gtk.gdk.screen_height()) def destroy_callback(self, widget): '''Handle the GTK destroy signal and close gracefully.''' self.shutdown() def confirm_exit(self): '''Confirm that the user wants to shut down.''' if self.current.name == "Question": # Confirmation dialog is already displayed. return kwargs = { 'question' : _('Are you sure you want to exit Entertainer?'), 'answers' : (_('Yes'), _('No')), 'callbacks' : (self.shutdown, None) } self.move_to_new_screen("question", kwargs) def start_up(self): '''Start the user interface and make it visible.''' self.show() self.stage.hide_cursor() self.current = self.create_screen("main") self.transition.forward_effect(None, self.current) self.enable_menu_overlay() def shutdown(self): '''Shut down the user interface.''' self.quit_client_callback() def _toggle_fullscreen(self): '''Set the User Interface to fullscreen mode or back to window mode.''' if self.is_fullscreen: self.stage.set_fullscreen(False) self.window.unfullscreen() self.config.stage_width = self.old_width self.config.stage_height = self.old_height self.is_fullscreen = False else: self._fullscreen() self.is_fullscreen = True def create_screen(self, screen_type, data=None): '''Delegate to the screen factory to generate a screen.''' screen = self.screen_factory.generate_screen(screen_type, data) self.stage.add(screen) return screen def move_to_new_screen(self, screen_type, kwargs=None, transition=Transition.FORWARD): '''Callback method for screens and tabs to ask for new screens''' screen = self.create_screen(screen_type, kwargs) self.change_screen(screen, transition) def move_to_previous_screen(self): '''Callback method to return to the previous screen in history.''' screen = self.history.get_screen() screen.update() self.change_screen(screen, Transition.BACKWARD) def show(self): '''Show the user interface.''' self.window.show_all() def hide(self): '''Hide the user interface.''' self.window.hide_all() def _remove_from_stage(self, group): '''Remove the listed group from the stage''' self.stage.remove(group) def enable_menu_overlay(self): """ Enable menu overlay. Overlay should be enabled always when there is a video playing and menu showing at the same time. Overlay is not part of any specific screen. It is used for all screens when neccesary. """ if not self.is_overlay: self.is_overlay = True self.menu_overlay.fade_in() self.player.is_reactive_allowed = False def disable_menu_overlay(self): """ Disable menu overlay. Overlay should be disabled when current screen is a type of Screen.OSD. """ if self.is_overlay: self.is_overlay = False self.menu_overlay.fade_out() self.player.is_reactive_allowed = True def change_screen(self, screen, direction): '''Transition the given screen in the direction provided.''' # Enable/Disable menu overlay if screen.kind == Screen.OSD: self.disable_menu_overlay() else: self.enable_menu_overlay() # Add current screen to screen history if direction == Transition.FORWARD: self.history.add_screen(self.current) # Change screen (Logical). Graphics is changed via animation from_screen = self.current self.current = screen # Animate screen change if direction == Transition.FORWARD: self.transition.forward_effect(from_screen, screen) elif direction == Transition.BACKWARD: self.transition.backward_effect(from_screen, screen) def _hide_cursor_timeout_callback(self): '''Hide the cursor''' self.stage.hide_cursor() return True def _handle_motion_event(self, stage, clutter_event): '''Show the cursor and start a timeout to hide it after 4 seconds.''' self.stage.show_cursor() if self._hide_cursor_timeout_key is not None: gobject.source_remove(self._hide_cursor_timeout_key) self._hide_cursor_timeout_key = gobject.timeout_add(4000, self._hide_cursor_timeout_callback) def handle_keyboard_event(self, stage, clutter_event, event_handler=None): '''Translate all received keyboard events to UserEvents.''' if event_handler is None: event_handler = self.handle_user_event user_event = self.key_to_user_event[clutter_event.keyval] event_handler(UserEvent(user_event)) def handle_user_event(self, event): '''Delegate the user event to its proper handler method.''' kind = event.get_type() self.event_handlers[kind](event) def _handle_aspect_ratio(self, event): '''Handle UserEvent.USE_ASPECT_RATIO_*.''' kind = event.get_type() set_methods = { UserEvent.USE_ASPECT_RATIO_1 : self.player.set_native_ratio, UserEvent.USE_ASPECT_RATIO_2 : self.player.set_widescreen_ratio, UserEvent.USE_ASPECT_RATIO_3 : self.player.set_zoom_ratio, UserEvent.USE_ASPECT_RATIO_4 : self.player.set_intelligent_ratio } set_methods[kind]() self.current.handle_user_event(event) def _handle_default(self, event): '''Handle the most basic case where the event is passed to the current screen.''' self.current.handle_user_event(event) def _handle_navigate_back(self, event): '''Handle UserEvent.NAVIGATE_BACK.''' if not self.history.is_empty: self.move_to_previous_screen() def _handle_navigate_home(self, event): '''Handle UserEvent.NAVIGATE_HOME.''' self.move_to_new_screen('main') def _handle_player_next(self, event): '''Handle UserEvent.PLAYER_NEXT.''' self.player.next() def _handle_player_play_pause(self, event): '''Handle UserEvent.PLAYER_PLAY_PAUSE.''' if self.current.is_interested_in_play_action(): self.current.execute_play_action() else: if self.player.is_playing: self.player.pause() self.current.handle_user_event(event) else: self.player.play() self.current.handle_user_event(event) def _handle_player_previous(self, event): '''Handle UserEvent.PLAYER_PREVIOUS.''' self.player.previous() def _handle_player_skip_backward(self, event): '''Handle UserEvent.PLAYER_SKIP_BACKWARD.''' self.player.skip_backward() self.current.handle_user_event(event) def _handle_player_skip_forward(self, event): '''Handle UserEvent.PLAYER_SKIP_FORWARD.''' self.player.skip_forward() self.current.handle_user_event(event) def _handle_player_stop(self, event): '''Handle UserEvent.PLAYER_STOP.''' if self.player.is_playing: self.player.stop() self.current.handle_user_event(event) def _handle_player_volume_up(self, event): '''Handle UserEvent.PLAYER_VOLUME_UP.''' self.player.volume_up() def _handle_player_volume_down(self, event): '''Handle UserEvent.PLAYER_VOLUME_DOWN.''' self.player.volume_down() def _handle_toggle_fullscreen(self, event): '''Handle UserEvent.TOGGLE_FULLSCREEN.''' self._toggle_fullscreen() def _handle_quit_client(self, event): '''Handle UserEvent.QUIT.''' self.confirm_exit() def _on_volume_changed(self, event): '''Show volume indicator and fade out the screen (if needed).''' if not self.volume_indicator.visible: if not self.fade_screen_behaviour.is_applied(self.current): self.fade_screen_behaviour.apply(self.current) self.fade_screen_behaviour.set_bounds(255, 50) self.fade_screen_timeline.start() self.volume_indicator.show_volume(self.player.volume) def _on_volume_indicator_hiding(self, event): '''Restore previous screen opacity.''' self.fade_screen_behaviour.set_bounds(50, 255) self.fade_screen_timeline.start()
class MessageBus: """ MessageBus is the heart of the backend messaging system. Almost all communication between components goes through this MessageBus. Components communicate with Messages, which are delivered to MessageHandlers via MessageBus. MessageBus knows which MessageHandlers are interested in which type of Messages. MessageBus is also aware of MessageHandler priorities and this way can serve high priority components first. When MessageHandler is registered to the MessageBus there is also another parameter besides handler itself. Second parameter is a dictionary that defines MessageTypes that registered handler wants to be notified of and also priorities for those message types.""" # This determines number of message types avaialble. In other words, this # variable tells how many variables is defined in MessageType class. NUMBER_OF_MESSAGE_TYPES = len( [k for k, v in vars(MessageType).items() if type(v) is int] ) def __init__(self): """ Create a new MessageBus object. """ # MessageHandlers - index is MessageType and data is a list of # tuples (priority, MessageHandler object) that is sorted by # priorities. #XXX: rockstar - WTF?! Why is there a list comprehension being used # and still only returning an empty list? # pylint: disable-msg=W0612 self.message_handlers = [ [] for i in range(self.NUMBER_OF_MESSAGE_TYPES) ] self.lock = threading.Lock() self.logger = Logger().getLogger('backend.core.MessageBus') def registerMessageHandler(self, message_handler, message_priority_list): """ Register a new MessageHandler to this MessageBus @param message_handler: MessageHandler object @param message_priority_list: Priority list for this MessageHandler """ if isinstance(message_handler, MessageHandler): for key in message_priority_list: rule = (message_priority_list[key], message_handler) self.message_handlers[key].append(rule) self.message_handlers[key].sort() # Keep priority order else: self.logger.critical( "MessageHandler registration failed. Object " + repr(message_handler) +" is invalid type.") raise TypeError("Only MessageHandlers can be registered!") self.logger.debug("MessageHandler '" + str(message_handler) + "' registered to the message bus.") def unregisterMessageHandler(self, message_handler): """ Unregister MessageHandler form this MessageBus. @param message_handler: MessageHandler object that should be removed from bus """ if isinstance(message_handler, MessageHandler): for i in range(self.NUMBER_OF_MESSAGE_TYPES): if len(self.message_handlers[i]) != 0: rules = self.message_handlers[i] for element in rules: if element[1] is message_handler: del element else: raise TypeError("Only MessageHandlers can be unregistered!") self.logger.debug("MessageHandler '" + str(message_handler) + "' unregistered from the message bus.") def notifyMessage(self, message): """ Emit a new Message to this MessageBus. @param message: Message object """ if isinstance(message, Message): self.lock.acquire() # Lock messagebus self.logger.debug("Message bus locked. Message of type '" + str(message.get_type()) + "' is on the bus.") handler_list = self.message_handlers[message.get_type()] for element in handler_list: element[1].handleMessage(message) self.lock.release() # Release messagebus lock else: message = "TypeError occured when message was notified to the bus." self.logger.error(message) exmessage = "Notified message must be instances of 'Message' type" raise TypeError(exmessage)
class MusicCache(Cache): """ Handles audio file cache. """ # Supported file formats __SUPPORTED_FILE_EXTENSIONS = ['mp3', 'ogg'] # SQLite database stuff __db_conn = None __db_cursor = None # Default values __DEFAULT = { "artist": "Unknown artist", "album": "Unknown album", "title": "Unknown track", "genre": "Unknown" } def __init__(self): """Create a new music database object.""" self.logger = Logger().getLogger( 'backend.components.mediacache.MusicCache') self.config = Configuration() if not os.path.exists(self.config.MUSIC_DB): self.__createMusicCacheDatabase() self.__db_conn = sqlite.connect(self.config.MUSIC_DB) self.__db_cursor = self.__db_conn.cursor() def clearCache(self): """ Clear music cache. Clean cache database and remova all albumart. """ covers = os.listdir(self.config.ALBUM_ART_DIR) for element in covers: os.remove(os.path.join(self.config.ALBUM_ART_DIR, element)) os.remove(self.config.MUSIC_DB) self.__createMusicCacheDatabase() def addFile(self, filename): """Add audio file to the cache.""" filename = filename.encode('utf8') if (not self.isFileInCache(filename) and self.isSupportedFormat(filename)): if self.__getFileExtension(filename) == "mp3": self.__addMP3file(filename) elif self.__getFileExtension(filename) == "ogg": self.__addOGGfile(filename) def removeFile(self, filename): """Remove audio file from the cache.""" if self.isFileInCache(filename): # Check if we should remove albumart self.__db_cursor.execute( """SELECT artist, album FROM track WHERE filename=:fn""", {"fn": filename}) result = self.__db_cursor.fetchall() artist = result[0][0] album = result[0][1] self.__db_cursor.execute( """ SELECT * FROM track WHERE artist=:artist AND album=:album""", { "artist": artist, "album": album }) result = self.__db_cursor.fetchall() # If only one found then it's the file that is going to be removed if (len(result) == 1 and artist != self.__DEFAULT['artist'] and album != self.__DEFAULT['album']): albumart_file = artist + " - " + album + ".jpg" try: os.remove( os.path.join(self.config.ALBUM_ART_DIR, albumart_file)) except OSError: self.logger.error( "Couldn't remove albumart: " + os.path.join(self.config.ALBUM_ART_DIR, albumart_file)) # Remove track from cache self.__db_cursor.execute( """DELETE FROM track WHERE filename=:fn""", {"fn": filename}) self.__db_cursor.execute( """DELETE FROM playlist_relation WHERE filename=:fn""", {"fn": filename}) self.__db_conn.commit() def updateFile(self, filename): """Update audio file that is already in the cache.""" if self.isFileInCache(filename): self.removeFile(filename) self.addFile(filename) def addDirectory(self, path): """Add directory that contains audio files to the cache.""" # pylint: disable-msg=W0612 if not os.path.isdir(path) or not os.path.exists(path): self.logger.error( "Adding a directory to the music cache failed. " + "Path doesn't exist: " + path) else: for root, dirs, files in os.walk(path): for name in files: self.addFile(os.path.join(root, name)) time.sleep(float(self.SLEEP_TIME_BETWEEN_FILES) / 1000) def removeDirectory(self, path): """Remove directory from the cache.""" # Get current artist and albums that are on the removed path self.__db_cursor.execute(""" SELECT DISTINCT artist, album FROM track WHERE filename LIKE ' """ + path + "%'") result = self.__db_cursor.fetchall() # Remove tracks from database self.__db_cursor.execute("DELETE FROM track WHERE filename LIKE '" + path + "%'") self.__db_cursor.execute( "DELETE FROM playlist_relation WHERE filename LIKE '" + path + "%'") self.__db_conn.commit() # Check which album art we should remove for element in result: artist = element[0] album = element[1] self.__db_cursor.execute( """SELECT * FROM track WHERE artist=:artist AND album=:album""", { "artist": artist, "album": album }) found = self.__db_cursor.fetchall() # After delete there is no artist, album combination, so we can # remove album art if (len(found) == 0 and artist != self.__DEFAULT['artist'] and album != self.__DEFAULT['album']): albumart_file = artist + " - " + album + ".jpg" try: os.remove( os.path.join(self.config.ALBUM_ART_DIR, albumart_file)) except OSError: self.logger.error( "Couldn't remove albumart: " + os.path.join(self.config.ALBUM_ART_DIR, albumart_file)) def updateDirectory(self, path): """Update directory that is already in the cache.""" self.removeDirectory(path) self.addDirectory(path) def isDirectoryInCache(self, path): """Check if directory is in cache.""" self.__db_cursor.execute("SELECT * FROM track WHERE filename LIKE '" + path + "%'") result = self.__db_cursor.fetchall() if len(result) == 0: return False else: return True def isFileInCache(self, filename): """Check if file is in cache.""" self.__db_cursor.execute( """SELECT * FROM track WHERE filename=:fn""", {"fn": filename}) result = self.__db_cursor.fetchall() if len(result) == 0: return False else: return True def isSupportedFormat(self, filename): """Check if file is supported.""" if (self.__getFileExtension(filename) in self.__SUPPORTED_FILE_EXTENSIONS): return True else: return False def __createMusicCacheDatabase(self): """Creates a music cache database file.""" db_conn = sqlite.connect(self.config.MUSIC_DB) db_cursor = db_conn.cursor() db_cursor.execute("""CREATE TABLE track( filename TEXT, title VARCHAR(255), artist VARCHAR(255), album VARCHAR(255), tracknumber INTEGER, bitrate INTEGER, year INTEGER, rating INTEGER DEFAULT NULL, length INTEGER, genre VARCHAR(128), comment TEXT, lyrics TEXT DEFAULT "", PRIMARY KEY(filename))""") db_cursor.execute("""CREATE TABLE playlist( title TEXT, PRIMARY KEY(title))""") db_cursor.execute("""CREATE TABLE playlist_relation( title TEXT, filename TEXT, PRIMARY KEY(title, filename))""") db_conn.commit() db_conn.close() self.logger.debug("MusicCache database created successfully") def __getFileExtension(self, filename): """Return lower case file extension""" return filename[filename.rfind('.') + 1:].lower() def __addMP3file(self, filename): """ Add mp3 file to the music cache Process: - Open file - Get tags - Insert data to the music cache database """ try: mp3_file = eyeD3.Mp3AudioFile(filename, eyeD3.ID3_ANY_VERSION) tags = mp3_file.getTag() except ValueError: # Tags are corrupt self.logger.error("Couldn't read ID3tags: " + filename) return if tags is None: self.logger.error("Couldn't read ID3tags: " + filename) return # Get track lenght in seconds length = mp3_file.getPlayTime() # Get avarage bitrate bitrate = mp3_file.getBitRate()[1] # Get artist name artist = tags.getArtist() if artist is None or len(artist) == 0: artist = self.__DEFAULT['artist'] # Get album name album = tags.getAlbum() if album is None or len(album) == 0: album = self.__DEFAULT['album'] # Get track title title = tags.getTitle() if title is None or len(title) == 0: title = self.__DEFAULT['title'] # Get track genre genre = str(tags.getGenre()) if genre is None or len(genre) == 0: genre = self.__DEFAULT['genre'] # Get track number tracknumber = tags.getTrackNum()[0] if tracknumber is None: tracknumber = 0 # Get track comment comment = tags.getComment() if comment is None or len(comment) == 0: comment = "" # Get track release year year = tags.getYear() if year is None or len(year) == 0: year = 0 db_row = (filename, title, artist, album, genre, length, tracknumber, bitrate, comment, year) self.__db_cursor.execute( """INSERT INTO track(filename, title, artist, album, genre, length, tracknumber, bitrate, comment, year) VALUES(?,?,?,?,?,?,?,?,?,?)""", db_row) self.__db_conn.commit() # Get song lyrics lyrics = tags.getLyrics() if len(lyrics) != 0: lyrics = str(lyrics[0]) self.__db_cursor.execute( """UPDATE track SET lyrics=:lyrics WHERE filename=:fn""", { "lyrics": lyrics, "fn": filename }) self.__db_conn.commit() # Get album art self.__searchAlbumArt(artist, album, filename) def __addOGGfile(self, filename): """ Add ogg file to the music cache Process: - Open file - Get tags - Insert data to the music cache database """ ogg_file = ogg.vorbis.VorbisFile(filename) info = ogg_file.comment().as_dict() # Get length length = round(ogg_file.time_total(-1)) # Get avarage bitrate bitrate = round(ogg_file.bitrate(-1) / 1000) # Get album name if info.has_key('ALBUM'): album = info['ALBUM'][0] else: album = self.__DEFAULT['album'] # Get artist name if info.has_key('ARTIST'): artist = info['ARTIST'][0] else: artist = self.__DEFAULT['artist'] # Get track title if info.has_key('TITLE'): if info.has_key('VERSION'): title = (str(info['TITLE'][0]) + " (" + str(info['VERSION'][0]) + ")") else: title = info['TITLE'][0] else: title = self.__DEFAULT['title'] # Get track number if info.has_key('TRACKNUMBER'): track_number = info['TRACKNUMBER'][0] else: track_number = 0 # Get track genre if info.has_key('GENRE'): genre = info['GENRE'][0] else: genre = self.__DEFAULT['genre'] # Get track comment if info.has_key('DESCRIPTION'): comment = info['DESCRIPTION'][0] elif info.has_key('COMMENT'): comment = info['COMMENT'][0] else: comment = "" # Get track year if info.has_key('DATE'): date = info['DATE'][0] year = date[:date.find('-')] else: year = 0 db_row = (filename, title, artist, album, genre, length, track_number, bitrate, comment, year) self.__db_cursor.execute( """INSERT INTO track(filename, title, artist, album, genre, length, tracknumber, bitrate, comment, year) VALUES(?,?,?,?,?,?,?,?,?,?)""", db_row) self.__db_conn.commit() # Get album art self.__searchAlbumArt(artist, album, filename) def __searchAlbumArt(self, artist, album, filename): """Execute album art search thread""" # base64 encode artist and album so there can be a '/' in the artist or # album artist_album = artist + " - " + album artist_album = artist_album.encode("base64") album_art_file = os.path.join(self.config.ALBUM_ART_DIR, artist_album + ".jpg") if not os.path.exists(album_art_file): # Search for local albumart if os.path.exists(filename[:filename.rfind('/') + 1] + "cover.jpg"): shutil.copyfile( filename[:filename.rfind('/') + 1] + "cover.jpg", album_art_file) elif os.path.exists(filename[:filename.rfind('/') + 1] + "folder.jpg"): shutil.copyfile( filename[:filename.rfind('/') + 1] + "folder.jpg", album_art_file) # Local not found -> try internet else: if self.config.download_album_art: if album != "Unknown album" and artist != "Unknown Artist": loader_thread = AlbumArtDownloader( album, artist, self.config.ALBUM_ART_DIR) loader_thread.start()
class ImageCache(Cache): """ This class is responsible of updating image cache as requested. ImageCache has a public interface that consists of 3 mehtods: addFile, removeFile and updateFile. All these methods get filename as a parameter. When ImageCache is called with filename it checks if the filename is supported format. This is done simply by checking the file extension. Supported file formats are: JPEG """ # Supported file formats SUPPORTED_FILE_EXTENSIONS = ['jpg', 'jpeg', 'png'] def __init__(self): """ Create a new ImageCache. Creates a new database if not already exists and opens a connection to it. """ self.logger = Logger().getLogger( 'backend.components.mediacache.ImageCache') self.config = Configuration() if not os.path.exists(self.config.IMAGE_DB): self._createImageCacheDatabase() self.db_conn = sqlite.connect(self.config.IMAGE_DB) self.db_cursor = self.db_conn.cursor() def clearCache(self): """ Clean image cache completely. Clean cache database and remove all thumbnails. """ thumbnails = os.listdir(self.config.IMAGE_THUMB_DIR) for element in thumbnails: thumb_file = os.path.join(self.config.IMAGE_THUMB_DIR, element) try: os.remove(thumb_file) except OSError: self.logger.error( "Media manager couldn't remove thumbnail : %s" % thumb_file) os.remove(self.config.IMAGE_DB) self._createImageCacheDatabase() def addFile(self, filename): """ Add image file to the cache. Do nothing if file is already cached. """ filename = filename.encode('utf8') if (not self.isFileInCache(filename) and self.isSupportedFormat(filename)): # Do not add album thumbnail to images if (filename[filename.rfind('/') +1:filename.rfind('.')] == ".entertainer_album"): return self._addJPEGfile(filename) def removeFile(self, filename): """ Remove image file from the cache. Do nothing if file is not in cache. """ # Remove image file if self.isFileInCache(filename): self.db_cursor.execute("""SELECT hash FROM image WHERE filename=:fn""", { "fn" : filename}) result = self.db_cursor.fetchall() if len(result) > 0: name = result[0][0] + '.jpg' thumb = os.path.join(self.config.IMAGE_THUMB_DIR, name) try: os.remove(thumb) except OSError: self.logger.error("Couldn't remove thumbnail: " + thumb) self.db_cursor.execute("""DELETE FROM image WHERE filename=:fn""", { "fn" : filename }) self.db_conn.commit() def updateFile(self, filename): """Update image file that is already in the cache.""" if self.isFileInCache(filename): self.removeFile(filename) self.addFile(filename) def addDirectory(self, path): """ Adds a new directory to the cache. Sub directories are added recursively and all files in them. """ # pylint: disable-msg=W0612 if not os.path.isdir(path) or not os.path.exists(path): self.logger.error( "Adding a directory to the image cache failed. " + "Path doesn't exist: " + path) else: for root, dirs, files in os.walk(path): if os.path.split(root)[-1][0] == ".": continue if not self.isDirectoryInCache(root): self._addAlbum(root) for name in files: if os.path.split(name)[-1][0] == ".": continue if self.isSupportedFormat(name): self.addFile(os.path.join(root, name)) time.sleep(float(self.SLEEP_TIME_BETWEEN_FILES) / 1000) def removeDirectory(self, path): """ Removes directory from the cache. Also removes all subdirectories and all files in them. @param path - Absolute path """ # Remove image file thumbnails self.db_cursor.execute("""SELECT hash FROM image WHERE filename LIKE '""" + path + "%'") for row in self.db_cursor: thumb_file = row[0] + ".jpg" os.remove(os.path.join(self.config.IMAGE_THUMB_DIR, thumb_file)) # Remove folder thumbnails self.db_cursor.execute("""SELECT hash FROM album WHERE path LIKE '""" + path + "%'") for row in self.db_cursor: thumb_file = row[0] + ".jpg" os.remove(os.path.join(self.config.IMAGE_THUMB_DIR, thumb_file)) # Clean cache database self.db_cursor.execute( "DELETE FROM album WHERE path LIKE '" + path + "%'") self.db_cursor.execute( "DELETE FROM image WHERE album_path LIKE '" + path + "%'") self.db_conn.commit() def updateDirectory(self, path): """ Update directory. """ self.removeDirectory(path) self.addDirectory(path) def isFileInCache(self, filename): """Check if file is already in cache. Returns boolean value.""" self.db_cursor.execute("""SELECT * FROM image WHERE filename=:fn""", { "fn" : filename }) result = self.db_cursor.fetchall() if len(result) == 0: return False else: return True def isDirectoryInCache(self, path): """Check if album is already in cache. Returns boolean value.""" self.db_cursor.execute("""SELECT * FROM album WHERE path=:p""", { "p" : path}) result = self.db_cursor.fetchall() if len(result) == 0: return False else: return True def isSupportedFormat(self, filename): """Check if file is supported.""" if (filename[filename.rfind('.') + 1 :].lower() in self.SUPPORTED_FILE_EXTENSIONS): return True else: return False def _createImageCacheDatabase(self): """Creates a image cache database file.""" db_conn = sqlite.connect(self.config.IMAGE_DB) db_cursor = db_conn.cursor() db_cursor.execute( """ CREATE TABLE image( filename TEXT, album_path TEXT, title TEXT, description TEXT, date DATE, time TIME, width INTEGER, height INTEGER, filesize LONG, hash VARCHAR(32), PRIMARY KEY(filename))""") db_cursor.execute( """ CREATE TABLE album( path TEXT, title TEXT, description TEXT, hash VARCHAR(32), PRIMARY KEY(path))""") db_conn.commit() db_conn.close() self.logger.debug("ImageCache database created successfully") def _addAlbum(self, path): """ Create a new album into image cache. Folders are handled as albums. Nested folders are not nested in database! All albums are on top level. """ album_info = os.path.join(path, ".entertainer_album.info") album_thumb = os.path.join(path, ".entertainer_album.jpg") # Get album information if os.path.exists(album_info): try: inf_f = open(album_info) a_title = inf_f.readline()[6:] a_description = inf_f.readline()[12:] except IOError: a_title = path[path.rfind('/')+1:].replace('_',' ').title() a_description = "" else: a_title = path[path.rfind('/')+1:].replace('_',' ').title() a_description = "" if os.path.exists(album_thumb): thumbnailer = ImageThumbnailer(album_thumb) thumbnailer.create_thumbnail() a_hash = thumbnailer.get_hash() else: a_hash = "" album_row = (path, a_title, a_description, a_hash) self.db_cursor.execute( """ INSERT INTO album(path, title, description, hash) VALUES(?,?,?,?) """, album_row) self.db_conn.commit() #print "Album added to cache: " + a_title def _addJPEGfile(self, filename): """ Add JPEG image to the image cache. Raises exception if adding fails. Process: - Open file - Get image date and time - Get image title and description - Get image size - Generate thumbnail / get hash from thumbnailer - Insert data to image cache database """ tmp = datetime.datetime.fromtimestamp(os.stat(filename)[-1]) timestamp = [str(tmp.year) + "-" + str(tmp.month) + "-" + str(tmp.day), str(tmp.hour) + ":" + str(tmp.minute) + ":" + str(tmp.second)] # Generate name from filename tmp = filename[filename.rfind('/') + 1 : filename.rfind('.')] title = tmp.replace('_',' ').title() # Make title more attractive description = "" # No description for this image file try: im = Image.open(filename) width, height = im.size except IOError: self.logger.error("Couldn't identify image file: " + filename) return # Create thumbnail and return hash thumbnailer = ImageThumbnailer(filename) thumbnailer.create_thumbnail() thumb_hash = thumbnailer.get_hash() del thumbnailer album_path = filename[:filename.rfind('/')] db_row = (filename, # Filename (full path) title, # Title of the image description, # Description of the image timestamp[0], # Image's taken date timestamp[1], # Image's taken time width, # Image's width height, # Image's height os.path.getsize(filename), # Image file size in bytes thumb_hash, # Thumbnail hash (hash of the filename) album_path) # Path of the album (folder of this image) self.db_cursor.execute( """ INSERT INTO image(filename, title, description, date, time, width, height, filesize, hash, album_path) VALUES(?,?,?,?,?,?,?,?,?,?)""", db_row) self.db_conn.commit()
class VideoCache(Cache): """Handles video file cache.""" # Supported file extensions __SUPPORTED_FILE_EXTENSIONS = [ 'avi', 'mpg', 'mpeg', 'mov', 'wmv', 'ogm', 'mkv', 'mp4', 'm4v' ] # SQLite database stuff __db_conn = None __db_cursor = None # Thread lock for metadata search __metadata_lock = None def __init__(self): self.logger = Logger().getLogger( 'backend.components.mediacache.VideoCache') self.config = Configuration() if not os.path.exists(self.config.VIDEO_DB): self.__createVideoCacheDatabase() self.__db_conn = sqlite.connect(self.config.VIDEO_DB) self.__db_cursor = self.__db_conn.cursor() def clearCache(self): """ Clear video cache. Clean cache database and remova all metadata. """ covers = os.listdir(self.config.MOVIE_ART_DIR) for element in covers: if element[-3:] == "jpg": os.remove(os.path.join(self.config.MOVIE_ART_DIR, element)) os.remove(self.config.VIDEO_DB) self.__createVideoCacheDatabase() def addFile(self, filename): """ This method adds a new file to the cache. """ filename = filename.encode('utf8') if not self.isFileInCache(filename) and \ self.isSupportedFormat(filename): self._addVideoFile(filename) def removeFile(self, filename): """ This method removes file from the cache. """ print "removeFile(): " + filename if self.isFileInCache(filename): self.__db_cursor.execute( """ SELECT title, hash, series_title FROM videofile, metadata WHERE videofile.filename=:fn AND videofile.filename=metadata.filename""", { "fn" : filename }) result = self.__db_cursor.fetchall() title = result[0][0] thash = result[0][1] series = result[0][2] # Series cover art is named by series title (not episode title) if series is not None and len(series) != 0: title = series # Generate absolute path of thumbnail and cover art art = os.path.join(self.config.MOVIE_ART_DIR, str(title) + ".jpg") thumb = os.path.join(self.config.VIDEO_THUMB_DIR, str(thash) + ".jpg") # Remove video from video cache database self.__db_cursor.execute('''DELETE FROM videofile WHERE filename=:fn''', { "fn" : filename }) self.__db_cursor.execute('''DELETE FROM metadata WHERE filename=:fn''', { "fn" : filename }) self.__db_conn.commit() # Remove thumbnail and cover art if os.path.exists(art) and not self.__hasSeriesEpisodes(series): os.remove(art) if os.path.exists(thumb): os.remove(thumb) def updateFile(self, filename): """ This method is executed when a file, that is already in cache, changes. """ if self.isFileInCache(filename): self.removeFile(filename) self.addFile(filename) def addDirectory(self, path): """ This method adds a new directory to the cache. Sub directories are added recursively and all files in them. """ if not os.path.isdir(path) or not os.path.exists(path): self.logger.error( "Adding a directory to the video cache failed. " + "Path doesn't exist: '" + path + "'") else: self.logger.debug( "Adding a directory to the video cache. Path is: '" + path + "'") # pylint: disable-msg=W0612 for root, dirs, files in os.walk(path): for name in files: self.addFile(os.path.join(root, name)) time.sleep(float(self.SLEEP_TIME_BETWEEN_FILES) / 1000) def removeDirectory(self, path): """ This method removes directory from the cache. Also removes all subdirectories and all files in them. """ self.__db_cursor.execute("""SELECT filename FROM videofile WHERE filename LIKE '""" + path + "%'") result = self.__db_cursor.fetchall() for row in result: self.removeFile(row[0]) def updateDirectory(self, path): """ This method is executed when a directory, that is already in cache, changes. """ self.removeDirectory(path) self.addDirectory(path) def isDirectoryInCache(self, path): """ This method returns True if given directory is in cache. Otherwise method returns False. """ self.__db_cursor.execute("""SELECT * FROM videofile WHERE filename LIKE '""" + path + "%'") result = self.__db_cursor.fetchall() if len(result) == 0: return False else: return True def isFileInCache(self, filename): """ This method returns True if given file is in cache. Otherwise method returns False. """ self.__db_cursor.execute("""SELECT * FROM videofile WHERE filename=:fn""", { "fn" : filename}) result = self.__db_cursor.fetchall() if len(result) == 0: return False else: return True def isSupportedFormat(self, filename): """Check if file is supported.""" if (self.__getFileExtension(filename) in self.__SUPPORTED_FILE_EXTENSIONS): return True else: return False def __createVideoCacheDatabase(self): """Creates a video cache database file.""" db_conn = sqlite.connect(self.config.VIDEO_DB) db_cursor = db_conn.cursor() db_cursor.execute("""CREATE TABLE videofile( filename TEXT, hash VARCHAR(32), length INTEGER, resolution VARCHAR(16), PRIMARY KEY(filename))""") db_cursor.execute("""CREATE TABLE metadata( type VARCHAR(16) DEFAULT 'CLIP', filename TEXT, title TEXT, series_title VARCHAR(128), runtime INTEGER, genres VARCHAR(128), rating INTEGER, year VARCHAR(16), plot_outline TEXT, plot TEXT, season INTEGER, episode INTEGER, actor_1 VARCHAR(128), actor_2 VARCHAR(128), actor_3 VARCHAR(128), actor_4 VARCHAR(128), actor_5 VARCHAR(128), writer_1 VARCHAR(128), writer_2 VARCHAR(128), director_1 VARCHAR(128), director_2 VARCHAR(128), PRIMARY KEY(filename))""") db_conn.commit() db_conn.close() self.logger.debug("VideoCache database created successfully") def __getFileExtension(self, filename): """Return lower case file extension""" return filename[filename.rfind('.') + 1 :].lower() def _addVideoFile(self, filename): """Add video file to the video cache.""" # Generate thumbnail thumbnailer = VideoThumbnailer(filename) thumbnailer.create_thumbnail() thash = thumbnailer.get_hash() del thumbnailer self.__db_cursor.execute("""INSERT INTO videofile(filename, hash) VALUES (:fn, :hash)""", { 'fn': filename, 'hash': thash, } ) self.__db_cursor.execute("""INSERT INTO metadata(filename) VALUES (:fn)""", { "fn" : filename } ) self.__db_conn.commit() if self.config.download_metadata: self.__searchMetadata(filename) def __searchMetadata(self, filename): """Search metadata for video file from the Internet.""" search_thread = None search_thread = VideoMetadataSearch(filename) if search_thread is not None: search_thread.start() def __hasSeriesEpisodes(self, series_title): """ Return True if there are episodes for given series, otherwise False. This is used when removing file from cache. """ if len(series_title) == 0: return False else: self.__db_cursor.execute("""SELECT * FROM metadata WHERE series_title=:sn""", { "sn" : series_title} ) result = self.__db_cursor.fetchall() if len(result) == 0: return False else: return True
class ImageCache(Cache): """ This class is responsible of updating image cache as requested. ImageCache has a public interface that consists of 3 mehtods: addFile, removeFile and updateFile. All these methods get filename as a parameter. When ImageCache is called with filename it checks if the filename is supported format. This is done simply by checking the file extension. Supported file formats are: JPEG """ # Supported file formats SUPPORTED_FILE_EXTENSIONS = ['jpg', 'jpeg', 'png'] def __init__(self): """ Create a new ImageCache. Creates a new database if not already exists and opens a connection to it. """ self.logger = Logger().getLogger( 'backend.components.mediacache.ImageCache') self.config = Configuration() if not os.path.exists(self.config.IMAGE_DB): self._createImageCacheDatabase() self.db_conn = sqlite.connect(self.config.IMAGE_DB) self.db_cursor = self.db_conn.cursor() def clearCache(self): """ Clean image cache completely. Clean cache database and remove all thumbnails. """ thumbnails = os.listdir(self.config.IMAGE_THUMB_DIR) for element in thumbnails: thumb_file = os.path.join(self.config.IMAGE_THUMB_DIR, element) try: os.remove(thumb_file) except OSError: self.logger.error( "Media manager couldn't remove thumbnail : %s" % thumb_file) os.remove(self.config.IMAGE_DB) self._createImageCacheDatabase() def addFile(self, filename): """ Add image file to the cache. Do nothing if file is already cached. """ filename = filename.encode('utf8') if (not self.isFileInCache(filename) and self.isSupportedFormat(filename)): # Do not add album thumbnail to images if (filename[filename.rfind('/') + 1:filename.rfind('.')] == ".entertainer_album"): return self._addJPEGfile(filename) def removeFile(self, filename): """ Remove image file from the cache. Do nothing if file is not in cache. """ # Remove image file if self.isFileInCache(filename): self.db_cursor.execute( """SELECT hash FROM image WHERE filename=:fn""", {"fn": filename}) result = self.db_cursor.fetchall() if len(result) > 0: name = result[0][0] + '.jpg' thumb = os.path.join(self.config.IMAGE_THUMB_DIR, name) try: os.remove(thumb) except OSError: self.logger.error("Couldn't remove thumbnail: " + thumb) self.db_cursor.execute( """DELETE FROM image WHERE filename=:fn""", {"fn": filename}) self.db_conn.commit() def updateFile(self, filename): """Update image file that is already in the cache.""" if self.isFileInCache(filename): self.removeFile(filename) self.addFile(filename) def addDirectory(self, path): """ Adds a new directory to the cache. Sub directories are added recursively and all files in them. """ # pylint: disable-msg=W0612 if not os.path.isdir(path) or not os.path.exists(path): self.logger.error( "Adding a directory to the image cache failed. " + "Path doesn't exist: " + path) else: for root, dirs, files in os.walk(path): if os.path.split(root)[-1][0] == ".": continue if not self.isDirectoryInCache(root): self._addAlbum(root) for name in files: if os.path.split(name)[-1][0] == ".": continue if self.isSupportedFormat(name): self.addFile(os.path.join(root, name)) time.sleep(float(self.SLEEP_TIME_BETWEEN_FILES) / 1000) def removeDirectory(self, path): """ Removes directory from the cache. Also removes all subdirectories and all files in them. @param path - Absolute path """ # Remove image file thumbnails self.db_cursor.execute("""SELECT hash FROM image WHERE filename LIKE '""" + path + "%'") for row in self.db_cursor: thumb_file = row[0] + ".jpg" os.remove(os.path.join(self.config.IMAGE_THUMB_DIR, thumb_file)) # Remove folder thumbnails self.db_cursor.execute("""SELECT hash FROM album WHERE path LIKE '""" + path + "%'") for row in self.db_cursor: thumb_file = row[0] + ".jpg" os.remove(os.path.join(self.config.IMAGE_THUMB_DIR, thumb_file)) # Clean cache database self.db_cursor.execute("DELETE FROM album WHERE path LIKE '" + path + "%'") self.db_cursor.execute("DELETE FROM image WHERE album_path LIKE '" + path + "%'") self.db_conn.commit() def updateDirectory(self, path): """ Update directory. """ self.removeDirectory(path) self.addDirectory(path) def isFileInCache(self, filename): """Check if file is already in cache. Returns boolean value.""" self.db_cursor.execute( """SELECT * FROM image WHERE filename=:fn""", {"fn": filename}) result = self.db_cursor.fetchall() if len(result) == 0: return False else: return True def isDirectoryInCache(self, path): """Check if album is already in cache. Returns boolean value.""" self.db_cursor.execute( """SELECT * FROM album WHERE path=:p""", {"p": path}) result = self.db_cursor.fetchall() if len(result) == 0: return False else: return True def isSupportedFormat(self, filename): """Check if file is supported.""" if (filename[filename.rfind('.') + 1:].lower() in self.SUPPORTED_FILE_EXTENSIONS): return True else: return False def _createImageCacheDatabase(self): """Creates a image cache database file.""" db_conn = sqlite.connect(self.config.IMAGE_DB) db_cursor = db_conn.cursor() db_cursor.execute(""" CREATE TABLE image( filename TEXT, album_path TEXT, title TEXT, description TEXT, date DATE, time TIME, width INTEGER, height INTEGER, filesize LONG, hash VARCHAR(32), PRIMARY KEY(filename))""") db_cursor.execute(""" CREATE TABLE album( path TEXT, title TEXT, description TEXT, hash VARCHAR(32), PRIMARY KEY(path))""") db_conn.commit() db_conn.close() self.logger.debug("ImageCache database created successfully") def _addAlbum(self, path): """ Create a new album into image cache. Folders are handled as albums. Nested folders are not nested in database! All albums are on top level. """ album_info = os.path.join(path, ".entertainer_album.info") album_thumb = os.path.join(path, ".entertainer_album.jpg") # Get album information if os.path.exists(album_info): try: inf_f = open(album_info) a_title = inf_f.readline()[6:] a_description = inf_f.readline()[12:] except IOError: a_title = path[path.rfind('/') + 1:].replace('_', ' ').title() a_description = "" else: a_title = path[path.rfind('/') + 1:].replace('_', ' ').title() a_description = "" if os.path.exists(album_thumb): thumbnailer = ImageThumbnailer(album_thumb) thumbnailer.create_thumbnail() a_hash = thumbnailer.get_hash() else: a_hash = "" album_row = (path, a_title, a_description, a_hash) self.db_cursor.execute( """ INSERT INTO album(path, title, description, hash) VALUES(?,?,?,?) """, album_row) self.db_conn.commit() #print "Album added to cache: " + a_title def _addJPEGfile(self, filename): """ Add JPEG image to the image cache. Raises exception if adding fails. Process: - Open file - Get image date and time - Get image title and description - Get image size - Generate thumbnail / get hash from thumbnailer - Insert data to image cache database """ tmp = datetime.datetime.fromtimestamp(os.stat(filename)[-1]) timestamp = [ str(tmp.year) + "-" + str(tmp.month) + "-" + str(tmp.day), str(tmp.hour) + ":" + str(tmp.minute) + ":" + str(tmp.second) ] # Generate name from filename tmp = filename[filename.rfind('/') + 1:filename.rfind('.')] title = tmp.replace('_', ' ').title() # Make title more attractive description = "" # No description for this image file try: im = Image.open(filename) width, height = im.size except IOError: self.logger.error("Couldn't identify image file: " + filename) return # Create thumbnail and return hash thumbnailer = ImageThumbnailer(filename) thumbnailer.create_thumbnail() thumb_hash = thumbnailer.get_hash() del thumbnailer album_path = filename[:filename.rfind('/')] db_row = ( filename, # Filename (full path) title, # Title of the image description, # Description of the image timestamp[0], # Image's taken date timestamp[1], # Image's taken time width, # Image's width height, # Image's height os.path.getsize(filename), # Image file size in bytes thumb_hash, # Thumbnail hash (hash of the filename) album_path) # Path of the album (folder of this image) self.db_cursor.execute( """ INSERT INTO image(filename, title, description, date, time, width, height, filesize, hash, album_path) VALUES(?,?,?,?,?,?,?,?,?,?)""", db_row) self.db_conn.commit()
class MusicCache(Cache): """ Handles audio file cache. """ # Supported file formats __SUPPORTED_FILE_EXTENSIONS = ['mp3', 'ogg'] # SQLite database stuff __db_conn = None __db_cursor = None # Default values __DEFAULT = { "artist" : "Unknown artist", "album" : "Unknown album", "title" : "Unknown track", "genre" : "Unknown" } def __init__(self): """Create a new music database object.""" self.logger = Logger().getLogger( 'backend.components.mediacache.MusicCache') self.config = Configuration() if not os.path.exists(self.config.MUSIC_DB): self.__createMusicCacheDatabase() self.__db_conn = sqlite.connect(self.config.MUSIC_DB) self.__db_cursor = self.__db_conn.cursor() def clearCache(self): """ Clear music cache. Clean cache database and remova all albumart. """ covers = os.listdir(self.config.ALBUM_ART_DIR) for element in covers: os.remove(os.path.join(self.config.ALBUM_ART_DIR, element)) os.remove(self.config.MUSIC_DB) self.__createMusicCacheDatabase() def addFile(self, filename): """Add audio file to the cache.""" filename = filename.encode('utf8') if (not self.isFileInCache(filename) and self.isSupportedFormat(filename)): if self.__getFileExtension(filename) == "mp3": self.__addMP3file(filename) elif self.__getFileExtension(filename) == "ogg": self.__addOGGfile(filename) def removeFile(self, filename): """Remove audio file from the cache.""" if self.isFileInCache(filename): # Check if we should remove albumart self.__db_cursor.execute("""SELECT artist, album FROM track WHERE filename=:fn""", { "fn" : filename }) result = self.__db_cursor.fetchall() artist = result[0][0] album = result[0][1] self.__db_cursor.execute( """ SELECT * FROM track WHERE artist=:artist AND album=:album""", { "artist" : artist, "album" : album}) result = self.__db_cursor.fetchall() # If only one found then it's the file that is going to be removed if (len(result) == 1 and artist != self.__DEFAULT['artist'] and album != self.__DEFAULT['album']): albumart_file = artist + " - " + album + ".jpg" try: os.remove(os.path.join(self.config.ALBUM_ART_DIR, albumart_file)) except OSError: self.logger.error("Couldn't remove albumart: " + os.path.join(self.config.ALBUM_ART_DIR, albumart_file)) # Remove track from cache self.__db_cursor.execute("""DELETE FROM track WHERE filename=:fn""", { "fn" : filename}) self.__db_cursor.execute("""DELETE FROM playlist_relation WHERE filename=:fn""", { "fn" : filename}) self.__db_conn.commit() def updateFile(self, filename): """Update audio file that is already in the cache.""" if self.isFileInCache(filename): self.removeFile(filename) self.addFile(filename) def addDirectory(self, path): """Add directory that contains audio files to the cache.""" # pylint: disable-msg=W0612 if not os.path.isdir(path) or not os.path.exists(path): self.logger.error( "Adding a directory to the music cache failed. " + "Path doesn't exist: " + path) else: for root, dirs, files in os.walk(path): for name in files: self.addFile(os.path.join(root, name)) time.sleep(float(self.SLEEP_TIME_BETWEEN_FILES) / 1000) def removeDirectory(self, path): """Remove directory from the cache.""" # Get current artist and albums that are on the removed path self.__db_cursor.execute( """ SELECT DISTINCT artist, album FROM track WHERE filename LIKE ' """ + path + "%'") result = self.__db_cursor.fetchall() # Remove tracks from database self.__db_cursor.execute( "DELETE FROM track WHERE filename LIKE '" + path + "%'") self.__db_cursor.execute( "DELETE FROM playlist_relation WHERE filename LIKE '" + path + "%'") self.__db_conn.commit() # Check which album art we should remove for element in result: artist = element[0] album = element[1] self.__db_cursor.execute("""SELECT * FROM track WHERE artist=:artist AND album=:album""", { "artist" : artist, "album" : album}) found = self.__db_cursor.fetchall() # After delete there is no artist, album combination, so we can # remove album art if (len(found) == 0 and artist != self.__DEFAULT['artist'] and album != self.__DEFAULT['album']): albumart_file = artist + " - " + album + ".jpg" try: os.remove(os.path.join(self.config.ALBUM_ART_DIR, albumart_file)) except OSError: self.logger.error( "Couldn't remove albumart: " + os.path.join(self.config.ALBUM_ART_DIR, albumart_file)) def updateDirectory(self, path): """Update directory that is already in the cache.""" self.removeDirectory(path) self.addDirectory(path) def isDirectoryInCache(self, path): """Check if directory is in cache.""" self.__db_cursor.execute( "SELECT * FROM track WHERE filename LIKE '" + path + "%'") result = self.__db_cursor.fetchall() if len(result) == 0: return False else: return True def isFileInCache(self, filename): """Check if file is in cache.""" self.__db_cursor.execute("""SELECT * FROM track WHERE filename=:fn""", {"fn":filename} ) result = self.__db_cursor.fetchall() if len(result) == 0: return False else: return True def isSupportedFormat(self, filename): """Check if file is supported.""" if (self.__getFileExtension(filename) in self.__SUPPORTED_FILE_EXTENSIONS): return True else: return False def __createMusicCacheDatabase(self): """Creates a music cache database file.""" db_conn = sqlite.connect(self.config.MUSIC_DB) db_cursor = db_conn.cursor() db_cursor.execute("""CREATE TABLE track( filename TEXT, title VARCHAR(255), artist VARCHAR(255), album VARCHAR(255), tracknumber INTEGER, bitrate INTEGER, year INTEGER, rating INTEGER DEFAULT NULL, length INTEGER, genre VARCHAR(128), comment TEXT, lyrics TEXT DEFAULT "", PRIMARY KEY(filename))""") db_cursor.execute("""CREATE TABLE playlist( title TEXT, PRIMARY KEY(title))""") db_cursor.execute("""CREATE TABLE playlist_relation( title TEXT, filename TEXT, PRIMARY KEY(title, filename))""") db_conn.commit() db_conn.close() self.logger.debug("MusicCache database created successfully") def __getFileExtension(self, filename): """Return lower case file extension""" return filename[filename.rfind('.') + 1 :].lower() def __addMP3file(self, filename): """ Add mp3 file to the music cache Process: - Open file - Get tags - Insert data to the music cache database """ try: mp3_file = eyeD3.Mp3AudioFile(filename, eyeD3.ID3_ANY_VERSION) tags = mp3_file.getTag() except ValueError: # Tags are corrupt self.logger.error("Couldn't read ID3tags: " + filename) return if tags is None: self.logger.error("Couldn't read ID3tags: " + filename) return # Get track lenght in seconds length = mp3_file.getPlayTime() # Get avarage bitrate bitrate = mp3_file.getBitRate()[1] # Get artist name artist = tags.getArtist() if artist is None or len(artist) == 0: artist = self.__DEFAULT['artist'] # Get album name album = tags.getAlbum() if album is None or len(album) == 0: album = self.__DEFAULT['album'] # Get track title title = tags.getTitle() if title is None or len(title) == 0: title = self.__DEFAULT['title'] # Get track genre genre = str(tags.getGenre()) if genre is None or len(genre) == 0: genre = self.__DEFAULT['genre'] # Get track number tracknumber = tags.getTrackNum()[0] if tracknumber is None: tracknumber = 0 # Get track comment comment = tags.getComment() if comment is None or len(comment) == 0: comment = "" # Get track release year year = tags.getYear() if year is None or len(year) == 0: year = 0 db_row = (filename, title, artist, album, genre, length, tracknumber, bitrate, comment, year) self.__db_cursor.execute("""INSERT INTO track(filename, title, artist, album, genre, length, tracknumber, bitrate, comment, year) VALUES(?,?,?,?,?,?,?,?,?,?)""", db_row) self.__db_conn.commit() # Get song lyrics lyrics = tags.getLyrics() if len(lyrics) != 0: lyrics = str(lyrics[0]) self.__db_cursor.execute("""UPDATE track SET lyrics=:lyrics WHERE filename=:fn""", { "lyrics" : lyrics, "fn" : filename }) self.__db_conn.commit() # Get album art self.__searchAlbumArt(artist, album, filename) def __addOGGfile(self, filename): """ Add ogg file to the music cache Process: - Open file - Get tags - Insert data to the music cache database """ ogg_file = ogg.vorbis.VorbisFile(filename) info = ogg_file.comment().as_dict() # Get length length = round(ogg_file.time_total(-1)) # Get avarage bitrate bitrate = round(ogg_file.bitrate(-1) / 1000) # Get album name if info.has_key('ALBUM'): album = info['ALBUM'][0] else: album = self.__DEFAULT['album'] # Get artist name if info.has_key('ARTIST'): artist = info['ARTIST'][0] else: artist = self.__DEFAULT['artist'] # Get track title if info.has_key('TITLE'): if info.has_key('VERSION'): title = (str(info['TITLE'][0]) + " (" + str(info['VERSION'][0]) + ")") else: title = info['TITLE'][0] else: title = self.__DEFAULT['title'] # Get track number if info.has_key('TRACKNUMBER'): track_number = info['TRACKNUMBER'][0] else: track_number = 0 # Get track genre if info.has_key('GENRE'): genre = info['GENRE'][0] else: genre = self.__DEFAULT['genre'] # Get track comment if info.has_key('DESCRIPTION'): comment = info['DESCRIPTION'][0] elif info.has_key('COMMENT'): comment = info['COMMENT'][0] else: comment = "" # Get track year if info.has_key('DATE'): date = info['DATE'][0] year = date[:date.find('-')] else: year = 0 db_row = (filename, title, artist, album, genre, length, track_number, bitrate, comment, year) self.__db_cursor.execute("""INSERT INTO track(filename, title, artist, album, genre, length, tracknumber, bitrate, comment, year) VALUES(?,?,?,?,?,?,?,?,?,?)""", db_row) self.__db_conn.commit() # Get album art self.__searchAlbumArt(artist, album, filename) def __searchAlbumArt(self, artist, album, filename): """Execute album art search thread""" # base64 encode artist and album so there can be a '/' in the artist or # album artist_album = artist + " - " + album artist_album = artist_album.encode("base64") album_art_file = os.path.join( self.config.ALBUM_ART_DIR, artist_album + ".jpg") if not os.path.exists(album_art_file): # Search for local albumart if os.path.exists(filename[:filename.rfind('/')+1]+"cover.jpg"): shutil.copyfile(filename[:filename.rfind('/')+1]+"cover.jpg", album_art_file) elif os.path.exists(filename[:filename.rfind('/')+1]+"folder.jpg"): shutil.copyfile(filename[:filename.rfind('/')+1]+"folder.jpg", album_art_file) # Local not found -> try internet else: if self.config.download_album_art: if album != "Unknown album" and artist != "Unknown Artist": loader_thread = AlbumArtDownloader(album, artist, self.config.ALBUM_ART_DIR) loader_thread.start()