Python制作托盘工具,还能控制系统音乐播放?

前情

用电脑的时候,我一直羡慕苹果的一个功能:

 

是的,你可以随时随地控制音乐。

有一天我想暂停一下歌曲,但是正好我把音乐软件最小化了,于是我要先打开它,然后点暂停。

然后我再也受不了了,我准备写一个能在托盘打开,然后快速控制音乐的工具。

后来我查了很多资料,终于我在Stack Overflow上找到一些头绪,我将在这篇随笔中分享整个软件制作过程。


 

探索和拆分的代码

这部分的代码你拼起来可能也跑不了,我不对这个主题下的代码做能跑保证,所以你可以看看完整代码。

歌曲名显示

这部分其实不难,给予我灵感的那篇帖子已经将这作为例子,直接贴出了代码。我通过这个帖子找到了一个叫做winrt的库和它的文档,这个库看上去十分强大,似乎可以直接调用UWP的API。

 

我将关于获取媒体信息的这部分代码这部分代码几乎直接照抄到了我的程序里。

async def get_media_name():
    sessions = await MediaManager.request_async()

    # This source_app_user_model_id check and if statement is optional
    # Use it if you want to only get a certain player/program's media
    # (e.g. only chrome.exe's media not any other program's).

    # To get the ID, use a breakpoint() to run sessions.get_current_session()
    # while the media you want to get is playing.
    # Then set TARGET_ID to the string this call returns.

    current_session = sessions.get_current_session()
    if current_session:  # there needs to be a media session running
        #if current_session.source_app_user_model_id == TARGET_ID:
        info = await current_session.try_get_media_properties_async()

        # song_attr[0] != '_' ignores system attributes
        info_dict = {song_attr: info.__getattribute__(song_attr) for song_attr in dir(info) if song_attr[0] != '_'}

        # converts winrt vector to list
        info_dict['genres'] = list(info_dict['genres'])

        name=info_dict['title']
        if info_dict['artist']!='':
            artist=info_dict['artist']
        else:
            artist='未知歌手'
        if info_dict['album_title']!='':
            artist+=''+info_dict['album_title']
        else:
            artist+=''+'未知专辑'

        return name,artist
    else:
        return '未在播放','N / A'
    # It could be possible to select a program from a list of current
    # available ones. I just haven't implemented this here for my use case.
    # See references for more information.
    raise Exception('歌曲信息获取函数未按计划结束')

async def get_media_info():
    sessions = await MediaManager.request_async()

    # This source_app_user_model_id check and if statement is optional
    # Use it if you want to only get a certain player/program's media
    # (e.g. only chrome.exe's media not any other program's).

    # To get the ID, use a breakpoint() to run sessions.get_current_session()
    # while the media you want to get is playing.
    # Then set TARGET_ID to the string this call returns.

    current_session = sessions.get_current_session()
    if current_session:  # there needs to be a media session running
        #if current_session.source_app_user_model_id == TARGET_ID:
        info = await current_session.try_get_media_properties_async()

        # song_attr[0] != '_' ignores system attributes
        info_dict = {song_attr: info.__getattribute__(song_attr) for song_attr in dir(info) if song_attr[0] != '_'}

        # converts winrt vector to list
        info_dict['genres'] = list(info_dict['genres'])

        return info_dict
    else:
        return {
    'album_artist': '',
    'album_title': '',
    'album_track_count': 0, 
    'artist': '',
    'genres': [],
    'playback_type': 0,
    'subtitle': '', 
    'thumbnail': 'NO_DATA', 
    'title': '',
    'track_number': 0,}
    # It could be possible to select a program from a list of current
    # available ones. I just haven't implemented this here for my use case.
    # See references for more information.
    raise Exception('歌曲信息获取函数未按计划结束')

这两个函数,一个用于获取完整信息,一个用于获取歌曲名、专辑、艺人三个信息,其中艺人与专辑合并在一个字符串中,它们在软件界面上也像这样显示在一起。

此外还有个并不起眼而且不知道干啥用的函数:

async def read_stream_into_buffer(stream_ref, buffer):
    readable_stream = await stream_ref.open_read_async()
    readable_stream.read_async(buffer, buffer.capacity, InputStreamOptions.READ_AHEAD)

 

至于async是什么,请不要问我,因为我(们?)并不且没必要知道。

歌曲控制

帖子作者说,TA并不需要写出这些功能,所以TA只留了些提示,我只好自己探索了。

 

相关内容译文

控制媒体

正如楼主所说,他们的最终目标是控制媒体,在相同的库中,这是可能实现的。这里可能有更多信息(在我的案例中,我不需要这些):

微软 WinRT 文档 - Windows.Media.Control - GlobalSystemMediaTranportSession类(例.await current_session.try_pause_async())

以上是自己翻译的,可能不准。

根据大佬的指示,我直接查阅了链接指向的文档Microsoft WinRT Docs - Windows.Media.Control - GlobalSystemMediaTransportControlsSession class

打开文档后,只需要像这样简单地改改链接就能看中文版了。

 

 

我整理了一下对我来说比较有用的方法(咱也不懂,咱也不用懂,我只是希望我能解释得通俗些,懂的大佬不要喷,有话好说,谢谢):

 

然后我直接修改了前面获取歌曲信息的代码,写出了常用的控制函数

 

 

async def stop():
    sessions = await MediaManager.request_async()
    current_session = sessions.get_current_session()
    info = await current_session.try_toggle_play_pause_async()

async def nextm():
    sessions = await MediaManager.request_async()
    current_session = sessions.get_current_session()
    info = await current_session.try_skip_next_async()

async def prem():
    sessions = await MediaManager.request_async()
    current_session = sessions.get_current_session()
    info = await current_session.try_skip_previous_async()

界面

是的,我用tkinter已经用魔怔了……

模仿着iOS的音乐控制界面和Win10的界面风格,我以自己和tkinter的水平做了个布局差不多,但是功能差很多的界面,然后还整了个托盘图标。

#界面
win=tk.Tk()
win.title('音乐控制')
win.resizable(0,0)
win.protocol('WM_DELETE_WINDOW',win.withdraw)
win.attributes("-toolwindow", True)

imgf=Image.open("media_thumb_none.jpg")
imgf = imgf.resize((250, 250))
img=ImageTk.PhotoImage(imgf)
imgt=tk.Label(win,image=img)
imgt.pack()

nametxt=tk.Label(win,text='加载中',font=('等线',20))
nametxt.pack()

artxt=tk.Label(win,text='加载中',font=('等线',12),fg='#909090')
artxt.pack()

btnpt=tk.Frame(win)
btnpt.pack(fill=tk.X)

prembtn=tk.Button(btnpt,text='9',command=lambda:asyncio.run(prem()),font=('webdings',25),bd=0)
prembtn.pack(side=tk.LEFT,fill=tk.X)
nextmbtn=tk.Button(btnpt,text=':',command=lambda:asyncio.run(nextm()),font=('webdings',25),bd=0)
nextmbtn.pack(side=tk.RIGHT,fill=tk.X)
stopbtn=tk.Button(btnpt,text='4‖',command=lambda:asyncio.run(stop()),font=('webdings',25),bd=0,bg='#A6D8FF')
stopbtn.pack(fill=tk.X)

#获取窗口默认大小
win.update()
winw=win.winfo_width()
winh=win.winfo_height()

#屏幕尺寸
scr_w=win32api.GetSystemMetrics(win32con.SM_CXSCREEN)
scr_h=win32api.GetSystemMetrics(win32con.SM_CYSCREEN)

#窗口大小与位置
win.geometry(str(winw)+'x'+str(winh)+'+'+str(int(scr_w-winw-10))+'+'+str(int(scr_h-winh-75)))

#先进行一次外观刷新
if get_dark():
    win.configure(background='#101010')
    nametxt['bg']='#101010'
    nametxt['fg']='#FFFFFF'
    artxt['bg']='#101010'
    #artxt['fg']='#FFFFFF'
    imgt['bg']='#101010'
    prembtn['bg']='#101010'
    prembtn['fg']='#FFFFFF'
    nextmbtn['bg']='#101010'
    nextmbtn['fg']='#FFFFFF'
    stopbtn['bg']='#0078D7'
    stopbtn['fg']='#FFFFFF'
else:
    win.configure(background='#FFFFFF')
    nametxt['bg']='#FFFFFF'
    nametxt['fg']='#000000'
    artxt['bg']='#FFFFFF'
    #artxt['fg']='#000000'
    imgt['bg']='#FFFFFF'
    prembtn['bg']='#FFFFFF'
    prembtn['fg']='#000000'
    nextmbtn['bg']='#FFFFFF'
    nextmbtn['fg']='#000000'
    stopbtn['bg']='#A6D8FF'
    stopbtn['fg']='#000000'

rft=0#ReFresh Time
refresh_t=threading.Thread(target=refresh)
refresh_t.start()

win.withdraw()

#托盘
use_color_icon=False
menu = (pystray.MenuItem('显示音乐控制中心',show_window,default=True),pystray.MenuItem('退出',close),pystray.MenuItem('使用彩色托盘图标',change_icon,checked=lambda item: use_color_icon))
iconimg = Image.open("icon.png")
icon = pystray.Icon("PyMusicCtrl",iconimg,"音乐控制",menu)
icon.run_detached()

win.mainloop()

实时更新歌曲信息

首先一个while True,然后套进各种信息的刷新。

大部分就是直接修改界面上各种控件的属性

获取专辑图

我也不知道为啥这个API不直接把专辑图放在属性里,这就算了,它还只给缩略图!?

 

管他呢,反正没别的图了,直接拿来用吧。

最后加起来,代码是这样的:

def refresh():
    global rft,winw,winh,scr_w,scr_h,win
    while True:
        try:
            #print('刷新')
            #名称
            mname,mar=asyncio.run(get_media_name())
            nametxt['text']=mname
            artxt['text']=mar
            #专辑图(PART1)
            cover=asyncio.run(get_media_info())['thumbnail']
            if rft%50==0:#此处表示每刷新5次才会刷新部分信息,这样可以避免大量读写导致专辑图加载(读取)错误,也可以节省性能
                #print('刷新专辑图')
                #专辑图(PART2)
                if cover!='NO_DATA':
                    # create the current_media_info dict with the earlier code first
                    thumb_stream_ref = cover

                    # 5MB (5 million byte) buffer - thumbnail unlikely to be larger
                    thumb_read_buffer = Buffer(5000000)

                    # copies data from data stream reference into buffer created above
                    asyncio.run(read_stream_into_buffer(thumb_stream_ref, thumb_read_buffer))

                    # reads data (as bytes) from buffer
                    buffer_reader = DataReader.from_buffer(thumb_read_buffer)
                    byte_buffer = buffer_reader.read_bytes(thumb_read_buffer.length)

                    with open('media_thumb.jpg', 'wb+') as fobj:
                        fobj.write(bytearray(byte_buffer))
                    imgf=Image.open("media_thumb.jpg")
                    imgf = imgf.resize((250, 250))
                    img=ImageTk.PhotoImage(imgf)
                    imgt['image']=img
                else:
                    imgf=Image.open("media_thumb_none.jpg")
                    imgf = imgf.resize((250, 250))
                    img=ImageTk.PhotoImage(imgf)
                    imgt['image']=img
                #亮暗模式
                if get_dark():
                    win.configure(background='#101010')
                    nametxt['bg']='#101010'
                    nametxt['fg']='#FFFFFF'
                    artxt['bg']='#101010'
                    #artxt['fg']='#FFFFFF'
                    imgt['bg']='#101010'
                    prembtn['bg']='#101010'
                    prembtn['fg']='#FFFFFF'
                    nextmbtn['bg']='#101010'
                    nextmbtn['fg']='#FFFFFF'
                    stopbtn['bg']='#0078D7'
                    stopbtn['fg']='#FFFFFF'
                else:
                    win.configure(background='#FFFFFF')
                    nametxt['bg']='#FFFFFF'
                    nametxt['fg']='#000000'
                    artxt['bg']='#FFFFFF'
                    #artxt['fg']='#000000'
                    imgt['bg']='#FFFFFF'
                    prembtn['bg']='#FFFFFF'
                    prembtn['fg']='#000000'
                    nextmbtn['bg']='#FFFFFF'
                    nextmbtn['fg']='#000000'
                    stopbtn['bg']='#A6D8FF'
                    stopbtn['fg']='#000000'
            #强制大小
            win.geometry(str(winw)+'x'+str(winh))
            #刷新间隔,避免循环过于紧凑导致无法控制时间以及产生未知的BUG
            time.sleep(0.01)
            rft+=1
        except Exception as e:
            print(e)
            #raise Exception('信息刷新错误:'+str(e))

界面相关函数

也就三个,主要就是负责一些界面相关的东西

def get_dark(): 
    registry = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER)
    reg_keypath = r'SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize'
    try:
        reg_key = winreg.OpenKey(registry, reg_keypath)
    except FileNotFoundError:
        return False

    for i in range(1024):
        try:
            value_name, value, _ = winreg.EnumValue(reg_key, i)
            if value_name == 'AppsUseLightTheme':
                return value == 0
        except OSError:
            break
    return False

def close(icon,item):
    exit()
 
def show_window(icon,item):
    win.geometry(str(winw)+'x'+str(winh)+'+'+str(int(scr_w-winw-10))+'+'+str(int(scr_h-winh-75)))
    win.deiconify()

def change_icon(icona,item):
    global use_color_icon,icon,iconimg,menu
    if not use_color_icon:
        use_color_icon=True
        iconimg = Image.open("icon_colorful.png")
    else:
        use_color_icon=False
        iconimg = Image.open("icon.png")
    icon.stop()
    icon = pystray.Icon("PyMusicCtrl",iconimg,"音乐控制",menu)
    icon.run_detached()

getdark,用来获取系统当前的亮暗模式设置。

close,用来在托盘右键,然后选择“退出”后退出软件。

show_window,用来在托盘中选择相应项后显示窗口

change_icon,用来切换彩色/黑白托盘图标

完整代码和图片资源

全打包丢蓝奏云了,自己下载去

下载链接

参考资料

标题 网站 链接

How can I get the title of the currently playing media in windows 10 with python

Stack Overflow https://stackoverflow.com/questions/65011660/how-can-i-get-the-title-of-the-currently-playing-media-in-windows-10-with-python/66037406

Detect OS dark mode in Python

Stack Overflow https://stackoverflow.com/questions/65294987/detect-os-dark-mode-in-python

GlobalSystemMediaTransportControlsSession 类

Microsoft Docs  https://docs.microsoft.com/zh-cn/uwp/api/windows.media.control.globalsystemmediatransportcontrolssession?view=winrt-22621

Creating a system tray icon

pystray Package Documentation

https://pystray.readthedocs.io/en/latest/

 

posted @ 2022-08-07 19:12  真_人工智障  阅读(288)  评论(0编辑  收藏  举报