Python下载网易云收藏

提前声明
  • 仅作为个人学习使用,任何版权问题作者概不负责
  • 本文的语言不会且不可能很严谨
  • 博客园的编辑器有点BUG把我搞晕头了,所以本文可能有点鬼畜

前情

不知道各位有几个是对国内大厂的软件设计很满意的?

至少我是几乎受不了的。

所以我就没装过网易云的Windows端……

这段时间用着网页也受不了了。

于是我就想:既然我几乎每次用网易云都是听收藏,那能不能……

直接用系统的!?

正片

首先,我需要把我收藏的音乐拿到手。

其次,我们需要把音乐下载下来,才能让一个长得漂亮,但是不咋聪明的系统播放器播放。

探索

收藏的问题麻烦了我很久,直到有一天,我注意到了某网易云第三方客户端……

我当时就想,它能显示收藏,为嘛我不行?

于是我就赶紧看了它的GitHub,作者也很好,直接提供了一整套API的来源。

 

 

 于是我就把API部署到了Vercel,它也顺利地跑了起来

我是个乐于分享的人,所以在此直接送上现成的API地址和它的GitHub:

https://netease-cloud-music-cwl0bfvmk-totowang-hhh.vercel.app/
https://github.com/Binaryify/NeteaseCloudMusicApi

获取收藏歌单

登录

看收藏歌单前,我们要登录。

登陆是啥来着?一个界面,要输入账号密码……

那就自己写!

def loginui(event=''):
    global up,e_usr,e_pwd,e_dir
    up = tk.Tk()
    up.title("配置")

    #第一行,用户名标签及输入框
    tk.Label(up,text='邮箱').pack(fill=tk.X)
    e_usr = ttk.Entry(up)
    e_usr.pack(fill=tk.X)
    tk.Label(up,text='密码').pack(fill=tk.X)
    e_pwd = ttk.Entry(up,show='')
    e_pwd.pack(fill=tk.X)
     
    #第三行登陆按扭,command绑定事件
    ttk.Button(up,text='一键下载网易云收藏',command=login).pack(side=tk.BOTTOM,fill=tk.X)
    e_usr.bind('<KeyPress-Return>',lambda event:e_pwd.focus())
    e_pwd.bind('<KeyPress-Return>',lambda event:login())
    e_usr.focus()

    up.mainloop()

def login():
    global usr,pwd,path
    usr=e_usr.get()
    pwd=e_pwd.get()
    path=filebox.askdirectory(title='请选择音乐保存路径')
    up.destroy()

登录之后,电脑里存了啥来着?Cookie呀!

正好,我们的API也会返回Cookie。我们只需要在后面的请求中把Cookie硬塞到cookie参数里就可以了。

管他三七二十一,直接先把Cookie拿到手

res=requests.get(url="https://netease-cloud-music-cwl0bfvmk-totowang-hhh.vercel.app/login?email="+usr+"&password="+pwd)
json=res.json()
cookie=json['cookie']

你以为这就可以获取到收藏歌单了?不是不是,你可不知道为了获取一个收藏歌单网易云付出了多少……

众所周知,收藏的本质就是一个歌单,所以我们直接查看获取歌单所有音乐的API

说明 : 由于网易云接口限制,歌单详情只会提供 10 首歌,通过调用此接口,传入对应的歌单id,即可获得对应的所有歌曲

必选参数 : id : 歌单 id

可以看到,我们还要把id搞到手,所以我们应该先看看用户的歌单,这就去看文档

说明 : 登录后调用此接口 , 传入用户 id, 可以获取用户歌单

必选参数 : uid : 用户 id

这么说,我们还要用户的id(禁止套娃

那么这个id从哪来?

从“获取账号信息”来

说明 : 登录后调用此接口 ,可获取用户账号信息

接口地址 : /user/account

所以,我们只要再反着套娃回去,然后稍加亿点数据处理的细节,就可以得到收藏音乐的列表了。

获取UID

res=requests.get(url="https://netease-cloud-music-cwl0bfvmk-totowang-hhh.vercel.app/user/account?cookie="+str(cookie))
json=res.json()
uid=str(json['account']['id'])

获取用户歌单并截出收藏歌单的ID

res=requests.get(url="https://netease-cloud-music-cwl0bfvmk-totowang-hhh.vercel.app/user/playlist?limit=1&uid="+uid)
json=res.json()
favlstid=str(json['playlist'][0]['id'])

获取收藏歌单

res=requests.get(url="https://netease-cloud-music-cwl0bfvmk-totowang-hhh.vercel.app/playlist/track/all?id="+favlstid+"&cookie="+cookie)
json=res.json()

数据整理

获得了收藏歌单之后,我们可以看看里面的信息有多全,每首歌的名字、id、专辑、封面、歌手……几乎你能在网易云里看到的信息都已经全了,我们当然不会浪费对吧,要榨干:)

for song in json['songs']:
    favlst.append(str(song['id']))
    favnamelst.append(str(song['name']))
    favallst.append(str(song['al']['name']))
    favarlst.append([])
    for ar in song['ar']:
        favarlst[json['songs'].index(song)].append(ar['name'])

然后我们就把这些信息整理到了好几个列表里面……(虽然这么做用处不大

下载

所以……怎么下载呢?

直接用外链肯定是不行的,不然我可能只能下载里面的一丢丢,这可是我无法接受的

欸,我们的收藏歌单咋来的来着?

网易云API是个好东西!

这部分思路很简单,我们只需要遍历歌单id,然后把id作为参数,对获取播放直链的API进行GET请求,再拿到音乐直链,然后直接再GET一次进行下载,就搞定了。

这样,理论上来讲,只要是你能播放的,都能下载!

不对……要实现你能播放的都能下载,那我们还得带个Cookie啊!嘿嘿我真是天才!

以下是下载部分的完整代码:

for mid in favlst:
    rpg('正在下载('+str(favlst.index(mid)+1)+'/'+str(len(favlst))+'',favnamelst[favlst.index(mid)],(favlst.index(mid)/len(favlst))*100)
    if not os.path.exists(path+'/'+favnamelst[favlst.index(mid)]+".mp3"):#避免重复下载
        res=requests.get(url="https://netease-cloud-music-cwl0bfvmk-totowang-hhh.vercel.app/song/url?id="+mid+"&br=320000"+'&cookie='+cookie)
        json=res.json()
        murl=json['data'][0]['url']
        res=requests.get(url=murl)
        m=res.content
        f=open(path+'/'+favnamelst[favlst.index(mid)]+".mp3",'wb')
        f.write(m)
        f.close()

界面和其他细节

虽然我们的数据处理搞完了(希望我强行拆散的这些代码跑起来没问题

但是!你不感觉等着不耐烦吗?

所以,进度条得整上!

为了避免程序员对#FF0000”这个颜色过于敏感,我在进度显示窗口里显示常见错误类型,还贴心地选用了比较淡的红“FF9090”作为背景

(不会真有人信我的这套鬼话吧

另外,为了自己顺眼,我把动画也整上了。

我还弄了些实用的功能,这样可以防止程序报错直接退出,也可以减少其他常见原因导致的下载失败。

不过由于这部分代码过于分散,有需要的直接看完整代码吧。

完整代码

import requests
import tkinter as tk
import tkinter.ttk as ttk
import tkinter.filedialog as filebox
import tkinter.messagebox as msgbox
import win32.win32api as win32api
import win32.lib.win32con as win32con
import os
import time

#杂项准备工作
#屏幕尺寸
scr_w=win32api.GetSystemMetrics(win32con.SM_CXSCREEN)
scr_h=win32api.GetSystemMetrics(win32con.SM_CYSCREEN)

def loginui(event=''):
    global up,e_usr,e_pwd,e_dir
    up = tk.Tk()
    up.title("登录")

    #第一行,用户名标签及输入框
    tk.Label(up,text='邮箱').pack(fill=tk.X)
    e_usr = ttk.Entry(up)
    e_usr.pack(fill=tk.X)
    tk.Label(up,text='密码').pack(fill=tk.X)
    e_pwd = ttk.Entry(up,show='')
    e_pwd.pack(fill=tk.X)
     
    #第三行登陆按扭,command绑定事件
    ttk.Button(up,text='登录',command=login).pack(side=tk.BOTTOM,fill=tk.X)
    e_usr.bind('<KeyPress-Return>',lambda event:e_pwd.focus())
    e_pwd.bind('<KeyPress-Return>',lambda event:login())
    e_usr.focus()

    up.update()
    up.geometry('350x'+str(int(up.winfo_height())))
    up.resizable(0,0)

    up.mainloop()

def login():
    global usr,pwd,path
    usr=e_usr.get()
    pwd=e_pwd.get()
    path=filebox.askdirectory(title='请选择音乐保存路径')
    up.destroy()def rpg(a,b,c):
    pga['text']=str(a)
    pgb['text']=str(b)
    pgc['value']=int(c)
    win.update()

def pgcani():#准备下载时,进度条的过渡动画,写在单独的函数以通过多线程实现
    for i in range(1,12+1):
        pgc['value']+=2
        win.update()
        time.sleep(0.01)

loginui()

#toaster.show_toast('程序仍在工作','请勿惊慌,程序仍在后台工作!',threaded=True)

try:
    win=tk.Tk()
    win.title('网易云收藏音乐下载')
    pga=tk.Label(win,text='进度A未显示')
    pga.pack()
    pgb=tk.Label(win,text='进度B未显示')
    pgb.pack()
    pgc=ttk.Progressbar(win,length=250,value=0)
    pgc.pack(fill=tk.X)
    win.overrideredirect(True)
    win.attributes('-topmost',True)
    win.attributes('-alpha',0.6)
    for i in range(1,27+1):
        win.geometry('250x68'+'+'+str(int(scr_w-250))+'+'+str(int(scr_h-(108-28*4+i*4))))
        time.sleep(0.01)
        win.update()
    win.geometry('250x68+'+str(int(scr_w-250))+'+'+str(int(scr_h-108)))
    win.update()

    rpg('准备下载','获取登录Cookie',0)
    res=requests.get(url="https://netease-cloud-music-cwl0bfvmk-totowang-hhh.vercel.app/login?email="+usr+"&password="+pwd)
    json=res.json()
    cookie=json['cookie']

    #过渡动画
    for i in range(1,12+1):
        pgc['value']+=2
        win.update()
        time.sleep(0.01)
    
    rpg('准备下载','获取用户ID',25)
    res=requests.get(url="https://netease-cloud-music-cwl0bfvmk-totowang-hhh.vercel.app/user/account?cookie="+str(cookie))
    json=res.json()
    uid=str(json['account']['id'])

    #过渡动画
    for i in range(1,12+1):
        pgc['value']+=2
        win.update()
        time.sleep(0.01)

    rpg('准备下载','获取收藏歌单ID',50)
    res=requests.get(url="https://netease-cloud-music-cwl0bfvmk-totowang-hhh.vercel.app/user/playlist?limit=1&uid="+uid)
    json=res.json()
    favlstid=str(json['playlist'][0]['id'])

    #过渡动画
    for i in range(1,12+1):
        pgc['value']+=2
        win.update()
        time.sleep(0.01)

    rpg('准备下载','获取收藏歌单',75)
    res=requests.get(url="https://netease-cloud-music-cwl0bfvmk-totowang-hhh.vercel.app/playlist/track/all?id="+favlstid+"&cookie="+cookie)
    json=res.json()

    #过渡动画
    for i in range(1,12+1):
        pgc['value']+=2
        win.update()
        time.sleep(0.01)
    
    favlst=[]#id
    favnamelst=[]#歌名
    favarlst=[]#艺人
    favallst=[]#专辑

    rpg('准备下载','整理信息',100)
    #收藏音乐信息挨个存列表
    for song in json['songs']:
        favlst.append(str(song['id']))
        favnamelst.append(str(song['name']))
        favallst.append(str(song['al']['name']))
        favarlst.append([])
        for ar in song['ar']:
            favarlst[json['songs'].index(song)].append(ar['name'])

    for mid in favlst:
        rpg('正在下载('+str(favlst.index(mid)+1)+'/'+str(len(favlst))+'',favnamelst[favlst.index(mid)],(favlst.index(mid)/len(favlst))*100)
        if not os.path.exists(path+'/'+favnamelst[favlst.index(mid)]+".mp3"):#避免重复下载
            try:#遇到问题跳过而不中断
                res=requests.get(url="https://netease-cloud-music-cwl0bfvmk-totowang-hhh.vercel.app/song/url?id="+mid+"&br=320000"+'&cookie='+cookie)
                json=res.json()
                murl=json['data'][0]['url']
                if murl==None:
                    pga['text']='无版权 将跳过'
                    win.attributes('-alpha',1)
                    win.update()
                    time.sleep(5)
                    win.attributes('-alpha',0.6)
                    win.update()
                else:
                    res=requests.get(url=murl)
                    m=res.content
                    filename=favnamelst[favlst.index(mid)]
                    if '*' in favnamelst[favlst.index(mid)] or '/' in favnamelst[favlst.index(mid)] or '\\' in favnamelst[favlst.index(mid)] or ':' in favnamelst[favlst.index(mid)] or '"' in favnamelst[favlst.index(mid)] or \
                       '?' in favnamelst[favlst.index(mid)] or '|' in favnamelst[favlst.index(mid)]:#防止歌曲名称带非法字符导致下载失败
                        dorename=msgbox.askyesno('网易云收藏音乐下载','歌曲 '+favnamelst[favlst.index(mid)]+' 无法按照原名保存,您需要重命名吗?')
                        if dorename:
                            rwin = tk.Tk()
                            rwin.title("重命名音乐")

                            tk.Label(rwin,text='旧名称:'+favnamelst[favlst.index(mid)]+'.mp3').pack(fill=tk.X)
                            
                            ef=tk.Frame(rwin)
                            tk.Label(ef,text='新名称:').pack(fill=tk.X,side=tk.LEFT)
                            e_name = ttk.Entry(ef)
                            tk.Label(ef,text='.mp3').pack(fill=tk.X,side=tk.RIGHT)
                            e_name.pack(fill=tk.X)
                            ef.pack(fill=tk.X)
                            
                            ttk.Button(rwin,text='以该名称保存',command=rename).pack(fill=tk.X,expand=True)
                            
                            e_name.bind('<KeyPress-Return>',lambda event:rename())
                            e_name.focus()
                            
                            rwin.update()
                            if rwin.winfo_width()<=350:
                                rwin.geometry('350x'+str(int(rwin.winfo_height())))
                            else:
                                rwin.geometry(str(int(rwin.winfo_width()))+'x'+str(int(rwin.winfo_height())))
                            rwin.resizable(0,0)
                            while True:
                                try:#别无选择,只能通过在无法成功刷新的时候break出循环来实现效果
                                    rwin.update()
                                except:
                                    break
                            print('重命名操作完成 '+favnamelst[favlst.index(mid)])
                    print('下载中 '+favnamelst[favlst.index(mid)])
                    f=open(path+'/'+filename+".mp3",'wb')
                    f.write(m)
                    f.close()
            except Exception as e:
                #toaster.show_toast('可接受的错误','下载 '+favnamelst[favlst.index(mid)]+' 时遇到错误,将跳过本音乐\n\n'+str(e),duration=10)
                print(str(e))
                pga['text']='下载错误 将跳过'
                win.attributes('-alpha',1)
                win.update()
                time.sleep(5)
                win.attributes('-alpha',0.6)
                win.update()

    pga['text']='下载完成!'
    pgb['text']='恭喜!全部音乐下载完成!'
    win.configure(background='#90FF90')
    pga['bg']='#90FF90'
    pgb['bg']='#90FF90'
    win.attributes('-alpha',1)
    win.update()
    time.sleep(5)
    for i in range(1,27+1):
        win.geometry('250x68'+'+'+str(int(scr_w-250))+'+'+str(int(scr_h-(108-i*4))))
        time.sleep(0.01)
        win.update()
    win.destroy()
    exit()

except Exception as e:
    try:
        win.configure(background='#FF9090')
        pga['bg']='#FF9090'
        pgb['bg']='#FF9090'
        win.attributes('-alpha',1)
        pga['text']='错误'
        pgb['text']=json['message']
        win.update()
    except:
        win.configure(background='#FF9090')
        pga['bg']='#FF9090'
        pgb['bg']='#FF9090'
        win.attributes('-alpha',1)
        pga['text']='本地错误'
        pgb['text']=str(e)
        win.update()

就到这里吧

P.S.后面的这段我写了三次才写上去,博客园你这编辑器是不是对标User Inyerface啊!?

 

posted @ 2022-07-23 12:56  真_人工智障  阅读(78)  评论(2编辑  收藏  举报