#!/usr/bin/env python3 import gi, sys, os, mutagen, base64 gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk, GLib, GdkPixbuf, GObject from utils import blur_image, pic_to_pixbuf, base64_to_pixbuf, idle from assets import image_dict from ui.image import ScaleImage from ui.image_button import ImageButton from ui.title_text import TitleText from ui.ctrl_box import CtrlBox from ui.timebar import Timebar from ui.option_menu import OptionMenu class SonistWindow(Gtk.Window): app = None mpd = None stat = {} def __init__(self, app): Gtk.Window.__init__(self) self.app = app self.mpd = app.mpd self.style_context = Gtk.StyleContext() self.screen = Gdk.Screen.get_default() self.css_provider = None self.set_default_style() self.connect("destroy", lambda win: app.remove_window(win)) self.mpd.connect('offline', lambda o: self.reset_player()) self.mpd.connect('online', lambda o: self.sync_state(None, None, True)) self.mpd.connect('state_changed', lambda o, stat: self.update_play_stat(stat == 'play')) self.mpd.connect('song_changed', lambda o, stat, song: self.sync_state(stat, song, False)) self.mpd.connect('volume_changed', lambda o, vol: self.update_volume(vol)) self.mpd.connect('playing', lambda o, stat, song: self.update_playtime(stat)) self.set_name('SonistWindow') self.set_title('Sonist') self.set_default_size(320, 384) self.set_resizable(False) self.set_wmclass('Sonist', 'Sonist') self.set_opacity(0.9) self.set_background_image(image_dict['disk']) layout = Gtk.Layout() # 菜单按钮 menu_btn = ImageButton(image_dict['menu']) popup_menu = OptionMenu(app) menu_btn.connect('clicked', lambda btn: popup_menu.show(btn)) layout.put(menu_btn, 276, 6) # 唱片 disk = ScaleImage(image_dict['disk']) handler = ScaleImage(image_dict['handler']) album = ScaleImage() disk.resize(192, 192) album.clip_resize(128).set_radius(64) handler.resize(48, 96) self.handler = handler self.album = album box = Gtk.Fixed() box.put(disk, 16, 16) box.put(album, 48, 48) box.put(handler, 0, 16) layout.put(box, 48, 16) # title self.title_box = TitleText() layout.put(self.title_box, 27, 244) # 播放进度 self.timebar = Timebar() self.timebar.connect('seeked', lambda a,v: self.mpd.seekcur(v)) layout.put(self.timebar, 24, 272) # 控制条 self.ctrl_box = CtrlBox() self.ctrl_box.connect('clicked', self.ctrl_clicked) self.ctrl_box.connect('volume_changed', lambda box, vol: self.mpd.setvol(vol)) layout.put(self.ctrl_box, 48, 312) self.add(layout) def set_default_style(self): provider = Gtk.CssProvider() css = f""" #SonistWindow {{ background-image: url('{image_dict['disk']}'); background-size: 100% 100%; background-position: center; }} #text {{ color: #f2f5fc; }} #ImageButton {{ border: 0; border-radius: 50%; background-color: transparent; border-color:transparent; outline: transparent; }} #ImageButton:hover {{ background-color: rgba(255,255,255,.1); outline: transparent; }} #Slider {{ outline: none; }} #Slider trough {{ background-color: rgba(129, 161, 193, 0.35); outline: none; }} #Slider trough highlight {{ background-color: rgba(163, 190, 140, 0.75); }} #Slider slider {{ background-color: transparent; border-color: transparent; outline: none; }} """ provider.load_from_data(css.encode('UTF-8')) self.style_context.add_provider_for_screen(self.screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) def get_mpd_stat(self): try: self.stat = self.mpd.status() except: self.stat = {} return self.stat def set_background_image(self, 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', [], []) css = f""" #SonistWindow {{ background-image: url('/tmp/sonist_album_cache'); }} """ # 加载CSS样式 if self.css_provider is None: self.css_provider = Gtk.CssProvider() else: self.style_context.remove_provider_for_screen(self.screen, self.css_provider) self.css_provider.load_from_data(css.encode('UTF-8')) self.style_context.add_provider_for_screen(self.screen, self.css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) def ctrl_clicked(self, box, btn): if btn == 'play_btn': self.toggle_play() elif btn == 'mode_btn': # repeat all if self.ctrl_box.curr_mode == 0: self.mpd.repeat(1) self.mpd.random(0) self.mpd.single(0) # random elif self.ctrl_box.curr_mode == 1: self.mpd.repeat(0) self.mpd.random(1) self.mpd.single(0) # single else: self.mpd.repeat(0) self.mpd.random(0) self.mpd.single(1) elif btn == 'prev_btn': self.prev_song() elif btn == 'next_btn': self.next_song() elif btn == 'vol_btn': self.toggle_play() def toggle_play(self): state = self.stat.get('state') is_play = state == 'play' try: if state == 'stop': self.mpd.play() state = 'play' else: self.mpd.pause() state = 'pause' if is_play else 'play' except: pass self.stat['state'] = state self.update_play_stat(state == 'play') def prev_song(self): try: self.mpd.previous() except: pass def next_song(self): try: self.mpd.next() except: pass @idle def update_play_stat(self, is_play = True): if is_play: self.handler.reset(image_dict['handler_a']) else: self.handler.reset(image_dict['handler']) # 切换播放按钮状态 self.ctrl_box.toggle_play_btn(is_play) @idle def update_playtime(self, stat = {}): self.stat = stat times = stat.get('time') or '0:0' times = times.split(':') self.timebar.update_time(int(times[0]), int(times[1])) @idle def update_volume(self, vol = 100): self.ctrl_box.set_volume(vol) @idle def sync_state(self, stat = None, song = None, first = False): self.ctrl_box.disabled = False self.stat = stat or self.get_mpd_stat() state = self.stat.get('state') # 首次启动时, 更新数据库 if first: self.mpd.update() song_num = int(self.mpd.stats().get('songs') or 0) playlist = [it['file'] for it in self.mpd.playlistinfo()] self.ctrl_box.disabled = song_num == 0 # 这里只做添加, 不做删除, 重建播放列表在设置里 if song_num > 0 and len(playlist) < song_num: songs = self.mpd.listall() for it in songs: if it['file'] in playlist: continue self.mpd.add(it['file']) self.get_mpd_stat() self.update_play_stat(state == 'play') self.update_volume(int(self.stat.get('volume') or 100)) if self.stat.get('single') == '1': self.ctrl_box.toggle_mode_btn(mode = 'single') elif self.stat.get('random') == '1': self.ctrl_box.toggle_mode_btn(mode = 'random') if state != 'stop': song = song or self.mpd.currentsong() if song.get('id'): # 更新歌曲信息 self.title_box.update(f"{song.get('artist')} - {song.get('title')}") title_hex = base64.b64encode(song.get('title').encode()).hex() filepath = f"{self.app.album_cache_dir}/{title_hex}.png" songpath = f"{self.app.music_dir}/{song.get('file')}" if os.path.isfile(filepath): self.update_album(filepath) else: id3 = mutagen.File(songpath) pic = None try: if id3.tags.get('APIC:'): pic = id3.tags['APIC:'] elif len(id3.pictures) > 0: pic = id3.pictures[0] if pic is None: self.update_album() else: album = pic_to_pixbuf(pic) self.update_album(album) except Exception as err: pass @idle def reset_player(self): self.ctrl_box.disabled = True self.title_box.update('mpd is offline...') self.timebar.update_time() self.update_album() @idle def update_album(self, filepath = None): self.set_background_image(filepath or image_dict['disk']) self.album.reset(filepath, True).set_radius(64)