比较菜所以先用面向过程形式写的,到后面确实有点乱,出现的主要问题代码里面都标注起来了,希望有大佬能指点一下,十分感谢了
音乐播放器文件
# 还有如下问题没解决
# 1.暂停/继续功能没有实现
# 2.爬虫对qq音乐/网易无效
# 3.试听音乐移动歌曲后提示标签销毁问题
# 4.当音乐过多,希望搜索指定音乐直接播放的功能没做成(找到容易,但是现有基础上想办法界面显示出来太复杂懒得弄)
# 5.tkinter的滚动条有问题,每次都弄不出来,最后只能设置按钮上下页多了不少代码
# 6.顺序播放与单曲循环时中途切换问题
# 7.正在播放音乐启动爬虫,还没选择下载的音乐时点击切换音乐会卡死,但是不将子窗口设置Toplevel无法获取点击选择下载的音乐(两个窗口抢Toplevel问题,
# 从一开始只做一个窗口可以避免但是查找歌单播放音乐时看的不够爽)
# 其他: 一般情况下最好不要设置了控件然后place,pack,grid(直接写成一行),因为有副作用可能会报错,拆分成多条语句就没问题
# 获取当前播放的音乐应该可以使用Stringvar()去get,这样不用time.sleep应该也能马上切换,但是不太清楚这个方法应该插在哪里,怎样插(目前的方法是直接删除Label标签并重新创建)
# playsound模块不清楚如何切换/暂停音乐,导致如果在播放.m4a音乐时切歌(非m4a)/暂停会是2首歌同时播放,会出现以下情况:
# 1.m4a先手,切歌后手,m4a先手播放完毕执行下一首,如果下一首占用pygame,切歌后手会被中断并播放m4a的下一首
# 2.m4a先手,切歌后手先播放完毕,index+1,然后m4a播放完毕,这时再次index+1播放m4a的下第二首,如果为非m4a回到第1条
from tkinter import ttk
import tkinter as tk
import os, shutil
import pygame # pygame模块不支持m4a格式
from playsound import playsound # playsound模块不支持flac格式
import threading
from time import sleep
from myTkinter import mySongSpider # 音乐爬虫
# 创建子线程
def create_thread(funcName, **kwargs):
sub_thread = threading.Thread(target=funcName, args=kwargs["args"], daemon=True)
sub_thread.start()
# 子线程启动播放/切换音乐(仅适用于pygame,播放m4a音乐再选择其他任意格式音乐会同时播放)
def songLoad(*args):
choose, index, window = args
global change, play_type
if choose == 1:
song_dir = f"D:\学习资料\我的音乐\我喜欢/"
end_index = len(myFavorList)
choose_song_list = myFavorList
elif choose == 0:
song_dir = f"D:\学习资料\我的音乐/"
end_index = len(myMusicList)
choose_song_list = myMusicList
lb = tk.Label(window, text=("当前播放的音乐: " + choose_song_list[index])) # 显示当前播放的音乐
lb.place(x=100, y=750)
# 从选择的音乐开始顺序播放
if play_type == 0:
if '.m4a' in choose_song_list[index]:
# playsounde模块
playsound(song_dir + choose_song_list[index])
lb.destroy() # 一首音乐播放完毕,销毁标签
else:
# pygame 模块
pygame.mixer.music.load(song_dir + choose_song_list[index])
pygame.mixer.music.set_volume(0.2)
pygame.mixer.music.play()
while pygame.mixer.music.get_busy():
if not change:
pygame.time.Clock().tick(10) # 设置帧率与切换音乐的time.sleep配合,避免手动切换音乐无法切换显示播放的音乐名称
else:
lb.destroy()
change = not change # 用户切换播放的音乐,结束当前子线程,避免多次切换音乐导致线程越创建越多
# 现在线程最多为4,即主线程,子线程1(pygame切换音乐前),子线程2(pygame切换音乐后)——点击切换时先创建子线程播放而不是先停止,所以这里数量可以是3
# 加上子线程3(爬虫获取网上音乐信息),共计4线程
return
# 判断是否中途切换成单曲循环,如果是则不跳转下一首
if play_type != 0:
lb.destroy() # 一首音乐播放完毕,销毁标签
songLoad(choose, index, window)
else:
if index < end_index:
index += 1
songLoad(choose, index, window)
# 单曲循环
elif play_type == 1:
if '.m4a' in choose_song_list[index]:
# playsounde模块
playsound(song_dir + choose_song_list[index])
songLoad(choose, index, window) # 循环播放(由于只做了pygame的切换,也没有做暂停功能,如果单曲.m4a音乐后如果想停止只能重启播放器)——————————————————————————————————————————
# 因此同样不支持单曲循环中途切换顺序播放
else:
# pygame 模块
pygame.mixer.music.load(song_dir + choose_song_list[index])
pygame.mixer.music.set_volume(0.2)
pygame.mixer.music.play(loops=-1)
while pygame.mixer.music.get_busy():
if change:
lb.destroy()
change = not change # 用户切换播放的音乐,结束当前子线程,避免多次切换音乐导致线程越创建越多
# 现在线程最多为4,即主线程,子线程1(pygame切换音乐前),子线程2(pygame切换音乐后)——点击切换时先创建子线程播放而不是先停止,所以这里数量可以是3
# 加上子线程3爬虫获取网上音乐信息,共计4上限
return
# 判断是否中途单曲改成顺序,是的话重新播放——貌似无法中途更改pygame.mixer.music.play(loops=0),只能重新播放音乐
if play_type != 1:
pygame.mixer.music.stop()
songLoad(choose, index, window)
# 创建子线程爬虫结果的小窗口
def child_window(*args):
spider_type, searchName = args
child_window = tk.Toplevel()
child_window.title("歌曲搜索中...")
child_window.geometry('700x700')
child_l = tk.Listbox(child_window, width=95, height=33)
child_l.place(x=5, y=5)
child_var = tk.StringVar()
page = 1
# 启动子线程爬虫
if spider_type == "酷狗音乐":
songList = mySongSpider.kugou_1(searchName)
elif spider_type == "QQ音乐":
qq_login = mySongSpider.qqmusicScanqr()
songList = qq_login.login(get_song=searchName)
elif spider_type == "网易云音乐":
songList = mySongSpider.wangyiyun_1(searchName)
# 下载音乐
def download_it():
if spider_type == "酷狗音乐":
download_info = mySongSpider.kugou_2(songList, child_var.get()) # 启动爬虫下载
elif spider_type == "QQ音乐":
download_info = qq_login.download_it(songList, child_var.get())
elif spider_type == "网易云音乐":
download_info = mySongSpider.wangyiyun_2(songList, child_var.get())
lb = tk.Label(child_window, text=download_info)
lb.place(x=300, y=610)
if isinstance(songList, str):
error_lb = tk.Label(child_l, text=songList, font=("微软雅黑", 20))
error_lb.place(x=30, y=300)
else:
# 爬虫小窗口还没优化(点击下一页是新生成的覆盖,原来的并没有销毁
def printDownloadSong(click=0):
nonlocal page
childplace_x, childplace_y = 10, 10
if ((click == -1) and (page >=2)) or ((click == 1) and (page <= (len(songList) / 10))):
page += click
if spider_type == "酷狗音乐":
for i in songList:
if ((page - 1) * 10) < int(i["id"]) <= (page * 10):
child_button = tk.Radiobutton(child_l, text=i["FileName"], value=i["id"], variable=child_var, command=download_it)
child_button.place(x=childplace_x, y=childplace_y)
childplace_y += 50
elif int(i["id"]) > (page * 10):
break
elif spider_type == "QQ音乐":
qq_lb = tk.Label(child_l, text="qq音乐爬虫未完善,暂不支持继续操作", font=("微软雅黑", 20))
qq_lb.place(x=50, y=300)
elif spider_type == "网易云音乐":
download_info = mySongSpider.wangyiyun_2(songList, "1") # 测试用代码(默认选择第一首)———————————————————————————————————————————————————————————————————————————————
wy_lb = tk.Label(child_l, text=download_info, font=("微软雅黑", 20))
wy_lb.place(x=50, y=300)
printDownloadSong(click=0)
child_last = tk.Button(child_window, text='上一页', command=lambda: printDownloadSong(click=-1)) # 上一页
child_last.config(width=10, height=2)
child_last.place(x=50, y=600)
child_next = tk.Button(child_window, text='下一页', command=lambda: printDownloadSong(click=1)) # 下一页
child_next.config(width=10, height=2)
child_next.place(x=200, y=600)
child_window.wait_window(child_window)
# 音乐播放器主窗口
def mySongPlayer():
window = tk.Tk()
window.title("我的音乐播放器")
window.geometry('1500x800')
pygame.init()
pygame.mixer.init()
# 函数嵌套保存歌单按钮信息,点击按钮时调用执行
def playMySong(choose, index):
def inner():
global is_play, change
def change_pause(): # 现在代码写法貌似不支持暂停/继续功能,可能还是要想办法改成要tk.StringVar方法——————————————————————————————————————————————————————————
global is_pause
is_pause = not is_pause
is_play = not is_play
# 创建一个按钮用于暂停/继续(还没写实现的功能,单纯只有一个按钮和标志的切换)—————————————————————————————————————————————————————————————————————————————————————————————————————
pause_button = tk.Button(window, text='继续', width=10, height=2, command=change_pause)
pause_button.place(x=15, y=730)
if is_play:
# 接收到播放歌单音乐信号,启动子线程
create_thread(songLoad, args=(choose, index, window,))
pause_button.config(text='暂停', command=change_pause) # 还没写实现的功能——————————————————————————————————————————————————————————————————————————————————————————
# print("当前的线程总数(包括主线程): ", len(threading.enumerate()))
else:
change = not change # 切换标志,让正在播放的子线程退出工作
sleep(0.1) # 等待正在播放音乐的标签销毁(使用StringVar方法可能更好)
if pygame.mixer.music.get_busy(): # 正在播放音乐时点击播放歌单音乐按钮,将当前播放的音乐停止
pygame.mixer.music.stop()
inner() # 并将新选择的歌曲启动播放,inner自己有is_play标志切换,直接自调用即可
return inner
# 指定窗口显示音乐
def print_mymusic(choose, onclick):
global now_page
nonlocal l, button_list
if onclick == False:
now_page = 1
elif onclick == 2:
if now_page >= 2:
now_page -= 1
place_x, place_y = 10, 10 # 控制歌单歌曲的排列
# 关键字搜索结果
def search_with_target(choose):
if choose == 1:
for i in myFavorList:
if search_target.get() in i:
print(i) # 把搜到的全部显示出来,需要按钮播放——————————————————————————————————————————————————————————————————————————————————————————————
elif choose == 0:
for i in myMusicList:
if (search_target.get() in i) and (i != "我喜欢"):
print(i) # 显示出来按钮播放,注意剔除"我喜欢"文件夹——————————————————————————————————————————————————————————————————————————————————
elif choose == 2:
for i in auditionList:
if search_target.get() in i:
print(i) # 还是移动音乐功能————————————————————————————————————————————————————————————————————————————————————————————————————
# 指定关键字音乐搜索————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
search_target = tk.Entry(window, width=25)
search_target.place(x=600, y=20)
search_target_button = tk.Button(window, text="关键字搜索音乐", command=lambda : search_with_target(choose))
search_target_button.config(width=20)
search_target_button.place(x=800, y=20)
# 我喜欢
if choose == 1:
if onclick == 3:
if now_page <= (len(myFavorList) / 60):
now_page += 1
if len(button_list) != 0: # 不是第一次生成音乐列表,销毁上次产生的按钮
l.destroy() # 不知道如何清除歌单区域的按钮,直接删除整个歌单区域并重新创建
l = tk.Listbox(window, width=193, height=34)
l.place(x=100, y=100)
button_list.clear()
for index in range(len(myFavorList)):
if (((now_page - 1) * 60) <= index < (now_page * 60)): # 一页展示60首
if place_x >= 1200:
place_x = 10
place_y += 30
song = tk.Button(l, text=str(myFavorList[index]), command=playMySong(choose, index)) # 每个按钮都是一个inner函数
song.config(width=60, height=1)
button_list.append(song)
button_list[index % 60].place(x=place_x, y=place_y)
place_x += 450
elif (index >= (now_page * 60)): # 展示大于60首后直接中断循环,节省时间
break
last_page = tk.Button(window, text='上一页',
command=lambda: print_mymusic(choose=1, onclick=2)) # 上一页
last_page.config(width=10, height=2)
last_page.place(x=15, y=600)
next_page = tk.Button(window, text='下一页',
command=lambda: print_mymusic(choose=1, onclick=3)) # 下一页
next_page.config(width=10, height=2)
next_page.place(x=15, y=650)
# 普通音乐
elif choose == 0:
if onclick == 3:
if now_page < (len(myMusicList) / 60):
now_page += 1
if len(button_list) != 0: # 不是第一次生成音乐列表,销毁上次产生的按钮
l.destroy() # 不知道如何清除歌单区域的按钮,直接删除整个歌单区域并重新创建
l = tk.Listbox(window, width=193, height=34)
l.place(x=100, y=100)
button_list.clear()
favor_dir = 0
for index in range(len(myMusicList)):
if (((now_page - 1) * 60) <= index < (now_page * 60)): # 一页展示60首
if myMusicList[index] == '我喜欢': # 剔除文件夹"我喜欢",会有一页有空缺只有59首
favor_dir = 1
else:
if place_x >= 1200:
place_x = 10
place_y += 30
if not favor_dir:
song = tk.Button(l, text=str(myMusicList[index]), command=playMySong(choose, index))
song.config(width=60, height=1)
button_list.append(song)
button_list[index % 60].place(x=place_x, y=place_y)
else:
song = tk.Button(l, text=str(myMusicList[index]), command=playMySong(choose, index-1))
song.config(width=60, height=1)
button_list.append(song)
button_list[(index - 1) % 60].place(x=place_x, y=place_y)
place_x += 450
elif (index >= (now_page * 60)): # 大于60首后直接中断循环,节省时间
break
last_page = tk.Button(window, text='上一页',
command=lambda: print_mymusic(choose=0, onclick=2)) # 上一页
last_page.config(width=10, height=2)
last_page.place(x=15, y=600)
next_page = tk.Button(window, text='下一页',
command=lambda: print_mymusic(choose=0, onclick=3)) # 下一页
next_page.config(width=10, height=2)
next_page.place(x=15, y=650)
# 试听音乐文件夹(网上下载的音乐试听)
else:
if onclick == 3:
if now_page <= (len(auditionList) / 60):
now_page += 1
if len(button_list) != 0: # 不是第一次生成音乐列表,销毁上次产生的按钮
l.destroy() # 不知道如何清除歌单区域的按钮,直接删除整个歌单区域并重新创建
l = tk.Listbox(window, width=193, height=34)
l.place(x=100, y=100)
button_list.clear()
var = tk.StringVar()
def moveSong(**kwargs):
global auditionList
move_music = tk.Button(window, text="移至普通音乐", command=lambda: moveSong(type=0))
move_music.config(width=10, height=2)
move_music.place(x=300, y=730)
move_favor = tk.Button(window, text="移至我喜欢", command=lambda: moveSong(type=1))
move_favor.config(width=10, height=2)
move_favor.place(x=200, y=730)
if len(kwargs) != 0:
if kwargs["type"] == 0:
auditionList.remove(var.get())
myMusicList.insert(0, var.get())
shutil.move(("./试听音乐/" + var.get()), "D:\学习资料\我的音乐/")
text = var.get() + "-> D:\学习资料\我的音乐/" + var.get() + "移动成功"
move_lb = tk.Label(window, text=text)
move_lb.place(x=500, y=750)
# move_music.destroy() # 自调用生成了两次按钮,但是即使拆分函数还是销毁不了,不清楚怎样销毁window上指定东西———————————————————————————————————————————————
# move_favor.destroy()
# move_lb.destroy() # 这里其实希望点击3歌单之一时才销毁提示标签,没想到怎样实现——————————————————————————————————————————————————————————————————————
elif kwargs["type"] == 1:
auditionList.remove(var.get())
myFavorList.insert(0, var.get())
shutil.move(("./试听音乐/" + var.get()), "D:\学习资料\我的音乐\我喜欢/")
text = var.get() + "-> D:\学习资料\我的音乐\我喜欢/" + var.get() + "移动成功"
move_lb = tk.Label(window, text=text)
move_lb.place(x=500, y=750)
# move_music.destroy() # 自调用生成了两次按钮,但是即使拆分函数还是销毁不了,不清楚怎样销毁window上指定东西———————————————————————————————————————————————
# move_favor.destroy()
# move_lb.destroy() # 这里其实希望点击3歌单之一时才销毁提示标签,没想到怎样实现——————————————————————————————————————————————————————————————————————
# 更新试听音乐歌单
l.destroy()
print_mymusic(choose, onclick=0)
for index in range(len(auditionList)):
if (((now_page - 1) * 60) <= index < (now_page * 60)): # 一页展示60首
if place_x >= 1200:
place_x = 10
place_y += 30
song = tk.Radiobutton(l, text=str(auditionList[index]), value=str(auditionList[index]), variable=var, command=moveSong)
song.config(width=60, height=1)
button_list.append(song)
button_list[index % 60].place(x=place_x, y=place_y)
place_x += 450
elif (index >= (now_page * 60)): # 展示大于60首后直接中断循环,节省时间
break
last_page = tk.Button(window, text='上一页',
command=lambda: print_mymusic(choose=1, onclick=2)) # 上一页
last_page.config(width=10, height=2)
last_page.place(x=15, y=600)
next_page = tk.Button(window, text='下一页',
command=lambda: print_mymusic(choose=1, onclick=3)) # 下一页
next_page.config(width=10, height=2)
next_page.place(x=15, y=650)
# 爬虫搜索音乐
def getTheSong(*args):
text = "正在使用 " + typelist.get() + " 方式搜索歌曲: ", get_song.get() + "..."
tk.Label(window, text=text).place(x=490, y=20)
create_thread(child_window, args=(typelist.get(), get_song.get()))
# 刚启动程序的窗口界面
tk.Label(window, text='点击选择歌单').place(x=20, y=5)
# 搜索歌曲label,输入框及搜索按钮
tk.Label(window, text='搜索歌曲: ').place(x=200, y=20)
get_song = tk.Entry(window)
get_song.place(x=270, y=20)
get_song_button = tk.Button(window, text='搜索', command=getTheSong)
get_song_button.config(width=10)
get_song_button.place(x=400, y=18)
# 搜索方式下拉列表
tk.Label(window, text='搜索方式: ').place(x=200, y=50) #创建下拉列表切换选择搜索酷狗,qq音乐,网易云的音乐
typelist = tk.StringVar()
comboxlist = ttk.Combobox(window, textvariable=typelist)
comboxlist["values"] = ("酷狗音乐", "QQ音乐", "网易云音乐",)
comboxlist.current(0)
comboxlist.place(x=270, y=50)
def change_play_type(*args):
global play_type
if comboxlist1.get() == "顺序播放":
play_type = 0
elif comboxlist1.get() == "单曲循环":
play_type = 1
# 播放模式下拉列表
tk.Label(window, text='播放模式: ').place(x=200, y=75)
typelist1 = tk.StringVar()
comboxlist1 = ttk.Combobox(window, textvariable=typelist1)
comboxlist1["values"] = ("顺序播放", "单曲循环",)
comboxlist1.current(0)
comboxlist1.bind("<<ComboboxSelected>>", change_play_type)
comboxlist1.place(x=270, y=75)
# 显示歌单的窗口
l = tk.Listbox(window, width=193, height=34)
l.place(x=100, y=100)
button_list = [] # 判断是否已存在按钮
# 选择打开我喜欢/普通音乐文件夹的按钮
my_favor = tk.Button(window, text='我喜欢', command=lambda: print_mymusic(choose=1, onclick=False)) # 我喜欢音乐按钮
my_favor.config(width=10, height=2) # 按钮的尺寸大小
my_favor.place(x=10, y=30) # 把按钮放到窗口指定位置(pack, grid, place只能同时用一个,不能混用)
my_music = tk.Button(window, text='普通音乐', command=lambda : print_mymusic(choose=0, onclick=False)) # 普通音乐按钮
my_music.config(width = 10, height = 2)
my_music.place(x=10, y=75)
audition_music = tk.Button(window, text='试听音乐', command=lambda: print_mymusic(choose=2, onclick=False)) # 试听音乐按钮(网上下载的音乐)
audition_music.config(width=10, height=2)
audition_music.place(x=10, y=120)
window.mainloop()
if __name__ == '__main__':
# 一些全局变量
myFavorList = os.listdir("D:\学习资料\我的音乐\我喜欢")
myMusicList = os.listdir("D:\学习资料\我的音乐")
auditionList = os.listdir("试听音乐")
is_play, is_pause, change = False, False, False # 判断是否正在播放,暂停,切换, 改变播放模式类型
now_page, play_type = 1, 0
mySongPlayer()
这个playsound和pygame的情况就像这个图
爬虫文件(QQ音乐的sign值实在弄不出来,网易只能爬到输入框输入时的下拉推荐列表)
import requests # 将来http2普及,如果requests模块还没更新,更改使用httpx模块
import logging
import time
import execjs
import re
import json
from fake_useragent import UserAgent
import os, sys, subprocess, random
from Crypto.Cipher import AES
from base64 import b64encode
from selenium.webdriver import Chrome
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.options import Options
ua = UserAgent()
logging.basicConfig(filemode='a', filename='error.txt', format='%(asctime)s %(levelname)s [位置: %(filename)s:%(lineno)d] : %(message)s')
# 获取加密参数
def use_execjs(**kwargs):
filename = kwargs["filename"] # 处理加密需要的文件名
funcname = kwargs["funcname"] # 调用的函数名
params = kwargs["params"] # 需要的参数(传过来需要一个元组,即使没有参数也要是小括号)
node = execjs.get()
ctx = node.compile(open(filename, encoding='utf-8').read())
funcName = funcname + str(params)
encrypt_words = ctx.eval(funcName)
return encrypt_words
# 酷狗音乐
def kugou_1(get_song):
# 第一次请求,搜索歌单获取有哪些歌
url1 = 'https://complexsearch.kugou.com/v2/search/song?'
t1 = str(round(time.time() * 1000))
sig = f"NVPh5oo715z5DIWAeQlhMDsWXXQV4hwtbitrate=0callback=callback123clienttime={t1}clientver=2000dfid=-inputtype=0iscorrection=1isfuzzy=0" \
f"keyword={get_song}mid={t1}page=1pagesize=30platform=WebFilterprivilege_filter=0srcappid=2919token=userid=0uuid={t1}NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt"
signature = use_execjs(filename="kg_signature.js", funcname="faultylabs.MD5", params=(sig, ))
params1 = {
"callback": "callback123",
"keyword": get_song,
"page": "1",
"pagesize": "30",
"bitrate": "0",
"isfuzzy": "0",
"inputtype": "0",
"platform": "WebFilter",
"userid": "0",
"clientver": "2000",
"iscorrection": "1",
"privilege_filter": "0",
"token": "",
"srcappid": "2919",
"clienttime": t1,
"mid": t1,
"uuid": t1,
"dfid": "-",
"signature": signature,
}
logging.debug("酷狗爬虫正在获取第一页30条歌曲信息")
resp1 = requests.get(url1, params=params1).text
moban1 = 'callback123\((.*)\)'
song_list = re.findall(moban1, resp1, re.S)[0]
data_dict = json.loads(song_list)["data"]["lists"]
id = 1
b_list = []
for data in data_dict:
dic = {}
dic["id"] = str(id)
dic["FileName"] = data["FileName"]
dic["FileHash"] = data["FileHash"]
dic["AlbumID"] = data["AlbumID"]
b_list.append(dic)
id += 1
# print(dic)
return b_list
def kugou_2(b_list, choose):
for i in b_list:
if i["id"] == choose:
get_info = i
logging.debug("酷狗爬虫请求具体音乐信息")
# 第二次请求,获取指定歌曲的的播放url(碰上jQuery紧随20位随机数加时间戳)
# 这个是jQuery在进行跨域请求的时候利用jsonp进行处理造成的现象,
# 1.需要设置Referer头,因为这是跨域请求,这也是为什么直接复制到Postman会失效的原因,Postman中加了Referer头之后也能成功;
# 2._=13位时间戳
# 3.想要获得原始的JSON数据,在获得返回数据后,直接前后的JSONP的padding删去即可;
# 方法1: 不发送jQuery这个随机值和时间戳过去,请求头Referer也去掉即可(这里用方法1)
# 方法2: 用jsnp和callback搜索,是随机值
url2 = 'https://wwwapi.kugou.com/yy/index.php?'
if get_info["AlbumID"] != "":
params2 = {
"r": "play/getdata",
"hash": get_info["FileHash"],
"dfid": "4VymLa43965C3ouXOp4JzU92",
"appid": "1014",
"mid": "4f61630fc9add9abb4128a6119732fb8",
"platid": "4",
"album_id": get_info["AlbumID"],
}
else:
params2 = {
"r": "play/getdata",
"hash": get_info["FileHash"],
"dfid": "4VymLa43965C3ouXOp4JzU92",
"appid": "1014",
"mid": "4f61630fc9add9abb4128a6119732fb8",
"platid": "4",
}
headers2 = {
"user-agent": ua.random,
}
resp2 = requests.get(url2, params=params2, headers=headers2).json()
play_url = resp2["data"]["play_url"].replace('\\', '')
houzhui = play_url.split(".")[-1]
download_song_path = "./试听音乐/" + get_info["FileName"] + "." + houzhui
# 第3次请求,请求播放歌曲的url(play_url),下载音乐
logging.debug("酷狗爬虫下载音乐")
resp3 = requests.get(play_url)
with open(download_song_path, "wb") as f:
f.write(resp3.content)
return "下载成功,存放到的路径: " + download_song_path
else:
logging.error("没有想要的歌曲信息")
return "没有想要的歌曲信息"
# QQ音乐的登录使用原文https://blog.csdn.net/pythonlaodi/article/details/110871422
# 密码登录,电脑QQ授权登录都可能需要手机验证,直接使用二维码扫码固定方式登录
def showImage(img_path):
try:
if sys.platform.find('darwin') >= 0:
subprocess.call(['open', img_path])
elif sys.platform.find('linux') >= 0:
subprocess.call(['xdg-open', img_path])
else:
os.startfile(img_path)
except:
from PIL import Image
img = Image.open(img_path)
img.show()
img.close()
def removeImage(img_path):
if sys.platform.find('darwin') >= 0:
os.system("osascript -e 'quit app \"Preview\"'")
os.remove(img_path)
def saveImage(img, img_path):
if os.path.isfile(img_path):
os.remove(img_path)
fp = open(img_path, 'wb')
fp.write(img)
fp.close()
class qqmusicScanqr():
is_callable = True
def __init__(self, **kwargs):
for key, value in kwargs.items(): setattr(self, key, value)
self.info = 'login in qqmusic in scanqr mode'
self.cur_path = os.getcwd()
self.session = requests.Session()
self.__initialize()
'''登录函数'''
def login(self, username='', password='', crack_captcha_func=None, get_song=None, **kwargs):
# 设置代理
self.session.proxies.update(kwargs.get('proxies', {}))
# 获得pt_login_sig
params = {
'appid': '716027609',
'daid': '383',
'style': '33',
'login_text': '授权并登录',
'hide_title_bar': '1',
'hide_border': '1',
'target': 'self',
's_url': 'https://graph.qq.com/oauth2.0/login_jump',
'pt_3rd_aid': '100497308',
'pt_feedback_link': 'https://support.qq.com/products/77942?customInfo=.appid100497308',
}
response = self.session.get(self.xlogin_url, params=params)
pt_login_sig = self.session.cookies.get('pt_login_sig')
# 获取二维码
params = {
'appid': '716027609',
'e': '2',
'l': 'M',
's': '3',
'd': '72',
'v': '4',
't': str(random.random()),
'daid': '383',
'pt_3rd_aid': '100497308',
}
response = self.session.get(self.ptqrshow_url, params=params)
saveImage(response.content, os.path.join(self.cur_path, 'qrcode.jpg'))
showImage(os.path.join(self.cur_path, 'qrcode.jpg'))
qrsig = self.session.cookies.get('qrsig')
ptqrtoken = self.__decryptQrsig(qrsig)
# 检测二维码状态
while True:
params = {
'u1': 'https://graph.qq.com/oauth2.0/login_jump',
'ptqrtoken': ptqrtoken,
'ptredirect': '0',
'h': '1',
't': '1',
'g': '1',
'from_ui': '1',
'ptlang': '2052',
'action': '0-0-%s' % int(time.time() * 1000),
'js_ver': '20102616',
'js_type': '1',
'login_sig': pt_login_sig,
'pt_uistyle': '40',
'aid': '716027609',
'daid': '383',
'pt_3rd_aid': '100497308',
'has_onekey': '1',
}
response = self.session.get(self.ptqrlogin_url, params=params)
print(response.text)
if '二维码未失效' in response.text or '二维码认证中' in response.text:
logging.debug("qq音乐登录,二维码未失效/认证中")
elif '二维码已经失效' in response.text:
logging.error("qq音乐登录,二维码失效")
raise RuntimeError('Fail to login, qrcode has expired')
else:
break
time.sleep(0.5)
removeImage(os.path.join(self.cur_path, 'qrcode.jpg'))
# 登录成功
qq_number = re.findall(r'&uin=(.+?)&service', response.text)[0]
url_refresh = re.findall(r"'(https:.*?)'", response.text)[0]
response = self.session.get(url_refresh, allow_redirects=False, verify=False)
logging.debug('QQ音乐账号「%s」登陆成功' % qq_number)
self.after_login(get_song, qq_number)
'''qrsig转ptqrtoken, hash33函数'''
def __decryptQrsig(self, qrsig):
e = 0
for c in qrsig:
e += (e << 5) + ord(c)
return 2147483647 & e
'''初始化'''
def __initialize(self):
self.headers = {
'User-Agent': ua.random,
}
self.ptqrshow_url = 'https://ssl.ptlogin2.qq.com/ptqrshow?'
self.xlogin_url = 'https://xui.ptlogin2.qq.com/cgi-bin/xlogin?'
self.ptqrlogin_url = 'https://ssl.ptlogin2.qq.com/ptqrlogin?'
self.session.headers.update(self.headers)
# 获取音乐数据
self.url1 = 'https://u.y.qq.com/cgi-bin/musics.fcg?'
# 获取具体音乐url
self.url2 = ''
# 下载音乐
self.url3 = ''
def after_login(self, get_song=None, qq_number=None):
# qq音乐接口(差个sign实在整不出来,转非加密通道了), 加密方式是__sign_hash_20200305
# https://u.y.qq.com/cgi-bin/musicu.fcg 支持加密和非加密
# https://u.y.qq.com/cgi-bin/musics.fcg 仅支持加密
t1 = str(round(time.time() * 1000))
searchid = use_execjs(filename="qm_searchid.js", funcname="getSearchid", params=())
data = {"comm":
{"cv":4747474,
"ct":24,
"format":"json",
"inCharset":"utf-8",
"outCharset":"utf-8",
"notice":0,
"platform":"yqq.json",
"needNewCode":1,
"uin":qq_number,
"g_tk_new_20200303":310935989,
"g_tk":310935989},
"req_1":
{"method":"DoSearchForQQMusicDesktop",
"module":"music.search.SearchCgiService",
"param":
{"remoteplace":"txt.yqq.top",
"searchid":searchid,
"search_type":0,
"query":get_song,
"page_num":1,
"num_per_page":10
}
}
}
# sign = use_execjs(filename="qm_sign.js", funcname="getSign", params=(data,)) # 需要处理,找到了加密位置但是看不懂,暂时跳过(将data以md5或__sign_hash_20200305加密)
params = {
"_": t1,
# "sign": sign,
}
# logging.debug("qq音乐请求第一页10条歌曲信息")
# resp1 = self.session.post(self.url1, params=params, data=data).json()
# song_lists = resp1["req_1"]["data"]["body"]["song"]["list"]
# print(song_lists)
logging.warning("qq音乐爬虫未完善,暂不支持继续操作")
return "qq音乐爬虫未完善,暂不支持继续操作"
# 下载音乐
def download_it(self, songList, choose):
pass
# 网易加密处理函数
def encrypt_text(text):
e = "010001"
f = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf69528010" \
"4e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
g = "0CoJUm6Qyw8W8jud"
# 定死随机值i,encSecKey随之固定
i = "CI8U1f30P53cVzSD"
encSecKey = "bf6de17a21ee091867e7d5faf35d6a85c119a8bbb84e241f90219f0197a0c138f54f7b4a99186e36680f0f409c9ac2e3ab6f4a8afba1e1c0451de3811a03442eb38cbb" \
"1fc92144c57f0d394989cd4765ff709d1b9781c8cb71a7fdd7b195e1848050dd80285f195032a8379fdccf190f07b06ca7ab8c533611d17c1265a9a066"
# params的加密由song_str与key3使用函数b(a, b)加密一次获得结果h.encText
# 将h.encText与i再次使用函数b(a, b)加密获得
def getParams(a, b): # a需要加密的文本, b密钥
key = "0102030405060708"
# 要求a的长度为16的倍数
length = len(a.encode('utf-8'))
pad = 16 - (length % 16)
a += (chr(pad) * pad)
aes = AES.new(key=b.encode('utf-8'), iv=key.encode('utf-8'), mode=AES.MODE_CBC) # iv偏移量
encryptText = aes.encrypt(a.encode('utf-8'))
return str(b64encode((encryptText)), 'utf-8')
first = getParams(text, g) #需要text为json字符串
params = getParams(first, i)
print("params: ", params)
return params, encSecKey
# 网易云音乐
def wangyiyun_1(get_song):
# 第一次请求,搜索歌单获取有哪些歌
url1 = 'https://music.163.com/weapi/search/suggest/web?csrf_token=' # 这个网址只是输入时下拉列表的推荐歌单,并不是真正数据, js还看到weapi改成api但不清楚做什么
"https://music.163.com/weapi/cloudsearch/get/web?csrf_token=" # 真正的数据页,加密结果不对,就是不清楚请求参数未加密时是什么,肯定不是song_str
song_str = '{"s":"%s","limit":"8","csrf_token":""}' % get_song
params1, encSecKey1 = encrypt_text(song_str)
data = {
"params": params1,
"encSecKey": encSecKey1,
}
headers = {
"origin": "https://music.163.com",
"referer": "https://music.163.com/search/",
"user-agent": ua.random,
"accept": "*/*",
"accept-encoding": "gzip, deflate, br",
"accept-language": "zh-CN,zh;q=0.9",
"content-type": "application/x-www-form-urlencoded",
"cookie": "_iuqxldmzr_=32; _ntes_nnid=51c8b57ec94de94a4067307d5363dfa3,1622541211235; _ntes_nuid=51c8b57ec94de94a4067307d5363dfa3; "
"NMTID=00Ot9Mx7P21MNGAGkExqVzTug8n46IAAAF5xv4TLw; WEVNSM=1.0.0; WM_TID=n28apPVdx3hEBFEEQVNu0vV56vlWPvUn; WNMCID=rzooic.1630248738073.01.0; "
"_ga=GA1.1.1751570701.1636894838; Qs_lvt_382223=1636894837; _clck=10oeh04|1|ewf|0; Qs_pv_382223=1718244415921661000%2C1832664885106975200; "
"_ga_C6TGHFPQ1H=GS1.1.1636894837.1.1.1636894933.0; WM_NI=Rc0kkgRIXH92fDCUUPElwRjWnym7zUt1apPbT%2BvMQf6iplJsqnlR%2F%2BGZlrRdnMgcqae50edLDs814g"
"TZnUS%2FR84Mlg6lfXLqQDtqbJdYTfP41Y2wk3h0P8KgLb0KP0MQTlk%3D; WM_NIKE=9ca17ae2e6ffcda170e2e6ee98ea70bc90aaacb268b5868aa6c84a828f8f84b67afb979ea"
"fcb7b90bd8d8ae72af0fea7c3b92a92bd008cea6d929798a9bc3aa69c8db6d85c87b4b9b3e95daea7a3d8f95fb8efaf91d36a98f5baaed46ef6b7c08dc979b3b2a4b3ae218586a"
"493d170ae93a984f46ba7eb0086e74888adf8d2b76d9886a2b6d22592e8fcaeef6df6b4aab9cf7b939cbc92e142a8879c87d340f387fe98ea39acf0a0d9ca6d90be00acd33c8f8f9c8dea37e2a3; "
"ntes_kaola_ad=1; JSESSIONID-WYYY=qqPYZyXZPesEWnvW3K3gBvpY46UNQkbEvyoOdrT03vjsWX4Vm8mR67hk9wg3We1bfWW%2F74DbpY%5CdowNS563sSmv4BGjBrawcfzmqB3dUc9iDXS7RuUmFGvZ1haWIjdkQZk%5CcN%2FZkDrgsP2Zq0gpKQljpXrprPt6jECISfYvx1sibjhlB%3A1647442749410"
}
# JSESSIONID-WYYY=qqPYZyXZPesEWnvW3K3往后可能会变?(cookies)
logging.debug("网易云爬虫正在请求歌单")
resp1 = requests.post(url1, data=data, headers=headers)
data = resp1.json()["result"]
data_list = []
num = 1
for it in data:
length = len(data[it])
for i in range(length):
if isinstance(data[it][i], dict):
data[it][i]["num"] = num
data_list.append(data[it][i])
print(data[it][i])
num += 1
# 使用selenium
# deseird_capabilities = DesiredCapabilities.CHROME
# deseird_capabilities["pageLoadStrategy"] = "none"
#
# opt = Options()
# opt.add_argument("--headless")
# opt.add_argument("--disbale-gpu")
# driver = Chrome(options=opt)
#
# driver.maximize_window()
#
# try:
# # action = ActionChains(driver)
# # action.send_keys(Keys.F12).perform()
# driver.get(f'https://music.163.com/#/search/m/?s={get_song}')
#
# time.sleep(5)
# print(driver.switch_to.frame('g_iframe')) # 找不到iframe
# driver.find_element_by_xpath('//div[@class="srchsongst"]/text()')
#
# except Exception as e:
# print("出错原因: ", e)
# driver.close()
# time.sleep(3)
# wangyiyun(get_song)
# finally:
# driver.close()
return data_list
def wangyiyun_2(data_list, choose):
for it in data_list:
if choose == str(it["num"]):
get_info = it
# 第2次请求获取具体歌曲的下载url(再次post请求提取json中的url)
url2 = 'https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token='
text = '"{"ids":"%s","level":"standard","encodeType":"aac","csrf_token":""}"' % get_info[
"id"] # 请求参数未加密时可能是这个
params2, encSecKey2 = encrypt_text(text)
data = {
"params": params2,
"encSecKey": encSecKey2,
}
resp2 = requests.post(url2, data=data)
print("响应状态码: ", resp2.status_code, "响应内容: ", resp2.text) # 响应200但无内容,因此上面需要加密的参数应该是错的(
logging.warning("网易爬虫未完善,暂不支持继续操作")
# 第3次请求下载音乐(get)
# url3 = '(url2响应内容中提取出来的url)'
# resp3 = requests.get(url3)
# with open(, "wb") as f:
# f.write(resp3.content)
return "网易爬虫未完善,暂不支持继续操作"
else:
logging.error("没有想要的歌曲信息")
return "没有想要的歌曲信息"
if __name__ == '__main__':
get_song = '夜曲'
# text = kugou_1(get_song)
# qq_login = qqmusicScanqr()
# qq_login.login(get_song=get_song)
wangyiyun_1(get_song)