예제 #1
0
def get_token(window, callback, force=False):
    settings = Settings()
    settings.acquire()
    has_token = settings.cp.has_option('vk', 'access_token')
    settings.release()

    if force or not has_token:

        def _callback(access_token):
            settings.acquire()
            settings.cp.set('vk', 'access_token', access_token)
            settings.release()
            callback(access_token)

        _start_login_process(window, _callback)
    else:
        callback(settings.cp.get('vk', 'access_token'))
예제 #2
0
파일: app.py 프로젝트: ppsirg/vkplayer
class App(object):
    def __init__(self):
        self.settings = Settings()
        self.vk = None
        self.itunes = ITunesApi()
        self.player = Player()
        self.ipc = MessagingBus(self)
        self.player.start()
        self.song_length = 0
        self.current_song_iter = None
        self.current_title_string = 'Idle'
        self.is_seeking = False

        self.settings.acquire()
        if not self.settings.cp.has_option('general', 'volume'):
            self.settings.cp.set('general', 'volume', '1.0')
        self.last_volume = self.settings.cp.getfloat('general', 'volume')
        self.settings.release()

        self.player.on_download_started_cb = lambda *args: Gdk.threads_add_idle(
            0, lambda: self._on_download_started(*args))
        self.player.on_progress_update_cb = lambda *args: Gdk.threads_add_idle(
            0, lambda: self._on_progress_update(*args))
        self.player.on_download_finished_cb = lambda *args: Gdk.threads_add_idle(
            0, lambda: self._on_download_finished(*args))

    def start(self):
        if setproctitle:
            setproctitle('vkplayer')

        self.ipc.start()
        self.ipc.broadcast('state_changed', [
            self.player.is_downloading, self.player.is_playing,
            self.current_title_string
        ])

        self.window = Gtk.Window()
        self.window.resize(600, 400)
        self.window.set_title('VK audio player')

        self.window.set_icon_from_file(os.path.join(DIR, 'icons/icon128.png'))

        self.accels = Gtk.AccelGroup()

        self.vbox = Gtk.VBox()
        self.window.add(self.vbox)

        # self.window.modify_bg(Gtk.StateFlags.NORMAL, Gdk.color_parse('#CCCCCC'))

        self.header = Gtk.HBox()
        # self.header.override_background_color(Gtk.StateFlags.NORMAL | Gtk.StateFlags.ACTIVE | Gtk.StateFlags.INSENSITIVE, color)
        self.header_left = Gtk.HBox()
        self.header_right = Gtk.VBox()
        self.header.pack_start(self.header_left, False, True, 0)
        self.header.pack_start(self.header_right, True, True, 0)
        self.vbox.pack_start(self.header, False, True, 0)

        self.controls = Gtk.HBox(spacing=8)
        self.controls.set_border_width(8)
        self.header_right.pack_start(self.controls, False, True, 0)

        img = Gtk.Image()
        img.set_from_pixbuf(
            GdkPixbuf.Pixbuf.new_from_file_at_scale(
                os.path.join(DIR, 'icons/icon-play-128.png'),
                width=48,
                height=48,
                preserve_aspect_ratio=False))

        self.play = Gtk.Button('', image=img)
        self.play.set_always_show_image(True)
        self.play.set_relief(Gtk.ReliefStyle.NONE)
        self.play.connect('clicked', self._on_play_clicked)
        self.controls.pack_start(self.play, False, True, 0)

        img = Gtk.Image()
        img.set_from_pixbuf(
            GdkPixbuf.Pixbuf.new_from_file_at_scale(
                os.path.join(DIR, 'icons/icon-pause-128.png'),
                width=48,
                height=48,
                preserve_aspect_ratio=False))

        self.pause = Gtk.Button('', image=img)
        self.pause.set_always_show_image(True)
        self.pause.set_relief(Gtk.ReliefStyle.NONE)
        self.pause.connect('clicked', self._on_pause_clicked)
        self.controls.pack_start(self.pause, False, True, 0)

        img = Gtk.Image()
        img.set_from_pixbuf(
            GdkPixbuf.Pixbuf.new_from_file_at_scale(
                os.path.join(DIR, 'icons/icon-shuffle-128.png'),
                width=48,
                height=48,
                preserve_aspect_ratio=False))

        self.random = Gtk.Button('', image=img)
        self.random.set_always_show_image(True)
        self.random.set_relief(Gtk.ReliefStyle.NONE)
        self.random.connect('clicked', self._on_random_clicked)
        self.controls.pack_start(self.random, False, True, 0)

        self.controls.pack_start(Gtk.HSeparator(), False, True, 0)

        self.cover = Gtk.Image()
        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
            os.path.join(DIR, 'icons/cover512.jpg'),
            width=144,
            height=144,
            preserve_aspect_ratio=False)
        self.cover.set_from_pixbuf(pixbuf)
        # self.cover.connect('clicked', self._on_random_clicked)
        self.cover.set_size_request(144, 144)
        self.header_left.pack_start(self.cover, False, True, 0)

        self.seek_panel = Gtk.VBox()
        self.controls.pack_start(self.seek_panel, True, True, 0)

        self.seek_labels = Gtk.HBox()
        self.seek_panel.pack_start(self.seek_labels, True, True, 10)

        self.track_title = Gtk.Label('',
                                     halign=Gtk.Align.START,
                                     valign=Gtk.Align.END)
        self.track_title.set_use_markup(True)
        self.track_title.set_markup(
            '<span font="Roboto 24" weight="300">Idle</span>')
        self.seek_labels.pack_start(self.track_title, True, True, 0)

        self.track_time = Gtk.Label('05:00',
                                    halign=Gtk.Align.END,
                                    valign=Gtk.Align.END)
        self.track_time.set_use_markup(True)
        self.track_time.set_markup(
            '<span font="Roboto 24" weight="100">05:00</span>')
        self.seek_labels.pack_start(self.track_time, True, True, 0)

        self.seek_bar = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL,
                                  adjustment=Gtk.Adjustment(0, 0, 1, 1, 0, 0))

        self.seek_bar.set_draw_value(False)
        self.seek_bar.set_sensitive(False)
        self.seek_panel.pack_start(self.seek_bar, True, True, 0)

        self.seek_bar.connect('button-press-event', self._on_seek_start)
        self.seek_bar.connect('button-release-event', self._on_seek_end)

        self.precache_progress = Gtk.ProgressBar()
        self.seek_panel.pack_start(self.precache_progress, True, True, 0)

        self.volume_scale = Gtk.Scale(orientation=Gtk.Orientation.VERTICAL,
                                      adjustment=Gtk.Adjustment(
                                          1, 0, 1, 0.05, 0.05, 0.05),
                                      valign=Gtk.Align.CENTER)
        self.volume_scale.set_inverted(True)
        self.volume_scale.connect(
            'value_changed', lambda *args: self.player.set_volume(
                self.volume_scale.get_value()))
        self.volume_scale.set_draw_value(False)
        self.volume_scale.set_size_request(-1, 64)
        self.volume_scale.set_value(self.last_volume)
        self.controls.pack_start(self.volume_scale, False, True, 0)

        # self.vbox.pack_start(Gtk.VSeparator(), False, True, 0)

        #

        self.search_panel = Gtk.HBox(spacing=8)
        self.search_panel.set_border_width(8)
        self.header_right.pack_start(self.search_panel, False, True, 0)

        self.refresh = Gtk.Button('My audio')
        self.refresh.connect('clicked', self._on_refresh_clicked)
        accelerator = '<Ctrl>m'
        key, mod = Gtk.accelerator_parse(accelerator)
        self.refresh.add_accelerator('activate', self.accels, key, mod,
                                     Gtk.AccelFlags.VISIBLE)
        self.search_panel.pack_start(self.refresh, False, True, 0)

        self.search_panel.pack_start(Gtk.HSeparator(), False, True, 0)

        self.query = Gtk.Entry(placeholder_text='Search music')
        self.query.connect('activate', self._on_search_clicked)
        accelerator = '<Ctrl>f'
        key, mod = Gtk.accelerator_parse(accelerator)
        self.query.add_accelerator('grab-focus', self.accels, key, mod,
                                   Gtk.AccelFlags.VISIBLE)
        self.search_panel.pack_start(self.query, True, True, 0)

        self.search = Gtk.Button('Search')
        self.search.connect('clicked', self._on_search_clicked)
        self.search_panel.pack_start(self.search, False, True, 0)

        #

        self.scroll = Gtk.ScrolledWindow()
        self.scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)
        self.vbox.pack_start(self.scroll, True, True, 0)

        self.spinner = Gtk.Spinner()
        self.vbox.pack_start(self.spinner, True, False, 0)

        self.playlist = Gtk.TreeView()
        self.scroll.add(self.playlist)

        self.playlist.connect('row-activated', self._on_row_activated)

        self.playlist_store = Gtk.ListStore(str, str, str, str, int, int, int,
                                            bool)
        self.playlist.set_model(self.playlist_store)

        img = Gtk.Image()
        img.set_from_stock(Gtk.STOCK_MEDIA_PLAY, 32)

        col = Gtk.TreeViewColumn(
            "",
            Gtk.CellRendererPixbuf(icon_name='media-playback-start'),
            visible=7)
        col.set_expand(False)
        self.playlist.append_column(col)

        col = Gtk.TreeViewColumn("Artist",
                                 Gtk.CellRendererText(
                                     ellipsize=True,
                                     family='Roboto, DejaVu Sans',
                                     weight=500),
                                 text=1)
        # col.set_expand(True)
        col.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
        col.set_fixed_width(250)
        self.playlist.append_column(col)

        col = Gtk.TreeViewColumn("Title",
                                 Gtk.CellRendererText(
                                     ellipsize=True,
                                     family='Roboto, DejaVu Sans'),
                                 text=0)
        col.set_expand(True)
        self.playlist.append_column(col)

        col = Gtk.TreeViewColumn("Duration", Gtk.CellRendererText(), text=2)
        col.set_expand(False)
        self.playlist.append_column(col)

        self.vbox.show_all()
        self.window.add_accel_group(self.accels)
        self.window.maximize()
        self.window.show_all()

        seek_height = max(self.seek_bar.get_allocation().height,
                          self.precache_progress.get_allocation().height)
        self.seek_bar.set_size_request(-1, seek_height)
        self.precache_progress.set_size_request(-1, seek_height)

        self.window.connect('delete-event', self._show_or_hide)

        get_token(self.window, self._on_token_ready)

        self.set_busy(False)

        self._update()

        self._on_download_finished()

        Keybinder.init()
        Keybinder.bind('<Ctrl><Alt>Return', self._on_random_clicked)
        Keybinder.bind(
            '<Ctrl><Alt>Backspace', lambda *args:
            (self._on_pause_clicked
             if self.player.is_playing else self._on_play_clicked)())

        self.status_icon = Gtk.StatusIcon()
        self.status_icon.set_from_file(os.path.join(DIR, 'icons/play.png'))
        self.status_icon.connect('popup-menu', self._on_popup_menu)
        self.status_icon.connect('activate', self._show_or_hide)

        Gtk.main()
        self.player.stop()

    def _on_popup_menu(self, icon, button, time):
        menu = Gtk.Menu()

        show = Gtk.ImageMenuItem()
        img = Gtk.Image()
        img.set_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU)
        show.set_image(img)
        show.set_label("Show/hide player")
        quit = Gtk.ImageMenuItem()
        img = Gtk.Image()
        img.set_from_stock(Gtk.STOCK_QUIT, Gtk.IconSize.MENU)
        quit.set_image(img)
        quit.set_label("Quit")

        show.connect("activate", self._show_or_hide)
        quit.connect("activate", self.quit)

        menu.append(show)
        menu.append(quit)

        menu.show_all()

        menu.popup(None, None, None, self.status_icon, button, time)

    def _show_or_hide(self, *args):
        if self.window.get_property('visible'):
            self.window.hide()
        else:
            self.window.show()
        return True

    def _start_login_force(self):
        get_token(self.window, self._on_token_ready, True)

    def _on_token_ready(self, access_token):
        def cb(success):
            if not success:
                return self._start_login_force()
            else:
                self.refresh.set_label(u' '.join(self.vk.data[key]
                                                 for key in ('first_name',
                                                             'last_name')))
                self._on_refresh_clicked()

        self.vk = VKApi(access_token)
        self.vk.load_my_info(
            lambda success: Gdk.threads_add_idle(0, lambda: cb(success)))

    def _refresh(self):
        def cb(data):
            self.ipc.broadcast('state_changed', [
                self.player.is_downloading, self.player.is_playing,
                self.current_title_string
            ])
            if 'error' in data.keys():
                return self._start_login_force()
            Gdk.threads_add_idle(
                0, lambda: self._populate_playlist(data['response']))

        self.vk.audio_get(cb)

    def _populate_playlist(self, songs):
        self.set_busy(False)
        self.playlist_store.clear()
        for song in songs:
            song_duration = '%02d:%02d' % (song['duration'] / 60,
                                           song['duration'] % 60)
            self.playlist_store.append(
                (song['title'], song['artist'], song_duration, song['url'],
                 song['duration'], song['owner_id'], song['aid'], False))

    def _on_refresh_clicked(self, *args):
        self.ipc.broadcast('state_changed', [True, False, 'Refreshing...'])
        self.set_busy(True)
        Thread(target=self._refresh).start()

    def _on_play_clicked(self, *args):
        if self.player.is_downloading:
            return
        self.player.play()
        self.ipc.broadcast('state_changed', [
            self.player.is_downloading, self.player.is_playing,
            self.current_title_string
        ])

    def _on_pause_clicked(self, *args):
        if self.player.is_downloading:
            return
        self.player.pause()
        self.ipc.broadcast('state_changed', [
            self.player.is_downloading, self.player.is_playing,
            self.current_title_string
        ])

    def _on_random_clicked(self, *args):
        model = self.playlist.get_model()
        id_ = randint(0, len(model) - 1)
        path = Gtk.TreePath(id_)
        iter_ = model.get_iter(path)
        self._play_song_at_iter(iter_)

    def _on_row_activated(self, treeview, path, column):
        model = treeview.get_model()
        iter_ = model.get_iter(path)
        self._play_song_at_iter(iter_)

    def _play_song_at_iter(self, iter_):
        if self.current_song_iter is not None:
            self.playlist.get_model().set_value(self.current_song_iter, 7,
                                                False)
        self.playlist.get_model().set_value(iter_, 7, True)

        self.playlist.scroll_to_cell(self.playlist.get_model().get_path(iter_))
        self.playlist.get_selection().select_iter(iter_)
        self.current_song_iter = iter_
        title, artist, duration_text, url, duration, owner_id, aid, is_playing = [
            self.playlist.get_model().get_value(iter_, x)
            for x in xrange(0, 8)
        ]
        title_string = u'{} - {}'.format(artist.decode('utf-8'),
                                         title.decode('utf-8')).strip()
        self.current_title_string = title_string
        title_string_html = u'<b>{}</b> - {}'.format(
            artist.decode('utf-8'), title.decode('utf-8')).strip()
        notify('media-playback-start',
               'Now playing',
               u'<b>{}</b> - {}'.format(artist.decode('utf-8'),
                                        title.decode('utf-8')).strip(),
               timeout=5000)
        # self.track_title.set_text(title_string)
        self.track_title.set_markup(
            u'<span font="Roboto 24" weight="300">{}</span>'.format(
                title_string_html))
        self.window.set_title(title_string)
        self.track_time.set_text(
            '<span font="Roboto 24" weight="100">{}</span>'.format(
                duration_text))
        self.song_length = duration
        self.player.play(u'{}_{}'.format(owner_id, aid), url)
        self.seek_bar.set_adjustment(Gtk.Adjustment(0, 0, duration, 0.1, 5, 0))
        self.seek_bar.set_value(0)

        self.ipc.broadcast('state_changed', [
            self.player.is_downloading, self.player.is_playing,
            self.current_title_string
        ])

        title_string_cleaned = sub(
            '\s+', ' ',
            sub(r'\[[^\]]+\]', '', sub(r'\([^\)]+\)', '', title_string)))
        # print 'Fetching album art for', title_string_cleaned
        Thread(target=lambda: self.itunes.search(
            self._on_song_info_loaded, term=title_string_cleaned)).start()

    def _update(self):
        if self.current_song_iter is not None:
            progress = self.player.get_play_progress()
            if not self.is_seeking:
                self.seek_bar.set_value(progress)

            if self.player.is_finished:
                model = self.playlist.get_model()
                next_iter = model.iter_next(self.current_song_iter)

                self._play_song_at_iter(next_iter)

            self.track_time.set_markup(
                '<span font="Roboto 24" weight="100">{}</span>'.format(
                    '%02d:%02d / %02d:%02d' %
                    (int(progress) / 60, int(progress) % 60,
                     self.song_length / 60, self.song_length % 60)))
        if self.last_volume != self.volume_scale.get_value():
            self.last_volume = self.volume_scale.get_value()
            self.settings.acquire()
            self.settings.cp.set('general', 'volume', self.last_volume)
            self.settings.release()
        Gdk.threads_add_timeout(0, 100, self._update)

    def _on_song_info_loaded(self, data):
        if data['resultCount'] > 0:
            result = data['results'][0]
            artworkUrl = result['artworkUrl100']

            file = Gio.File.new_for_uri(artworkUrl)
            pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale(
                file.read(cancellable=None),
                width=144,
                height=144,
                preserve_aspect_ratio=False,
                cancellable=None)
        else:
            pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
                os.path.join(DIR, 'icons/cover512.jpg'),
                width=144,
                height=144,
                preserve_aspect_ratio=False)
        Gdk.threads_add_idle(0, lambda: self.cover.set_from_pixbuf(pixbuf))

    def _on_seek_start(self, *args):
        self.is_seeking = True

    def _on_seek_end(self, *args):
        self.player.pause()
        self.player.seek(self.seek_bar.get_value())
        self.player.play()
        self.is_seeking = False

    def _on_download_started(self):
        self.seek_bar.set_visible(False)
        self.precache_progress.set_visible(True)

    def _on_progress_update(self, read, length):
        self.precache_progress.set_fraction(float(read) / length)

    def _on_download_finished(self):
        self.player.is_downloading = False
        self.seek_bar.set_visible(True)
        self.precache_progress.set_visible(False)
        self.ipc.broadcast('state_changed', [
            self.player.is_downloading, self.player.is_playing,
            self.current_title_string
        ])

    def set_busy(self, is_busy):
        self.spinner.set_visible(is_busy)
        self.scroll.set_visible(not is_busy)
        self.window.set_sensitive(not is_busy)
        if is_busy:
            self.spinner.start()
        else:
            self.spinner.stop()

    def _on_search_clicked(self, *args):
        self.ipc.broadcast('state_changed', [True, False, 'Searching...'])
        self.set_busy(True)
        Thread(target=self._search).start()

    def _search(self):
        def cb(data):
            self.ipc.broadcast('state_changed', [
                self.player.is_downloading, self.player.is_playing,
                self.current_title_string
            ])
            if 'error' in data:
                return self._start_login_force()
            Gdk.threads_add_idle(
                0, lambda: self._populate_playlist(data['response'][1:]))
            Gdk.threads_add_idle(0, lambda: self.playlist.grab_focus())

        self.vk.audio_search(cb, q=self.query.get_text())

    def play_next(self):
        model = self.playlist.get_model()
        next_iter = model.iter_next(self.current_song_iter)

        self._play_song_at_iter(next_iter)

    def play_prev(self):
        model = self.playlist.get_model()

        prev_iter = None
        iter = model.get_iter_first()

        current_path = model.get_path(self.current_song_iter)

        while True:
            prev_iter = iter
            iter = model.iter_next(iter)
            if not iter:
                return
            if model.get_path(iter) == current_path:
                self._play_song_at_iter(prev_iter)
                return

    def quit(self, *args):
        self.ipc.stop()
        Gtk.main_quit()