Beispiel #1
0
class Player(Gtk.Box):
    def __init__(self, app):
        super().__init__()
        self.app = app

        self.fullscreen_sid = 0
        self.play_type = PlayType.NONE
        self.adj_timeout = 0
        self.recommend_imgs = None
        self.curr_song = None

        # use this to keep Net.AsyncSong object
        self.async_song = None
        self.async_next_song = None

        event_pic = Gtk.EventBox()
        event_pic.connect('button-press-event', self.on_pic_pressed)
        self.pack_start(event_pic, False, False, 0)

        self.artist_pic = Gtk.Image.new_from_pixbuf(app.theme['anonymous'])
        event_pic.add(self.artist_pic)

        control_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.pack_start(control_box, True, True, 0)

        toolbar = Gtk.Toolbar()
        toolbar.set_style(Gtk.ToolbarStyle.ICONS)
        toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_MENUBAR)
        toolbar.set_show_arrow(False)
        toolbar.set_icon_size(ICON_SIZE)
        control_box.pack_start(toolbar, False, False, 0)

        prev_button = Gtk.ToolButton()
        prev_button.set_label(_('Previous'))
        prev_button.set_icon_name('media-skip-backward-symbolic')
        prev_button.connect('clicked', self.on_prev_button_clicked)
        toolbar.insert(prev_button, 0)

        self.play_button = Gtk.ToolButton()
        self.play_button.set_label(_('Play'))
        self.play_button.set_icon_name('media-playback-start-symbolic')
        self.play_button.connect('clicked', self.on_play_button_clicked)
        toolbar.insert(self.play_button, 1)

        next_button = Gtk.ToolButton()
        next_button.set_label(_('Next'))
        next_button.set_icon_name('media-skip-forward-symbolic')
        next_button.connect('clicked', self.on_next_button_clicked)
        toolbar.insert(next_button, 2)

        self.shuffle_btn = Gtk.ToggleToolButton()
        self.shuffle_btn.set_label(_('Shuffle'))
        self.shuffle_btn.set_icon_name('media-playlist-shuffle-symbolic')
        self.shuffle_btn.props.margin_left = 10
        toolbar.insert(self.shuffle_btn, 3)

        self.repeat_type = RepeatType.NONE
        self.repeat_btn = Gtk.ToggleToolButton()
        self.repeat_btn.set_label(_('Repeat'))
        self.repeat_btn.set_icon_name('media-playlist-repeat-symbolic')
        self.repeat_btn.connect('clicked', self.on_repeat_button_clicked)
        toolbar.insert(self.repeat_btn, 4)

        self.show_mv_btn = Gtk.ToggleToolButton()
        self.show_mv_btn.set_label(_('Show MV'))
        self.show_mv_btn.set_icon_name('video-x-generic-symbolic')
        self.show_mv_btn.set_sensitive(False)
        self.show_mv_sid = self.show_mv_btn.connect(
                'toggled', self.on_show_mv_toggled)
        toolbar.insert(self.show_mv_btn, 5)

        self.fullscreen_btn = Gtk.ToolButton()
        self.fullscreen_btn.set_label(_('Fullscreen'))
        self.fullscreen_btn.set_icon_name('view-fullscreen-symbolic')
        self.fullscreen_btn.connect(
                'clicked', self.on_fullscreen_button_clicked)
        toolbar.insert(self.fullscreen_btn, 6)
        self.app.window.connect('key-press-event', self.on_window_key_pressed)

        # contro menu
        menu_tool_item = Gtk.ToolItem()
        toolbar.insert(menu_tool_item, 7)
        toolbar.child_set_property(menu_tool_item, 'expand', True)
        main_menu = Gtk.Menu()
        pref_item = Gtk.MenuItem(label=_('Preferences'))
        pref_item.connect(
                'activate', self.on_main_menu_pref_activate)
        main_menu.append(pref_item)
        sep_item = Gtk.SeparatorMenuItem()
        main_menu.append(sep_item)
        about_item = Gtk.MenuItem(label=_('About'))
        about_item.connect(
                'activate', self.on_main_menu_about_activate)
        main_menu.append(about_item)
        quit_item = Gtk.MenuItem(label=_('Quit'))
        key, mod = Gtk.accelerator_parse('<Ctrl>Q')
        quit_item.add_accelerator(
                'activate', app.accel_group, key, mod,
                Gtk.AccelFlags.VISIBLE)
        quit_item.connect(
                'activate', self.on_main_menu_quit_activate)
        main_menu.append(quit_item)
        main_menu.show_all()
        menu_image = Gtk.Image()
        menu_image.set_from_icon_name('view-list-symbolic', ICON_SIZE)
        if Config.GTK_LE_36:
            menu_btn = Gtk.Button()
            menu_btn.connect(
                    'clicked', self.on_main_menu_button_clicked, main_menu)
        else:
            menu_btn = Gtk.MenuButton()
            menu_btn.set_popup(main_menu)
            menu_btn.set_always_show_image(True)
        menu_btn.props.halign = Gtk.Align.END
        menu_btn.props.halign = Gtk.Align.END
        menu_btn.set_image(menu_image)
        menu_tool_item.add(menu_btn)

        self.label = Gtk.Label(
                '<b>{0}</b> <small>by {0}</small>'.format(_('Unknown')))
        self.label.props.use_markup = True
        self.label.props.xalign = 0
        self.label.props.margin_left = 10
        control_box.pack_start(self.label, False, False, 0)

        scale_box = Gtk.Box(spacing=3)
        scale_box.props.margin_left = 5
        control_box.pack_start(scale_box, True, False, 0)

        self.scale = Gtk.Scale()
        self.adjustment = Gtk.Adjustment(0, 0, 100, 1, 10, 0)
        self.adjustment.connect('changed', self.on_adjustment_changed)
        self.scale.set_adjustment(self.adjustment)
        self.scale.set_restrict_to_fill_level(False)
        self.scale.props.draw_value = False
        self.scale.connect('change-value', self.on_scale_change_value)
        scale_box.pack_start(self.scale, True, True, 0)

        self.time_label = Gtk.Label('0:00/0:00')
        scale_box.pack_start(self.time_label, False, False, 0)

        self.volume = Gtk.VolumeButton()
        self.volume.props.use_symbolic = True
        self.volume.set_value(app.conf['volume'] ** 0.33)
        self.volume_sid = self.volume.connect(
                'value-changed', self.on_volume_value_changed)
        scale_box.pack_start(self.volume, False, False, 0)

        # init playbin and dbus
        self.playbin = PlayerBin()
        self.playbin.set_volume(self.app.conf['volume'] ** 0.33)
        self.playbin.connect('eos', self.on_playbin_eos)
        self.playbin.connect('error', self.on_playbin_error)
        self.playbin.connect('mute-changed', self.on_playbin_mute_changed)
        self.playbin.connect(
                'volume-changed', self.on_playbin_volume_changed)
        self.dbus = PlayerDBus(self)
        self.notify = PlayerNotify(self)

    def after_init(self):
        self.init_meta()

    def do_destroy(self):
        self.playbin.destroy()
        if self.async_song:
            self.async_song.destroy()
        if self.async_next_song:
            self.async_next_song.destroy()

    def load(self, song):
        self.play_type = PlayType.SONG
        self.curr_song = song
        self.stop_player()
        self.scale.set_fill_level(0)
        self.scale.set_show_fill_level(True)
        self.async_song = Net.AsyncSong(self.app)
        self.async_song.connect('chunk-received', self.on_chunk_received)
        self.async_song.connect('can-play', self.on_song_can_play)
        self.async_song.connect('downloaded', self.on_song_downloaded)
        self.async_song.get_song(song)

    def failed_to_download(self, song_path, status):
        self.stop_player_cb()
        
        if status == 'FileNotFoundError':
            Widgets.filesystem_error(self.app.window, song_path)
        elif status == 'URLError':
            if self.play_type == PlayType.MV:
                msg = _('Failed to download MV')
            elif self.play_type in (PlayType.SONG, PlayType.RADIO):
                msg = _('Failed to download song')
            #Widgets.network_error(self.app.window, msg)
            print('Error:', msg)
            self.load_next_cb()

    def on_chunk_received(self, widget, percent):
        def _update_fill_level():
            self.scale.set_fill_level(percent)
        GLib.idle_add(_update_fill_level)

    def on_song_can_play(self, widget, song_path, status):
        def _on_song_can_play():
            uri = 'file://' + song_path
            self.meta_url = uri

            self.scale.set_fill_level(0)
            self.scale.set_show_fill_level(False)
            if self.play_type in (PlayType.SONG, PlayType.RADIO):
                self.app.lrc.show_music()
                self.playbin.load_audio(uri)
                self.get_lrc()
                self.get_mv_link()
                self.get_recommend_lists()
            elif self.play_type == PlayType.MV:
                self.show_mv_btn.set_sensitive(True)
                self.show_mv_btn.handler_block(self.show_mv_sid)
                self.show_mv_btn.set_active(True)
                self.show_mv_btn.handler_unblock(self.show_mv_sid)
                self.app.lrc.show_mv()
                self.playbin.load_video(uri, self.app.lrc.xid)
            self.start_player(load=True)
            self.update_player_info()

        if status == 'OK':
            GLib.idle_add(_on_song_can_play)
        else:
            GLib.idle_add(self.failed_to_download, song_path, status)

    def on_song_downloaded(self, widget, song_path):
        def _on_song_download():
            self.init_adjustment()
            if self.play_type in (PlayType.SONG, PlayType.MV):
                self.app.playlist.on_song_downloaded(play=True)
                self.next_song = self.app.playlist.get_next_song(
                        self.repeat_btn.get_active(),
                        self.shuffle_btn.get_active())
            elif self.play_type == PlayType.RADIO:
                self.next_song = self.curr_radio_item.get_next_song()
            if self.next_song:
                self.cache_next_song()
            # update metadata in dbus
            self.dbus.update_meta()
            self.dbus.enable_seek()

        self.scale.set_sensitive(True)
        GLib.idle_add(_on_song_download)

    def cache_next_song(self):
        if self.play_type == PlayType.MV:
            # NOTE:if next song has no MV, cache will be failed
            self.async_next_song= Net.AsyncSong(self.app)
            self.async_next_song.get_song(self.next_song, use_mv=True)
        elif self.play_type in (PlayType.SONG, PlayType.RADIO):
            self.async_next_song = Net.AsyncSong(self.app)
            self.async_next_song.get_song(self.next_song)

    def init_adjustment(self):
        self.adjustment.set_value(0.0)
        self.adjustment.set_lower(0.0)
        # when song is not totally downloaded but can play, query_duration
        # might give incorrect/inaccurate result.
        status, duration = self.playbin.get_duration()
        if status and duration > 0:
            self.adjustment.set_upper(duration)
            return False
        return True

    def sync_adjustment(self):
        status, offset = self.playbin.get_position()
        if not status:
            return True

        self.dbus.update_pos(offset // 1000)

        status, duration = self.playbin.get_duration()
        self.adjustment.set_value(offset)
        self.adjustment.set_upper(duration)
        self.sync_label_by_adjustment()
        if offset >= duration - 800000000:
            self.load_next()
            return False
        if self.play_type == PlayType.MV:
            return True
        self.app.lrc.sync_lrc(offset)
        if self.recommend_imgs and len(self.recommend_imgs) > 0:
            # change lyrics background image every 20 seconds
            div, mod = divmod(int(offset / 10**9), 20)
            if mod == 0:
                div2, mod2 = divmod(div, len(self.recommend_imgs))
                self.update_lrc_background(self.recommend_imgs[mod2])
        return True

    def sync_label_by_adjustment(self):
        curr = delta(self.adjustment.get_value())
        total = delta(self.adjustment.get_upper())
        self.time_label.set_label('{0}/{1}'.format(curr, total))

    # Control panel
    def on_pic_pressed(self, eventbox, event):
        if event.type == GDK_2BUTTON_PRESS and \
                self.play_type == PlayType.SONG:
            self.app.playlist.locate_curr_song()

    def on_prev_button_clicked(self, button):
        self.load_prev()

    def on_play_button_clicked(self, button):
        if self.play_type == PlayType.NONE:
            return
        self.play_pause()

    def on_next_button_clicked(self, button):
        self.load_next()

    def on_repeat_button_clicked(self, button):
        if self.repeat_type == RepeatType.NONE:
            self.repeat_type = RepeatType.ALL
            button.set_active(True)
            button.set_icon_name('media-playlist-repeat-symbolic')
        elif self.repeat_type == RepeatType.ALL:
            self.repeat_type = RepeatType.ONE
            button.set_active(True)
            button.set_icon_name('media-playlist-repeat-song-symbolic')
        elif self.repeat_type == RepeatType.ONE:
            self.repeat_type = RepeatType.NONE
            button.set_active(False)
            button.set_icon_name('media-playlist-repeat-symbolic')

    def on_scale_change_value(self, scale, scroll_type, value):
        self.seek_cb(value)

    def on_volume_value_changed(self, volume_button, volume):
        self.playbin.set_volume(volume ** 3)
        self.app.conf['volume'] = volume ** 3
        if self.playbin.get_mute():
            self.playbin.set_mute(False)

    def update_player_info(self):
        def _update_pic(info, error=None):
            if not info or error:
                return
            self.artist_pic.set_tooltip_text(
                    Widgets.short_tooltip(info['info'], length=500))
            if info['pic']:
                self.meta_artUrl = info['pic']
                pix = GdkPixbuf.Pixbuf.new_from_file_at_size(
                        info['pic'], 100, 100)
                self.artist_pic.set_from_pixbuf(pix)
            else:
                self.meta_artUrl = self.app.theme_path['anonymous']
            self.notify.refresh()
            self.dbus.update_meta()
            
        song = self.curr_song
        name = Widgets.short_tooltip(song['name'], 45)
        if song['artist']:
            artist = Widgets.short_tooltip(song['artist'], 20)
        else:
            artist = _('Unknown')
        if song['album']:
            album = Widgets.short_tooltip(song['album'], 30)
        else:
            album = _('Unknown')
        label = '<b>{0}</b> <small>by {1} from {2}</small>'.format(
                name, artist, album)
        self.label.set_label(label)
        self.app.window.set_title(name)
        self.artist_pic.set_from_pixbuf(self.app.theme['anonymous'])
        Net.async_call(
                Net.get_artist_info, _update_pic, 
                song['artistid'], song['artist'])

    def get_lrc(self):
        def _update_lrc(lrc_text, error=None):
            self.app.lrc.set_lrc(lrc_text)
        Net.async_call(Net.get_lrc, _update_lrc, self.curr_song)

    def get_recommend_lists(self):
        self.recommend_imgs = None
        def _on_list_received(imgs, error=None):
            if not imgs or len(imgs) < 10:
                self.recommend_imgs = None
            else:
                self.recommend_imgs = imgs.splitlines()
        Net.async_call(
                Net.get_recommend_lists, _on_list_received, 
                self.curr_song['artist'])

    def update_lrc_background(self, url):
        def _update_background(filepath, error=None):
            self.app.lrc.update_background(filepath)
        Net.async_call(Net.get_recommend_image, _update_background, url)

    # Radio part
    def load_radio(self, song, radio_item):
        '''Load Radio song.

        song from radio, only contains name, artist, rid, artistid
        Remember to update its information.
        '''
        self.play_type = PlayType.RADIO
        self.stop_player()
        self.curr_radio_item = radio_item
        self.curr_song = song
        self.scale.set_sensitive(False)
        self.async_song = Net.AsyncSong(self.app)
        self.async_song.connect('chunk-received', self.on_chunk_received)
        self.async_song.connect('can-play', self.on_song_can_play)
        self.async_song.connect('downloaded', self.on_song_downloaded)
        self.async_song.get_song(song)


    # MV part
    def on_show_mv_toggled(self, toggle_button):
        if self.play_type == PlayType.NONE:
            toggle_button.set_active(False)
            return
        state = toggle_button.get_active()
        if state:
            self.app.lrc.show_mv()
            self.load_mv(self.curr_song)
            self.app.popup_page(self.app.lrc.app_page)
        else:
            self.app.lrc.show_music()
            self.load(self.curr_song)

    def load_mv(self, song):
        self.play_type = PlayType.MV
        self.curr_song = song
        self.stop_player()
        self.scale.set_fill_level(0)
        self.scale.set_show_fill_level(True)
        self.async_song = Net.AsyncSong(self.app)
        self.async_song.connect('chunk-received', self.on_chunk_received)
        self.async_song.connect('can-play', self.on_song_can_play)
        self.async_song.connect('downloaded', self.on_song_downloaded)
        self.async_song.get_song(song, use_mv=True)

    def get_mv_link(self):
        def _update_mv_link(mv_args, error=None):
            mv_link, mv_path = mv_args
            self.show_mv_btn.set_sensitive(mv_link is not False)
        Net.async_call(
                Net.get_song_link, _update_mv_link,
                self.curr_song, self.app.conf, True)

    # Fullscreen
    def get_fullscreen(self):
        '''Check if player is in fullscreen mode.'''
        return self.fullscreen_sid > 0

    def toggle_fullscreen(self):
        '''Switch between fullscreen and unfullscreen mode.'''
        self.fullscreen_btn.emit('clicked')

    def on_window_key_pressed(self, widget, event):
        # press Esc to exit fullscreen mode
        if event.keyval == Gdk.KEY_Escape and self.get_fullscreen():
            self.toggle_fullscreen()
        # press F11 to toggle fullscreen mode
        elif event.keyval == Gdk.KEY_F11:
            self.toggle_fullscreen()

    def on_fullscreen_button_clicked(self, button):
        window = self.app.window
        if self.fullscreen_sid > 0:
        # unfullscreen
            self.app.notebook.set_show_tabs(True)
            button.set_icon_name('view-fullscreen-symbolic')
            self.show()
            window.realize()
            window.unfullscreen()
            window.disconnect(self.fullscreen_sid)
            self.fullscreen_sid = 0
        else:
        # fullscreen
            self.app.notebook.set_show_tabs(False)
            button.set_icon_name('view-restore-symbolic')
            self.app.popup_page(self.app.lrc.app_page)
            self.hide()
            window.realize()
            window.fullscreen()
            self.fullscreen_sid = window.connect(
                    'motion-notify-event', self.on_window_motion_notified)
            self.playbin.expose_fullscreen()

    def on_window_motion_notified(self, widget, event):
        # if mouse_point.y is not in [0, 50], ignore it
        if event.y > 50:
            return

        # show control_panel and notebook labels
        self.show_all()
        # delay 3 seconds and hide them
        self.fullscreen_timestamp = time.time()
        GLib.timeout_add(
                3000, self.hide_control_panel_and_label, 
                self.fullscreen_timestamp)
        self.playbin.expose_fullscreen()

    def hide_control_panel_and_label(self, timestamp):
        if (timestamp == self.fullscreen_timestamp and 
                self.fullscreen_sid > 0):
            self.app.notebook.set_show_tabs(False)
            self.hide()

    # menu button
    def on_main_menu_button_clicked(self, button, main_menu):
        main_menu.popup(
                None, None, None, None, 1, Gtk.get_current_event_time())

    def on_main_menu_pref_activate(self, menu_item):
        dialog = Preferences(self.app)
        dialog.run()
        dialog.destroy()
        self.app.load_styles()
        self.app.lrc.update_highlighted_tag()
        self.app.shortcut.rebind_keys()

    def on_main_menu_about_activate(self, menu_item):
        dialog = Gtk.AboutDialog()
        dialog.set_modal(True)
        dialog.set_transient_for(self.app.window)
        dialog.set_program_name(Config.APPNAME)
        dialog.set_logo(self.app.theme['app-logo'])
        dialog.set_version(Config.VERSION)
        dialog.set_comments(Config.DESCRIPTION)
        dialog.set_copyright('Copyright (c) 2013 LiuLang')
        dialog.set_website(Config.HOMEPAGE)
        dialog.set_license_type(Gtk.License.GPL_3_0)
        dialog.set_authors(Config.AUTHORS)
        dialog.run()
        dialog.destroy()

    def on_main_menu_quit_activate(self, menu_item):
        self.app.quit()


    # playbin signal handlers
    def on_playbin_eos(self, playbin, eos_msg):
        self.load_next()

    def on_playbin_error(self, playbin, error_msg):
        print('Player.on_playbin_error(), ', error_msg)
        self.load_next()

    def on_playbin_mute_changed(self, playbin, mute):
        self.update_gtk_volume_value_cb()

    def on_playbin_volume_changed(self, playbin, volume):
        self.update_gtk_volume_value_cb()

    def update_gtk_volume_value(self):
        mute = self.playbin.get_mute()
        volume = self.playbin.get_volume()
        if mute:
            self.volume.handler_block(self.volume_sid)
            self.volume.set_value(0.0)
            self.volume.handler_unblock(self.volume_sid)
        else:
            self.volume.handler_block(self.volume_sid)
            self.volume.set_value(volume ** 0.33)
            self.volume.handler_unblock(self.volume_sid)
        self.app.conf['volume'] = volume

    def update_gtk_volume_value_cb(self):
        GLib.idle_add(self.update_gtk_volume_value)


    # control player, UI and dbus
    def is_playing(self):
        #return self.playbin.is_playing()
        return self._is_playing

    def start_player(self, load=False):
        if self.play_type == PlayType.NONE:
            return
        self._is_playing = True

        self.dbus.set_Playing()

        self.play_button.set_icon_name('media-playback-pause-symbolic')
        self.playbin.play()
        self.adj_timeout = GLib.timeout_add(250, self.sync_adjustment)
        if load:
            self.playbin.set_volume(self.app.conf['volume'])
            self.init_meta()
            GLib.timeout_add(1500, self.init_adjustment)
        self.notify.refresh()

    def start_player_cb(self, load=False):
        GLib.idle_add(self.start_player, load)

    def pause_player(self):
        if self.play_type == PlayType.NONE:
            return
        self._is_playing = False
        self.dbus.set_Pause()
        self.play_button.set_icon_name('media-playback-start-symbolic')
        self.playbin.pause()
        if self.adj_timeout > 0:
            GLib.source_remove(self.adj_timeout)
            self.adj_timeout = 0
        self.notify.refresh()

    def pause_player_cb(self):
        GLib.idle_add(self.pause_player)

    def play_pause(self):
        if self.play_type == PlayType.NONE:
            return
        if self.playbin.is_playing():
            self.pause_player()
        else:
            self.start_player()

    def play_pause_cb(self):
        GLib.idle_add(self.play_pause)

    def stop_player(self):
        if self.play_type == PlayType.NONE:
            return
        self._is_playing = False
        self.play_button.set_icon_name('media-playback-pause-symbolic')
        self.playbin.stop()
        self.scale.set_value(0)
        #self.scale.set_sensitive(False)
        self.show_mv_btn.set_sensitive(False)
        self.show_mv_btn.handler_block(self.show_mv_sid)
        self.show_mv_btn.set_active(False)
        self.show_mv_btn.handler_unblock(self.show_mv_sid)
        self.time_label.set_label('0:00/0:00')
        if self.adj_timeout > 0:
            GLib.source_remove(self.adj_timeout)
            self.adj_timeout = 0

    def stop_player_cb(self):
        GLib.idle_add(self.stop_player)

    def load_prev(self):
        if self.play_type == PlayType.NONE or not self.can_go_previous():
            return
        self.stop_player()
        _repeat = self.repeat_btn.get_active()
        if self.play_type == PlayType.SONG:
            self.app.playlist.play_prev_song(repeat=_repeat, use_mv=False)
        elif self.play_type == PlayType.MV:
            self.app.playlist.play_prev_song(repeat=_repeat, use_mv=True)

    def load_prev_cb(self):
        GLib.idle_add(self.load_prev)

    def load_next(self):
        if self.play_type == PlayType.NONE:
            return
        self.stop_player()
        if self.repeat_type == RepeatType.ONE:
            if self.play_type == PlayType.MV:
                self.load_mv(self.curr_song)
            else:
                self.load(self.curr_song)
            return

        repeat = self.repeat_btn.get_active()
        shuffle = self.shuffle_btn.get_active()
        if self.play_type == PlayType.RADIO:
            self.curr_radio_item.play_next_song()
        elif self.play_type == PlayType.SONG:
            self.app.playlist.play_next_song(repeat, shuffle, use_mv=False)
        elif self.play_type == PlayType.MV:
            self.app.playlist.play_next_song(repeat, shuffle, use_mv=True)

    def load_next_cb(self):
        GLib.idle_add(self.load_next)

    def get_volume(self):
        return self.volume.get_value()

    def set_volume(self, volume):
        self.volume.set_value(volume)

    def set_volume_cb(self, volume):
        GLib.idle_add(self.set_volume, volume)

    def get_volume(self):
        return self.playbin.get_volume()

    def toggle_mute(self):
        mute = self.playbin.get_mute()
        self.playbin.set_mute(not mute)
        if mute:
            self.volume.handler_block(self.volume_sid)
            self.volume.set_value(self.app.conf['volume'])
            self.volume.handler_unblock(self.volume_sid)
        else:
            self.volume.handler_block(self.volume_sid)
            self.volume.set_value(0.0)
            self.volume.handler_unblock(self.volume_sid)

    def toggle_mute_cb(self):
        GLib.idle_add(self.toggle_mute)

    def seek(self, offset):
        if self.play_type == PlayType.NONE:
            return
        self.pause_player()
        self.playbin.seek(offset)
        GLib.timeout_add(300, self.start_player_cb)
        self.sync_label_by_adjustment()

    def seek_cb(self, offset):
        GLib.idle_add(self.seek, offset)

    def can_go_previous(self):
        if self.play_type in (PlayType.MV, PlayType.SONG):
            return True
        return False


    # dbus parts
    def init_meta(self):
        self.adjustment_upper = 0
        self.dbus.disable_seek()
        self.meta_url = ''
        self.meta_artUrl = ''

    def on_adjustment_changed(self, adj):
        self.dbus.update_meta()
        self.adjustment_upper = adj.get_upper()
Beispiel #2
0
class Player(Gtk.Box):
    def __init__(self, app):
        super().__init__()
        self.app = app

        self.fullscreen_sid = 0
        self.play_type = PlayType.NONE
        self.adj_timeout = 0
        self.recommend_imgs = None
        self.curr_song = None

        # use this to keep Net.AsyncSong object
        self.async_song = None
        self.async_next_song = None

        event_pic = Gtk.EventBox()
        event_pic.connect('button-press-event', self.on_pic_pressed)
        self.pack_start(event_pic, False, False, 0)

        self.artist_pic = Gtk.Image.new_from_pixbuf(app.theme['anonymous'])
        event_pic.add(self.artist_pic)

        control_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.pack_start(control_box, True, True, 0)

        toolbar = Gtk.Toolbar()
        toolbar.set_style(Gtk.ToolbarStyle.ICONS)
        toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_MENUBAR)
        toolbar.set_show_arrow(False)
        toolbar.set_icon_size(ICON_SIZE)
        control_box.pack_start(toolbar, False, False, 0)

        prev_button = Gtk.ToolButton()
        prev_button.set_label(_('Previous'))
        prev_button.set_icon_name('media-skip-backward-symbolic')
        prev_button.connect('clicked', self.on_prev_button_clicked)
        toolbar.insert(prev_button, 0)

        self.play_button = Gtk.ToolButton()
        self.play_button.set_label(_('Play'))
        self.play_button.set_icon_name('media-playback-start-symbolic')
        self.play_button.connect('clicked', self.on_play_button_clicked)
        toolbar.insert(self.play_button, 1)

        next_button = Gtk.ToolButton()
        next_button.set_label(_('Next'))
        next_button.set_icon_name('media-skip-forward-symbolic')
        next_button.connect('clicked', self.on_next_button_clicked)
        toolbar.insert(next_button, 2)

        self.shuffle_btn = Gtk.ToggleToolButton()
        self.shuffle_btn.set_label(_('Shuffle'))
        self.shuffle_btn.set_icon_name('media-playlist-shuffle-symbolic')
        self.shuffle_btn.props.margin_left = 10
        toolbar.insert(self.shuffle_btn, 3)

        self.repeat_type = RepeatType.NONE
        self.repeat_btn = Gtk.ToggleToolButton()
        self.repeat_btn.set_label(_('Repeat'))
        self.repeat_btn.set_icon_name('media-playlist-repeat-symbolic')
        self.repeat_btn.connect('clicked', self.on_repeat_button_clicked)
        toolbar.insert(self.repeat_btn, 4)

        self.use_audio_btn = Gtk.RadioToolButton()
        self.use_audio_btn.set_label(_('Play audio'))
        self.use_audio_btn.set_icon_name('audio-x-generic-symbolic')
        self.use_audio_btn.props.margin_left = 10
        self.use_audio_btn.set_active(True)
        self.use_audio_sid = self.use_audio_btn.connect(
            'toggled', self.on_use_audio_toggled)
        toolbar.insert(self.use_audio_btn, 5)

        self.use_mtv_btn = Gtk.RadioToolButton()
        self.use_mtv_btn.set_label(_('Play MTV'))
        self.use_mtv_btn.set_tooltip_text(_('Play MTV'))
        self.use_mtv_btn.set_icon_name('video-x-generic-symbolic')
        self.use_mtv_btn.set_sensitive(False)
        self.use_mtv_btn.props.group = self.use_audio_btn
        self.use_mtv_btn.connect('toggled', self.on_use_mtv_toggled)
        toolbar.insert(self.use_mtv_btn, 6)

        self.use_ok_btn = Gtk.RadioToolButton()
        self.use_ok_btn.set_label(_('Play Karaoke'))
        self.use_ok_btn.set_tooltip_text(
            _('Play Karaoke\nPlease use mkv format'))
        self.use_ok_btn.set_icon_name('audio-input-microphone-symbolic')
        self.use_ok_btn.set_sensitive(False)
        self.use_ok_btn.props.group = self.use_audio_btn
        self.use_ok_btn.connect('toggled', self.on_use_ok_toggled)
        toolbar.insert(self.use_ok_btn, 7)

        self.fullscreen_btn = Gtk.ToolButton()
        self.fullscreen_btn.set_label(_('Fullscreen'))
        self.fullscreen_btn.set_icon_name('view-fullscreen-symbolic')
        self.fullscreen_btn.props.margin_left = 10
        self.fullscreen_btn.connect('clicked',
                                    self.on_fullscreen_button_clicked)
        toolbar.insert(self.fullscreen_btn, 8)
        self.app.window.connect('key-press-event', self.on_window_key_pressed)

        # contro menu
        menu_tool_item = Gtk.ToolItem()
        toolbar.insert(menu_tool_item, 9)
        toolbar.child_set_property(menu_tool_item, 'expand', True)
        main_menu = Gtk.Menu()
        pref_item = Gtk.MenuItem(label=_('Preferences'))
        pref_item.connect('activate', self.on_main_menu_pref_activate)
        main_menu.append(pref_item)
        sep_item = Gtk.SeparatorMenuItem()
        main_menu.append(sep_item)
        about_item = Gtk.MenuItem(label=_('About'))
        about_item.connect('activate', self.on_main_menu_about_activate)
        main_menu.append(about_item)
        quit_item = Gtk.MenuItem(label=_('Quit'))
        key, mod = Gtk.accelerator_parse('<Ctrl>Q')
        quit_item.add_accelerator('activate', app.accel_group, key, mod,
                                  Gtk.AccelFlags.VISIBLE)
        quit_item.connect('activate', self.on_main_menu_quit_activate)
        main_menu.append(quit_item)
        main_menu.show_all()
        menu_image = Gtk.Image()
        menu_image.set_from_icon_name('view-list-symbolic', ICON_SIZE)
        if Config.GTK_LE_36:
            menu_btn = Gtk.Button()
            menu_btn.connect('clicked', self.on_main_menu_button_clicked,
                             main_menu)
        else:
            menu_btn = Gtk.MenuButton()
            menu_btn.set_popup(main_menu)
            menu_btn.set_always_show_image(True)
        menu_btn.props.halign = Gtk.Align.END
        menu_btn.props.halign = Gtk.Align.END
        menu_btn.set_image(menu_image)
        menu_tool_item.add(menu_btn)

        self.label = Gtk.Label('<b>{0}</b> <small>by {0}</small>'.format(
            _('Unknown')))
        self.label.props.use_markup = True
        self.label.props.xalign = 0
        self.label.props.margin_left = 10
        control_box.pack_start(self.label, False, False, 0)

        scale_box = Gtk.Box(spacing=3)
        scale_box.props.margin_left = 5
        control_box.pack_start(scale_box, True, False, 0)

        self.scale = Gtk.Scale()
        self.adjustment = Gtk.Adjustment(0, 0, 100, 1, 10, 0)
        self.adjustment.connect('changed', self.on_adjustment_changed)
        self.scale.set_adjustment(self.adjustment)
        self.scale.set_restrict_to_fill_level(False)
        self.scale.props.draw_value = False
        self.scale.connect('change-value', self.on_scale_change_value)
        scale_box.pack_start(self.scale, True, True, 0)

        self.time_label = Gtk.Label('0:00/0:00')
        scale_box.pack_start(self.time_label, False, False, 0)

        self.volume = Gtk.VolumeButton()
        self.volume.props.use_symbolic = True
        self.volume.set_value(app.conf['volume']**0.33)
        self.volume_sid = self.volume.connect('value-changed',
                                              self.on_volume_value_changed)
        scale_box.pack_start(self.volume, False, False, 0)

        # init playbin and dbus
        self.playbin = PlayerBin()
        self.playbin.set_volume(self.app.conf['volume']**0.33)
        self.playbin.connect('eos', self.on_playbin_eos)
        self.playbin.connect('error', self.on_playbin_error)
        self.playbin.connect('mute-changed', self.on_playbin_mute_changed)
        self.playbin.connect('volume-changed', self.on_playbin_volume_changed)
        self.dbus = PlayerDBus(self)
        self.notify = PlayerNotify(self)

    def after_init(self):
        self.init_meta()

    def do_destroy(self):
        self.playbin.destroy()
        if self.async_song:
            self.async_song.destroy()
        if self.async_next_song:
            self.async_next_song.destroy()

    def load(self, song):
        self.play_type = PlayType.SONG
        self.curr_song = song
        self.stop_player()
        self.scale.set_fill_level(0)
        self.scale.set_show_fill_level(True)
        self.async_song = Net.AsyncSong(self.app)
        self.async_song.connect('chunk-received', self.on_chunk_received)
        self.async_song.connect('can-play', self.on_song_can_play)
        self.async_song.connect('downloaded', self.on_song_downloaded)
        self.async_song.get_song(song)

    def failed_to_download(self, song_path, status):
        self.stop_player_cb()

        if status == 'FileNotFoundError':
            Widgets.filesystem_error(self.app.window, song_path)
        elif status == 'URLError':
            if self.play_type == PlayType.MV:
                msg = _('Failed to download MV')
            elif self.play_type in (PlayType.SONG, PlayType.RADIO):
                msg = _('Failed to download song')
            #Widgets.network_error(self.app.window, msg)
            print('Error:', msg)
            self.load_next_cb()

    def on_chunk_received(self, widget, percent):
        def _update_fill_level():
            self.scale.set_fill_level(percent)

        GLib.idle_add(_update_fill_level)

    def on_song_can_play(self, widget, song_path, status):
        def _on_song_can_play():
            uri = 'file://' + song_path
            self.meta_url = uri

            self.scale.set_fill_level(0)
            self.scale.set_show_fill_level(False)
            if self.play_type in (PlayType.SONG, PlayType.RADIO):
                self.app.lrc.show_music()
                self.playbin.load_audio(uri)
                self.get_lrc()
                self.get_mv_link()
                self.get_recommend_lists()
            elif self.play_type == PlayType.MV:
                self.use_mtv_btn.set_sensitive(True)
                if not self.use_ok_btn.get_sensitive():
                    GLib.timeout_add(2000, self.check_audio_streams)
                self.app.lrc.show_mv()
                audio_stream = MTV_AUDIO
                if self.use_ok_btn.get_active():
                    audio_stream = OK_AUDIO
                self.playbin.load_video(uri, self.app.lrc.xid, audio_stream)
            self.start_player(load=True)
            self.update_player_info()

        if status == 'OK':
            GLib.idle_add(_on_song_can_play)
        else:
            GLib.idle_add(self.failed_to_download, song_path, status)

    def on_song_downloaded(self, widget, song_path):
        def _on_song_download():
            self.init_adjustment()
            if self.play_type in (PlayType.SONG, PlayType.MV):
                self.app.playlist.on_song_downloaded(play=True)
                self.next_song = self.app.playlist.get_next_song(
                    self.repeat_btn.get_active(),
                    self.shuffle_btn.get_active())
            elif self.play_type == PlayType.RADIO:
                self.next_song = self.curr_radio_item.get_next_song()
            if self.next_song:
                self.cache_next_song()
            # update metadata in dbus
            self.dbus.update_meta()
            self.dbus.enable_seek()

        self.scale.set_sensitive(True)
        GLib.idle_add(_on_song_download)

    def cache_next_song(self):
        if self.play_type == PlayType.MV:
            use_mv = True
        elif self.play_type in (PlayType.SONG, PlayType.RADIO):
            use_mv = False
        self.async_next_song = Net.AsyncSong(self.app)
        self.async_next_song.get_song(self.next_song, use_mv=use_mv)

    def init_adjustment(self):
        self.adjustment.set_value(0.0)
        self.adjustment.set_lower(0.0)
        # when song is not totally downloaded but can play, query_duration
        # might give incorrect/inaccurate result.
        status, duration = self.playbin.get_duration()
        if status and duration > 0:
            self.adjustment.set_upper(duration)
            return False
        return True

    def sync_adjustment(self):
        status, offset = self.playbin.get_position()
        if not status:
            return True

        self.dbus.update_pos(offset // 1000)

        status, duration = self.playbin.get_duration()
        self.adjustment.set_value(offset)
        self.adjustment.set_upper(duration)
        self.sync_label_by_adjustment()
        if offset >= duration - 800000000:
            self.load_next()
            return False
        if self.play_type == PlayType.MV:
            return True
        self.app.lrc.sync_lrc(offset)
        if self.recommend_imgs and len(self.recommend_imgs) > 0:
            # change lyrics background image every 20 seconds
            div, mod = divmod(int(offset / 10**9), 20)
            if mod == 0:
                div2, mod2 = divmod(div, len(self.recommend_imgs))
                self.update_lrc_background(self.recommend_imgs[mod2])
        return True

    def sync_label_by_adjustment(self):
        curr = delta(self.adjustment.get_value())
        total = delta(self.adjustment.get_upper())
        self.time_label.set_label('{0}/{1}'.format(curr, total))

    # Control panel
    def on_pic_pressed(self, eventbox, event):
        if event.type == GDK_2BUTTON_PRESS and \
                self.play_type == PlayType.SONG:
            self.app.playlist.locate_curr_song()

    def on_prev_button_clicked(self, button):
        self.load_prev()

    def on_play_button_clicked(self, button):
        if self.play_type == PlayType.NONE:
            return
        self.play_pause()

    def on_next_button_clicked(self, button):
        self.load_next()

    def on_repeat_button_clicked(self, button):
        if self.repeat_type == RepeatType.NONE:
            self.repeat_type = RepeatType.ALL
            button.set_active(True)
            button.set_icon_name('media-playlist-repeat-symbolic')
        elif self.repeat_type == RepeatType.ALL:
            self.repeat_type = RepeatType.ONE
            button.set_active(True)
            button.set_icon_name('media-playlist-repeat-song-symbolic')
        elif self.repeat_type == RepeatType.ONE:
            self.repeat_type = RepeatType.NONE
            button.set_active(False)
            button.set_icon_name('media-playlist-repeat-symbolic')

    def on_scale_change_value(self, scale, scroll_type, value):
        self.seek_cb(value)

    def on_volume_value_changed(self, volume_button, volume):
        self.playbin.set_volume(volume**3)
        self.app.conf['volume'] = volume**3
        if self.playbin.get_mute():
            self.playbin.set_mute(False)

    def update_player_info(self):
        def _update_pic(info, error=None):
            if not info or error:
                return
            self.artist_pic.set_tooltip_text(
                Widgets.short_tooltip(info['info'], length=500))
            if info['pic']:
                self.meta_artUrl = info['pic']
                pix = GdkPixbuf.Pixbuf.new_from_file_at_size(
                    info['pic'], 100, 100)
                self.artist_pic.set_from_pixbuf(pix)
            else:
                self.meta_artUrl = self.app.theme_path['anonymous']
            self.notify.refresh()
            self.dbus.update_meta()

        song = self.curr_song
        name = Widgets.short_tooltip(song['name'], 45)
        if song['artist']:
            artist = Widgets.short_tooltip(song['artist'], 20)
        else:
            artist = _('Unknown')
        if song['album']:
            album = Widgets.short_tooltip(song['album'], 30)
        else:
            album = _('Unknown')
        label = '<b>{0}</b> <small>by {1} from {2}</small>'.format(
            name, artist, album)
        self.label.set_label(label)
        self.app.window.set_title(name)
        self.artist_pic.set_from_pixbuf(self.app.theme['anonymous'])
        Net.async_call(Net.get_artist_info, _update_pic, song['artistid'],
                       song['artist'])

    def get_lrc(self):
        def _update_lrc(lrc_text, error=None):
            self.app.lrc.set_lrc(lrc_text)

        Net.async_call(Net.get_lrc, _update_lrc, self.curr_song)

    def get_recommend_lists(self):
        self.recommend_imgs = None

        def _on_list_received(imgs, error=None):
            if not imgs or len(imgs) < 10:
                self.recommend_imgs = None
            else:
                self.recommend_imgs = imgs.splitlines()

        Net.async_call(Net.get_recommend_lists, _on_list_received,
                       self.curr_song['artist'])

    def update_lrc_background(self, url):
        def _update_background(filepath, error=None):
            self.app.lrc.update_background(filepath)

        Net.async_call(Net.get_recommend_image, _update_background, url)

    # Radio part
    def load_radio(self, song, radio_item):
        '''Load Radio song.

        song from radio, only contains name, artist, rid, artistid
        Remember to update its information.
        '''
        self.play_type = PlayType.RADIO
        self.stop_player()
        self.curr_radio_item = radio_item
        self.curr_song = song
        self.scale.set_sensitive(False)
        self.async_song = Net.AsyncSong(self.app)
        self.async_song.connect('chunk-received', self.on_chunk_received)
        self.async_song.connect('can-play', self.on_song_can_play)
        self.async_song.connect('downloaded', self.on_song_downloaded)
        self.async_song.get_song(song)

    # MV part
    def check_audio_streams(self):
        self.use_ok_btn.set_sensitive(self.playbin.get_audios() > 1)

    def on_use_audio_toggled(self, toggle_button):
        if not toggle_button.get_active():
            return
        self.app.lrc.show_music()
        self.load(self.curr_song)

    def on_use_mtv_toggled(self, toggle_button):
        if not toggle_button.get_active():
            return
        if self.play_type == PlayType.NONE:
            return
        # If current playtype is MV, only switch audio stream
        if self.play_type == PlayType.MV:
            self.playbin.set_current_audio(MTV_AUDIO)
        else:
            self.app.lrc.show_mv()
            self.load_mv(self.curr_song)
            self.app.popup_page(self.app.lrc.app_page)

    def on_use_ok_toggled(self, toggle_button):
        if not toggle_button.get_active():
            return
        if self.play_type == PlayType.NONE:
            return
        if self.play_type == PlayType.MV:
            self.playbin.set_current_audio(OK_AUDIO)
        else:
            self.app.lrc.show_mv()
            self.load_mv(self.curr_song)
            self.app.popup_page(self.app.lrc.app_page)

    def load_mv(self, song):
        self.play_type = PlayType.MV
        self.curr_song = song
        self.stop_player()
        self.scale.set_fill_level(0)
        self.scale.set_show_fill_level(True)
        self.async_song = Net.AsyncSong(self.app)
        self.async_song.connect('chunk-received', self.on_chunk_received)
        self.async_song.connect('can-play', self.on_song_can_play)
        self.async_song.connect('downloaded', self.on_song_downloaded)
        self.async_song.get_song(song, use_mv=True)

    def get_mv_link(self):
        def _update_mv_link(mv_args, error=None):
            mv_link, mv_path = mv_args
            self.use_mtv_btn.set_sensitive(mv_link is not False)

        Net.async_call(Net.get_song_link, _update_mv_link, self.curr_song,
                       self.app.conf, True)

    # Fullscreen
    def get_fullscreen(self):
        '''Check if player is in fullscreen mode.'''
        return self.fullscreen_sid > 0

    def toggle_fullscreen(self):
        '''Switch between fullscreen and unfullscreen mode.'''
        self.fullscreen_btn.emit('clicked')

    def on_window_key_pressed(self, widget, event):
        # press Esc to exit fullscreen mode
        if event.keyval == Gdk.KEY_Escape and self.get_fullscreen():
            self.toggle_fullscreen()
        # press F11 to toggle fullscreen mode
        elif event.keyval == Gdk.KEY_F11:
            self.toggle_fullscreen()

    def on_fullscreen_button_clicked(self, button):
        window = self.app.window
        if self.fullscreen_sid > 0:
            # unfullscreen
            self.app.notebook.set_show_tabs(True)
            button.set_icon_name('view-fullscreen-symbolic')
            self.show()
            window.realize()
            window.unfullscreen()
            window.disconnect(self.fullscreen_sid)
            self.fullscreen_sid = 0
        else:
            # fullscreen
            self.app.notebook.set_show_tabs(False)
            button.set_icon_name('view-restore-symbolic')
            self.app.popup_page(self.app.lrc.app_page)
            self.hide()
            window.realize()
            window.fullscreen()
            self.fullscreen_sid = window.connect(
                'motion-notify-event', self.on_window_motion_notified)
            self.playbin.expose_fullscreen()

    def on_window_motion_notified(self, widget, event):
        # if mouse_point.y is not in [0, 50], ignore it
        if event.y > 50:
            return

        # show control_panel and notebook labels
        self.show_all()
        # delay 3 seconds and hide them
        self.fullscreen_timestamp = time.time()
        GLib.timeout_add(3000, self.hide_control_panel_and_label,
                         self.fullscreen_timestamp)
        self.playbin.expose_fullscreen()

    def hide_control_panel_and_label(self, timestamp):
        if (timestamp == self.fullscreen_timestamp
                and self.fullscreen_sid > 0):
            self.app.notebook.set_show_tabs(False)
            self.hide()

    # menu button
    def on_main_menu_button_clicked(self, button, main_menu):
        main_menu.popup(None, None, None, None, 1,
                        Gtk.get_current_event_time())

    def on_main_menu_pref_activate(self, menu_item):
        dialog = Preferences(self.app)
        dialog.run()
        dialog.destroy()
        self.app.load_styles()
        self.app.lrc.update_highlighted_tag()
        self.app.shortcut.rebind_keys()

    def on_main_menu_about_activate(self, menu_item):
        dialog = Gtk.AboutDialog()
        dialog.set_modal(True)
        dialog.set_transient_for(self.app.window)
        dialog.set_program_name(Config.APPNAME)
        dialog.set_logo(self.app.theme['app-logo'])
        dialog.set_version(Config.VERSION)
        dialog.set_comments(Config.DESCRIPTION)
        dialog.set_copyright('Copyright (c) 2013 LiuLang')
        dialog.set_website(Config.HOMEPAGE)
        dialog.set_license_type(Gtk.License.GPL_3_0)
        dialog.set_authors(Config.AUTHORS)
        dialog.run()
        dialog.destroy()

    def on_main_menu_quit_activate(self, menu_item):
        self.app.quit()

    # playbin signal handlers
    def on_playbin_eos(self, playbin, eos_msg):
        self.load_next()

    def on_playbin_error(self, playbin, error_msg):
        print('Player.on_playbin_error(), ', error_msg)
        self.load_next()

    def on_playbin_mute_changed(self, playbin, mute):
        self.update_gtk_volume_value_cb()

    def on_playbin_volume_changed(self, playbin, volume):
        self.update_gtk_volume_value_cb()

    def update_gtk_volume_value(self):
        mute = self.playbin.get_mute()
        volume = self.playbin.get_volume()
        if mute:
            self.volume.handler_block(self.volume_sid)
            self.volume.set_value(0.0)
            self.volume.handler_unblock(self.volume_sid)
        else:
            self.volume.handler_block(self.volume_sid)
            self.volume.set_value(volume**0.33)
            self.volume.handler_unblock(self.volume_sid)
        self.app.conf['volume'] = volume

    def update_gtk_volume_value_cb(self):
        GLib.idle_add(self.update_gtk_volume_value)

    # control player, UI and dbus
    def is_playing(self):
        #return self.playbin.is_playing()
        return self._is_playing

    def start_player(self, load=False):
        if self.play_type == PlayType.NONE:
            return
        self._is_playing = True

        self.dbus.set_Playing()

        self.play_button.set_icon_name('media-playback-pause-symbolic')
        self.playbin.play()
        self.adj_timeout = GLib.timeout_add(250, self.sync_adjustment)
        if load:
            self.playbin.set_volume(self.app.conf['volume'])
            self.init_meta()
            GLib.timeout_add(1500, self.init_adjustment)
        self.notify.refresh()

    def start_player_cb(self, load=False):
        GLib.idle_add(self.start_player, load)

    def pause_player(self):
        if self.play_type == PlayType.NONE:
            return
        self._is_playing = False
        self.dbus.set_Pause()
        self.play_button.set_icon_name('media-playback-start-symbolic')
        self.playbin.pause()
        if self.adj_timeout > 0:
            GLib.source_remove(self.adj_timeout)
            self.adj_timeout = 0
        self.notify.refresh()

    def pause_player_cb(self):
        GLib.idle_add(self.pause_player)

    def play_pause(self):
        if self.play_type == PlayType.NONE:
            return
        if self.playbin.is_playing():
            self.pause_player()
        else:
            self.start_player()

    def play_pause_cb(self):
        GLib.idle_add(self.play_pause)

    def stop_player(self):
        if self.play_type == PlayType.NONE:
            return
        self._is_playing = False
        self.play_button.set_icon_name('media-playback-pause-symbolic')
        self.playbin.stop()
        self.scale.set_value(0)
        #self.scale.set_sensitive(False)
        if self.play_type != PlayType.MV:
            self.use_audio_btn.handler_block(self.use_audio_sid)
            self.use_audio_btn.set_active(True)
            self.use_audio_btn.handler_unblock(self.use_audio_sid)
            self.use_mtv_btn.set_sensitive(False)
            self.use_ok_btn.set_sensitive(False)
        self.time_label.set_label('0:00/0:00')
        if self.adj_timeout > 0:
            GLib.source_remove(self.adj_timeout)
            self.adj_timeout = 0

    def stop_player_cb(self):
        GLib.idle_add(self.stop_player)

    def load_prev(self):
        if self.play_type == PlayType.NONE or not self.can_go_previous():
            return
        self.stop_player()
        _repeat = self.repeat_btn.get_active()
        if self.play_type == PlayType.SONG:
            self.app.playlist.play_prev_song(repeat=_repeat, use_mv=False)
        elif self.play_type == PlayType.MV:
            self.app.playlist.play_prev_song(repeat=_repeat, use_mv=True)

    def load_prev_cb(self):
        GLib.idle_add(self.load_prev)

    def load_next(self):
        if self.play_type == PlayType.NONE:
            return
        self.stop_player()
        if self.repeat_type == RepeatType.ONE:
            if self.play_type == PlayType.MV:
                self.load_mv(self.curr_song)
            else:
                self.load(self.curr_song)
            return

        repeat = self.repeat_btn.get_active()
        shuffle = self.shuffle_btn.get_active()
        if self.play_type == PlayType.RADIO:
            self.curr_radio_item.play_next_song()
        elif self.play_type == PlayType.SONG:
            self.app.playlist.play_next_song(repeat, shuffle, use_mv=False)
        elif self.play_type == PlayType.MV:
            self.app.playlist.play_next_song(repeat, shuffle, use_mv=True)

    def load_next_cb(self):
        GLib.idle_add(self.load_next)

    def get_volume(self):
        return self.volume.get_value()

    def set_volume(self, volume):
        self.volume.set_value(volume)

    def set_volume_cb(self, volume):
        GLib.idle_add(self.set_volume, volume)

    def get_volume(self):
        return self.playbin.get_volume()

    def toggle_mute(self):
        mute = self.playbin.get_mute()
        self.playbin.set_mute(not mute)
        if mute:
            self.volume.handler_block(self.volume_sid)
            self.volume.set_value(self.app.conf['volume'])
            self.volume.handler_unblock(self.volume_sid)
        else:
            self.volume.handler_block(self.volume_sid)
            self.volume.set_value(0.0)
            self.volume.handler_unblock(self.volume_sid)

    def toggle_mute_cb(self):
        GLib.idle_add(self.toggle_mute)

    def seek(self, offset):
        if self.play_type == PlayType.NONE:
            return
        self.pause_player()
        self.playbin.seek(offset)
        GLib.timeout_add(300, self.start_player_cb)
        self.sync_label_by_adjustment()

    def seek_cb(self, offset):
        GLib.idle_add(self.seek, offset)

    def can_go_previous(self):
        if self.play_type in (PlayType.MV, PlayType.SONG):
            return True
        return False

    # dbus parts
    def init_meta(self):
        self.adjustment_upper = 0
        self.dbus.disable_seek()
        self.meta_url = ''
        self.meta_artUrl = ''

    def on_adjustment_changed(self, adj):
        self.dbus.update_meta()
        self.adjustment_upper = adj.get_upper()
Beispiel #3
0
class Player(Gtk.Box):
    def __init__(self, app):
        super().__init__()
        self.app = app

        self.play_type = PlayType.NONE
        self.adj_timeout = 0
        self.recommend_imgs = None
        self.curr_song = None
        self.fullscreen_sid = 0
        self.fullscreen_timestamp = 0
        self.default_cursor = None
        self.blank_cursor = Gdk.Cursor.new(Gdk.CursorType.BLANK_CURSOR)

        # use this to keep Net.AsyncSong object
        self.async_song = None
        self.async_next_song = None

        event_pic = Gtk.EventBox()
        event_pic.connect('button-press-event', self.on_pic_pressed)
        self.pack_start(event_pic, False, False, 0)

        self.artist_pic = Gtk.Image.new_from_pixbuf(Config.ANONYMOUS_PIXBUF)
        event_pic.add(self.artist_pic)

        control_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.pack_start(control_box, True, True, 0)

        toolbar = Gtk.Toolbar()
        toolbar.set_style(Gtk.ToolbarStyle.ICONS)
        toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_MENUBAR)
        toolbar.set_show_arrow(False)
        toolbar.set_icon_size(ICON_SIZE)
        control_box.pack_start(toolbar, False, False, 0)

        prev_button = Gtk.ToolButton()
        prev_button.set_label(_('Previous'))
        prev_button.set_icon_name('media-skip-backward-symbolic')
        prev_button.connect('clicked', self.on_prev_button_clicked)
        toolbar.insert(prev_button, 0)

        self.playback_action = Gtk.ToggleAction('playback-action', _('Play'),
                                                _('Play'), None)
        self.playback_action.set_icon_name('media-playback-start-symbolic')
        self.playback_action.set_label(_('Play'))
        self.playback_action.set_active(False)
        self.playback_action.connect('toggled',
                                     self.on_playback_action_toggled)

        play_button = Gtk.ToolButton()
        play_button.props.related_action = self.playback_action
        toolbar.insert(play_button, 1)

        next_button = Gtk.ToolButton()
        next_button.set_label(_('Next'))
        next_button.set_icon_name('media-skip-forward-symbolic')
        next_button.connect('clicked', self.on_next_button_clicked)
        toolbar.insert(next_button, 2)

        self.shuffle_btn = Gtk.ToggleToolButton()
        self.shuffle_btn.set_label(_('Shuffle'))
        self.shuffle_btn.set_icon_name('media-playlist-shuffle-symbolic')
        self.shuffle_btn.props.margin_left = 10
        toolbar.insert(self.shuffle_btn, 3)

        self.repeat_type = RepeatType.NONE
        self.repeat_btn = Gtk.ToggleToolButton()
        self.repeat_btn.set_label(_('Repeat'))
        self.repeat_btn.set_icon_name('media-playlist-repeat-symbolic')
        self.repeat_btn.connect('clicked', self.on_repeat_button_clicked)
        toolbar.insert(self.repeat_btn, 4)

        self.use_audio_btn = Gtk.RadioToolButton()
        self.use_audio_btn.set_label(_('Play audio'))
        self.use_audio_btn.set_icon_name('audio-x-generic-symbolic')
        self.use_audio_btn.props.margin_left = 10
        self.use_audio_btn.set_active(True)
        self.use_audio_sid = self.use_audio_btn.connect(
            'toggled', self.on_play_type_toggled, PlayType.SONG)
        toolbar.insert(self.use_audio_btn, 5)

        self.use_mtv_btn = Gtk.RadioToolButton()
        self.use_mtv_btn.set_label(_('Play MTV'))
        self.use_mtv_btn.set_tooltip_text(_('Play MTV'))
        self.use_mtv_btn.set_icon_name('video-x-generic-symbolic')
        self.use_mtv_btn.set_sensitive(False)
        self.use_mtv_btn.props.group = self.use_audio_btn
        self.use_mtv_sid = self.use_mtv_btn.connect('toggled',
                                                    self.on_play_type_toggled,
                                                    PlayType.MV)
        toolbar.insert(self.use_mtv_btn, 6)

        self.fullscreen_btn = Gtk.ToggleToolButton()
        self.fullscreen_btn.set_label(_('Fullscreen'))
        self.fullscreen_btn.set_icon_name('view-fullscreen-symbolic')
        self.fullscreen_btn.props.margin_left = 10
        self.fullscreen_btn.connect('toggled',
                                    self.on_fullscreen_button_toggled)
        toolbar.insert(self.fullscreen_btn, 7)
        self.app.window.connect('key-press-event', self.on_window_key_pressed)

        self.favorite_btn = Gtk.ToolButton()
        self.favorite_btn.set_label(_('Favorite'))
        self.favorite_btn.set_icon_name('emblem-favorite-symbolic')
        self.favorite_btn.set_tooltip_text(_('Add to Favorite playlist'))
        self.favorite_btn.props.margin_left = 10
        self.favorite_btn.connect('clicked', self.on_favorite_btn_clicked)
        toolbar.insert(self.favorite_btn, 8)

        osd_lrc_btn = Gtk.ToggleToolButton()
        osd_lrc_btn.props.related_action = self.app.osdlrc.show_window_action
        toolbar.insert(osd_lrc_btn, 9)

        # control menu
        menu_tool_item = Gtk.ToolItem()
        toolbar.insert(menu_tool_item, 10)
        toolbar.child_set_property(menu_tool_item, 'expand', True)
        main_menu = Gtk.Menu()
        pref_item = Gtk.MenuItem(label=_('Preferences'))
        pref_item.connect('activate', self.on_main_menu_pref_activate)
        main_menu.append(pref_item)
        sep_item = Gtk.SeparatorMenuItem()
        main_menu.append(sep_item)

        show_osd_item = Gtk.MenuItem()
        show_osd_item.props.related_action = self.app.osdlrc.show_window_action
        main_menu.append(show_osd_item)
        lock_osd_item = Gtk.MenuItem()
        lock_osd_item.props.related_action = self.app.osdlrc.lock_window_action
        main_menu.append(lock_osd_item)

        sep_item = Gtk.SeparatorMenuItem()
        main_menu.append(sep_item)
        about_item = Gtk.MenuItem(label=_('About'))
        about_item.connect('activate', self.on_main_menu_about_activate)
        main_menu.append(about_item)
        quit_item = Gtk.MenuItem(label=_('Quit'))
        key, mod = Gtk.accelerator_parse('<Ctrl>Q')
        quit_item.add_accelerator('activate', app.accel_group, key, mod,
                                  Gtk.AccelFlags.VISIBLE)
        quit_item.connect('activate', self.on_main_menu_quit_activate)
        main_menu.append(quit_item)
        main_menu.show_all()
        menu_image = Gtk.Image()
        menu_image.set_from_icon_name('view-list-symbolic', ICON_SIZE)
        if Config.GTK_LE_36:
            menu_btn = Gtk.Button()
            menu_btn.connect('clicked', self.on_main_menu_button_clicked,
                             main_menu)
        else:
            menu_btn = Gtk.MenuButton()
            menu_btn.set_popup(main_menu)
            menu_btn.set_always_show_image(True)
        menu_btn.props.halign = Gtk.Align.END
        menu_btn.props.relief = Gtk.ReliefStyle.NONE
        menu_btn.set_image(menu_image)
        menu_tool_item.add(menu_btn)

        self.label = Gtk.Label('<b>{0}</b> <small>{0}</small>'.format(
            _('Unknown')))
        self.label.props.use_markup = True
        self.label.props.xalign = 0
        self.label.props.margin_left = 10
        control_box.pack_start(self.label, False, False, 0)

        scale_box = Gtk.Box(spacing=3)
        scale_box.props.margin_left = 5
        control_box.pack_start(scale_box, True, False, 0)

        self.scale = Gtk.Scale()
        self.adjustment = Gtk.Adjustment(0, 0, 100, 1, 10, 0)
        self.adjustment.connect('changed', self.on_adjustment_changed)
        self.scale.set_adjustment(self.adjustment)
        self.scale.set_show_fill_level(False)
        self.scale.set_restrict_to_fill_level(False)
        self.scale.props.draw_value = False
        self.scale.connect('change-value', self.on_scale_change_value)
        scale_box.pack_start(self.scale, True, True, 0)

        self.time_label = Gtk.Label('0:00/0:00')
        scale_box.pack_start(self.time_label, False, False, 0)

        self.volume = Gtk.VolumeButton()
        self.volume.props.use_symbolic = True
        self.volume.set_value(app.conf['volume']**0.33)
        self.volume_sid = self.volume.connect('value-changed',
                                              self.on_volume_value_changed)
        scale_box.pack_start(self.volume, False, False, 0)

        # init playbin and dbus
        self.playbin = PlayerBin()
        self.playbin.set_volume(self.app.conf['volume']**0.33)
        self.playbin.connect('eos', self.on_playbin_eos)
        self.playbin.connect('error', self.on_playbin_error)
        self.playbin.connect('mute-changed', self.on_playbin_mute_changed)
        self.playbin.connect('volume-changed', self.on_playbin_volume_changed)
        self.dbus = PlayerDBus(self)
        self.notify = PlayerNotify(self)

    def after_init(self):
        self.init_meta()

    def do_destroy(self):
        self.playbin.destroy()
        if self.async_song:
            self.async_song.destroy()
        if self.async_next_song:
            self.async_next_song.destroy()

    def load(self, song):
        self.play_type = PlayType.SONG
        self.curr_song = song
        self.update_favorite_button_status()
        self.stop_player()
        self.use_audio_btn.handler_block(self.use_audio_sid)
        self.use_audio_btn.set_active(True)
        self.use_audio_btn.handler_unblock(self.use_audio_sid)
        self.create_new_async(song)

    def create_new_async(self, *args, **kwds):
        self.scale.set_fill_level(0)
        self.scale.set_show_fill_level(True)
        self.scale.set_restrict_to_fill_level(True)
        self.adjustment.set_lower(0.0)
        self.adjustment.set_upper(100.0)
        if self.async_song:
            self.async_song.destroy()
        self.async_song = Net.AsyncSong(self.app)
        self.async_song.connect('chunk-received', self.on_chunk_received)
        self.async_song.connect('can-play', self.on_song_can_play)
        self.async_song.connect('downloaded', self.on_song_downloaded)
        self.async_song.connect('disk-error', self.on_song_disk_error)
        self.async_song.connect('network-error', self.on_song_network_error)
        self.async_song.get_song(*args, **kwds)

    def on_chunk_received(self, widget, percent):
        def _update_fill_level():
            self.scale.set_fill_level(percent * self.adjustment.get_upper())

        GLib.idle_add(_update_fill_level)

    def on_song_disk_error(self, widget, song_path):
        '''Disk error: occurs when disk is not available.'''
        GLib.idle_add(Widgets.filesystem_error, self.app.window, song_path)
        self.stop_player_cb()

    def on_song_network_error(self, widget, song_link):
        '''Failed to get source link, or failed to download song'''
        self.stop_player_cb()
        if self.play_type == PlayType.MV:
            msg = _('Failed to download MV')
        elif self.play_type in (PlayType.SONG, PlayType.RADIO):
            msg = _('Failed to download song')
        GLib.idle_add(Widgets.network_error, self.app.window, msg)
        self.stop_player_cb()

    def on_song_can_play(self, widget, song_path):
        def _on_song_can_play():
            uri = 'file://' + song_path
            self.meta_url = uri

            if self.play_type in (PlayType.SONG, PlayType.RADIO):
                self.app.lrc.show_music()
                self.playbin.load_audio(uri)
                self.get_lrc()
                self.get_recommend_lists()
            elif self.play_type == PlayType.MV:
                self.use_mtv_btn.set_sensitive(True)
                self.app.lrc.show_mv()
                self.playbin.load_video(uri, self.app.lrc.xid)

            self.playback_action.set_active(True)
            self.playbin.set_volume(self.app.conf['volume'])
            self.init_meta()
            GLib.timeout_add(1500, self.init_adjustment)

            self.update_player_info()
            if self.play_type == PlayType.SONG:
                if self.curr_song.get('formats', ''):
                    self.use_mtv_btn.set_sensitive(
                        'MP4' in self.curr_song['formats'])
                else:
                    # for v3.4.7, remove this in v3.6.1
                    self.get_mv_link()

        GLib.idle_add(_on_song_can_play)

    def on_song_downloaded(self, widget, song_path):
        def _on_song_download():
            self.async_song.destroy()
            self.async_song = None
            self.scale.set_fill_level(self.adjustment.get_upper())
            self.scale.set_show_fill_level(False)
            self.scale.set_restrict_to_fill_level(False)
            self.init_adjustment()
            if self.play_type in (PlayType.SONG, PlayType.MV):
                self.app.playlist.on_song_downloaded(play=True)
                self.next_song = self.app.playlist.get_next_song(
                    self.repeat_btn.get_active(),
                    self.shuffle_btn.get_active())
            elif self.play_type == PlayType.RADIO:
                self.next_song = self.curr_radio_item.get_next_song()
            if self.next_song:
                self.cache_next_song()
            # update metadata in dbus
            self.dbus.update_meta()
            self.dbus.enable_seek()

        GLib.idle_add(_on_song_download)

    def cache_next_song(self):
        if self.play_type == PlayType.MV:
            use_mv = True
        elif self.play_type in (PlayType.SONG, PlayType.RADIO):
            use_mv = False
        if self.async_next_song:
            self.async_next_song.destroy()
        self.async_next_song = Net.AsyncSong(self.app)
        self.async_next_song.get_song(self.next_song, use_mv=use_mv)

    def init_adjustment(self):
        self.adjustment.set_value(0.0)
        self.adjustment.set_lower(0.0)
        # when song is not totally downloaded but can play, query_duration
        # might give incorrect/inaccurate result.
        status, duration = self.playbin.get_duration()
        if status and duration > 0:
            self.adjustment.set_upper(duration)
            return False
        return True

    def sync_adjustment(self):
        status, offset = self.playbin.get_position()
        if not status:
            return True

        self.dbus.update_pos(offset // 1000)

        status, duration = self.playbin.get_duration()
        self.adjustment.set_value(offset)
        self.adjustment.set_upper(duration)
        self.sync_label_by_adjustment()
        if offset >= duration - 800000000:
            self.load_next()
            return False
        if self.play_type == PlayType.MV:
            return True
        self.app.lrc.sync_lrc(offset)
        if self.recommend_imgs:
            # change lyrics background image every 20 seconds
            div, mod = divmod(int(offset / 10**9), 20)
            if mod == 0:
                div2, mod2 = divmod(div, len(self.recommend_imgs))
                self.update_lrc_background(self.recommend_imgs[mod2])
        return True

    def sync_label_by_adjustment(self):
        curr = delta(self.adjustment.get_value())
        total = delta(self.adjustment.get_upper())
        self.time_label.set_label('{0}/{1}'.format(curr, total))

    # Control panel
    def on_pic_pressed(self, eventbox, event):
        if event.type == GDK_2BUTTON_PRESS and self.play_type == PlayType.SONG:
            self.app.playlist.locate_curr_song()

    def on_prev_button_clicked(self, button):
        self.load_prev()

    def on_playback_action_toggled(self, action):
        if self.play_type == PlayType.NONE:
            return
        if action.get_active():
            self.start_player()
        else:
            self.pause_player()

    def on_next_button_clicked(self, button):
        self.load_next()

    def on_repeat_button_clicked(self, button):
        if self.repeat_type == RepeatType.NONE:
            self.repeat_type = RepeatType.ALL
            button.set_active(True)
            button.set_icon_name('media-playlist-repeat-symbolic')
        elif self.repeat_type == RepeatType.ALL:
            self.repeat_type = RepeatType.ONE
            button.set_active(True)
            button.set_icon_name('media-playlist-repeat-song-symbolic')
        elif self.repeat_type == RepeatType.ONE:
            self.repeat_type = RepeatType.NONE
            button.set_active(False)
            button.set_icon_name('media-playlist-repeat-symbolic')

    def on_scale_change_value(self, scale, scroll_type, value):
        self.app.lrc.reset_tags()
        self.seek_cb(value)

    def on_volume_value_changed(self, volume_button, volume):
        self.playbin.set_volume(volume**3)
        self.app.conf['volume'] = volume**3
        if self.playbin.get_mute():
            self.playbin.set_mute(False)

    def update_player_info(self):
        def _update_pic(info, error=None):
            if not info or error:
                logger.error('update_player_info(): %s, %s' % (info, error))
                return
            self.artist_pic.set_tooltip_text(
                Widgets.short_tooltip(info['info'], length=500))
            if info['pic']:
                self.meta_artUrl = info['pic']
                pix = GdkPixbuf.Pixbuf.new_from_file_at_size(
                    info['pic'], 100, 100)
                self.artist_pic.set_from_pixbuf(pix)
            else:
                self.meta_artUrl = Config.ANONYMOUS_IMG
            self.notify.refresh()
            self.dbus.update_meta()

        song = self.curr_song
        name = Widgets.short_tooltip(song['name'], 45)
        if song['artist']:
            artist = Widgets.short_tooltip(song['artist'], 20)
        else:
            artist = _('Unknown')
        if song['album']:
            album = Widgets.short_tooltip(song['album'], 30)
        else:
            album = _('Unknown')
        label = '<b>{0}</b> <small>{1}</small> <span size="x-small">{2}</span>'.format(
            name, artist, album)
        self.label.set_label(label)
        self.app.window.set_title(name)
        self.artist_pic.set_from_pixbuf(Config.ANONYMOUS_PIXBUF)
        Net.async_call(Net.get_artist_info,
                       song['artistid'],
                       song['artist'],
                       callback=_update_pic)

    def get_lrc(self):
        def _update_lrc(lrc_text, error=None):
            if error:
                logger.error('get_lrc(): %s', error)
            self.app.lrc.set_lrc(lrc_text)

        Net.async_call(Net.get_lrc, self.curr_song, callback=_update_lrc)

    def get_recommend_lists(self):
        self.recommend_imgs = None

        def _on_list_received(imgs, error=None):
            if error or not imgs or len(imgs) < 10:
                logger.debug('get_recommend_lists(): %s, %s' % (imgs, error))
                self.recommend_imgs = None
            else:
                self.recommend_imgs = imgs.strip().splitlines()

        Net.async_call(Net.get_recommend_lists,
                       self.curr_song['artist'],
                       callback=_on_list_received)

    def update_lrc_background(self, url):
        def _update_background(filepath, error=None):
            if error or not filepath:
                logger.error('update_lrc_background(): %s, %s' %
                             (filepath, error))
                return
            self.app.lrc.update_background(filepath)

        Net.async_call(Net.get_recommend_image,
                       url,
                       callback=_update_background)

    # Radio part
    def load_radio(self, song, radio_item):
        '''Load Radio song.

        song from radio, only contains name, artist, rid, artistid
        Remember to update its information.
        '''
        self.play_type = PlayType.RADIO
        self.stop_player()
        self.curr_radio_item = radio_item
        self.curr_song = song
        self.update_favorite_button_status()
        self.create_new_async(song)

    # MV part
    def on_play_type_toggled(self, toggle_button, play_type):
        if (not toggle_button.get_active() or not self.curr_song
                or self.play_type == PlayType.NONE):
            return
        elif play_type == PlayType.SONG or play_type == PlayType.RADIO:
            self.app.lrc.show_music()
            self.load(self.curr_song)
        elif play_type == PlayType.MV:
            self.app.lrc.show_mv()
            self.load_mv(self.curr_song)
            self.app.popup_page(self.app.lrc.app_page)

    def load_mv(self, song):
        self.play_type = PlayType.MV
        self.curr_song = song
        self.update_favorite_button_status()
        self.stop_player()
        self.use_mtv_btn.handler_block(self.use_mtv_sid)
        self.use_mtv_btn.set_active(True)
        self.use_mtv_btn.handler_unblock(self.use_mtv_sid)
        self.create_new_async(song, use_mv=True)

    def get_mv_link(self):
        def _update_mv_link(mv_args, error=None):
            if error or not mv_args:
                logger.error('get_mv_link(): %s, %s' % (mv_args, error))
                self.use_mtv_btn.set_sensitive(False)
            else:
                cached, mv_link, mv_path = mv_args
                if cached or mv_link:
                    self.use_mtv_btn.set_sensitive(True)
                else:
                    self.use_mtv_btn.set_sensitive(False)

        Net.async_call(Net.get_song_link,
                       self.curr_song,
                       self.app.conf,
                       True,
                       callback=_update_mv_link)

    # Fullscreen
    def toggle_fullscreen(self):
        '''Switch between fullscreen and unfullscreen mode.'''
        def hide_control_panel_and_label(timestamp):
            if self.fullscreen_sid > 0:
                gdk_window.set_cursor(self.blank_cursor)
            if (timestamp == self.fullscreen_timestamp
                    and self.fullscreen_sid > 0):
                self.hide()
                GLib.timeout_add(100, self.playbin.expose)
                self.app.notebook.set_show_tabs(False)

        def on_window_motion_notified(window, event):
            window.get_window().set_cursor(self.default_cursor)
            if self.fullscreen_sid == 0:
                return
            lower = 70
            upper = window.get_size()[1] - 40
            if lower < event.y < upper:
                GLib.timeout_add(
                    2000,
                    lambda *args: gdk_window.set_cursor(self.blank_cursor))
            else:
                self.show_all()
                self.app.notebook.set_show_tabs(True)

            # Delay 2 seconds to hide them
            self.fullscreen_timestamp = time.time()
            GLib.timeout_add(100, self.playbin.expose)
            GLib.timeout_add(2000, hide_control_panel_and_label,
                             self.fullscreen_timestamp)

        window = self.app.window
        gdk_window = window.get_window()
        button = self.fullscreen_btn
        if not self.default_cursor:
            self.default_cursor = gdk_window.get_cursor()
        if not self.default_cursor:
            self.default_cursor = Gdk.Cursor.new(Gdk.CursorType.ARROW)

        if not button.get_active():
            # unfullscreen
            window.unfullscreen()
            button.set_icon_name('view-fullscreen-symbolic')
            window.realize()
            self.playbin.expose()
            self.show()
            self.app.notebook.set_show_tabs(True)
            self.fullscreen_sid = 0
            gdk_window.set_cursor(self.default_cursor)
        else:
            # fullscreen
            self.app.popup_page(self.app.lrc.app_page)
            window.realize()
            window.fullscreen()
            button.set_icon_name('view-restore-symbolic')
            self.playbin.expose_fullscreen()

            self.hide()
            self.app.notebook.set_show_tabs(False)
            gdk_window.set_cursor(self.blank_cursor)
            self.fullscreen_sid = window.connect('motion-notify-event',
                                                 on_window_motion_notified)

    def on_window_key_pressed(self, widget, event):
        # press Esc to exit fullscreen mode
        if event.keyval == Gdk.KEY_Escape and self.fullscreen_btn.get_active():
            self.fullscreen_btn.set_active(False)
        # press F11 to toggle fullscreen mode
        elif event.keyval == Gdk.KEY_F11:
            self.fullscreen_btn.set_active(
                not self.fullscreen_btn.get_active())

    def on_fullscreen_button_toggled(self, button):
        self.toggle_fullscreen()

    def on_favorite_btn_clicked(self, button):
        if not self.curr_song:
            return
        self.toggle_favorite_status()

    def update_favorite_button_status(self):
        if not self.curr_song:
            return
        if self.get_favorite_status():
            self.favorite_btn.set_icon_name('favorite')
        else:
            self.favorite_btn.set_icon_name('emblem-favorite-symbolic')

    def get_favorite_status(self):
        return self.app.playlist.check_song_in_playlist(
            self.curr_song, 'Favorite')

    def toggle_favorite_status(self):
        if not self.curr_song:
            return
        if self.app.playlist.check_song_in_playlist(self.curr_song,
                                                    'Favorite'):
            self.app.playlist.remove_song_from_playlist(
                self.curr_song, 'Favorite')
            self.favorite_btn.set_icon_name('emblem-favorite-symbolic')
        else:
            self.app.playlist.add_song_to_playlist(self.curr_song, 'Favorite')
            self.favorite_btn.set_icon_name('favorite')

    # menu button
    def on_main_menu_button_clicked(self, button, main_menu):
        main_menu.popup(None, None, None, None, 1,
                        Gtk.get_current_event_time())

    def on_main_menu_pref_activate(self, menu_item):
        '''重新载入设置'''
        dialog = Preferences(self.app)
        dialog.run()
        dialog.destroy()
        self.app.load_styles()
        self.app.init_status_icon()
        self.app.lrc.update_highlighted_tag()
        self.app.shortcut.rebind_keys()

    def on_main_menu_about_activate(self, menu_item):
        dialog = Gtk.AboutDialog()
        dialog.set_modal(True)
        dialog.set_transient_for(self.app.window)
        dialog.set_program_name(Config.APPNAME)
        dialog.set_logo_icon_name(Config.NAME)
        dialog.set_version(Config.VERSION)
        dialog.set_comments(Config.DESCRIPTION)
        dialog.set_copyright(Config.COPYRIGHT)
        dialog.set_website(Config.HOMEPAGE)
        dialog.set_license_type(Gtk.License.GPL_3_0)
        dialog.set_authors(Config.AUTHORS)
        dialog.run()
        dialog.destroy()

    def on_main_menu_quit_activate(self, menu_item):
        self.app.quit()

    # playbin signal handlers
    def on_playbin_eos(self, playbin, eos_msg):
        self.load_next()

    def on_playbin_error(self, playbin, error_msg):
        logger.warn('Player.on_playbin_error(): %s.' % error_msg)
        self.app.toast('Player Error: %s' % error_msg)
        self.stop_player()

    def on_playbin_mute_changed(self, playbin, mute):
        self.update_gtk_volume_value_cb()

    def on_playbin_volume_changed(self, playbin, volume):
        self.update_gtk_volume_value_cb()

    def update_gtk_volume_value(self):
        mute = self.playbin.get_mute()
        volume = self.playbin.get_volume()
        if mute:
            self.volume.handler_block(self.volume_sid)
            self.volume.set_value(0.0)
            self.volume.handler_unblock(self.volume_sid)
        else:
            self.volume.handler_block(self.volume_sid)
            self.volume.set_value(volume**0.33)
            self.volume.handler_unblock(self.volume_sid)
        self.app.conf['volume'] = volume

    def update_gtk_volume_value_cb(self):
        GLib.idle_add(self.update_gtk_volume_value)

    def start_player(self, load=False):
        if self.play_type == PlayType.NONE:
            return

        self.dbus.set_Playing()

        self.playback_action.set_icon_name('media-playback-pause-symbolic')
        self.playback_action.set_label(_('Pause'))
        self.playbin.play()
        self.adj_timeout = GLib.timeout_add(250, self.sync_adjustment)
        if load:
            self.playbin.set_volume(self.app.conf['volume'])
            self.init_meta()
            GLib.timeout_add(1500, self.init_adjustment)
        self.notify.refresh()

    def start_player_cb(self):
        GLib.idle_add(lambda: self.playback_action.set_active(True))

    def pause_player(self):
        if self.play_type == PlayType.NONE:
            return
        self.dbus.set_Pause()
        self.playback_action.set_icon_name('media-playback-start-symbolic')
        self.playback_action.set_label(_('Play'))
        self.playbin.pause()
        if self.adj_timeout > 0:
            GLib.source_remove(self.adj_timeout)
            self.adj_timeout = 0
        self.notify.refresh()

    def pause_player_cb(self):
        GLib.idle_add(lambda: self.playback_action.set_active(False))

    def play_pause(self):
        if self.play_type == PlayType.NONE:
            return
        self.playback_action.set_active(not self.playback_action.get_active())

    def play_pause_cb(self):
        GLib.idle_add(self.play_pause)

    def stop_player(self):
        if self.play_type == PlayType.NONE:
            return
        self.playback_action.set_active(False)
        self.playbin.stop()
        self.scale.set_value(0)
        if self.play_type != PlayType.MV:
            self.use_audio_btn.handler_block(self.use_audio_sid)
            self.use_audio_btn.set_active(True)
            self.use_audio_btn.handler_unblock(self.use_audio_sid)
            self.use_mtv_btn.set_sensitive(False)
        self.time_label.set_label('0:00/0:00')
        if self.adj_timeout > 0:
            GLib.source_remove(self.adj_timeout)
            self.adj_timeout = 0

    def stop_player_cb(self):
        GLib.idle_add(self.stop_player)

    def load_prev(self):
        if self.play_type == PlayType.NONE or not self.can_go_previous():
            return
        self.stop_player()
        repeat = self.repeat_btn.get_active()
        if self.play_type == PlayType.SONG:
            self.app.playlist.play_prev_song(repeat=repeat, use_mv=False)
        elif self.play_type == PlayType.MV:
            self.app.playlist.play_prev_song(repeat=repeat, use_mv=True)

    def load_prev_cb(self):
        GLib.idle_add(self.load_prev)

    def load_next(self):
        if self.play_type == PlayType.NONE:
            return
        self.stop_player()
        if self.repeat_type == RepeatType.ONE:
            if self.play_type == PlayType.MV:
                self.load_mv(self.curr_song)
            else:
                self.load(self.curr_song)
            return

        repeat = self.repeat_btn.get_active()
        shuffle = self.shuffle_btn.get_active()
        if self.play_type == PlayType.RADIO:
            self.curr_radio_item.play_next_song()
        elif self.play_type == PlayType.SONG:
            self.app.playlist.play_next_song(repeat, shuffle, use_mv=False)
        elif self.play_type == PlayType.MV:
            self.app.playlist.play_next_song(repeat, shuffle, use_mv=True)

    def load_next_cb(self):
        GLib.idle_add(self.load_next)

    def get_volume(self):
        return self.volume.get_value()

    def set_volume(self, volume):
        self.volume.set_value(volume)

    def set_volume_cb(self, volume):
        GLib.idle_add(self.set_volume, volume)

    def get_volume(self):
        return self.playbin.get_volume()

    def toggle_mute(self):
        mute = self.playbin.get_mute()
        self.playbin.set_mute(not mute)
        if mute:
            self.volume.handler_block(self.volume_sid)
            self.volume.set_value(self.app.conf['volume'])
            self.volume.handler_unblock(self.volume_sid)
        else:
            self.volume.handler_block(self.volume_sid)
            self.volume.set_value(0.0)
            self.volume.handler_unblock(self.volume_sid)

    def toggle_mute_cb(self):
        GLib.idle_add(self.toggle_mute)

    def seek(self, offset):
        if self.play_type == PlayType.NONE:
            return
        self.playback_action.set_active(False)
        self.playbin.seek(offset)
        GLib.timeout_add(300, self.start_player_cb)
        self.sync_label_by_adjustment()

    def seek_cb(self, offset):
        GLib.idle_add(self.seek, offset)

    def can_go_previous(self):
        if self.play_type in (PlayType.MV, PlayType.SONG):
            return True
        return False

    # dbus parts
    def init_meta(self):
        self.adjustment_upper = 0
        self.dbus.disable_seek()
        self.meta_url = ''
        self.meta_artUrl = ''

    def on_adjustment_changed(self, adj):
        self.dbus.update_meta()
        self.adjustment_upper = adj.get_upper()
Beispiel #4
0
class Player(Gtk.Box):
    def __init__(self, app):
        super().__init__()
        self.app = app

        self.play_type = PlayType.NONE
        self.adj_timeout = 0
        self.recommend_imgs = None
        self.curr_song = None
        self.fullscreen_sid = 0
        self.fullscreen_timestamp = 0
        self.default_cursor = None
        self.blank_cursor = Gdk.Cursor.new(Gdk.CursorType.BLANK_CURSOR)

        # use this to keep Net.AsyncSong object
        self.async_song = None
        self.async_next_song = None

        event_pic = Gtk.EventBox()
        event_pic.connect('button-press-event', self.on_pic_pressed)
        self.pack_start(event_pic, False, False, 0)

        self.artist_pic = Gtk.Image.new_from_pixbuf(Config.ANONYMOUS_PIXBUF)
        event_pic.add(self.artist_pic)

        control_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.pack_start(control_box, True, True, 0)

        toolbar = Gtk.Toolbar()
        toolbar.set_style(Gtk.ToolbarStyle.ICONS)
        toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_MENUBAR)
        toolbar.set_show_arrow(False)
        toolbar.set_icon_size(ICON_SIZE)
        control_box.pack_start(toolbar, False, False, 0)

        prev_button = Gtk.ToolButton()
        prev_button.set_label(_('Previous'))
        prev_button.set_icon_name('media-skip-backward-symbolic')
        prev_button.connect('clicked', self.on_prev_button_clicked)
        toolbar.insert(prev_button, 0)

        self.playback_action = Gtk.ToggleAction('playback-action', _('Play'),
                                                _('Play'), None)
        self.playback_action.set_icon_name('media-playback-start-symbolic')
        self.playback_action.set_label(_('Play'))
        self.playback_action.set_active(False)
        self.playback_action.connect('toggled', self.on_playback_action_toggled)

        play_button = Gtk.ToolButton()
        play_button.props.related_action = self.playback_action
        toolbar.insert(play_button, 1)

        next_button = Gtk.ToolButton()
        next_button.set_label(_('Next'))
        next_button.set_icon_name('media-skip-forward-symbolic')
        next_button.connect('clicked', self.on_next_button_clicked)
        toolbar.insert(next_button, 2)

        self.shuffle_btn = Gtk.ToggleToolButton()
        self.shuffle_btn.set_label(_('Shuffle'))
        self.shuffle_btn.set_icon_name('media-playlist-shuffle-symbolic')
        self.shuffle_btn.props.margin_left = 10
        toolbar.insert(self.shuffle_btn, 3)

        self.repeat_type = RepeatType.NONE
        self.repeat_btn = Gtk.ToggleToolButton()
        self.repeat_btn.set_label(_('Repeat'))
        self.repeat_btn.set_icon_name('media-playlist-repeat-symbolic')
        self.repeat_btn.connect('clicked', self.on_repeat_button_clicked)
        toolbar.insert(self.repeat_btn, 4)

        self.use_audio_btn = Gtk.RadioToolButton()
        self.use_audio_btn.set_label(_('Play audio'))
        self.use_audio_btn.set_icon_name('audio-x-generic-symbolic')
        self.use_audio_btn.props.margin_left = 10
        self.use_audio_btn.set_active(True)
        self.use_audio_sid = self.use_audio_btn.connect('toggled',
                self.on_play_type_toggled, PlayType.SONG)
        toolbar.insert(self.use_audio_btn, 5)

        self.use_mtv_btn = Gtk.RadioToolButton()
        self.use_mtv_btn.set_label(_('Play MTV'))
        self.use_mtv_btn.set_tooltip_text(_('Play MTV'))
        self.use_mtv_btn.set_icon_name('video-x-generic-symbolic')
        self.use_mtv_btn.set_sensitive(False)
        self.use_mtv_btn.props.group = self.use_audio_btn
        self.use_mtv_sid = self.use_mtv_btn.connect('toggled',
                self.on_play_type_toggled, PlayType.MV)
        toolbar.insert(self.use_mtv_btn, 6)

        self.fullscreen_btn = Gtk.ToggleToolButton()
        self.fullscreen_btn.set_label(_('Fullscreen'))
        self.fullscreen_btn.set_icon_name('view-fullscreen-symbolic')
        self.fullscreen_btn.props.margin_left = 10
        self.fullscreen_btn.connect('toggled',
                self.on_fullscreen_button_toggled)
        toolbar.insert(self.fullscreen_btn, 7)
        self.app.window.connect('key-press-event', self.on_window_key_pressed)

        self.favorite_btn = Gtk.ToolButton()
        self.favorite_btn.set_label(_('Favorite'))
        self.favorite_btn.set_icon_name('emblem-favorite-symbolic')
        self.favorite_btn.set_tooltip_text(_('Add to Favorite playlist'))
        self.favorite_btn.props.margin_left = 10
        self.favorite_btn.connect('clicked', self.on_favorite_btn_clicked)
        toolbar.insert(self.favorite_btn, 8)

        osd_lrc_btn = Gtk.ToggleToolButton()
        osd_lrc_btn.props.related_action = self.app.osdlrc.show_window_action
        toolbar.insert(osd_lrc_btn, 9)

        # control menu
        menu_tool_item = Gtk.ToolItem()
        toolbar.insert(menu_tool_item, 10)
        toolbar.child_set_property(menu_tool_item, 'expand', True)
        main_menu = Gtk.Menu()
        pref_item = Gtk.MenuItem(label=_('Preferences'))
        pref_item.connect('activate', self.on_main_menu_pref_activate)
        main_menu.append(pref_item)
        sep_item = Gtk.SeparatorMenuItem()
        main_menu.append(sep_item)

        show_osd_item = Gtk.MenuItem()
        show_osd_item.props.related_action = self.app.osdlrc.show_window_action
        main_menu.append(show_osd_item)
        lock_osd_item = Gtk.MenuItem()
        lock_osd_item.props.related_action = self.app.osdlrc.lock_window_action
        main_menu.append(lock_osd_item)

        sep_item = Gtk.SeparatorMenuItem()
        main_menu.append(sep_item)
        about_item = Gtk.MenuItem(label=_('About'))
        about_item.connect('activate', self.on_main_menu_about_activate)
        main_menu.append(about_item)
        quit_item = Gtk.MenuItem(label=_('Quit'))
        key, mod = Gtk.accelerator_parse('<Ctrl>Q')
        quit_item.add_accelerator('activate', app.accel_group, key, mod,
                                  Gtk.AccelFlags.VISIBLE)
        quit_item.connect('activate', self.on_main_menu_quit_activate)
        main_menu.append(quit_item)
        main_menu.show_all()
        menu_image = Gtk.Image()
        menu_image.set_from_icon_name('view-list-symbolic', ICON_SIZE)
        if Config.GTK_LE_36:
            menu_btn = Gtk.Button()
            menu_btn.connect('clicked', self.on_main_menu_button_clicked,
                             main_menu)
        else:
            menu_btn = Gtk.MenuButton()
            menu_btn.set_popup(main_menu)
            menu_btn.set_always_show_image(True)
        menu_btn.props.halign = Gtk.Align.END
        menu_btn.props.relief = Gtk.ReliefStyle.NONE
        menu_btn.set_image(menu_image)
        menu_tool_item.add(menu_btn)

        self.label = Gtk.Label(
                '<b>{0}</b> <small>{0}</small>'.format(_('Unknown')))
        self.label.props.use_markup = True
        self.label.props.xalign = 0
        self.label.props.margin_left = 10
        control_box.pack_start(self.label, False, False, 0)

        scale_box = Gtk.Box(spacing=3)
        scale_box.props.margin_left = 5
        control_box.pack_start(scale_box, True, False, 0)

        self.scale = Gtk.Scale()
        self.adjustment = Gtk.Adjustment(0, 0, 100, 1, 10, 0)
        self.adjustment.connect('changed', self.on_adjustment_changed)
        self.scale.set_adjustment(self.adjustment)
        self.scale.set_show_fill_level(False)
        self.scale.set_restrict_to_fill_level(False)
        self.scale.props.draw_value = False
        self.scale.connect('change-value', self.on_scale_change_value)
        scale_box.pack_start(self.scale, True, True, 0)

        self.time_label = Gtk.Label('0:00/0:00')
        scale_box.pack_start(self.time_label, False, False, 0)

        self.volume = Gtk.VolumeButton()
        self.volume.props.use_symbolic = True
        self.volume.set_value(app.conf['volume'] ** 0.33)
        self.volume_sid = self.volume.connect('value-changed',
                                              self.on_volume_value_changed)
        scale_box.pack_start(self.volume, False, False, 0)

        # init playbin and dbus
        self.playbin = PlayerBin()
        self.playbin.set_volume(self.app.conf['volume'] ** 0.33)
        self.playbin.connect('eos', self.on_playbin_eos)
        self.playbin.connect('error', self.on_playbin_error)
        self.playbin.connect('mute-changed', self.on_playbin_mute_changed)
        self.playbin.connect('volume-changed', self.on_playbin_volume_changed)
        self.dbus = PlayerDBus(self)
        self.notify = PlayerNotify(self)

    def after_init(self):
        self.init_meta()

    def do_destroy(self):
        self.playbin.destroy()
        if self.async_song:
            self.async_song.destroy()
        if self.async_next_song:
            self.async_next_song.destroy()

    def load(self, song):
        self.play_type = PlayType.SONG
        self.curr_song = song
        self.update_favorite_button_status()
        self.stop_player()
        self.use_audio_btn.handler_block(self.use_audio_sid)
        self.use_audio_btn.set_active(True)
        self.use_audio_btn.handler_unblock(self.use_audio_sid)
        self.create_new_async(song)

    def create_new_async(self, *args, **kwds):
        self.scale.set_fill_level(0)
        self.scale.set_show_fill_level(True)
        self.scale.set_restrict_to_fill_level(True)
        self.adjustment.set_lower(0.0)
        self.adjustment.set_upper(100.0)
        if self.async_song:
            self.async_song.destroy()
        self.async_song = Net.AsyncSong(self.app)
        self.async_song.connect('chunk-received', self.on_chunk_received)
        self.async_song.connect('can-play', self.on_song_can_play)
        self.async_song.connect('downloaded', self.on_song_downloaded)
        self.async_song.connect('disk-error', self.on_song_disk_error)
        self.async_song.connect('network-error', self.on_song_network_error)
        self.async_song.get_song(*args, **kwds)

    def on_chunk_received(self, widget, percent):
        def _update_fill_level():
            self.scale.set_fill_level(percent * self.adjustment.get_upper())
        GLib.idle_add(_update_fill_level)

    def on_song_disk_error(self, widget, song_path):
        '''Disk error: occurs when disk is not available.'''
        GLib.idle_add(Widgets.filesystem_error, self.app.window, song_path)
        self.stop_player_cb()

    def on_song_network_error(self, widget, song_link):
        '''Failed to get source link, or failed to download song'''
        self.stop_player_cb()
        if self.play_type == PlayType.MV:
            msg = _('Failed to download MV')
        elif self.play_type in (PlayType.SONG, PlayType.RADIO):
            msg = _('Failed to download song')
        GLib.idle_add(Widgets.network_error, self.app.window, msg)
        self.stop_player_cb()

    def on_song_can_play(self, widget, song_path):
        def _on_song_can_play():
            uri = 'file://' + song_path
            self.meta_url = uri

            if self.play_type in (PlayType.SONG, PlayType.RADIO):
                self.app.lrc.show_music()
                self.playbin.load_audio(uri)
                self.get_lrc()
                self.get_recommend_lists()
            elif self.play_type == PlayType.MV:
                self.use_mtv_btn.set_sensitive(True)
                self.app.lrc.show_mv()
                self.playbin.load_video(uri, self.app.lrc.xid)

            self.playback_action.set_active(True)
            self.playbin.set_volume(self.app.conf['volume'])
            self.init_meta()
            GLib.timeout_add(1500, self.init_adjustment)

            self.update_player_info()
            if self.play_type == PlayType.SONG:
                if self.curr_song.get('formats', ''):
                    self.use_mtv_btn.set_sensitive(
                            'MP4' in self.curr_song['formats'])
                else:
                    # for v3.4.7, remove this in v3.6.1
                    self.get_mv_link()
        GLib.idle_add(_on_song_can_play)

    def on_song_downloaded(self, widget, song_path):
        def _on_song_download():
            self.async_song.destroy()
            self.async_song = None
            self.scale.set_fill_level(self.adjustment.get_upper())
            self.scale.set_show_fill_level(False)
            self.scale.set_restrict_to_fill_level(False)
            self.init_adjustment()
            if self.play_type in (PlayType.SONG, PlayType.MV):
                self.app.playlist.on_song_downloaded(play=True)
                self.next_song = self.app.playlist.get_next_song(
                        self.repeat_btn.get_active(),
                        self.shuffle_btn.get_active())
            elif self.play_type == PlayType.RADIO:
                self.next_song = self.curr_radio_item.get_next_song()
            if self.next_song:
                self.cache_next_song()
            # update metadata in dbus
            self.dbus.update_meta()
            self.dbus.enable_seek()

        GLib.idle_add(_on_song_download)

    def cache_next_song(self):
        if self.play_type == PlayType.MV:
            use_mv = True
        elif self.play_type in (PlayType.SONG, PlayType.RADIO):
            use_mv = False
        if self.async_next_song:
            self.async_next_song.destroy()
        self.async_next_song = Net.AsyncSong(self.app)
        self.async_next_song.get_song(self.next_song, use_mv=use_mv)

    def init_adjustment(self):
        self.adjustment.set_value(0.0)
        self.adjustment.set_lower(0.0)
        # when song is not totally downloaded but can play, query_duration
        # might give incorrect/inaccurate result.
        status, duration = self.playbin.get_duration()
        if status and duration > 0:
            self.adjustment.set_upper(duration)
            return False
        return True

    def sync_adjustment(self):
        status, offset = self.playbin.get_position()
        if not status:
            return True

        self.dbus.update_pos(offset // 1000)

        status, duration = self.playbin.get_duration()
        self.adjustment.set_value(offset)
        self.adjustment.set_upper(duration)
        self.sync_label_by_adjustment()
        if offset >= duration - 800000000:
            self.load_next()
            return False
        if self.play_type == PlayType.MV:
            return True
        self.app.lrc.sync_lrc(offset)
        if self.recommend_imgs:
            # change lyrics background image every 20 seconds
            div, mod = divmod(int(offset / 10**9), 20)
            if mod == 0:
                div2, mod2 = divmod(div, len(self.recommend_imgs))
                self.update_lrc_background(self.recommend_imgs[mod2])
        return True

    def sync_label_by_adjustment(self):
        curr = delta(self.adjustment.get_value())
        total = delta(self.adjustment.get_upper())
        self.time_label.set_label('{0}/{1}'.format(curr, total))

    # Control panel
    def on_pic_pressed(self, eventbox, event):
        if event.type == GDK_2BUTTON_PRESS and self.play_type == PlayType.SONG:
            self.app.playlist.locate_curr_song()

    def on_prev_button_clicked(self, button):
        self.load_prev()

    def on_playback_action_toggled(self, action):
        if self.play_type == PlayType.NONE:
            return
        if action.get_active():
            self.start_player()
        else:
            self.pause_player()

    def on_next_button_clicked(self, button):
        self.load_next()

    def on_repeat_button_clicked(self, button):
        if self.repeat_type == RepeatType.NONE:
            self.repeat_type = RepeatType.ALL
            button.set_active(True)
            button.set_icon_name('media-playlist-repeat-symbolic')
        elif self.repeat_type == RepeatType.ALL:
            self.repeat_type = RepeatType.ONE
            button.set_active(True)
            button.set_icon_name('media-playlist-repeat-song-symbolic')
        elif self.repeat_type == RepeatType.ONE:
            self.repeat_type = RepeatType.NONE
            button.set_active(False)
            button.set_icon_name('media-playlist-repeat-symbolic')

    def on_scale_change_value(self, scale, scroll_type, value):
        self.app.lrc.reset_tags()
        self.seek_cb(value)

    def on_volume_value_changed(self, volume_button, volume):
        self.playbin.set_volume(volume ** 3)
        self.app.conf['volume'] = volume ** 3
        if self.playbin.get_mute():
            self.playbin.set_mute(False)

    def update_player_info(self):
        def _update_pic(info, error=None):
            if not info or error:
                logger.error('update_player_info(): %s, %s' % (info, error))
                return
            self.artist_pic.set_tooltip_text(
                    Widgets.short_tooltip(info['info'], length=500))
            if info['pic']:
                self.meta_artUrl = info['pic']
                pix = GdkPixbuf.Pixbuf.new_from_file_at_size(info['pic'],
                                                             100, 100)
                self.artist_pic.set_from_pixbuf(pix)
            else:
                self.meta_artUrl = Config.ANONYMOUS_IMG
            self.notify.refresh()
            self.dbus.update_meta()
            
        song = self.curr_song
        name = Widgets.short_tooltip(song['name'], 45)
        if song['artist']:
            artist = Widgets.short_tooltip(song['artist'], 20)
        else:
            artist = _('Unknown')
        if song['album']:
            album = Widgets.short_tooltip(song['album'], 30)
        else:
            album = _('Unknown')
        label = '<b>{0}</b> <small>{1}</small> <span size="x-small">{2}</span>'.format(
                name, artist, album)
        self.label.set_label(label)
        self.app.window.set_title(name)
        self.artist_pic.set_from_pixbuf(Config.ANONYMOUS_PIXBUF)
        Net.async_call(Net.get_artist_info, song['artistid'], song['artist'],
                       callback=_update_pic)

    def get_lrc(self):
        def _update_lrc(lrc_text, error=None):
            if error:
                logger.error('get_lrc(): %s', error)
            self.app.lrc.set_lrc(lrc_text)

        Net.async_call(Net.get_lrc, self.curr_song, callback=_update_lrc)

    def get_recommend_lists(self):
        self.recommend_imgs = None
        def _on_list_received(imgs, error=None):
            if error or not imgs or len(imgs) < 10:
                logger.debug('get_recommend_lists(): %s, %s' % (imgs, error))
                self.recommend_imgs = None
            else:
                self.recommend_imgs = imgs.strip().splitlines()

        Net.async_call(Net.get_recommend_lists, self.curr_song['artist'],
                       callback=_on_list_received)

    def update_lrc_background(self, url):
        def _update_background(filepath, error=None):
            if error or not filepath:
                logger.error('update_lrc_background(): %s, %s' %
                             (filepath, error))
                return
            self.app.lrc.update_background(filepath)

        Net.async_call(Net.get_recommend_image, url,
                       callback=_update_background)

    # Radio part
    def load_radio(self, song, radio_item):
        '''Load Radio song.

        song from radio, only contains name, artist, rid, artistid
        Remember to update its information.
        '''
        self.play_type = PlayType.RADIO
        self.stop_player()
        self.curr_radio_item = radio_item
        self.curr_song = song
        self.update_favorite_button_status()
        self.create_new_async(song)

    # MV part
    def on_play_type_toggled(self, toggle_button, play_type):
        if (not toggle_button.get_active() or not self.curr_song or
                self.play_type == PlayType.NONE):
            return
        elif play_type == PlayType.SONG or play_type == PlayType.RADIO:
            self.app.lrc.show_music()
            self.load(self.curr_song)
        elif play_type == PlayType.MV:
            self.app.lrc.show_mv()
            self.load_mv(self.curr_song)
            self.app.popup_page(self.app.lrc.app_page)

    def load_mv(self, song):
        self.play_type = PlayType.MV
        self.curr_song = song
        self.update_favorite_button_status()
        self.stop_player()
        self.use_mtv_btn.handler_block(self.use_mtv_sid)
        self.use_mtv_btn.set_active(True)
        self.use_mtv_btn.handler_unblock(self.use_mtv_sid)
        self.create_new_async(song, use_mv=True)

    def get_mv_link(self):
        def _update_mv_link(mv_args, error=None):
            if error or not mv_args:
                logger.error('get_mv_link(): %s, %s' % (mv_args, error))
                self.use_mtv_btn.set_sensitive(False)
            else:
                cached, mv_link, mv_path = mv_args
                if cached or mv_link:
                    self.use_mtv_btn.set_sensitive(True)
                else:
                    self.use_mtv_btn.set_sensitive(False)
        Net.async_call(Net.get_song_link, self.curr_song, self.app.conf, True,
                       callback=_update_mv_link)

    # Fullscreen
    def toggle_fullscreen(self):
        '''Switch between fullscreen and unfullscreen mode.'''

        def hide_control_panel_and_label(timestamp):
            if self.fullscreen_sid > 0:
                gdk_window.set_cursor(self.blank_cursor)
            if (timestamp == self.fullscreen_timestamp and
                    self.fullscreen_sid > 0):
                self.hide()
                GLib.timeout_add(100, self.playbin.expose)
                self.app.notebook.set_show_tabs(False)

        def on_window_motion_notified(window, event):
            window.get_window().set_cursor(self.default_cursor)
            if self.fullscreen_sid == 0:
                return
            lower = 70
            upper = window.get_size()[1] - 40
            if lower < event.y < upper:
                GLib.timeout_add(2000,
                        lambda *args: gdk_window.set_cursor(self.blank_cursor))
            else:
                self.show_all()
                self.app.notebook.set_show_tabs(True)

            # Delay 2 seconds to hide them
            self.fullscreen_timestamp = time.time()
            GLib.timeout_add(100, self.playbin.expose)
            GLib.timeout_add(2000, hide_control_panel_and_label,
                             self.fullscreen_timestamp)

        window = self.app.window
        gdk_window = window.get_window()
        button = self.fullscreen_btn
        if not self.default_cursor:
            self.default_cursor = gdk_window.get_cursor()
        if not self.default_cursor:
            self.default_cursor = Gdk.Cursor.new(Gdk.CursorType.ARROW)

        if not button.get_active():
        # unfullscreen
            window.unfullscreen()
            button.set_icon_name('view-fullscreen-symbolic')
            window.realize()
            self.playbin.expose()
            self.show()
            self.app.notebook.set_show_tabs(True)
            self.fullscreen_sid = 0
            gdk_window.set_cursor(self.default_cursor)
        else:
        # fullscreen
            self.app.popup_page(self.app.lrc.app_page)
            window.realize()
            window.fullscreen()
            button.set_icon_name('view-restore-symbolic')
            self.playbin.expose_fullscreen()

            self.hide()
            self.app.notebook.set_show_tabs(False)
            gdk_window.set_cursor(self.blank_cursor)
            self.fullscreen_sid = window.connect('motion-notify-event',
                                                 on_window_motion_notified)

    def on_window_key_pressed(self, widget, event):
        # press Esc to exit fullscreen mode
        if event.keyval == Gdk.KEY_Escape and self.fullscreen_btn.get_active():
            self.fullscreen_btn.set_active(False)
        # press F11 to toggle fullscreen mode
        elif event.keyval == Gdk.KEY_F11:
            self.fullscreen_btn.set_active(not self.fullscreen_btn.get_active())

    def on_fullscreen_button_toggled(self, button):
        self.toggle_fullscreen()

    def on_favorite_btn_clicked(self, button):
        if not self.curr_song:
            return
        self.toggle_favorite_status()

    def update_favorite_button_status(self):
        if not self.curr_song:
            return
        if self.get_favorite_status():
            self.favorite_btn.set_icon_name('favorite')
        else:
            self.favorite_btn.set_icon_name('emblem-favorite-symbolic')

    def get_favorite_status(self):
        return self.app.playlist.check_song_in_playlist(self.curr_song,
                                                        'Favorite')

    def toggle_favorite_status(self):
        if not self.curr_song:
            return
        if self.app.playlist.check_song_in_playlist(self.curr_song,
                                                    'Favorite'):
            self.app.playlist.remove_song_from_playlist(self.curr_song,
                                                        'Favorite')
            self.favorite_btn.set_icon_name('emblem-favorite-symbolic')
        else:
            self.app.playlist.add_song_to_playlist(self.curr_song, 'Favorite')
            self.favorite_btn.set_icon_name('favorite')

    # menu button
    def on_main_menu_button_clicked(self, button, main_menu):
        main_menu.popup(None, None, None, None, 1,
                        Gtk.get_current_event_time())

    def on_main_menu_pref_activate(self, menu_item):
        '''重新载入设置'''
        dialog = Preferences(self.app)
        dialog.run()
        dialog.destroy()
        self.app.load_styles()
        self.app.init_status_icon()
        self.app.lrc.update_highlighted_tag()
        self.app.shortcut.rebind_keys()

    def on_main_menu_about_activate(self, menu_item):
        dialog = Gtk.AboutDialog()
        dialog.set_modal(True)
        dialog.set_transient_for(self.app.window)
        dialog.set_program_name(Config.APPNAME)
        dialog.set_logo_icon_name(Config.NAME)
        dialog.set_version(Config.VERSION)
        dialog.set_comments(Config.DESCRIPTION)
        dialog.set_copyright(Config.COPYRIGHT)
        dialog.set_website(Config.HOMEPAGE)
        dialog.set_license_type(Gtk.License.GPL_3_0)
        dialog.set_authors(Config.AUTHORS)
        dialog.run()
        dialog.destroy()

    def on_main_menu_quit_activate(self, menu_item):
        self.app.quit()


    # playbin signal handlers
    def on_playbin_eos(self, playbin, eos_msg):
        self.load_next()

    def on_playbin_error(self, playbin, error_msg):
        logger.warn('Player.on_playbin_error(): %s.' % error_msg)
        self.app.toast('Player Error: %s' % error_msg)
        self.stop_player()

    def on_playbin_mute_changed(self, playbin, mute):
        self.update_gtk_volume_value_cb()

    def on_playbin_volume_changed(self, playbin, volume):
        self.update_gtk_volume_value_cb()

    def update_gtk_volume_value(self):
        mute = self.playbin.get_mute()
        volume = self.playbin.get_volume()
        if mute:
            self.volume.handler_block(self.volume_sid)
            self.volume.set_value(0.0)
            self.volume.handler_unblock(self.volume_sid)
        else:
            self.volume.handler_block(self.volume_sid)
            self.volume.set_value(volume ** 0.33)
            self.volume.handler_unblock(self.volume_sid)
        self.app.conf['volume'] = volume

    def update_gtk_volume_value_cb(self):
        GLib.idle_add(self.update_gtk_volume_value)


    def start_player(self, load=False):
        if self.play_type == PlayType.NONE:
            return

        self.dbus.set_Playing()

        self.playback_action.set_icon_name('media-playback-pause-symbolic')
        self.playback_action.set_label(_('Pause'))
        self.playbin.play()
        self.adj_timeout = GLib.timeout_add(250, self.sync_adjustment)
        if load:
            self.playbin.set_volume(self.app.conf['volume'])
            self.init_meta()
            GLib.timeout_add(1500, self.init_adjustment)
        self.notify.refresh()

    def start_player_cb(self):
        GLib.idle_add(lambda: self.playback_action.set_active(True))

    def pause_player(self):
        if self.play_type == PlayType.NONE:
            return
        self.dbus.set_Pause()
        self.playback_action.set_icon_name('media-playback-start-symbolic')
        self.playback_action.set_label(_('Play'))
        self.playbin.pause()
        if self.adj_timeout > 0:
            GLib.source_remove(self.adj_timeout)
            self.adj_timeout = 0
        self.notify.refresh()

    def pause_player_cb(self):
        GLib.idle_add(lambda: self.playback_action.set_active(False))

    def play_pause(self):
        if self.play_type == PlayType.NONE:
            return
        self.playback_action.set_active(not self.playback_action.get_active())

    def play_pause_cb(self):
        GLib.idle_add(self.play_pause)

    def stop_player(self):
        if self.play_type == PlayType.NONE:
            return
        self.playback_action.set_active(False)
        self.playbin.stop()
        self.scale.set_value(0)
        if self.play_type != PlayType.MV:
            self.use_audio_btn.handler_block(self.use_audio_sid)
            self.use_audio_btn.set_active(True)
            self.use_audio_btn.handler_unblock(self.use_audio_sid)
            self.use_mtv_btn.set_sensitive(False)
        self.time_label.set_label('0:00/0:00')
        if self.adj_timeout > 0:
            GLib.source_remove(self.adj_timeout)
            self.adj_timeout = 0

    def stop_player_cb(self):
        GLib.idle_add(self.stop_player)

    def load_prev(self):
        if self.play_type == PlayType.NONE or not self.can_go_previous():
            return
        self.stop_player()
        repeat = self.repeat_btn.get_active()
        if self.play_type == PlayType.SONG:
            self.app.playlist.play_prev_song(repeat=repeat, use_mv=False)
        elif self.play_type == PlayType.MV:
            self.app.playlist.play_prev_song(repeat=repeat, use_mv=True)

    def load_prev_cb(self):
        GLib.idle_add(self.load_prev)

    def load_next(self):
        if self.play_type == PlayType.NONE:
            return
        self.stop_player()
        if self.repeat_type == RepeatType.ONE:
            if self.play_type == PlayType.MV:
                self.load_mv(self.curr_song)
            else:
                self.load(self.curr_song)
            return

        repeat = self.repeat_btn.get_active()
        shuffle = self.shuffle_btn.get_active()
        if self.play_type == PlayType.RADIO:
            self.curr_radio_item.play_next_song()
        elif self.play_type == PlayType.SONG:
            self.app.playlist.play_next_song(repeat, shuffle, use_mv=False)
        elif self.play_type == PlayType.MV:
            self.app.playlist.play_next_song(repeat, shuffle, use_mv=True)

    def load_next_cb(self):
        GLib.idle_add(self.load_next)

    def get_volume(self):
        return self.volume.get_value()

    def set_volume(self, volume):
        self.volume.set_value(volume)

    def set_volume_cb(self, volume):
        GLib.idle_add(self.set_volume, volume)

    def get_volume(self):
        return self.playbin.get_volume()

    def toggle_mute(self):
        mute = self.playbin.get_mute()
        self.playbin.set_mute(not mute)
        if mute:
            self.volume.handler_block(self.volume_sid)
            self.volume.set_value(self.app.conf['volume'])
            self.volume.handler_unblock(self.volume_sid)
        else:
            self.volume.handler_block(self.volume_sid)
            self.volume.set_value(0.0)
            self.volume.handler_unblock(self.volume_sid)

    def toggle_mute_cb(self):
        GLib.idle_add(self.toggle_mute)

    def seek(self, offset):
        if self.play_type == PlayType.NONE:
            return
        self.playback_action.set_active(False)
        self.playbin.seek(offset)
        GLib.timeout_add(300, self.start_player_cb)
        self.sync_label_by_adjustment()

    def seek_cb(self, offset):
        GLib.idle_add(self.seek, offset)

    def can_go_previous(self):
        if self.play_type in (PlayType.MV, PlayType.SONG):
            return True
        return False


    # dbus parts
    def init_meta(self):
        self.adjustment_upper = 0
        self.dbus.disable_seek()
        self.meta_url = ''
        self.meta_artUrl = ''

    def on_adjustment_changed(self, adj):
        self.dbus.update_meta()
        self.adjustment_upper = adj.get_upper()