diff --git a/main.py b/main.py index 7d61670..bafc999 100755 --- a/main.py +++ b/main.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 -import gi, sys, os, threading +import gi, sys, os, threading, time # import dbus # import dbus.service, dbus.mainloop.glib from pprint import pprint as print import musicbrainzngs as mus + gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, Gdk, GLib, GdkPixbuf +from gi.repository import Gtk, Gdk, GLib, GdkPixbuf, GObject from window import SonistWindow @@ -28,19 +29,30 @@ def run_async(func): class Application(Gtk.Application): + + __gsignals__ = { + 'playing': (GObject.SignalFlags.RUN_FIRST, None, (bool,)), + 'song_changed': (GObject.SignalFlags.RUN_FIRST, None, (bool,)), + 'state_changed': (GObject.SignalFlags.RUN_FIRST, None, (str,)) + } + + def __init__(self): Gtk.Application.__init__(self, application_id = app_id) - self.mpd = MPDClient() - self.mpd.timeout = 10 - self.mpd.connect("localhost", 6600) + self.mpd_state = None + self.mpd_curr_song = None - self.mpd.ping() + self.mpd = MPDClient() + + self.mpd_is_online = self.mpd_connect() self.connect('window-removed', self.on_window_removed) mus.set_useragent('Sonist Gtk', '0.0.1', 'https://github.com/app-cat/sonist-gtk') + + @run_async def get_cover(self, song, filepath, callback): try: @@ -53,6 +65,45 @@ class Application(Gtk.Application): callback(filepath) except: pass + + @run_async + def ping(self): + if self.mpd_is_online: + while True: + try: + self.mpd.ping() + stat = self.mpd.status() + song = self.mpd.currentsong() or {} + state = stat.get('state') + + if self.mpd_curr_song != song.get('id'): + self.mpd_curr_song = song.get('id') + self.emit('song_changed', self.mpd_curr_song) + + if state == 'play': + self.emit('playing', False) + + if self.mpd_state != state: + self.emit('state_changed', state) + + self.mpd_state = state + time.sleep(1) + + except Exception as e: + print('<><><><><><><><>') + print(e) + print('<><><><><><><><>') + self.mpd.kill() + time.sleep(1) + self.mpd_is_online = self.mpd_connect() + + + def mpd_connect(self): + try: + self.mpd.connect("127.0.0.1", 6600) + return True + except: + return False def do_activate(self): @@ -64,6 +115,7 @@ class Application(Gtk.Application): self.add_window(self.window) self.window.show_all() + self.ping() def on_window_removed(self, app, win): diff --git a/ui/ctrl_box.py b/ui/ctrl_box.py index 97e4238..3ac17b1 100644 --- a/ui/ctrl_box.py +++ b/ui/ctrl_box.py @@ -5,22 +5,25 @@ gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GObject from .image_button import ImageButton +from .toggle_button import ToggleButton class CtrlBox(Gtk.Box): __gsignals__ = { - 'clicked': (GObject.SIGNAL_RUN_FIRST, None, (str,)) + 'clicked': (GObject.SignalFlags.RUN_FIRST, None, (str,)) } def __init__(self, spacing = 6): Gtk.Box.__init__(self, spacing = spacing) + self.disabled = False + self.modes = ['./usr/share/sonist/all.png','./usr/share/sonist/rand.png','./usr/share/sonist/single.png'] self.curr_mode = 0 - self.mode_btn = ImageButton('./usr/share/sonist/all.png') + self.mode_btn = ImageButton(self.modes[0]) self.prev_btn = ImageButton('./usr/share/sonist/prev.png') - self.play_btn = ImageButton('./usr/share/sonist/pause.png', 48, 48) + self.play_btn = ToggleButton(['./usr/share/sonist/pause.png', './usr/share/sonist/play.png', './usr/share/sonist/play_a.png'], 48, 48) self.next_btn = ImageButton('./usr/share/sonist/next.png') self.vol_btn = ImageButton('./usr/share/sonist/volume.png') @@ -33,12 +36,16 @@ class CtrlBox(Gtk.Box): self.mode_btn.connect('clicked', self.on_btn_clicked) self.prev_btn.connect('clicked', self.on_btn_clicked) + # self.play_btn.connect('toggled', self.on_btn_clicked) self.play_btn.connect('clicked', self.on_btn_clicked) self.next_btn.connect('clicked', self.on_btn_clicked) self.vol_btn.connect('clicked', self.on_btn_clicked) def on_btn_clicked(self, btn): + + if self.disabled: + return if btn == self.play_btn: self.emit('clicked', 'play_btn') @@ -55,19 +62,22 @@ class CtrlBox(Gtk.Box): elif btn == self.next_btn: self.emit('clicked', 'next_btn') - + elif btn == self.vol_btn: self.emit('clicked','vol_btn') def toggle_play_btn(self, on = True): - if on: - self.play_btn.set_image('./usr/share/sonist/play_a.png') - else: - self.play_btn.set_image('./usr/share/sonist/pause.png') + if self.disabled: + return + + self.play_btn.toggle(on) def toggle_mode_btn(self, mode = 'single'): + if self.disabled: + return + if mode == 'single': self.curr_mode = 2 elif mode == 'random': diff --git a/ui/image_button.py b/ui/image_button.py index 488fcca..6840d47 100644 --- a/ui/image_button.py +++ b/ui/image_button.py @@ -40,9 +40,9 @@ class ImageButton(Gtk.Button): css_provider.load_from_data(style.encode('UTF-8')) context = self.get_style_context() - path = context.get_path() context.add_provider(css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER) + def set_image(self, filepath): if self._image_path == filepath: return diff --git a/ui/toggle_button.py b/ui/toggle_button.py new file mode 100644 index 0000000..4c35bd0 --- /dev/null +++ b/ui/toggle_button.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, Gdk + +from .image import ScaleImage + +class ToggleButton(Gtk.Button): + def __init__(self, files = [], width = 26, height = 26): + Gtk.Button.__init__(self) + + self.is_active = False + + self.set_name('ToggleButton') + self.set_size_request(width, height) + + self.set_valign(Gtk.Align.CENTER) + + # 针对macos的设置, 但只解决了普通状态下的边框问题, 鼠标经过的样式还在 + self.set_relief(Gtk.ReliefStyle.NONE) + + css_provider = Gtk.CssProvider() + br = '\n' + style = f""" + #ToggleButton {{ + padding: 4px; + border: 0; + border-radius: 50%; + background-image: url('{files[0]}'); + background-size: 100%; + background-color: transparent; + border-color:transparent; + outline: transparent; + }} + + #ToggleButton.active {{ + background-image: url('{files[1]}'); + }} + + #ToggleButton:hover {{ + background-color: rgba(255,255,255,.1); + }} + """ + + css_provider.load_from_data(style.encode('UTF-8')) + + self.style_ctx = self.get_style_context() + self.style_ctx.add_provider(css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER) + + + + def toggle(self, state = False): + + if self.is_active == state: + return + + self.is_active = state + + if self.is_active: + self.style_ctx.add_class('active') + else: + self.style_ctx.remove_class('active') \ No newline at end of file diff --git a/utils.py b/utils.py index 6cbb619..c1cad16 100644 --- a/utils.py +++ b/utils.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -import gi +import gi, io, base64 from gi.repository import Gdk, GLib, GdkPixbuf from PIL import Image, ImageFilter @@ -26,11 +26,20 @@ def pil_to_pixbuf(img): return GdkPixbuf.Pixbuf.new_from_bytes(GLib.Bytes.new(data), GdkPixbuf.Colorspace.RGB, alpha, 8, w, h, rowstride) +def base64_to_pixbuf(base64_str): + # 解码base64 + decoded = base64.b64decode(base64_str) + # 创建输入流 + input_stream = io.BytesIO(decoded) + # 从输入流创建pixbuf + pixbuf = GdkPixbuf.Pixbuf.new_from_stream(input_stream, None) + return pixbuf + def blur_image(pixbuf): # 加载图片并确认该图片为RGBA模式,保证透明度 img = pixbuf_to_pil(pixbuf).convert('RGBA') - mask = Image.new('RGBA', img.size, (32, 32, 32,160)) - img = img.filter(ImageFilter.GaussianBlur(radius = 16)) + mask = Image.new('RGBA', img.size, (64, 64, 64, 160)) + img = img.filter(ImageFilter.GaussianBlur(radius = 32)) img.alpha_composite(mask) return pil_to_pixbuf(img) \ No newline at end of file diff --git a/window.py b/window.py index a11e984..c32d1b8 100644 --- a/window.py +++ b/window.py @@ -1,13 +1,13 @@ #!/usr/bin/env python3 -import gi, sys, os +import gi, sys, os, mutagen from pprint import pprint as print gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk, GLib, GdkPixbuf -from utils import blur_image +from utils import blur_image, base64_to_pixbuf from ui.image import ScaleImage from ui.slider import Slider from ui.image_button import ImageButton @@ -26,6 +26,9 @@ class SonistWindow(Gtk.Window): self.connect("destroy", self.quit) + self.app.connect('state_changed', lambda a, x: self.update_play_stat(x == 'play')) + self.app.connect('song_changed', lambda a, id: self.sync_state(False)) + self.set_name('SonistWindow') self.set_default_size(320, 384) self.set_resizable(False) @@ -70,7 +73,7 @@ class SonistWindow(Gtk.Window): # self.title_box = TextBox(256, 20) self.title_box = Gtk.Label() - self.title_box.set_text('孙晓 - 丹歌惊鸿') + self.title_box.set_text('mpd loading...') layout.put(self.title_box, 32, 244) @@ -84,6 +87,8 @@ class SonistWindow(Gtk.Window): self.ctrl_box = CtrlBox() self.ctrl_box.connect('clicked', self.ctrl_clicked) + self.ctrl_box.disabled = not self.app.mpd_is_online + layout.put(self.ctrl_box, 48, 300) self.add(layout) @@ -93,7 +98,11 @@ class SonistWindow(Gtk.Window): def set_background_image(self, filepath): - pixbuf = GdkPixbuf.Pixbuf.new_from_file(filepath) + if type(filepath) == str: + pixbuf = GdkPixbuf.Pixbuf.new_from_file(filepath) + else: + pixbuf = filepath + pixbuf = blur_image(pixbuf) pixbuf.savev(f"/tmp/sonist_album_cache", 'png', [], []) @@ -155,7 +164,7 @@ class SonistWindow(Gtk.Window): self.update_play_stat(self.stat.get('state') == 'play') - def prev_song(self): + def prev_song(self): self.app.mpd.previous() self.sync_state() @@ -165,6 +174,9 @@ class SonistWindow(Gtk.Window): def update_play_stat(self, played = True): + if not self.app.mpd_is_online: + return + if played: self.handler.reset('./usr/share/sonist/handler_a.png') else: @@ -175,6 +187,9 @@ class SonistWindow(Gtk.Window): def sync_state(self, first = False): + if not self.app.mpd_is_online: + return + self.stat = self.app.mpd.status() played = self.stat.get('state') @@ -194,13 +209,15 @@ class SonistWindow(Gtk.Window): filepath = f"./album/{song['title']}.png" # print(self.stat) - print(song) + # print(song) if os.path.isfile(filepath): self.update_album(filepath) else: - buff = self.app.get_cover(song, filepath, self.update_album) + pass + # audio = mutagen.File() + # buff = self.app.get_cover(song, filepath, self.update_album) # print(buff) # with open(filepath, 'wb') as file: # output = file.write(buff)