├── .gitignore ├── .idea ├── RasNeteaseMusic.iml ├── misc.xml ├── modules.xml ├── vcs.xml └── workspace.xml ├── README.md ├── WxNeteaseMusic.py ├── itchat ├── __init__.py ├── components │ ├── __init__.py │ ├── contact.py │ ├── hotreload.py │ ├── login.py │ ├── messages.py │ └── register.py ├── config.py ├── content.py ├── core.py ├── log.py ├── returnvalues.py ├── storage.py └── utils.py ├── myapi.py ├── neteaseApi ├── __init__.py ├── api.py ├── cache.py ├── config.py ├── const.py ├── logger.py ├── menu.py ├── osdlyrics.py ├── player.py ├── scrollstring.py ├── singleton.py ├── storage.py ├── terminalsize.py ├── ui.py └── utils.py ├── run.py ├── test.py └── userInfo /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | -------------------------------------------------------------------------------- /.idea/RasNeteaseMusic.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #微信-网易云音乐播放器 2 | 3 | 注意,该版本为电脑版(Windows/Linux/OSX),如果需要使用树莓派,[请点击这里](https://github.com/yaphone/RasWxNeteaseMusic)。 4 | 5 | ## 来源 6 | 7 | 之前毕业的时候实在闲的无聊,正好手头上有个树莓派,就写了个简单的网易云音乐播放器,代码很简单,写的也很乱,功能更简单--只能搜索歌曲,然后播放之,放在了github上,没想到竟然收到三十多颗星,实在惭愧,然后放年假,就想着把功能稍微完善一下,于是就有了[WxNeteaseMusic](https://github.com/yaphone/RasWxNeteaseMusic),其实做的工作也不多,基于[itchat](https://github.com/littlecodersh/ItChat)和[网易去音乐的python API](https://github.com/yaphone/musicbox),废话不多说,容我简单介绍一下吧。 8 | 9 | 我的场景是这样的,实验室是有一台电脑放音乐的,大家切歌就要跑到那里操作,比较麻烦,后来我就想做个后台,用微信来操作切歌这些,这样大家只要加了我的微信号,发相关指令就可以了,还是比较方便的。再后来,电脑换成了树莓派,我就又移植到了树莓派上。不过这里吐槽一下,树莓派的原生音质确实渣,我们后来买了个 DAC ,完美。😁 10 | 11 | 12 | 13 | ## 安装 14 | 15 | 项目源码都都在[我的Github](https://github.com/yaphone/RasWxNeteaseMusic)上,大家先下载下来,麻烦大家顺手点个star哟~,谢谢。 16 | 我们以Ubuntu环境为例,安装其实很简单,都是一些python的pip依赖包: 17 | 18 | - sudo apt-get install python-dev 19 | - sudo pip install requests 20 | - sudo pip install future 21 | - sudo pip install crypto 22 | - sudo pip install bs4 23 | - sudo pip install pycrypto 24 | - sudo pip install mp3play 25 | 26 | 上面这些依赖应该够了,如果提示缺少包的话,大家根据提示自行安装就可以了,切换到WxNeteaseMusic目录,执行python run.py 27 | 用微信扫码登陆,Bingo, just enjoy it ! 28 | 29 | ## 功能 30 | 嗯,先来看看都有什么功能。 31 | 32 | - H: 帮助信息 33 | - L: 登陆网易云音乐 34 | - U: 用户歌单 35 | - M: 播放列表 36 | - N: 下一曲 37 | - R: 正在播放 38 | - S: 歌曲搜索 39 | - T: 热门单曲 40 | - G:推荐歌单 41 | - E: 退出 42 | 43 | 这就是WxNeteaseMusic V0.1版的功能菜单啦,后面如果大家有其它的需求或者使用过程中有什么问题,都可以提出来,github上提Issue或者在下面评论都可以,后面我会尽量完善。 44 | 45 | ## 使用 46 | 47 | 微信扫码登陆后,向登陆的微信号发送命令,就可以使用了。我的微信号是可以自己向自己发送信息的,使用起来比较方便,但是有些微信号好像不能自己给自己发信息,这种情况下,就需要通过另一个微信号向扫码登陆的微信号发命令。这里需要注意,扫码的时候itchat是以网页版/电脑版的方式登陆微信的,如果扫码的手机退出微信客户端,那么WxNeteaseMusic自然也不能正常使用。不过也有手机退出微信但是网页版/电脑版不退出的办法,大家自行百度一下。 48 | 如果大家看一下代码就会发现,WxNeteaseMusic是以空格为分隔符来切割命令的,所以对于有两个或者三个参数的命令时,需要以空格为分隔符,下面我具体来介绍一下。 49 | 50 | ### 获取帮助信息 51 | 52 | 发送 `H`。 53 | 54 | ### 登陆网易云音乐 55 | 56 | 命令格式为 `L 用户名 密码`,注意,`L`和`用户名`、`密码`之间以空格分开,这里的用户名和密码是你的网易云音乐的用户名和密码,邮箱格式。之后客户端会收到一条消息,登陆成功或者登陆失败,如果登陆成功,WxNeteaseMusic会保存你的UserId,所以并不需要每次使用都要登陆账号,除非要换其它账号,UserId在网易云音乐中是唯一的,用户的歌单、收藏列表等信息都是通过UserId来获取的。登陆成功后,就可以使用下面的功能了,默认是我的UserId哦,别忘记登录呀~ 57 | 58 | ### 获取用户歌单 59 | 60 | 登陆成功后,播放列表默认为网易云音乐的热歌榜,些时发送`U`可以获取用户的歌单,就是你在网易云音乐创建的歌单,获取歌单后,通过命令`U 序号`来选择对应的歌单,注意`U`和`序号`之间有空格,此时播放列表是你歌单里的歌曲。 61 | 62 | ### 播放列表 63 | 64 | 使用过程中,发送`M`可以随时查看当时的播放列表。 65 | 66 | ### 下一曲 67 | 68 | 发送命令`N`来播放下一曲,`N 序号`播放列表中对应的歌曲,当前列表通过命令`M`获取。这里需要注意,通过`N 序号`选择列表中的歌曲时,播放是临时的,并不保存在播放列表中,此时再发`R`命令时显示的播放信息是错误的。 69 | 70 | ### 正在播放 71 | 72 | 发送命令`R`可获取正在播放的歌曲详情。 73 | 74 | ### 歌曲搜索 75 | 76 | 发送命令`S 歌曲名`可进行歌曲搜索,成功后会返回搜索结果列表,再发送`S 歌曲名 序号`来播放对应序号的歌曲,注意,两次命令的歌曲名必须完全一致。 77 | 78 | ### 热门单曲榜 79 | 80 | 发送`T`获取网易云音乐的热门单曲榜,并更新播放列表。 81 | 82 | ### 推荐歌单 83 | 84 | 发送`G`获取网易云音乐的热门单曲榜,并更新播放列表。 85 | 86 | ### 退出 87 | 88 | 发送`E`退出播放,此时播放列表变为空,用户如果要恢复播放,需要获取歌单更新播放列表。 89 | 90 | ## 功能演示 91 | 92 | 好吧好吧,说了这么多,还是让我来实际来演示一下吧。注意,演示中的登陆密码我已经修改了,你们就不要试了哈。 93 | 94 | ![演示](http://oj5vdtyuu.bkt.clouddn.com/wxneteasemusic1.gif) 95 | 96 | 如果还不清楚的话,我还拍了个小视频,放在了优酷上,[请点击这里](http://v.youku.com/v_show/id_XMjUxODk5MDQxNg==.html)。 97 | 98 | [![视频演示](http://oj5vdtyuu.bkt.clouddn.com/screenshot.png)](http://v.youku.com/v_show/id_XMjUxODk5MDQxNg==.html?tpa=dW5pb25faWQ9MTAzMjUyXzEwMDAwMV8wMV8wMQ+) 99 | 100 | ## BUGS 101 | 102 | 1.网易云音乐中部分音乐链接已失效,所以可能导致播放失败的情况,这种情况下,因为树莓派版使用的是omxplayer,而非[其它平台下的WxNeteaseMusic](https://github.com/yaphone/WxNeteaseMusic)使用的mp3play模块,omxplayer并不能感知到播放失败,还会一直等待当前播放失败的歌曲的时长才会播放下一首,或者其它命令触发切换动作,如(N)等。而电脑版使用的是python的mp3play包,播放失败时会自动跳过。建议使用网易音乐的客户端把播放失败的这首歌直接删除掉,这样在树莓派上播放时就不会卡住了。 103 | 104 | 2.通过`N 序号`选择列表中的歌曲时,播放是临时的,并不保存在播放列表中,此时再发`R`命令时显示的播放信息是错误的。 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /WxNeteaseMusic.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | import itchat 3 | import threading 4 | import time 5 | import subprocess 6 | from myapi import MyNetease 7 | import os 8 | import mp3play 9 | 10 | class WxNeteaseMusic: 11 | def __init__(self): 12 | self.help_msg = \ 13 | u"H: 帮助信息\n" \ 14 | u"L: 登陆网易云音乐\n" \ 15 | u"M: 播放列表\n" \ 16 | u"N: 下一曲\n"\ 17 | u"U: 用户歌单\n"\ 18 | u"R: 正在播放\n"\ 19 | u"S: 歌曲搜索\n"\ 20 | u"T: 热门单曲\n"\ 21 | u"G: 推荐单曲\n"\ 22 | u"E: 退出\n" 23 | self.con = threading.Condition() 24 | self.myNetease = MyNetease() 25 | self.playlist = self.myNetease.get_top_songlist() #默认是热门歌单 26 | self.mp3 = None 27 | t = threading.Thread(target=self.play) 28 | t.start() 29 | 30 | 31 | def msg_handler(self, args): 32 | arg_list = args.split(" ") # 参数以空格为分割符 33 | if len(arg_list) == 1: # 如果接收长度为1 34 | arg = arg_list[0] 35 | res = "" 36 | if arg == u'H': # 帮助信息 37 | res = self.help_msg 38 | elif arg == u'N': # 下一曲 39 | if len(self.playlist) > 0: 40 | if self.con.acquire(): 41 | self.con.notifyAll() 42 | self.con.release() 43 | res = u'切换成功,正在播放: ' +self. playlist[0].get('song_name') 44 | else: 45 | res = u'当前播放列表为空' 46 | elif arg == u'U': # 用户歌单 47 | user_playlist = self.myNetease.get_user_playlist() 48 | if user_playlist == -1: 49 | res = u"用户播放列表为空" 50 | else: 51 | index = 0 52 | for data in user_playlist: 53 | res += str(index) + ". " + data['name'] + "\n" 54 | index += 1 55 | res += u"\n 回复 (U 序号) 切换歌单" 56 | elif arg == u'M': #当前歌单播放列表 57 | if len(self.playlist) == 0: 58 | res = u"当前播放列表为空,回复 (U) 选择播放列表" 59 | i = 0 60 | for song in self.playlist: 61 | res += str(i) + ". " + song["song_name"] + "\n" 62 | i += 1 63 | res += u'\n回复 (N) 播放下一曲, 回复 (N 序号)播放对应歌曲' 64 | elif arg == u'R': #当前正在播放的歌曲信息 65 | song_info = self.playlist[-1] 66 | artist = song_info.get("artist") 67 | song_name = song_info.get("song_name") 68 | album_name = song_info.get("album_name") 69 | res = u"歌曲:" + song_name + u"\n歌手:" + artist + u"\n专辑:" + album_name 70 | elif arg == u"S": #单曲搜索 71 | res = u"回复 (S 歌曲名) 进行搜索" 72 | elif arg == u'T': #热门单曲 73 | self.playlist = self.myNetease.get_top_songlist() 74 | if len(self.playlist) == 0: 75 | res = u"当前播放列表为空,请回复 (U) 选择播放列表" 76 | i = 0 77 | for song in self.playlist: 78 | res += str(i) + ". " + song["song_name"] + "\n" 79 | i += 1 80 | res += u'\n回复 (N) 播放下一曲, 回复 (N 序号)播放对应歌曲' 81 | elif arg == u'G':#推荐歌单 82 | self.playlist = self.myNetease.get_recommend_playlist() 83 | if len(self.playlist) == 0: 84 | res = u"当前播放列表为空,请回复 (U) 选择播放列表" 85 | i = 0 86 | for song in self.playlist: 87 | res += str(i) + ". " + song["song_name"] + "\n" 88 | i += 1 89 | res += u'\n回复 (N) 播放下一曲, 回复 (N 序号)播放对应歌曲' 90 | elif arg == u'E':#关闭音乐 91 | self.playlist = [] 92 | if self.con.acquire(): 93 | self.con.notifyAll() 94 | self.con.release() 95 | res = u'播放已退出,回复 (U) 更新列表后可恢复播放' 96 | else: 97 | try: 98 | index = int(arg) 99 | if index > len(self.playlist) - 1: 100 | res = u"输入不正确" 101 | else: 102 | if self.con.acquire(): 103 | self.con.notifyAll() 104 | self.con.release() 105 | except: 106 | res = u'输入不正确' 107 | elif len(arg_list) == 2: #接收信息长度为2 108 | arg1 = arg_list[0] 109 | arg2 = arg_list[1] 110 | if arg1 == u"U": 111 | user_playlist = self.myNetease.get_user_playlist() 112 | if user_playlist == -1: 113 | res = u"用户播放列表为空" 114 | else: 115 | try: 116 | index = int(arg2) 117 | data = user_playlist[index] 118 | playlist_id = data['id'] #歌单序号 119 | song_list = self.myNetease.get_song_list_by_playlist_id(playlist_id) 120 | self.playlist = song_list 121 | res = u"用户歌单切换成功,回复 (M) 可查看当前播放列表" 122 | if self.con.acquire(): 123 | self.con.notifyAll() 124 | self.con.release() 125 | except: 126 | res = u"输入有误" 127 | elif arg1 == u'N': #播放第X首歌曲 128 | index = int(arg2) 129 | tmp_song = self.playlist[index] 130 | self.playlist.insert(0, tmp_song) 131 | if self.con.acquire(): 132 | self.con.notifyAll() 133 | self.con.release() 134 | res = u'切换成功,正在播放: ' + self.playlist[0].get('song_name') 135 | time.sleep(.5) 136 | del self.playlist[-1] 137 | 138 | elif arg1 == u"S": #歌曲搜索+歌曲名 139 | song_name = arg2 140 | song_list = self.myNetease.search_by_name(song_name) 141 | res = "" 142 | i = 0 143 | for song in song_list: 144 | res += str(i) + ". " + song["song_name"] + "\n" 145 | i += 1 146 | res += u"\n回复(S 歌曲名 序号)播放对应歌曲" 147 | 148 | elif len(arg_list) == 3: #接收长度为3 149 | arg1 = arg_list[0] 150 | arg2 = arg_list[1] 151 | arg3 = arg_list[2] 152 | try: 153 | if arg1 == u'L': #用户登陆 154 | res = self.myNetease.login(arg2, arg3) 155 | elif arg1 == u"S": 156 | song_name = arg2 157 | song_list = self.myNetease.search_by_name(song_name) 158 | index = int(arg3) 159 | song = song_list[index] 160 | #把song放在播放列表的第一位置,唤醒播放线程,立即播放 161 | self.playlist.insert(0, song) 162 | if self.con.acquire(): 163 | self.con.notifyAll() 164 | self.con.release() 165 | artist = song.get("artist") 166 | song_name = song.get("song_name") 167 | album_name = song.get("album_name") 168 | res = u"歌曲:" + song_name + u"\n歌手:" + artist + u"\n专辑:" + album_name 169 | except: 170 | res = u"输入不正确" 171 | 172 | return res 173 | 174 | def play(self): 175 | while True: 176 | if self.con.acquire(): 177 | if len(self.playlist) != 0: 178 | # 循环播放,取出第一首歌曲,放在最后的位置,类似一个循环队列 179 | song = self.playlist[0] 180 | self.playlist.remove(song) 181 | self.playlist.append(song) 182 | mp3_url = song["mp3_url"] 183 | print mp3_url 184 | try: #有些音乐已失效,自动跳过 185 | self.mp3 = mp3play.load(mp3_url) 186 | self.mp3.play() 187 | except: 188 | pass 189 | else: 190 | try: 191 | self.mp3.stop() 192 | except: 193 | pass 194 | self.con.notifyAll() 195 | self.con.wait(int(song.get('playTime')) / 1000) 196 | 197 | 198 | 199 | -------------------------------------------------------------------------------- /itchat/__init__.py: -------------------------------------------------------------------------------- 1 | from . import content 2 | from .core import Core 3 | from .config import VERSION 4 | from .log import set_logging 5 | 6 | __version__ = VERSION 7 | 8 | instanceList = [] 9 | 10 | def new_instance(): 11 | newInstance = Core() 12 | instanceList.append(newInstance) 13 | return newInstance 14 | 15 | originInstance = new_instance() 16 | 17 | # I really want to use sys.modules[__name__] = originInstance 18 | # but it makes auto-fill a real mess, so forgive me for my following ** 19 | # actually it toke me less than 30 seconds, god bless Uganda 20 | 21 | # components.login 22 | login = originInstance.login 23 | get_QRuuid = originInstance.get_QRuuid 24 | get_QR = originInstance.get_QR 25 | check_login = originInstance.check_login 26 | web_init = originInstance.web_init 27 | show_mobile_login = originInstance.show_mobile_login 28 | start_receiving = originInstance.start_receiving 29 | get_msg = originInstance.get_msg 30 | logout = originInstance.logout 31 | # components.contact 32 | update_chatroom = originInstance.update_chatroom 33 | update_friend = originInstance.update_friend 34 | get_contact = originInstance.get_contact 35 | get_friends = originInstance.get_friends 36 | get_chatrooms = originInstance.get_chatrooms 37 | get_mps = originInstance.get_mps 38 | set_alias = originInstance.set_alias 39 | set_pinned = originInstance.set_pinned 40 | add_friend = originInstance.add_friend 41 | get_head_img = originInstance.get_head_img 42 | create_chatroom = originInstance.create_chatroom 43 | set_chatroom_name = originInstance.set_chatroom_name 44 | delete_member_from_chatroom = originInstance.delete_member_from_chatroom 45 | add_member_into_chatroom = originInstance.add_member_into_chatroom 46 | # components.messages 47 | send_raw_msg = originInstance.send_raw_msg 48 | send_msg = originInstance.send_msg 49 | upload_file = originInstance.upload_file 50 | send_file = originInstance.send_file 51 | send_image = originInstance.send_image 52 | send_video = originInstance.send_video 53 | send = originInstance.send 54 | # components.hotreload 55 | dump_login_status = originInstance.dump_login_status 56 | load_login_status = originInstance.load_login_status 57 | # components.register 58 | auto_login = originInstance.auto_login 59 | configured_reply = originInstance.configured_reply 60 | msg_register = originInstance.msg_register 61 | run = originInstance.run 62 | # other functions 63 | search_friends = originInstance.search_friends 64 | search_chatrooms = originInstance.search_chatrooms 65 | search_mps = originInstance.search_mps 66 | set_logging = set_logging 67 | -------------------------------------------------------------------------------- /itchat/components/__init__.py: -------------------------------------------------------------------------------- 1 | from .contact import load_contact 2 | from .hotreload import load_hotreload 3 | from .login import load_login 4 | from .messages import load_messages 5 | from .register import load_register 6 | 7 | def load_components(core): 8 | load_contact(core) 9 | load_hotreload(core) 10 | load_login(core) 11 | load_messages(core) 12 | load_register(core) 13 | -------------------------------------------------------------------------------- /itchat/components/contact.py: -------------------------------------------------------------------------------- 1 | import os, time, re, io 2 | import json, copy 3 | import traceback, logging 4 | 5 | import requests 6 | 7 | from .. import config, utils 8 | from ..returnvalues import ReturnValue 9 | 10 | logger = logging.getLogger('itchat') 11 | 12 | def load_contact(core): 13 | core.update_chatroom = update_chatroom 14 | core.update_friend = update_friend 15 | core.get_contact = get_contact 16 | core.get_friends = get_friends 17 | core.get_chatrooms = get_chatrooms 18 | core.get_mps = get_mps 19 | core.set_alias = set_alias 20 | core.set_pinned = set_pinned 21 | core.add_friend = add_friend 22 | core.get_head_img = get_head_img 23 | core.create_chatroom = create_chatroom 24 | core.set_chatroom_name = set_chatroom_name 25 | core.delete_member_from_chatroom = delete_member_from_chatroom 26 | core.add_member_into_chatroom = add_member_into_chatroom 27 | 28 | def update_chatroom(self, userName, detailedMember=False): 29 | if not isinstance(userName, list): 30 | userName = [userName] 31 | url = '%s/webwxbatchgetcontact?type=ex&r=%s' % ( 32 | self.loginInfo['url'], int(time.time())) 33 | headers = { 34 | 'ContentType': 'application/json; charset=UTF-8', 35 | 'User-Agent' : config.USER_AGENT } 36 | data = { 37 | 'BaseRequest': self.loginInfo['BaseRequest'], 38 | 'Count': len(userName), 39 | 'List': [{ 40 | 'UserName': u, 41 | 'ChatRoomId': '', } for u in userName], } 42 | chatroomList = json.loads(self.s.post(url, data=json.dumps(data), headers=headers 43 | ).content.decode('utf8', 'replace')).get('ContactList') 44 | if not chatroomList: 45 | return ReturnValue({'BaseResponse': { 46 | 'ErrMsg': 'No chatroom found', 47 | 'Ret': -1001, }}) 48 | 49 | if detailedMember: 50 | def get_detailed_member_info(encryChatroomId, memberList): 51 | url = '%s/webwxbatchgetcontact?type=ex&r=%s' % ( 52 | self.loginInfo['url'], int(time.time())) 53 | headers = { 54 | 'ContentType': 'application/json; charset=UTF-8', 55 | 'User-Agent' : config.USER_AGENT, } 56 | data = { 57 | 'BaseRequest': self.loginInfo['BaseRequest'], 58 | 'Count': len(memberList), 59 | 'List': [{ 60 | 'UserName': member['UserName'], 61 | 'EncryChatRoomId': encryChatroomId} \ 62 | for member in memberList], } 63 | return json.loads(self.s.post(url, data=json.dumps(data), headers=headers 64 | ).content.decode('utf8', 'replace'))['ContactList'] 65 | MAX_GET_NUMBER = 50 66 | for chatroom in chatroomList: 67 | totalMemberList = [] 68 | for i in range(int(len(chatroom['MemberList']) / MAX_GET_NUMBER + 1)): 69 | memberList = chatroom['MemberList'][i*MAX_GET_NUMBER: (i+1)*MAX_GET_NUMBER] 70 | totalMemberList += get_detailed_member_info(chatroom['EncryChatRoomId'], memberList) 71 | chatroom['MemberList'] = totalMemberList 72 | 73 | update_local_chatrooms(self, chatroomList) 74 | r = [self.storageClass.search_chatrooms(userName=c['UserName']) 75 | for c in chatroomList] 76 | return r if 1 < len(r) else r[0] 77 | 78 | def update_friend(self, userName): 79 | if not isinstance(userName, list): 80 | userName = [userName] 81 | url = '%s/webwxbatchgetcontact?type=ex&r=%s' % ( 82 | self.loginInfo['url'], int(time.time())) 83 | headers = { 84 | 'ContentType': 'application/json; charset=UTF-8', 85 | 'User-Agent' : config.USER_AGENT } 86 | data = { 87 | 'BaseRequest': self.loginInfo['BaseRequest'], 88 | 'Count': len(userName), 89 | 'List': [{ 90 | 'UserName': u, 91 | 'EncryChatRoomId': '', } for u in userName], } 92 | friendList = json.loads(self.s.post(url, data=json.dumps(data), headers=headers 93 | ).content.decode('utf8', 'replace')).get('ContactList') 94 | 95 | update_local_friends(self, friendList) 96 | r = [self.storageClass.search_friends(userName=f['UserName']) 97 | for f in friendList] 98 | return r if len(r) != 1 else r[0] 99 | 100 | def update_info_dict(oldInfoDict, newInfoDict): 101 | ''' 102 | only normal values will be updated here 103 | ''' 104 | for k, v in newInfoDict.items(): 105 | if any((isinstance(v, t) for t in (tuple, list, dict))): 106 | pass # these values will be updated somewhere else 107 | elif oldInfoDict.get(k) is None or v not in (None, '', '0', 0): 108 | oldInfoDict[k] = v 109 | 110 | def update_local_chatrooms(core, l): 111 | ''' 112 | get a list of chatrooms for updating local chatrooms 113 | return a list of given chatrooms with updated info 114 | ''' 115 | for chatroom in l: 116 | # format new chatrooms 117 | utils.emoji_formatter(chatroom, 'NickName') 118 | for member in chatroom['MemberList']: 119 | utils.emoji_formatter(member, 'NickName') 120 | utils.emoji_formatter(member, 'DisplayName') 121 | # update it to old chatrooms 122 | oldChatroom = utils.search_dict_list( 123 | core.chatroomList, 'UserName', chatroom['UserName']) 124 | if oldChatroom: 125 | update_info_dict(oldChatroom, chatroom) 126 | # - update other values 127 | memberList, oldMemberList = (c.get('MemberList', []) 128 | for c in (chatroom, oldChatroom)) 129 | if memberList: 130 | for member in memberList: 131 | oldMember = utils.search_dict_list( 132 | oldMemberList, 'UserName', member['UserName']) 133 | if oldMember: 134 | update_info_dict(oldMember, member) 135 | else: 136 | oldMemberList.append(member) 137 | else: 138 | oldChatroom = chatroom 139 | core.chatroomList.append(chatroom) 140 | # delete useless members 141 | if len(chatroom['MemberList']) != len(oldChatroom['MemberList']) and \ 142 | chatroom['MemberList']: 143 | existsUserNames = [member['UserName'] for member in chatroom['MemberList']] 144 | delList = [] 145 | for i, member in enumerate(oldChatroom['MemberList']): 146 | if member['UserName'] not in existsUserNames: delList.append(i) 147 | delList.sort(reverse=True) 148 | for i in delList: del oldChatroom['MemberList'][i] 149 | # - update OwnerUin 150 | if oldChatroom.get('ChatRoomOwner') and oldChatroom.get('MemberList'): 151 | oldChatroom['OwnerUin'] = utils.search_dict_list(oldChatroom['MemberList'], 152 | 'UserName', oldChatroom['ChatRoomOwner']).get('Uin', 0) 153 | # - update isAdmin 154 | if 'OwnerUin' in oldChatroom and oldChatroom['OwnerUin'] != 0: 155 | oldChatroom['isAdmin'] = \ 156 | oldChatroom['OwnerUin'] == int(core.loginInfo['wxuin']) 157 | else: 158 | oldChatroom['isAdmin'] = None 159 | # - update self 160 | newSelf = utils.search_dict_list(oldChatroom['MemberList'], 161 | 'UserName', core.storageClass.userName) 162 | oldChatroom['self'] = newSelf or copy.deepcopy(core.loginInfo['User']) 163 | return { 164 | 'Type' : 'System', 165 | 'Text' : [chatroom['UserName'] for chatroom in l], 166 | 'SystemInfo' : 'chatrooms', 167 | 'FromUserName' : core.storageClass.userName, 168 | 'ToUserName' : core.storageClass.userName, } 169 | 170 | def update_local_friends(core, l): 171 | ''' 172 | get a list of friends or mps for updating local contact 173 | ''' 174 | fullList = core.memberList + core.mpList 175 | for friend in l: 176 | if 'NickName' in friend: 177 | utils.emoji_formatter(friend, 'NickName') 178 | if 'DisplayName' in friend: 179 | utils.emoji_formatter(friend, 'DisplayName') 180 | oldInfoDict = utils.search_dict_list( 181 | fullList, 'UserName', friend['UserName']) 182 | if oldInfoDict is None: 183 | oldInfoDict = copy.deepcopy(friend) 184 | if oldInfoDict['VerifyFlag'] & 8 == 0: 185 | core.memberList.append(oldInfoDict) 186 | else: 187 | core.mpList.append(oldInfoDict) 188 | else: 189 | update_info_dict(oldInfoDict, friend) 190 | 191 | def update_local_uin(core, msg): 192 | ''' 193 | content contains uins and StatusNotifyUserName contains username 194 | they are in same order, so what I do is to pair them together 195 | 196 | I caught an exception in this method while not knowing why 197 | but don't worry, it won't cause any problem 198 | ''' 199 | uins = re.search('([^<]*?)<', msg['Content']) 200 | usernameChangedList = [] 201 | r = { 202 | 'Type': 'System', 203 | 'Text': usernameChangedList, 204 | 'SystemInfo': 'uins', } 205 | if uins: 206 | uins = uins.group(1).split(',') 207 | usernames = msg['StatusNotifyUserName'].split(',') 208 | if 0 < len(uins) == len(usernames): 209 | for uin, username in zip(uins, usernames): 210 | if not '@' in username: continue 211 | fullContact = core.memberList + core.chatroomList + core.mpList 212 | userDicts = utils.search_dict_list(fullContact, 213 | 'UserName', username) 214 | if userDicts: 215 | if userDicts.get('Uin', 0) == 0: 216 | userDicts['Uin'] = uin 217 | usernameChangedList.append(username) 218 | logger.debug('Uin fetched: %s, %s' % (username, uin)) 219 | else: 220 | if userDicts['Uin'] != uin: 221 | logger.debug('Uin changed: %s, %s' % ( 222 | userDicts['Uin'], uin)) 223 | else: 224 | if '@@' in username: 225 | update_chatroom(core, username) 226 | newChatroomDict = utils.search_dict_list( 227 | core.chatroomList, 'UserName', username) 228 | if newChatroomDict is None: 229 | newChatroomDict = utils.struct_friend_info({ 230 | 'UserName': username, 231 | 'Uin': uin, }) 232 | core.chatroomList.append(newChatroomDict) 233 | else: 234 | newChatroomDict['Uin'] = uin 235 | elif '@' in username: 236 | update_friend(core, username) 237 | newFriendDict = utils.search_dict_list( 238 | core.memberList, 'UserName', username) 239 | newFriendDict['Uin'] = uin 240 | usernameChangedList.append(username) 241 | logger.debug('Uin fetched: %s, %s' % (username, uin)) 242 | else: 243 | logger.debug('Wrong length of uins & usernames: %s, %s' % ( 244 | len(uins), len(usernames))) 245 | else: 246 | logger.debug('No uins in 51 message') 247 | logger.debug(msg['Content']) 248 | return r 249 | 250 | def get_contact(self, update=False): 251 | if not update: return copy.deepcopy(self.chatroomList) 252 | url = '%s/webwxgetcontact?r=%s&seq=0&skey=%s' % (self.loginInfo['url'], 253 | int(time.time()), self.loginInfo['skey']) 254 | headers = { 255 | 'ContentType': 'application/json; charset=UTF-8', 256 | 'User-Agent' : config.USER_AGENT, } 257 | r = self.s.get(url, headers=headers) 258 | tempList = json.loads(r.content.decode('utf-8', 'replace'))['MemberList'] 259 | chatroomList, otherList = [], [] 260 | for m in tempList: 261 | if m['Sex'] != 0: 262 | otherList.append(m) 263 | elif '@@' in m['UserName']: 264 | chatroomList.append(m) 265 | elif '@' in m['UserName']: 266 | # mp will be dealt in update_local_friends as well 267 | otherList.append(m) 268 | if chatroomList: update_local_chatrooms(self, chatroomList) 269 | if otherList: update_local_friends(self, otherList) 270 | return copy.deepcopy(chatroomList) 271 | 272 | def get_friends(self, update=False): 273 | if update: self.get_contact(update=True) 274 | return copy.deepcopy(self.memberList) 275 | 276 | def get_chatrooms(self, update=False, contactOnly=False): 277 | if contactOnly: 278 | return self.get_contact(update=True) 279 | else: 280 | if update: self.get_contact(True) 281 | return copy.deepcopy(self.chatroomList) 282 | 283 | def get_mps(self, update=False): 284 | if update: self.get_contact(update=True) 285 | return copy.deepcopy(self.mpList) 286 | 287 | def set_alias(self, userName, alias): 288 | oldFriendInfo = utils.search_dict_list( 289 | self.memberList, 'UserName', userName) 290 | if oldFriendInfo is None: 291 | return ReturnValue({'BaseResponse': { 292 | 'Ret': -1001, }}) 293 | url = '%s/webwxoplog?lang=%s&pass_ticket=%s' % ( 294 | self.loginInfo['url'], 'zh_CN', self.loginInfo['pass_ticket']) 295 | data = { 296 | 'UserName' : userName, 297 | 'CmdId' : 2, 298 | 'RemarkName' : alias, 299 | 'BaseRequest' : self.loginInfo['BaseRequest'], } 300 | headers = { 'User-Agent' : config.USER_AGENT } 301 | r = self.s.post(url, json.dumps(data, ensure_ascii=False).encode('utf8'), 302 | headers=headers) 303 | r = ReturnValue(rawResponse=r) 304 | if r: oldFriendInfo['RemarkName'] = alias 305 | return r 306 | 307 | def set_pinned(self, userName, isPinned=True): 308 | url = '%s/webwxoplog?pass_ticket=%s' % ( 309 | self.loginInfo['url'], self.loginInfo['pass_ticket']) 310 | data = { 311 | 'UserName' : userName, 312 | 'CmdId' : 3, 313 | 'OP' : int(isPinned), 314 | 'BaseRequest' : self.loginInfo['BaseRequest'], } 315 | headers = { 'User-Agent' : config.USER_AGENT } 316 | r = self.s.post(url, json=data, headers=headers) 317 | return ReturnValue(rawResponse=r) 318 | 319 | def add_friend(self, userName, status=2, verifyContent='', autoUpdate=True): 320 | ''' Add a friend or accept a friend 321 | * for adding status should be 2 322 | * for accepting status should be 3 323 | ''' 324 | url = '%s/webwxverifyuser?r=%s&pass_ticket=%s' % ( 325 | self.loginInfo['url'], int(time.time()), self.loginInfo['pass_ticket']) 326 | data = { 327 | 'BaseRequest': self.loginInfo['BaseRequest'], 328 | 'Opcode': status, # 3 329 | 'VerifyUserListSize': 1, 330 | 'VerifyUserList': [{ 331 | 'Value': userName, 332 | 'VerifyUserTicket': '', }], 333 | 'VerifyContent': verifyContent, 334 | 'SceneListCount': 1, 335 | 'SceneList': 33, # [33] 336 | 'skey': self.loginInfo['skey'], } 337 | headers = { 338 | 'ContentType': 'application/json; charset=UTF-8', 339 | 'User-Agent' : config.USER_AGENT } 340 | r = self.s.post(url, headers=headers, 341 | data=json.dumps(data, ensure_ascii=False).encode('utf8', 'replace')) 342 | if autoUpdate: self.update_friend(userName) 343 | return ReturnValue(rawResponse=r) 344 | 345 | def get_head_img(self, userName=None, chatroomUserName=None, picDir=None): 346 | ''' get head image 347 | * if you want to get chatroom header: only set chatroomUserName 348 | * if you want to get friend header: only set userName 349 | * if you want to get chatroom member header: set both 350 | ''' 351 | params = { 352 | 'userName': userName or chatroomUserName or self.storageClass.userName, 353 | 'skey': self.loginInfo['skey'], } 354 | url = '%s/webwxgeticon' % self.loginInfo['url'] 355 | if chatroomUserName is None: 356 | infoDict = self.storageClass.search_friends(userName=userName) 357 | if infoDict is None: 358 | return ReturnValue({'BaseResponse': { 359 | 'ErrMsg': 'No friend found', 360 | 'Ret': -1001, }}) 361 | else: 362 | if userName is None: 363 | url = '%s/webwxgetheadimg' % self.loginInfo['url'] 364 | else: 365 | chatroom = self.storageClass.search_chatrooms(userName=chatroomUserName) 366 | if chatroomUserName is None: 367 | return ReturnValue({'BaseResponse': { 368 | 'ErrMsg': 'No chatroom found', 369 | 'Ret': -1001, }}) 370 | if chatroom['EncryChatRoomId'] == '': 371 | chatroom = self.update_chatroom(chatroomUserName) 372 | params['chatroomid'] = chatroom['EncryChatRoomId'] 373 | headers = { 'User-Agent' : config.USER_AGENT } 374 | r = self.s.get(url, params=params, stream=True, headers=headers) 375 | tempStorage = io.BytesIO() 376 | for block in r.iter_content(1024): 377 | tempStorage.write(block) 378 | if picDir is None: 379 | return tempStorage.getvalue() 380 | with open(picDir, 'wb') as f: f.write(tempStorage.getvalue()) 381 | return ReturnValue({'BaseResponse': { 382 | 'ErrMsg': 'Successfully downloaded', 383 | 'Ret': 0, }}) 384 | 385 | def create_chatroom(self, memberList, topic=''): 386 | url = '%s/webwxcreatechatroom?pass_ticket=%s&r=%s' % ( 387 | self.loginInfo['url'], self.loginInfo['pass_ticket'], int(time.time())) 388 | data = { 389 | 'BaseRequest': self.loginInfo['BaseRequest'], 390 | 'MemberCount': len(memberList), 391 | 'MemberList': [{'UserName': member['UserName']} for member in memberList], 392 | 'Topic': topic, } 393 | headers = { 394 | 'content-type': 'application/json; charset=UTF-8', 395 | 'User-Agent' : config.USER_AGENT } 396 | r = self.s.post(url, headers=headers, 397 | data=json.dumps(data, ensure_ascii=False).encode('utf8', 'ignore')) 398 | return ReturnValue(rawResponse=r) 399 | 400 | def set_chatroom_name(self, chatroomUserName, name): 401 | url = '%s/webwxupdatechatroom?fun=modtopic&pass_ticket=%s' % ( 402 | self.loginInfo['url'], self.loginInfo['pass_ticket']) 403 | data = { 404 | 'BaseRequest': self.loginInfo['BaseRequest'], 405 | 'ChatRoomName': chatroomUserName, 406 | 'NewTopic': name, } 407 | headers = { 408 | 'content-type': 'application/json; charset=UTF-8', 409 | 'User-Agent' : config.USER_AGENT } 410 | r = self.s.post(url, headers=headers, 411 | data=json.dumps(data, ensure_ascii=False).encode('utf8', 'ignore')) 412 | return ReturnValue(rawResponse=r) 413 | 414 | def delete_member_from_chatroom(self, chatroomUserName, memberList): 415 | url = '%s/webwxupdatechatroom?fun=delmember&pass_ticket=%s' % ( 416 | self.loginInfo['url'], self.loginInfo['pass_ticket']) 417 | data = { 418 | 'BaseRequest': self.loginInfo['BaseRequest'], 419 | 'ChatRoomName': chatroomUserName, 420 | 'DelMemberList': ','.join([member['UserName'] for member in memberList]), } 421 | headers = { 422 | 'content-type': 'application/json; charset=UTF-8', 423 | 'User-Agent' : config.USER_AGENT} 424 | r = self.s.post(url, data=json.dumps(data),headers=headers) 425 | return ReturnValue(rawResponse=r) 426 | 427 | def add_member_into_chatroom(self, chatroomUserName, memberList, 428 | useInvitation=False): 429 | ''' add or invite member into chatroom 430 | * there are two ways to get members into chatroom: invite or directly add 431 | * but for chatrooms with more than 40 users, you can only use invite 432 | * but don't worry we will auto-force userInvitation for you when necessary 433 | ''' 434 | if not useInvitation: 435 | chatroom = self.storageClass.search_chatrooms(userName=chatroomUserName) 436 | if not chatroom: chatroom = self.update_chatroom(chatroomUserName) 437 | if len(chatroom['MemberList']) > self.loginInfo['InviteStartCount']: 438 | useInvitation = True 439 | if useInvitation: 440 | fun, memberKeyName = 'invitemember', 'InviteMemberList' 441 | else: 442 | fun, memberKeyName = 'addmember', 'AddMemberList' 443 | url = '%s/webwxupdatechatroom?fun=%s&pass_ticket=%s' % ( 444 | self.loginInfo['url'], fun, self.loginInfo['pass_ticket']) 445 | params = { 446 | 'BaseRequest' : self.loginInfo['BaseRequest'], 447 | 'ChatRoomName' : chatroomUserName, 448 | memberKeyName : ','.join([member['UserName'] for member in memberList]), } 449 | headers = { 450 | 'content-type': 'application/json; charset=UTF-8', 451 | 'User-Agent' : config.USER_AGENT} 452 | r = self.s.post(url, data=json.dumps(params),headers=headers) 453 | return ReturnValue(rawResponse=r) 454 | -------------------------------------------------------------------------------- /itchat/components/hotreload.py: -------------------------------------------------------------------------------- 1 | import pickle, os 2 | import logging, traceback 3 | 4 | import requests 5 | 6 | from ..config import VERSION 7 | from ..returnvalues import ReturnValue 8 | from .contact import update_local_chatrooms 9 | from .messages import produce_msg 10 | 11 | logger = logging.getLogger('itchat') 12 | 13 | def load_hotreload(core): 14 | core.dump_login_status = dump_login_status 15 | core.load_login_status = load_login_status 16 | 17 | def dump_login_status(self, fileDir=None): 18 | fileDir = fileDir or self.hotReloadDir 19 | try: 20 | with open(fileDir, 'w') as f: 21 | f.write('itchat - DELETE THIS') 22 | os.remove(fileDir) 23 | except: 24 | raise Exception('Incorrect fileDir') 25 | status = { 26 | 'version' : VERSION, 27 | 'loginInfo' : self.loginInfo, 28 | 'cookies' : self.s.cookies.get_dict(), 29 | 'storage' : self.storageClass.dumps()} 30 | with open(fileDir, 'wb') as f: 31 | pickle.dump(status, f) 32 | logger.debug('Dump login status for hot reload successfully.') 33 | 34 | def load_login_status(self, fileDir, 35 | loginCallback=None, exitCallback=None): 36 | try: 37 | with open(fileDir, 'rb') as f: 38 | j = pickle.load(f) 39 | except Exception as e: 40 | logger.debug('No such file, loading login status failed.') 41 | return ReturnValue({'BaseResponse': { 42 | 'ErrMsg': 'No such file, loading login status failed.', 43 | 'Ret': -1002, }}) 44 | 45 | if j.get('version', '') != VERSION: 46 | logger.debug(('you have updated itchat from %s to %s, ' + 47 | 'so cached status is ignored') % ( 48 | j.get('version', 'old version'), VERSION)) 49 | return ReturnValue({'BaseResponse': { 50 | 'ErrMsg': 'cached status ignored because of version', 51 | 'Ret': -1005, }}) 52 | self.loginInfo = j['loginInfo'] 53 | self.s.cookies = requests.utils.cookiejar_from_dict(j['cookies']) 54 | self.storageClass.loads(j['storage']) 55 | msgList, contactList = self.get_msg() 56 | if (msgList or contactList) is None: 57 | self.logout() 58 | logger.debug('server refused, loading login status failed.') 59 | return ReturnValue({'BaseResponse': { 60 | 'ErrMsg': 'server refused, loading login status failed.', 61 | 'Ret': -1003, }}) 62 | else: 63 | if contactList: 64 | for contact in contactList: 65 | if '@@' in contact['UserName']: 66 | update_local_chatrooms(self, [contact]) 67 | else: 68 | update_local_chatrooms(self, [contact]) 69 | if msgList: 70 | msgList = produce_msg(self, msgList) 71 | for msg in msgList: self.msgList.put(msg) 72 | self.start_receiving(exitCallback) 73 | logger.debug('loading login status succeeded.') 74 | if hasattr(loginCallback, '__call__'): 75 | loginCallback() 76 | return ReturnValue({'BaseResponse': { 77 | 'ErrMsg': 'loading login status succeeded.', 78 | 'Ret': 0, }}) 79 | -------------------------------------------------------------------------------- /itchat/components/login.py: -------------------------------------------------------------------------------- 1 | import os, sys, time, re, io 2 | import threading 3 | import json, xml.dom.minidom 4 | import copy, pickle, random 5 | import traceback, logging 6 | 7 | import requests 8 | 9 | from .. import config, utils 10 | from ..returnvalues import ReturnValue 11 | from .contact import update_local_chatrooms, update_local_friends 12 | from .messages import produce_msg 13 | 14 | logger = logging.getLogger('itchat') 15 | 16 | def load_login(core): 17 | core.login = login 18 | core.get_QRuuid = get_QRuuid 19 | core.get_QR = get_QR 20 | core.check_login = check_login 21 | core.web_init = web_init 22 | core.show_mobile_login = show_mobile_login 23 | core.start_receiving = start_receiving 24 | core.get_msg = get_msg 25 | core.logout = logout 26 | 27 | def login(self, enableCmdQR=False, picDir=None, qrCallback=None, 28 | loginCallback=None, exitCallback=None): 29 | if self.alive: 30 | logger.warning('itchat has already logged in.') 31 | return 32 | while 1: 33 | for getCount in range(10): 34 | logger.info('Getting uuid of QR code.') 35 | while not self.get_QRuuid(): time.sleep(1) 36 | logger.info('Downloading QR code.') 37 | qrStorage = self.get_QR(enableCmdQR=enableCmdQR, 38 | picDir=picDir, qrCallback=qrCallback) 39 | if qrStorage: 40 | break 41 | elif 9 == getCount: 42 | logger.info('Failed to get QR code, please restart the program.') 43 | sys.exit() 44 | logger.info('Please scan the QR code to log in.') 45 | isLoggedIn = False 46 | while not isLoggedIn: 47 | status = self.check_login() 48 | if hasattr(qrCallback, '__call__'): 49 | qrCallback(uuid=self.uuid, status=status, qrcode=qrStorage.getvalue()) 50 | if status == '200': 51 | isLoggedIn = True 52 | elif status == '201': 53 | if isLoggedIn is not None: 54 | logger.info('Please press confirm on your phone.') 55 | isLoggedIn = None 56 | elif status != '408': 57 | break 58 | if isLoggedIn: break 59 | logger.info('Log in time out, reloading QR code') 60 | self.web_init() 61 | self.show_mobile_login() 62 | self.get_contact(True) 63 | if hasattr(loginCallback, '__call__'): 64 | r = loginCallback() 65 | else: 66 | utils.clear_screen() 67 | if os.path.exists(picDir or config.DEFAULT_QR): 68 | os.remove(picDir or config.DEFAULT_QR) 69 | logger.info('Login successfully as %s' % self.storageClass.nickName) 70 | self.start_receiving(exitCallback) 71 | 72 | def get_QRuuid(self): 73 | url = '%s/jslogin' % config.BASE_URL 74 | params = { 75 | 'appid' : 'wx782c26e4c19acffb', 76 | 'fun' : 'new', } 77 | headers = { 'User-Agent' : config.USER_AGENT } 78 | r = self.s.get(url, params=params, headers=headers) 79 | regx = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)";' 80 | data = re.search(regx, r.text) 81 | if data and data.group(1) == '200': 82 | self.uuid = data.group(2) 83 | return self.uuid 84 | 85 | def get_QR(self, uuid=None, enableCmdQR=False, picDir=None, qrCallback=None): 86 | uuid = uuid or self.uuid 87 | picDir = picDir or config.DEFAULT_QR 88 | url = '%s/qrcode/%s' % (config.BASE_URL, uuid) 89 | headers = { 'User-Agent' : config.USER_AGENT } 90 | try: 91 | r = self.s.get(url, stream=True, headers=headers) 92 | except: 93 | return False 94 | qrStorage = io.BytesIO(r.content) 95 | if hasattr(qrCallback, '__call__'): 96 | qrCallback(uuid=uuid, status='0', qrcode=qrStorage.getvalue()) 97 | else: 98 | with open(picDir, 'wb') as f: f.write(r.content) 99 | if enableCmdQR: 100 | utils.print_cmd_qr(picDir, enableCmdQR=enableCmdQR) 101 | else: 102 | utils.print_qr(picDir) 103 | return qrStorage 104 | 105 | def check_login(self, uuid=None): 106 | uuid = uuid or self.uuid 107 | url = '%s/cgi-bin/mmwebwx-bin/login' % config.BASE_URL 108 | localTime = int(time.time()) 109 | params = 'loginicon=true&uuid=%s&tip=0&r=%s&_=%s' % ( 110 | uuid, localTime / 1579, localTime) 111 | headers = { 'User-Agent' : config.USER_AGENT } 112 | r = self.s.get(url, params=params, headers=headers) 113 | regx = r'window.code=(\d+)' 114 | data = re.search(regx, r.text) 115 | if data and data.group(1) == '200': 116 | process_login_info(self, r.text) 117 | return '200' 118 | elif data: 119 | return data.group(1) 120 | else: 121 | return '400' 122 | 123 | def process_login_info(core, loginContent): 124 | ''' when finish login (scanning qrcode) 125 | * syncUrl and fileUploadingUrl will be fetched 126 | * deviceid and msgid will be generated 127 | * skey, wxsid, wxuin, pass_ticket will be fetched 128 | ''' 129 | regx = r'window.redirect_uri="(\S+)";' 130 | core.loginInfo['url'] = re.search(regx, loginContent).group(1) 131 | headers = { 'User-Agent' : config.USER_AGENT } 132 | r = core.s.get(core.loginInfo['url'], headers=headers, allow_redirects=False) 133 | core.loginInfo['url'] = core.loginInfo['url'][:core.loginInfo['url'].rfind('/')] 134 | for indexUrl, detailedUrl in ( 135 | ("wx2.qq.com" , ("file.wx2.qq.com", "webpush.wx2.qq.com")), 136 | ("wx8.qq.com" , ("file.wx8.qq.com", "webpush.wx8.qq.com")), 137 | ("qq.com" , ("file.wx.qq.com", "webpush.wx.qq.com")), 138 | ("web2.wechat.com" , ("file.web2.wechat.com", "webpush.web2.wechat.com")), 139 | ("wechat.com" , ("file.web.wechat.com", "webpush.web.wechat.com"))): 140 | fileUrl, syncUrl = ['https://%s/cgi-bin/mmwebwx-bin' % url for url in detailedUrl] 141 | if indexUrl in core.loginInfo['url']: 142 | core.loginInfo['fileUrl'], core.loginInfo['syncUrl'] = \ 143 | fileUrl, syncUrl 144 | break 145 | else: 146 | core.loginInfo['fileUrl'] = core.loginInfo['syncUrl'] = core.loginInfo['url'] 147 | core.loginInfo['deviceid'] = 'e' + repr(random.random())[2:17] 148 | core.loginInfo['BaseRequest'] = {} 149 | for node in xml.dom.minidom.parseString(r.text).documentElement.childNodes: 150 | if node.nodeName == 'skey': 151 | core.loginInfo['skey'] = core.loginInfo['BaseRequest']['Skey'] = node.childNodes[0].data 152 | elif node.nodeName == 'wxsid': 153 | core.loginInfo['wxsid'] = core.loginInfo['BaseRequest']['Sid'] = node.childNodes[0].data 154 | elif node.nodeName == 'wxuin': 155 | core.loginInfo['wxuin'] = core.loginInfo['BaseRequest']['Uin'] = node.childNodes[0].data 156 | elif node.nodeName == 'pass_ticket': 157 | core.loginInfo['pass_ticket'] = core.loginInfo['BaseRequest']['DeviceID'] = node.childNodes[0].data 158 | 159 | def web_init(self): 160 | url = '%s/webwxinit?r=%s' % (self.loginInfo['url'], int(time.time())) 161 | data = { 'BaseRequest': self.loginInfo['BaseRequest'], } 162 | headers = { 163 | 'ContentType': 'application/json; charset=UTF-8', 164 | 'User-Agent' : config.USER_AGENT, } 165 | r = self.s.post(url, data=json.dumps(data), headers=headers) 166 | dic = json.loads(r.content.decode('utf-8', 'replace')) 167 | # deal with login info 168 | utils.emoji_formatter(dic['User'], 'NickName') 169 | self.loginInfo['InviteStartCount'] = int(dic['InviteStartCount']) 170 | self.loginInfo['User'] = utils.struct_friend_info(dic['User']) 171 | self.loginInfo['SyncKey'] = dic['SyncKey'] 172 | self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val']) 173 | for item in dic['SyncKey']['List']]) 174 | self.storageClass.userName = dic['User']['UserName'] 175 | self.storageClass.nickName = dic['User']['NickName'] 176 | return dic 177 | 178 | def show_mobile_login(self): 179 | url = '%s/webwxstatusnotify?lang=zh_CN&pass_ticket=%s' % ( 180 | self.loginInfo['url'], self.loginInfo['pass_ticket']) 181 | data = { 182 | 'BaseRequest' : self.loginInfo['BaseRequest'], 183 | 'Code' : 3, 184 | 'FromUserName' : self.storageClass.userName, 185 | 'ToUserName' : self.storageClass.userName, 186 | 'ClientMsgId' : int(time.time()), } 187 | headers = { 188 | 'ContentType': 'application/json; charset=UTF-8', 189 | 'User-Agent' : config.USER_AGENT, } 190 | r = self.s.post(url, data=json.dumps(data), headers=headers) 191 | return ReturnValue(rawResponse=r) 192 | 193 | def start_receiving(self, exitCallback=None, getReceivingFnOnly=False): 194 | self.alive = True 195 | def maintain_loop(): 196 | retryCount = 0 197 | while self.alive: 198 | try: 199 | i = sync_check(self) 200 | if i is None: 201 | self.alive = False 202 | elif i == '0': 203 | continue 204 | else: 205 | msgList, contactList = self.get_msg() 206 | if msgList: 207 | msgList = produce_msg(self, msgList) 208 | for msg in msgList: self.msgList.put(msg) 209 | if contactList: 210 | chatroomList, otherList = [], [] 211 | for contact in contactList: 212 | if '@@' in contact['UserName']: 213 | chatroomList.append(contact) 214 | else: 215 | otherList.append(contact) 216 | chatroomMsg = update_local_chatrooms(self, chatroomList) 217 | self.msgList.put(chatroomMsg) 218 | update_local_friends(self, otherList) 219 | retryCount = 0 220 | except: 221 | retryCount += 1 222 | logger.error(traceback.format_exc()) 223 | if self.receivingRetryCount < retryCount: 224 | self.alive = False 225 | else: 226 | time.sleep(1) 227 | self.logout() 228 | if hasattr(exitCallback, '__call__'): 229 | exitCallback() 230 | else: 231 | logger.info('LOG OUT!') 232 | if getReceivingFnOnly: 233 | return maintain_loop 234 | else: 235 | maintainThread = threading.Thread(target=maintain_loop) 236 | maintainThread.setDaemon(True) 237 | maintainThread.start() 238 | 239 | def sync_check(self): 240 | url = '%s/synccheck' % self.loginInfo.get('syncUrl', self.loginInfo['url']) 241 | params = { 242 | 'r' : int(time.time() * 1000), 243 | 'skey' : self.loginInfo['skey'], 244 | 'sid' : self.loginInfo['wxsid'], 245 | 'uin' : self.loginInfo['wxuin'], 246 | 'deviceid' : self.loginInfo['deviceid'], 247 | 'synckey' : self.loginInfo['synckey'], 248 | '_' : int(time.time() * 1000),} 249 | headers = { 'User-Agent' : config.USER_AGENT } 250 | r = self.s.get(url, params=params, headers=headers) 251 | regx = r'window.synccheck={retcode:"(\d+)",selector:"(\d+)"}' 252 | pm = re.search(regx, r.text) 253 | if pm is None or pm.group(1) != '0': 254 | logger.debug('Unexpected sync check result: %s' % r.text) 255 | return None 256 | return pm.group(2) 257 | 258 | def get_msg(self): 259 | url = '%s/webwxsync?sid=%s&skey=%s&pass_ticket=%s' % ( 260 | self.loginInfo['url'], self.loginInfo['wxsid'], 261 | self.loginInfo['skey'],self.loginInfo['pass_ticket']) 262 | data = { 263 | 'BaseRequest' : self.loginInfo['BaseRequest'], 264 | 'SyncKey' : self.loginInfo['SyncKey'], 265 | 'rr' : ~int(time.time()), } 266 | headers = { 267 | 'ContentType': 'application/json; charset=UTF-8', 268 | 'User-Agent' : config.USER_AGENT } 269 | r = self.s.post(url, data=json.dumps(data), headers=headers) 270 | dic = json.loads(r.content.decode('utf-8', 'replace')) 271 | if dic['BaseResponse']['Ret'] != 0: return None, None 272 | self.loginInfo['SyncKey'] = dic['SyncCheckKey'] 273 | self.loginInfo['synckey'] = '|'.join(['%s_%s' % (item['Key'], item['Val']) 274 | for item in dic['SyncCheckKey']['List']]) 275 | return dic['AddMsgList'], dic['ModContactList'] 276 | 277 | def logout(self): 278 | if self.alive: 279 | url = '%s/webwxlogout' % self.loginInfo['url'] 280 | params = { 281 | 'redirect' : 1, 282 | 'type' : 1, 283 | 'skey' : self.loginInfo['skey'], } 284 | headers = { 'User-Agent' : config.USER_AGENT } 285 | self.s.get(url, params=params, headers=headers) 286 | self.alive = False 287 | self.s.cookies.clear() 288 | del self.chatroomList[:] 289 | del self.memberList[:] 290 | del self.mpList[:] 291 | return ReturnValue({'BaseResponse': { 292 | 'ErrMsg': 'logout successfully.', 293 | 'Ret': 0, }}) 294 | -------------------------------------------------------------------------------- /itchat/components/messages.py: -------------------------------------------------------------------------------- 1 | import os, time, re, io 2 | import json 3 | import mimetypes, hashlib 4 | import traceback, logging 5 | from collections import OrderedDict 6 | 7 | import requests 8 | 9 | from .. import config, utils 10 | from ..returnvalues import ReturnValue 11 | from .contact import update_local_uin 12 | 13 | logger = logging.getLogger('itchat') 14 | 15 | def load_messages(core): 16 | core.send_raw_msg = send_raw_msg 17 | core.send_msg = send_msg 18 | core.upload_file = upload_file 19 | core.send_file = send_file 20 | core.send_image = send_image 21 | core.send_video = send_video 22 | core.send = send 23 | 24 | def get_download_fn(core, url, msgId): 25 | def download_fn(downloadDir=None): 26 | params = { 27 | 'msgid': msgId, 28 | 'skey': core.loginInfo['skey'],} 29 | headers = { 'User-Agent' : config.USER_AGENT } 30 | r = core.s.get(url, params=params, stream=True, headers = headers) 31 | tempStorage = io.BytesIO() 32 | for block in r.iter_content(1024): 33 | tempStorage.write(block) 34 | if downloadDir is None: return tempStorage.getvalue() 35 | with open(downloadDir, 'wb') as f: f.write(tempStorage.getvalue()) 36 | return ReturnValue({'BaseResponse': { 37 | 'ErrMsg': 'Successfully downloaded', 38 | 'Ret': 0, }}) 39 | return download_fn 40 | 41 | def produce_msg(core, msgList): 42 | ''' for messages types 43 | * 40 msg, 43 videochat, 50 VOIPMSG, 52 voipnotifymsg 44 | * 53 webwxvoipnotifymsg, 9999 sysnotice 45 | ''' 46 | rl = [] 47 | srl = [40, 43, 50, 52, 53, 9999] 48 | for m in msgList: 49 | if '@@' in m['FromUserName'] or '@@' in m['ToUserName']: 50 | produce_group_chat(core, m) 51 | else: 52 | utils.msg_formatter(m, 'Content') 53 | if m['MsgType'] == 1: # words 54 | if m['Url']: 55 | regx = r'(.+?\(.+?\))' 56 | data = re.search(regx, m['Content']) 57 | data = 'Map' if data is None else data.group(1) 58 | msg = { 59 | 'Type': 'Map', 60 | 'Text': data,} 61 | else: 62 | msg = { 63 | 'Type': 'Text', 64 | 'Text': m['Content'],} 65 | elif m['MsgType'] == 3 or m['MsgType'] == 47: # picture 66 | download_fn = get_download_fn(core, 67 | '%s/webwxgetmsgimg' % core.loginInfo['url'], m['NewMsgId']) 68 | msg = { 69 | 'Type' : 'Picture', 70 | 'FileName' : '%s.%s' % (time.strftime('%y%m%d-%H%M%S', time.localtime()), 71 | 'png' if m['MsgType'] == 3 else 'gif'), 72 | 'Text' : download_fn, } 73 | elif m['MsgType'] == 34: # voice 74 | download_fn = get_download_fn(core, 75 | '%s/webwxgetvoice' % core.loginInfo['url'], m['NewMsgId']) 76 | msg = { 77 | 'Type': 'Recording', 78 | 'FileName' : '%s.mp3' % time.strftime('%y%m%d-%H%M%S', time.localtime()), 79 | 'Text': download_fn,} 80 | elif m['MsgType'] == 37: # friends 81 | msg = { 82 | 'Type': 'Friends', 83 | 'Text': { 84 | 'status' : m['Status'], 85 | 'userName' : m['RecommendInfo']['UserName'], 86 | 'verifyContent' : m['Ticket'], 87 | 'autoUpdate' : m['RecommendInfo'], }, } 88 | elif m['MsgType'] == 42: # name card 89 | msg = { 90 | 'Type': 'Card', 91 | 'Text': m['RecommendInfo'], } 92 | elif m['MsgType'] in (43, 62): # tiny video 93 | msgId = m['MsgId'] 94 | def download_video(videoDir=None): 95 | url = '%s/webwxgetvideo' % core.loginInfo['url'] 96 | params = { 97 | 'msgid': msgId, 98 | 'skey': core.loginInfo['skey'],} 99 | headers = {'Range': 'bytes=0-', 'User-Agent' : config.USER_AGENT } 100 | r = core.s.get(url, params=params, headers=headers, stream=True) 101 | tempStorage = io.BytesIO() 102 | for block in r.iter_content(1024): 103 | tempStorage.write(block) 104 | if videoDir is None: return tempStorage.getvalue() 105 | with open(videoDir, 'wb') as f: f.write(tempStorage.getvalue()) 106 | return ReturnValue({'BaseResponse': { 107 | 'ErrMsg': 'Successfully downloaded', 108 | 'Ret': 0, }}) 109 | msg = { 110 | 'Type': 'Video', 111 | 'FileName' : '%s.mp4' % time.strftime('%y%m%d-%H%M%S', time.localtime()), 112 | 'Text': download_video, } 113 | elif m['MsgType'] == 49: # sharing 114 | if m['AppMsgType'] == 6: 115 | rawMsg = m 116 | cookiesList = {name:data for name,data in core.s.cookies.items()} 117 | def download_atta(attaDir=None): 118 | url = core.loginInfo['fileUrl'] + '/webwxgetmedia' 119 | params = { 120 | 'sender': rawMsg['FromUserName'], 121 | 'mediaid': rawMsg['MediaId'], 122 | 'filename': rawMsg['FileName'], 123 | 'fromuser': core.loginInfo['wxuin'], 124 | 'pass_ticket': 'undefined', 125 | 'webwx_data_ticket': cookiesList['webwx_data_ticket'],} 126 | headers = { 'User-Agent' : config.USER_AGENT } 127 | r = core.s.get(url, params=params, stream=True, headers=headers) 128 | tempStorage = io.BytesIO() 129 | for block in r.iter_content(1024): 130 | tempStorage.write(block) 131 | if attaDir is None: return tempStorage.getvalue() 132 | with open(attaDir, 'wb') as f: f.write(tempStorage.getvalue()) 133 | return ReturnValue({'BaseResponse': { 134 | 'ErrMsg': 'Successfully downloaded', 135 | 'Ret': 0, }}) 136 | msg = { 137 | 'Type': 'Attachment', 138 | 'Text': download_atta, } 139 | elif m['AppMsgType'] == 8: 140 | download_fn = get_download_fn(core, 141 | '%s/webwxgetmsgimg' % core.loginInfo['url'], m['NewMsgId']) 142 | msg = { 143 | 'Type' : 'Picture', 144 | 'FileName' : '%s.gif' % ( 145 | time.strftime('%y%m%d-%H%M%S', time.localtime())), 146 | 'Text' : download_fn, } 147 | elif m['AppMsgType'] == 17: 148 | msg = { 149 | 'Type': 'Note', 150 | 'Text': m['FileName'], } 151 | elif m['AppMsgType'] == 2000: 152 | regx = r'\[CDATA\[(.+?)\][\s\S]+?\[CDATA\[(.+?)\]' 153 | data = re.search(regx, m['Content']) 154 | if data: 155 | data = data.group(2).split(u'\u3002')[0] 156 | else: 157 | data = 'You may found detailed info in Content key.' 158 | msg = { 159 | 'Type': 'Note', 160 | 'Text': data, } 161 | else: 162 | msg = { 163 | 'Type': 'Sharing', 164 | 'Text': m['FileName'], } 165 | elif m['MsgType'] == 51: # phone init 166 | msg = update_local_uin(core, m) 167 | elif m['MsgType'] == 10000: 168 | msg = { 169 | 'Type': 'Note', 170 | 'Text': m['Content'],} 171 | elif m['MsgType'] == 10002: 172 | regx = r'\[CDATA\[(.+?)\]\]' 173 | data = re.search(regx, m['Content']) 174 | data = 'System message' if data is None else data.group(1).replace('\\', '') 175 | msg = { 176 | 'Type': 'Note', 177 | 'Text': data, } 178 | elif m['MsgType'] in srl: 179 | msg = { 180 | 'Type': 'Useless', 181 | 'Text': 'UselessMsg', } 182 | else: 183 | logger.debug('Useless message received: %s\n%s' % (m['MsgType'], str(m))) 184 | msg = { 185 | 'Type': 'Useless', 186 | 'Text': 'UselessMsg', } 187 | m = dict(m, **msg) 188 | rl.append(m) 189 | return rl 190 | 191 | def produce_group_chat(core, msg): 192 | r = re.match('(@[0-9a-z]*?):
(.*)$', msg['Content']) 193 | if not r: 194 | utils.msg_formatter(msg, 'Content') 195 | return 196 | actualUserName, content = r.groups() 197 | chatroom = core.storageClass.search_chatrooms(userName=msg['FromUserName']) 198 | member = utils.search_dict_list((chatroom or {}).get( 199 | 'MemberList') or [], 'UserName', actualUserName) 200 | if member is None: 201 | chatroom = core.update_chatroom(msg['FromUserName']) 202 | member = utils.search_dict_list((chatroom or {}).get( 203 | 'MemberList') or [], 'UserName', actualUserName) 204 | msg['ActualUserName'] = actualUserName 205 | msg['ActualNickName'] = member['DisplayName'] or member['NickName'] 206 | msg['Content'] = content 207 | utils.msg_formatter(msg, 'Content') 208 | atFlag = '@' + (chatroom['self']['DisplayName'] 209 | or core.storageClass.nickName) 210 | msg['isAt'] = ( 211 | (atFlag + (u'\u2005' if u'\u2005' in msg['Content'] else ' ')) 212 | in msg['Content'] 213 | or 214 | msg['Content'].endswith(atFlag)) 215 | 216 | def send_raw_msg(self, msgType, content, toUserName): 217 | url = '%s/webwxsendmsg' % self.loginInfo['url'] 218 | data = { 219 | 'BaseRequest': self.loginInfo['BaseRequest'], 220 | 'Msg': { 221 | 'Type': msgType, 222 | 'Content': content, 223 | 'FromUserName': self.storageClass.userName, 224 | 'ToUserName': (toUserName if toUserName else self.storageClass.userName), 225 | 'LocalID': int(time.time() * 1e4), 226 | 'ClientMsgId': int(time.time() * 1e4), 227 | }, 228 | 'Scene': 0, } 229 | headers = { 'ContentType': 'application/json; charset=UTF-8', 'User-Agent' : config.USER_AGENT } 230 | r = self.s.post(url, headers=headers, 231 | data=json.dumps(data, ensure_ascii=False).encode('utf8')) 232 | return ReturnValue(rawResponse=r) 233 | 234 | def send_msg(self, msg='Test Message', toUserName=None): 235 | logger.debug('Request to send a text message to %s: %s' % (toUserName, msg)) 236 | r = self.send_raw_msg(1, msg, toUserName) 237 | return r 238 | 239 | def upload_file(self, fileDir, isPicture=False, isVideo=False, 240 | toUserName='filehelper'): 241 | logger.debug('Request to upload a %s: %s' % ( 242 | 'picture' if isPicture else 'video' if isVideo else 'file', fileDir)) 243 | if not utils.check_file(fileDir): 244 | return ReturnValue({'BaseResponse': { 245 | 'ErrMsg': 'No file found in specific dir', 246 | 'Ret': -1002, }}) 247 | fileSize = os.path.getsize(fileDir) 248 | fileSymbol = 'pic' if isPicture else 'video' if isVideo else'doc' 249 | with open(fileDir, 'rb') as f: fileMd5 = hashlib.md5(f.read()).hexdigest() 250 | file = open(fileDir, 'rb') 251 | chunks = int((fileSize - 1) / 524288) + 1 252 | clientMediaId = int(time.time() * 1e4) 253 | uploadMediaRequest = json.dumps(OrderedDict([ 254 | ('UploadType', 2), 255 | ('BaseRequest', self.loginInfo['BaseRequest']), 256 | ('ClientMediaId', clientMediaId), 257 | ('TotalLen', fileSize), 258 | ('StartPos', 0), 259 | ('DataLen', fileSize), 260 | ('MediaType', 4), 261 | ('FromUserName', self.storageClass.userName), 262 | ('ToUserName', toUserName), 263 | ('FileMd5', fileMd5)] 264 | ), separators = (',', ':')) 265 | for chunk in range(chunks): 266 | r = upload_chunk_file(self, fileDir, fileSymbol, fileSize, 267 | file, chunk, chunks, uploadMediaRequest) 268 | file.close() 269 | return ReturnValue(rawResponse=r) 270 | 271 | def upload_chunk_file(core, fileDir, fileSymbol, fileSize, 272 | file, chunk, chunks, uploadMediaRequest): 273 | url = core.loginInfo.get('fileUrl', core.loginInfo['url']) + \ 274 | '/webwxuploadmedia?f=json' 275 | # save it on server 276 | cookiesList = {name:data for name,data in core.s.cookies.items()} 277 | fileType = mimetypes.guess_type(fileDir)[0] or 'application/octet-stream' 278 | files = OrderedDict([ 279 | ('id', (None, 'WU_FILE_0')), 280 | ('name', (None, os.path.basename(fileDir))), 281 | ('type', (None, fileType)), 282 | ('lastModifiedDate', (None, time.strftime('%a %b %d %Y %H:%M:%S GMT+0800 (CST)'))), 283 | ('size', (None, str(fileSize))), 284 | ('chunks', (None, None)), 285 | ('chunk', (None, None)), 286 | ('mediatype', (None, fileSymbol)), 287 | ('uploadmediarequest', (None, uploadMediaRequest)), 288 | ('webwx_data_ticket', (None, cookiesList['webwx_data_ticket'])), 289 | ('pass_ticket', (None, core.loginInfo['pass_ticket'])), 290 | ('filename' , (os.path.basename(fileDir), file.read(524288), 'application/octet-stream'))]) 291 | if chunks == 1: 292 | del files['chunk']; del files['chunks'] 293 | else: 294 | files['chunk'], files['chunks'] = (None, str(chunk)), (None, str(chunks)) 295 | headers = { 'User-Agent' : config.USER_AGENT } 296 | return requests.post(url, files=files, headers=headers) 297 | 298 | def send_file(self, fileDir, toUserName=None, mediaId=None): 299 | logger.debug('Request to send a file(mediaId: %s) to %s: %s' % ( 300 | mediaId, toUserName, fileDir)) 301 | if toUserName is None: toUserName = self.storageClass.userName 302 | if mediaId is None: 303 | r = self.upload_file(fileDir) 304 | if r: 305 | mediaId = r['MediaId'] 306 | else: 307 | return r 308 | url = '%s/webwxsendappmsg?fun=async&f=json' % self.loginInfo['url'] 309 | data = { 310 | 'BaseRequest': self.loginInfo['BaseRequest'], 311 | 'Msg': { 312 | 'Type': 6, 313 | 'Content': ("%s"%os.path.basename(fileDir) + 314 | "6" + 315 | "%s%s"%(str(os.path.getsize(fileDir)), mediaId) + 316 | "%s"%os.path.splitext(fileDir)[1].replace('.','')), 317 | 'FromUserName': self.storageClass.userName, 318 | 'ToUserName': toUserName, 319 | 'LocalID': int(time.time() * 1e4), 320 | 'ClientMsgId': int(time.time() * 1e4), }, 321 | 'Scene': 0, } 322 | headers = { 323 | 'User-Agent': config.USER_AGENT, 324 | 'Content-Type': 'application/json;charset=UTF-8', } 325 | r = self.s.post(url, headers=headers, 326 | data=json.dumps(data, ensure_ascii=False).encode('utf8')) 327 | return ReturnValue(rawResponse=r) 328 | 329 | def send_image(self, fileDir, toUserName=None, mediaId=None): 330 | logger.debug('Request to send a image(mediaId: %s) to %s: %s' % ( 331 | mediaId, toUserName, fileDir)) 332 | if toUserName is None: toUserName = self.storageClass.userName 333 | if mediaId is None: 334 | r = self.upload_file(fileDir, isPicture=not fileDir[-4:] == '.gif') 335 | if r: 336 | mediaId = r['MediaId'] 337 | else: 338 | return r 339 | url = '%s/webwxsendmsgimg?fun=async&f=json' % self.loginInfo['url'] 340 | data = { 341 | 'BaseRequest': self.loginInfo['BaseRequest'], 342 | 'Msg': { 343 | 'Type': 3, 344 | 'MediaId': mediaId, 345 | 'FromUserName': self.storageClass.userName, 346 | 'ToUserName': toUserName, 347 | 'LocalID': int(time.time() * 1e4), 348 | 'ClientMsgId': int(time.time() * 1e4), }, 349 | 'Scene': 0, } 350 | if fileDir[-4:] == '.gif': 351 | url = '%s/webwxsendemoticon?fun=sys' % self.loginInfo['url'] 352 | data['Msg']['Type'] = 47 353 | data['Msg']['EmojiFlag'] = 2 354 | headers = { 355 | 'User-Agent': config.USER_AGENT, 356 | 'Content-Type': 'application/json;charset=UTF-8', } 357 | r = self.s.post(url, headers=headers, 358 | data=json.dumps(data, ensure_ascii=False).encode('utf8')) 359 | return ReturnValue(rawResponse=r) 360 | 361 | def send_video(self, fileDir=None, toUserName=None, mediaId=None): 362 | logger.debug('Request to send a video(mediaId: %s) to %s: %s' % ( 363 | mediaId, toUserName, fileDir)) 364 | if toUserName is None: toUserName = self.storageClass.userName 365 | if mediaId is None: 366 | r = self.upload_file(fileDir, isVideo=True) 367 | if r: 368 | mediaId = r['MediaId'] 369 | else: 370 | return r 371 | url = '%s/webwxsendvideomsg?fun=async&f=json&pass_ticket=%s' % ( 372 | self.loginInfo['url'], self.loginInfo['pass_ticket']) 373 | data = { 374 | 'BaseRequest': self.loginInfo['BaseRequest'], 375 | 'Msg': { 376 | 'Type' : 43, 377 | 'MediaId' : mediaId, 378 | 'FromUserName' : self.storageClass.userName, 379 | 'ToUserName' : toUserName, 380 | 'LocalID' : int(time.time() * 1e4), 381 | 'ClientMsgId' : int(time.time() * 1e4), }, 382 | 'Scene': 0, } 383 | headers = { 384 | 'User-Agent' : config.USER_AGENT, 385 | 'Content-Type': 'application/json;charset=UTF-8', } 386 | r = self.s.post(url, headers=headers, 387 | data=json.dumps(data, ensure_ascii=False).encode('utf8')) 388 | return ReturnValue(rawResponse=r) 389 | 390 | def send(self, msg, toUserName=None, mediaId=None): 391 | if not msg: 392 | r = ReturnValue({'BaseResponse': { 393 | 'ErrMsg': 'No message.', 394 | 'Ret': -1005, }}) 395 | elif msg[:5] == '@fil@': 396 | if mediaId is None: 397 | r = self.send_file(msg[5:], toUserName) 398 | else: 399 | r = self.send_file(msg[5:], toUserName, mediaId) 400 | elif msg[:5] == '@img@': 401 | if mediaId is None: 402 | r = self.send_image(msg[5:], toUserName) 403 | else: 404 | r = self.send_image(msg[5:], toUserName, mediaId) 405 | elif msg[:5] == '@msg@': 406 | r = self.send_msg(msg[5:], toUserName) 407 | elif msg[:5] == '@vid@': 408 | if mediaId is None: 409 | r = self.send_video(msg[5:], toUserName) 410 | else: 411 | r = self.send_video(msg[5:], toUserName, mediaId) 412 | else: 413 | r = self.send_msg(msg, toUserName) 414 | return r 415 | -------------------------------------------------------------------------------- /itchat/components/register.py: -------------------------------------------------------------------------------- 1 | import logging, traceback, sys, threading 2 | try: 3 | import Queue 4 | except ImportError: 5 | import queue as Queue 6 | 7 | from ..log import set_logging 8 | from ..utils import test_connect 9 | 10 | logger = logging.getLogger('itchat') 11 | 12 | def load_register(core): 13 | core.auto_login = auto_login 14 | core.configured_reply = configured_reply 15 | core.msg_register = msg_register 16 | core.run = run 17 | 18 | def auto_login(self, hotReload=False, statusStorageDir='itchat.pkl', 19 | enableCmdQR=False, picDir=None, qrCallback=None, 20 | loginCallback=None, exitCallback=None): 21 | if not test_connect(): 22 | logger.info("You don't have access to internet or wechat domain, so exit.") 23 | sys.exit() 24 | self.useHotReload = hotReload 25 | if hotReload: 26 | if self.load_login_status(statusStorageDir, 27 | loginCallback=loginCallback, exitCallback=exitCallback): 28 | return 29 | self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback, 30 | loginCallback=loginCallback, exitCallback=exitCallback) 31 | self.dump_login_status(statusStorageDir) 32 | self.hotReloadDir = statusStorageDir 33 | else: 34 | self.login(enableCmdQR=enableCmdQR, picDir=picDir, qrCallback=qrCallback, 35 | loginCallback=loginCallback, exitCallback=exitCallback) 36 | 37 | def configured_reply(self): 38 | ''' determine the type of message and reply if its method is defined 39 | however, I use a strange way to determine whether a msg is from massive platform 40 | I haven't found a better solution here 41 | The main problem I'm worrying about is the mismatching of new friends added on phone 42 | If you have any good idea, pleeeease report an issue. I will be more than grateful. 43 | ''' 44 | try: 45 | msg = self.msgList.get(timeout=1) 46 | except Queue.Empty: 47 | pass 48 | else: 49 | if msg['FromUserName'] == self.storageClass.userName: 50 | actualOpposite = msg['ToUserName'] 51 | else: 52 | actualOpposite = msg['FromUserName'] 53 | if '@@' in actualOpposite: 54 | replyFn = self.functionDict['GroupChat'].get(msg['Type']) 55 | elif self.search_mps(userName=msg['FromUserName']): 56 | replyFn = self.functionDict['MpChat'].get(msg['Type']) 57 | elif '@' in actualOpposite or \ 58 | actualOpposite in ('filehelper', 'fmessage'): 59 | replyFn = self.functionDict['FriendChat'].get(msg['Type']) 60 | else: 61 | replyFn = self.functionDict['MpChat'].get(msg['Type']) 62 | if replyFn is None: 63 | r = None 64 | else: 65 | try: 66 | r = replyFn(msg) 67 | if r is not None: self.send(r, msg.get('FromUserName')) 68 | except: 69 | logger.warning('An error occurred in registered function, use `itchat.run(debug=True)` to show detailed information') 70 | logger.debug(traceback.format_exc()) 71 | 72 | def msg_register(self, msgType, isFriendChat=False, isGroupChat=False, isMpChat=False): 73 | ''' a decorator constructor 74 | return a specific decorator based on information given ''' 75 | if not isinstance(msgType, list): msgType = [msgType] 76 | def _msg_register(fn): 77 | for _msgType in msgType: 78 | if isFriendChat: 79 | self.functionDict['FriendChat'][_msgType] = fn 80 | if isGroupChat: 81 | self.functionDict['GroupChat'][_msgType] = fn 82 | if isMpChat: 83 | self.functionDict['MpChat'][_msgType] = fn 84 | if not any((isFriendChat, isGroupChat, isMpChat)): 85 | self.functionDict['FriendChat'][_msgType] = fn 86 | return _msg_register 87 | 88 | def run(self, debug=False, blockThread=True): 89 | logger.info('Start auto replying.') 90 | if debug: 91 | set_logging(loggingLevel=logging.DEBUG) 92 | def reply_fn(): 93 | try: 94 | while self.alive: self.configured_reply() 95 | except KeyboardInterrupt: 96 | if self.useHotReload: self.dump_login_status() 97 | self.alive = False 98 | logger.debug('itchat received an ^C and exit.') 99 | print('Bye~') 100 | if blockThread: 101 | reply_fn() 102 | else: 103 | replyThread = threading.Thread(target=reply_fn) 104 | replyThread.setDaemon(True) 105 | replyThread.start() 106 | -------------------------------------------------------------------------------- /itchat/config.py: -------------------------------------------------------------------------------- 1 | import os, platform 2 | 3 | VERSION = '1.2.18' 4 | BASE_URL = 'https://login.weixin.qq.com' 5 | OS = platform.system() #Windows, Linux, Darwin 6 | DIR = os.getcwd() 7 | DEFAULT_QR = 'QR.jpg' 8 | 9 | USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36' 10 | -------------------------------------------------------------------------------- /itchat/content.py: -------------------------------------------------------------------------------- 1 | TEXT = 'Text' 2 | MAP = 'Map' 3 | CARD = 'Card' 4 | NOTE = 'Note' 5 | SHARING = 'Sharing' 6 | PICTURE = 'Picture' 7 | RECORDING = 'Recording' 8 | ATTACHMENT = 'Attachment' 9 | VIDEO = 'Video' 10 | FRIENDS = 'Friends' 11 | SYSTEM = 'System' 12 | -------------------------------------------------------------------------------- /itchat/core.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import requests 4 | 5 | from . import config, storage, utils, log 6 | from .components import load_components 7 | 8 | class Core(object): 9 | def __init__(self): 10 | ''' init is the only method defined in core.py 11 | alive is value showing whether core is running 12 | - you should call logout method to change it 13 | - after logout, a core object can login again 14 | storageClass only uses basic python types 15 | - so for advanced uses, inherit it yourself 16 | receivingRetryCount is for receiving loop retry 17 | - it's 5 now, but actually even 1 is enough 18 | - failing is failing 19 | ''' 20 | self.alive = False 21 | self.storageClass = storage.Storage() 22 | self.memberList = self.storageClass.memberList 23 | self.mpList = self.storageClass.mpList 24 | self.chatroomList = self.storageClass.chatroomList 25 | self.msgList = self.storageClass.msgList 26 | self.loginInfo = {} 27 | self.s = requests.Session() 28 | self.uuid = None 29 | self.functionDict = {'FriendChat': {}, 'GroupChat': {}, 'MpChat': {}} 30 | self.useHotReload, self.hotReloadDir = False, 'itchat.pkl' 31 | self.receivingRetryCount = 5 32 | def login(self, enableCmdQR=False, picDir=None, qrCallback=None, 33 | loginCallback=None, exitCallback=None): 34 | ''' log in like web wechat does 35 | for log in 36 | - a QR code will be downloaded and opened 37 | - then scanning status is logged, it paused for you confirm 38 | - finally it logged in and show your nickName 39 | for options 40 | - enableCmdQR: show qrcode in command line 41 | - integers can be used to fit strange char length 42 | - picDir: place for storing qrcode 43 | - qrCallback: method that should accept uuid, status, qrcode 44 | - loginCallback: callback after successfully logged in 45 | - if not set, screen is cleared and qrcode is deleted 46 | - exitCallback: callback after logged out 47 | - it contains calling of logout 48 | for usage 49 | ..code::python 50 | 51 | import itchat 52 | itchat.login() 53 | 54 | it is defined in components/login.py 55 | and of course every single move in login can be called outside 56 | - you may scan source code to see how 57 | - and modified according to your own demond 58 | ''' 59 | raise NotImplementedError() 60 | def get_QRuuid(self): 61 | ''' get uuid for qrcode 62 | uuid is the symbol of qrcode 63 | - for logging in, you need to get a uuid first 64 | - for downloading qrcode, you need to pass uuid to it 65 | - for checking login status, uuid is also required 66 | if uuid has timed out, just get another 67 | it is defined in components/login.py 68 | ''' 69 | raise NotImplementedError() 70 | def get_QR(self, uuid=None, enableCmdQR=False, picDir=None, qrCallback=None): 71 | ''' download and show qrcode 72 | for options 73 | - uuid: if uuid is not set, latest uuid you fetched will be used 74 | - enableCmdQR: show qrcode in cmd 75 | - picDir: where to store qrcode 76 | - qrCallback: method that should accept uuid, status, qrcode 77 | it is defined in components/login.py 78 | ''' 79 | raise NotImplementedError() 80 | def check_login(self, uuid=None): 81 | ''' check login status 82 | for options: 83 | - uuid: if uuid is not set, latest uuid you fetched will be used 84 | for return values: 85 | - a string will be returned 86 | - for meaning of return values 87 | - 200: log in successfully 88 | - 201: waiting for press confirm 89 | - 408: uuid timed out 90 | - 0 : unknown error 91 | for processing: 92 | - syncUrl and fileUrl is set 93 | - BaseRequest is set 94 | blocks until reaches any of above status 95 | it is defined in components/login.py 96 | ''' 97 | raise NotImplementedError() 98 | def web_init(self): 99 | ''' get info necessary for initializing 100 | for processing: 101 | - own account info is set 102 | - inviteStartCount is set 103 | - syncKey is set 104 | - part of contact is fetched 105 | it is defined in components/login.py 106 | ''' 107 | raise NotImplementedError() 108 | def show_mobile_login(self): 109 | ''' show web wechat login sign 110 | the sign is on the top of mobile phone wechat 111 | sign will be added after sometime even without calling this function 112 | it is defined in components/login.py 113 | ''' 114 | raise NotImplementedError() 115 | def start_receiving(self, exitCallback=None, getReceivingFnOnly=False): 116 | ''' open a thread for heart loop and receiving messages 117 | for options: 118 | - exitCallback: callback after logged out 119 | - it contains calling of logout 120 | - getReceivingFnOnly: if True thread will not be created and started. Instead, receive fn will be returned. 121 | for processing: 122 | - messages: msgs are formatted and passed on to registered fns 123 | - contact : chatrooms are updated when related info is received 124 | it is defined in components/login.py 125 | ''' 126 | raise NotImplementedError() 127 | def get_msg(self): 128 | ''' fetch messages 129 | for fetching 130 | - method blocks for sometime util 131 | - new messages are to be received 132 | - or anytime they like 133 | - synckey is updated with returned synccheckkey 134 | it is defined in components/login.py 135 | ''' 136 | raise NotImplementedError() 137 | def logout(self): 138 | ''' logout 139 | if core is now alive 140 | logout will tell wechat backstage to logout 141 | and core gets ready for another login 142 | it is defined in components/login.py 143 | ''' 144 | raise NotImplementedError() 145 | def update_chatroom(self, userName, detailedMember=False): 146 | ''' update chatroom 147 | for chatroom contact 148 | - a chatroom contact need updating to be detailed 149 | - detailed means members, encryid, etc 150 | - auto updating of heart loop is a more detailed updating 151 | - member uin will also be filled 152 | - once called, updated info will be stored 153 | for options 154 | - userName: 'UserName' key of chatroom or a list of it 155 | - detailedMember: whether to get members of contact 156 | it is defined in components/contact.py 157 | ''' 158 | raise NotImplementedError() 159 | def update_friend(self, userName): 160 | ''' update chatroom 161 | for friend contact 162 | - once called, updated info will be stored 163 | for options 164 | - userName: 'UserName' key of a friend or a list of it 165 | it is defined in components/contact.py 166 | ''' 167 | raise NotImplementedError() 168 | def get_contact(self, update=False): 169 | ''' fetch part of contact 170 | for part 171 | - all the massive platforms and friends are fetched 172 | - if update, only starred chatrooms are fetched 173 | for options 174 | - update: if not set, local value will be returned 175 | for results 176 | - chatroomList will be returned 177 | it is defined in components/contact.py 178 | ''' 179 | raise NotImplementedError() 180 | def get_friends(self, update=False): 181 | ''' fetch friends list 182 | for options 183 | - update: if not set, local value will be returned 184 | for results 185 | - a list of friends' info dicts will be returned 186 | it is defined in components/contact.py 187 | ''' 188 | raise NotImplementedError() 189 | def get_chatrooms(self, update=False, contactOnly=False): 190 | ''' fetch chatrooms list 191 | for options 192 | - update: if not set, local value will be returned 193 | - contactOnly: if set, only starred chatrooms will be returned 194 | for results 195 | - a list of chatrooms' info dicts will be returned 196 | it is defined in components/contact.py 197 | ''' 198 | raise NotImplementedError() 199 | def get_mps(self, update=False): 200 | ''' fetch massive platforms list 201 | for options 202 | - update: if not set, local value will be returned 203 | for results 204 | - a list of platforms' info dicts will be returned 205 | it is defined in components/contact.py 206 | ''' 207 | raise NotImplementedError() 208 | def set_alias(self, userName, alias): 209 | ''' set alias for a friend 210 | for options 211 | - userName: 'UserName' key of info dict 212 | - alias: new alias 213 | it is defined in components/contact.py 214 | ''' 215 | raise NotImplementedError() 216 | def set_pinned(self, userName, isPinned=True): 217 | ''' set pinned for a friend or a chatroom 218 | for options 219 | - userName: 'UserName' key of info dict 220 | - isPinned: whether to pin 221 | it is defined in components/contact.py 222 | ''' 223 | raise NotImplementedError() 224 | def add_friend(self, userName, status=2, verifyContent='', autoUpdate=True): 225 | ''' add a friend or accept a friend 226 | for options 227 | - userName: 'UserName' for friend's info dict 228 | - status: 229 | - for adding status should be 2 230 | - for accepting status should be 3 231 | - ticket: greeting message 232 | - userInfo: friend's other info for adding into local storage 233 | it is defined in components/contact.py 234 | ''' 235 | raise NotImplementedError() 236 | def get_head_img(self, userName=None, chatroomUserName=None, picDir=None): 237 | ''' place for docs 238 | for options 239 | - if you want to get chatroom header: only set chatroomUserName 240 | - if you want to get friend header: only set userName 241 | - if you want to get chatroom member header: set both 242 | it is defined in components/contact.py 243 | ''' 244 | raise NotImplementedError() 245 | def create_chatroom(self, memberList, topic=''): 246 | ''' create a chatroom 247 | for creating 248 | - its calling frequency is strictly limited 249 | for options 250 | - memberList: list of member info dict 251 | - topic: topic of new chatroom 252 | it is defined in components/contact.py 253 | ''' 254 | raise NotImplementedError() 255 | def set_chatroom_name(self, chatroomUserName, name): 256 | ''' set chatroom name 257 | for setting 258 | - it makes an updating of chatroom 259 | - which means detailed info will be returned in heart loop 260 | for options 261 | - chatroomUserName: 'UserName' key of chatroom info dict 262 | - name: new chatroom name 263 | it is defined in components/contact.py 264 | ''' 265 | raise NotImplementedError() 266 | def delete_member_from_chatroom(self, chatroomUserName, memberList): 267 | ''' deletes members from chatroom 268 | for deleting 269 | - you can't delete yourself 270 | - if so, no one will be deleted 271 | - strict-limited frequency 272 | for options 273 | - chatroomUserName: 'UserName' key of chatroom info dict 274 | - memberList: list of members' info dict 275 | it is defined in components/contact.py 276 | ''' 277 | raise NotImplementedError() 278 | def add_member_into_chatroom(self, chatroomUserName, memberList, 279 | useInvitation=False): 280 | ''' add members into chatroom 281 | for adding 282 | - you can't add yourself or member already in chatroom 283 | - if so, no one will be added 284 | - if member will over 40 after adding, invitation must be used 285 | - strict-limited frequency 286 | for options 287 | - chatroomUserName: 'UserName' key of chatroom info dict 288 | - memberList: list of members' info dict 289 | - useInvitation: if invitation is not required, set this to use 290 | it is defined in components/contact.py 291 | ''' 292 | raise NotImplementedError() 293 | def send_raw_msg(self, msgType, content, toUserName): 294 | ''' many messages are sent in a common way 295 | for demo 296 | .. code:: python 297 | 298 | @itchat.msg_register(itchat.content.CARD) 299 | def reply(msg): 300 | itchat.send_raw_msg(msg['MsgType'], msg['Content'], msg['FromUserName']) 301 | 302 | there are some little tricks here, you may discover them yourself 303 | but remember they are tricks 304 | it is defined in components/messages.py 305 | ''' 306 | raise NotImplementedError() 307 | def send_msg(self, msg='Test Message', toUserName=None): 308 | ''' send plain text message 309 | for options 310 | - msg: should be unicode if there's non-ascii words in msg 311 | - toUserName: 'UserName' key of friend dict 312 | it is defined in components/messages.py 313 | ''' 314 | raise NotImplementedError() 315 | def upload_file(self, fileDir, isPicture=False, isVideo=False, 316 | toUserName='filehelper'): 317 | ''' upload file to server and get mediaId 318 | for options 319 | - fileDir: dir for file ready for upload 320 | - isPicture: whether file is a picture 321 | - isVideo: whether file is a video 322 | for return values 323 | will return a ReturnValue 324 | if succeeded, mediaId is in r['MediaId'] 325 | it is defined in components/messages.py 326 | ''' 327 | raise NotImplementedError() 328 | def send_file(self, fileDir, toUserName=None, mediaId=None): 329 | ''' send attachment 330 | for options 331 | - fileDir: dir for file ready for upload 332 | - mediaId: mediaId for file. 333 | - if set, file will not be uploaded twice 334 | - toUserName: 'UserName' key of friend dict 335 | it is defined in components/messages.py 336 | ''' 337 | raise NotImplementedError() 338 | def send_image(self, fileDir, toUserName=None, mediaId=None): 339 | ''' send image 340 | for options 341 | - fileDir: dir for file ready for upload 342 | - if it's a gif, name it like 'xx.gif' 343 | - mediaId: mediaId for file. 344 | - if set, file will not be uploaded twice 345 | - toUserName: 'UserName' key of friend dict 346 | it is defined in components/messages.py 347 | ''' 348 | raise NotImplementedError() 349 | def send_video(self, fileDir=None, toUserName=None, mediaId=None): 350 | ''' send video 351 | for options 352 | - fileDir: dir for file ready for upload 353 | - if mediaId is set, it's unnecessary to set fileDir 354 | - mediaId: mediaId for file. 355 | - if set, file will not be uploaded twice 356 | - toUserName: 'UserName' key of friend dict 357 | it is defined in components/messages.py 358 | ''' 359 | raise NotImplementedError() 360 | def send(self, msg, toUserName=None, mediaId=None): 361 | ''' wrapped function for all the sending functions 362 | for options 363 | - msg: message starts with different string indicates different type 364 | - list of type string: ['@fil@', '@img@', '@msg@', '@vid@'] 365 | - they are for file, image, plain text, video 366 | - if none of them matches, it will be sent like plain text 367 | - toUserName: 'UserName' key of friend dict 368 | - mediaId: if set, uploading will not be repeated 369 | it is defined in components/messages.py 370 | ''' 371 | raise NotImplementedError() 372 | def dump_login_status(self, fileDir=None): 373 | ''' dump login status to a specific file 374 | for option 375 | - fileDir: dir for dumping login status 376 | it is defined in components/hotreload.py 377 | ''' 378 | raise NotImplementedError() 379 | def load_login_status(self, fileDir, 380 | loginCallback=None, exitCallback=None): 381 | ''' load login status from a specific file 382 | for option 383 | - fileDir: file for loading login status 384 | - loginCallback: callback after successfully logged in 385 | - if not set, screen is cleared and qrcode is deleted 386 | - exitCallback: callback after logged out 387 | - it contains calling of logout 388 | it is defined in components/hotreload.py 389 | ''' 390 | raise NotImplementedError() 391 | def auto_login(self, hotReload=False, statusStorageDir='itchat.pkl', 392 | enableCmdQR=False, picDir=None, qrCallback=None, 393 | loginCallback=None, exitCallback=None): 394 | ''' log in like web wechat does 395 | for log in 396 | - a QR code will be downloaded and opened 397 | - then scanning status is logged, it paused for you confirm 398 | - finally it logged in and show your nickName 399 | for options 400 | - hotReload: enable hot reload 401 | - statusStorageDir: dir for storing log in status 402 | - enableCmdQR: show qrcode in command line 403 | - integers can be used to fit strange char length 404 | - picDir: place for storing qrcode 405 | - loginCallback: callback after successfully logged in 406 | - if not set, screen is cleared and qrcode is deleted 407 | - exitCallback: callback after logged out 408 | - it contains calling of logout 409 | - qrCallback: method that should accept uuid, status, qrcode 410 | for usage 411 | ..code::python 412 | 413 | import itchat 414 | itchat.auto_login() 415 | 416 | it is defined in components/register.py 417 | and of course every single move in login can be called outside 418 | - you may scan source code to see how 419 | - and modified according to your own demond 420 | ''' 421 | raise NotImplementedError() 422 | def configured_reply(self): 423 | ''' determine the type of message and reply if its method is defined 424 | however, I use a strange way to determine whether a msg is from massive platform 425 | I haven't found a better solution here 426 | The main problem I'm worrying about is the mismatching of new friends added on phone 427 | If you have any good idea, pleeeease report an issue. I will be more than grateful. 428 | ''' 429 | raise NotImplementedError() 430 | def msg_register(self, msgType, 431 | isFriendChat=False, isGroupChat=False, isMpChat=False): 432 | ''' a decorator constructor 433 | return a specific decorator based on information given 434 | ''' 435 | raise NotImplementedError() 436 | def run(self, debug=True, blockThread=True): 437 | ''' start auto respond 438 | for option 439 | - debug: if set, debug info will be shown on screen 440 | it is defined in components/register.py 441 | ''' 442 | raise NotImplementedError() 443 | def search_friends(self, name=None, userName=None, remarkName=None, nickName=None, 444 | wechatAccount=None): 445 | return self.storageClass.search_friends(name, userName, remarkName, 446 | nickName, wechatAccount) 447 | def search_chatrooms(self, name=None, userName=None): 448 | return self.storageClass.search_chatrooms(name, userName) 449 | def search_mps(self, name=None, userName=None): 450 | return self.storageClass.search_mps(name, userName) 451 | 452 | load_components(Core) 453 | -------------------------------------------------------------------------------- /itchat/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | class LogSystem(object): 4 | handlerList = [] 5 | showOnCmd = True 6 | loggingLevel = logging.INFO 7 | loggingFile = None 8 | def __init__(self): 9 | self.logger = logging.getLogger('itchat') 10 | self.logger.addHandler(logging.NullHandler()) 11 | self.logger.setLevel(self.loggingLevel) 12 | self.cmdHandler = logging.StreamHandler() 13 | self.fileHandler = None 14 | self.logger.addHandler(self.cmdHandler) 15 | def set_logging(self, showOnCmd=True, loggingFile=None, 16 | loggingLevel=logging.INFO): 17 | if showOnCmd != self.showOnCmd: 18 | if showOnCmd: 19 | self.logger.addHandler(self.cmdHandler) 20 | else: 21 | self.logger.removeHandler(self.cmdHandler) 22 | self.showOnCmd = showOnCmd 23 | if loggingFile != self.loggingFile: 24 | if self.loggingFile is not None: # clear old fileHandler 25 | self.logger.removeHandler(self.fileHandler) 26 | self.fileHandler.close() 27 | if loggingFile is not None: # add new fileHandler 28 | self.fileHandler = logging.FileHandler(loggingFile) 29 | self.logger.addHandler(self.fileHandler) 30 | self.loggingFile = loggingFile 31 | if loggingLevel != self.loggingLevel: 32 | self.logger.setLevel(loggingLevel) 33 | self.loggingLevel = loggingLevel 34 | 35 | ls = LogSystem() 36 | set_logging = ls.set_logging 37 | -------------------------------------------------------------------------------- /itchat/returnvalues.py: -------------------------------------------------------------------------------- 1 | #coding=utf8 2 | import sys 3 | 4 | TRANSLATE = 'Chinese' 5 | 6 | class ReturnValue(dict): 7 | def __init__(self, returnValueDict={}, rawResponse=None): 8 | if rawResponse: 9 | try: 10 | returnValueDict = rawResponse.json() 11 | except ValueError: 12 | returnValueDict = { 13 | 'BaseResponse': { 14 | 'Ret': -1004, 15 | 'ErrMsg': 'Unexpected return value', }, 16 | 'Data': rawResponse.content, } 17 | for k, v in returnValueDict.items(): self[k] = v 18 | if not 'BaseResponse' in self: 19 | self['BaseResponse'] = { 20 | 'ErrMsg': 'no BaseResponse in raw response', 21 | 'Ret': -1000, } 22 | if TRANSLATE: 23 | self['BaseResponse']['RawMsg'] = self['BaseResponse'].get('ErrMsg', '') 24 | self['BaseResponse']['ErrMsg'] = \ 25 | TRANSLATION[TRANSLATE].get( 26 | self['BaseResponse'].get('Ret', '')) \ 27 | or self['BaseResponse'].get('ErrMsg', u'No ErrMsg') 28 | self['BaseResponse']['RawMsg'] = \ 29 | self['BaseResponse']['RawMsg'] or self['BaseResponse']['ErrMsg'] 30 | def __nonzero__(self): 31 | return self['BaseResponse'].get('Ret') == 0 32 | def __bool__(self): 33 | return self.__nonzero__() 34 | def __str__(self): 35 | return '{%s}' % ', '.join( 36 | ['%s: %s' % (repr(k),repr(v)) for k,v in self.items()]) 37 | def __repr__(self): 38 | return '' % self.__str__() 39 | 40 | TRANSLATION = { 41 | 'Chinese': { 42 | -1000: u'返回值不带BaseResponse', 43 | -1001: u'无法找到对应的成员', 44 | -1002: u'文件位置错误', 45 | -1003: u'服务器拒绝连接', 46 | -1004: u'服务器返回异常值', 47 | -1005: u'参数错误', 48 | 0: u'请求成功', 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /itchat/storage.py: -------------------------------------------------------------------------------- 1 | import os, time, copy 2 | try: 3 | import Queue 4 | except ImportError: 5 | import queue as Queue 6 | 7 | class Storage(object): 8 | def __init__(self): 9 | self.userName = None 10 | self.nickName = None 11 | self.memberList = [] 12 | self.mpList = [] 13 | self.chatroomList = [] 14 | self.msgList = Queue.Queue(-1) 15 | self.lastInputUserName = None 16 | def dumps(self): 17 | return { 18 | 'userName' : self.userName, 19 | 'nickName' : self.nickName, 20 | 'memberList' : self.memberList, 21 | 'mpList' : self.mpList, 22 | 'chatroomList' : self.chatroomList, 23 | 'lastInputUserName' : self.lastInputUserName, } 24 | def loads(self, j): 25 | self.userName = j.get('userName', None) 26 | self.nickName = j.get('nickName', None) 27 | del self.memberList[:] 28 | for i in j.get('memberList', []): self.memberList.append(i) 29 | del self.mpList[:] 30 | for i in j.get('mpList', []): self.mpList.append(i) 31 | del self.chatroomList[:] 32 | for i in j.get('chatroomList', []): self.chatroomList.append(i) 33 | self.lastInputUserName = j.get('lastInputUserName', None) 34 | def search_friends(self, name=None, userName=None, remarkName=None, nickName=None, 35 | wechatAccount=None): 36 | if (name or userName or remarkName or nickName or wechatAccount) is None: 37 | return copy.deepcopy(self.memberList[0]) # my own account 38 | elif userName: # return the only userName match 39 | for m in self.memberList: 40 | if m['UserName'] == userName: return copy.deepcopy(m) 41 | else: 42 | matchDict = { 43 | 'RemarkName' : remarkName, 44 | 'NickName' : nickName, 45 | 'Alias' : wechatAccount, } 46 | for k in ('RemarkName', 'NickName', 'Alias'): 47 | if matchDict[k] is None: del matchDict[k] 48 | if name: # select based on name 49 | contract = [] 50 | for m in self.memberList: 51 | if any([m.get(k) == name for k in ('RemarkName', 'NickName', 'Alias')]): 52 | contract.append(m) 53 | else: 54 | contract = self.memberList[:] 55 | if matchDict: # select again based on matchDict 56 | friendList = [] 57 | for m in contract: 58 | if all([m.get(k) == v for k, v in matchDict.items()]): 59 | friendList.append(m) 60 | return copy.deepcopy(friendList) 61 | else: 62 | return copy.deepcopy(contract) 63 | def search_chatrooms(self, name=None, userName=None): 64 | if userName is not None: 65 | for m in self.chatroomList: 66 | if m['UserName'] == userName: return copy.deepcopy(m) 67 | elif name is not None: 68 | matchList = [] 69 | for m in self.chatroomList: 70 | if name in m['NickName']: matchList.append(copy.deepcopy(m)) 71 | return matchList 72 | def search_mps(self, name=None, userName=None): 73 | if userName is not None: 74 | for m in self.mpList: 75 | if m['UserName'] == userName: return copy.deepcopy(m) 76 | elif name is not None: 77 | matchList = [] 78 | for m in self.mpList: 79 | if name in m['NickName']: matchList.append(copy.deepcopy(m)) 80 | return matchList 81 | -------------------------------------------------------------------------------- /itchat/utils.py: -------------------------------------------------------------------------------- 1 | import re, os, sys, subprocess, copy 2 | 3 | try: 4 | from HTMLParser import HTMLParser 5 | except ImportError: 6 | from html.parser import HTMLParser 7 | 8 | import requests 9 | 10 | from . import config 11 | 12 | emojiRegex = re.compile(r'') 13 | htmlParser = HTMLParser() 14 | try: 15 | b = u'\u2588' 16 | sys.stdout.write(b + '\r') 17 | sys.stdout.flush() 18 | except UnicodeEncodeError: 19 | BLOCK = 'MM' 20 | else: 21 | BLOCK = b 22 | friendInfoTemplate = {} 23 | for k in ('UserName', 'City', 'DisplayName', 'PYQuanPin', 'RemarkPYInitial', 'Province', 24 | 'KeyWord', 'RemarkName', 'PYInitial', 'EncryChatRoomId', 'Alias', 'Signature', 25 | 'NickName', 'RemarkPYQuanPin', 'HeadImgUrl'): friendInfoTemplate[k] = '' 26 | for k in ('UniFriend', 'Sex', 'AppAccountFlag', 'VerifyFlag', 'ChatRoomId', 'HideInputBarFlag', 27 | 'AttrStatus', 'SnsFlag', 'MemberCount', 'OwnerUin', 'ContactFlag', 'Uin', 28 | 'StarFriend', 'Statues'): friendInfoTemplate[k] = 0 29 | friendInfoTemplate['MemberList'] = [] 30 | 31 | def clear_screen(): 32 | os.system('cls' if config.OS == 'Windows' else 'clear') 33 | def emoji_formatter(d, k): 34 | # _emoji_deebugger is for bugs about emoji match caused by wechat backstage 35 | # like :face with tears of joy: will be replaced with :cat face with tears of joy: 36 | def _emoji_debugger(d, k): 37 | s = d[k].replace('') # fix missing bug 39 | def __fix_miss_match(m): 40 | return '' % ({ 41 | '1f63c': '1f601', '1f639': '1f602', '1f63a': '1f603', 42 | '1f4ab': '1f616', '1f64d': '1f614', '1f63b': '1f60d', 43 | '1f63d': '1f618', '1f64e': '1f621', '1f63f': '1f622', 44 | }.get(m.group(1), m.group(1))) 45 | return emojiRegex.sub(__fix_miss_match, s) 46 | def _emoji_formatter(m): 47 | s = m.group(1) 48 | if len(s) == 6: 49 | return ('\\U%s\\U%s'%(s[:2].rjust(8, '0'), s[2:].rjust(8, '0')) 50 | ).encode('utf8').decode('unicode-escape', 'replace') 51 | elif len(s) == 10: 52 | return ('\\U%s\\U%s'%(s[:5].rjust(8, '0'), s[5:].rjust(8, '0')) 53 | ).encode('utf8').decode('unicode-escape', 'replace') 54 | else: 55 | return ('\\U%s'%m.group(1).rjust(8, '0') 56 | ).encode('utf8').decode('unicode-escape', 'replace') 57 | d[k] = _emoji_debugger(d, k) 58 | d[k] = emojiRegex.sub(_emoji_formatter, d[k]) 59 | def msg_formatter(d, k): 60 | emoji_formatter(d, k) 61 | d[k] = d[k].replace('
', '\n') 62 | d[k] = htmlParser.unescape(d[k]) 63 | def check_file(fileDir): 64 | try: 65 | with open(fileDir): pass 66 | return True 67 | except: 68 | return False 69 | def print_qr(fileDir): 70 | if config.OS == 'Darwin': 71 | subprocess.call(['open', fileDir]) 72 | elif config.OS == 'Linux': 73 | subprocess.call(['xdg-open', fileDir]) 74 | else: 75 | os.startfile(fileDir) 76 | try: 77 | from PIL import Image 78 | def print_cmd_qr(fileDir, size = 37, padding = 3, 79 | white = BLOCK, black = ' ', enableCmdQR = True): 80 | img = Image.open(fileDir) 81 | times = img.size[0] / (size + padding * 2) 82 | rgb = img.convert('RGB') 83 | try: 84 | blockCount = int(enableCmdQR) 85 | assert(0 < abs(blockCount)) 86 | except: 87 | blockCount = 1 88 | finally: 89 | white *= abs(blockCount) 90 | if blockCount < 0: white, black = black, white 91 | sys.stdout.write(' '*50 + '\r') 92 | sys.stdout.flush() 93 | qr = white * (size + 2) + '\n' 94 | startPoint = padding + 0.5 95 | for y in range(size): 96 | qr += white 97 | for x in range(size): 98 | r,g,b = rgb.getpixel(((x + startPoint) * times, (y + startPoint) * times)) 99 | qr += white if r > 127 else black 100 | qr += white + '\n' 101 | qr += white * (size + 2) + '\n' 102 | sys.stdout.write(qr) 103 | except ImportError: 104 | def print_cmd_qr(fileDir, size = 37, padding = 3, 105 | white = BLOCK, black = ' ', enableCmdQR = True): 106 | print('pillow should be installed to use command line QRCode: pip install pillow') 107 | print_qr(fileDir) 108 | def struct_friend_info(knownInfo): 109 | member = copy.deepcopy(friendInfoTemplate) 110 | for k, v in copy.deepcopy(knownInfo).items(): member[k] = v 111 | return member 112 | 113 | def search_dict_list(l, key, value): 114 | ''' Search a list of dict 115 | * return dict with specific value & key ''' 116 | for i in l: 117 | if i.get(key) == value: return i 118 | 119 | def print_line(msg, oneLine = False): 120 | if oneLine: 121 | sys.stdout.write(' '*40 + '\r') 122 | sys.stdout.flush() 123 | else: 124 | sys.stdout.write('\n') 125 | sys.stdout.write(msg.encode(sys.stdin.encoding or 'utf8', 'replace' 126 | ).decode(sys.stdin.encoding or 'utf8', 'replace')) 127 | sys.stdout.flush() 128 | 129 | def test_connect(retryTime=5): 130 | for i in range(retryTime): 131 | try: 132 | r = requests.get(config.BASE_URL) 133 | except: 134 | if i == retryTime-1: return False 135 | return True 136 | -------------------------------------------------------------------------------- /myapi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #encoding: UTF-8 3 | 4 | import hashlib 5 | from neteaseApi import api 6 | 7 | class MyNetease: 8 | def __init__(self): 9 | self.netease = api.NetEase() 10 | self.userId = int(open("./userInfo", 'r').read()) 11 | 12 | def get_recommend_playlist(self): # 每日推荐歌单 13 | music_list = self.netease.recommend_playlist() 14 | datalist = self.netease.dig_info(music_list, 'songs') 15 | playlist = [] 16 | for data in datalist: 17 | music_info = {} 18 | music_info.setdefault("song_name", data.get("song_name")) 19 | music_info.setdefault("artist", data.get("artist")) 20 | music_info.setdefault("album_name", data.get("album_name")) 21 | music_info.setdefault("mp3_url", data.get("mp3_url")) 22 | music_info.setdefault("playTime", data.get("playTime")) # 音乐时长 23 | music_info.setdefault("quality", data.get("quality")) 24 | playlist.append(music_info) 25 | return playlist 26 | 27 | def get_top_songlist(self):#热门单曲 28 | music_list = self.netease.top_songlist() 29 | datalist = self.netease.dig_info(music_list, 'songs') 30 | playlist = [] 31 | for data in datalist: 32 | music_info = {} 33 | music_info.setdefault("song_name", data.get("song_name")) 34 | music_info.setdefault("artist", data.get("artist")) 35 | music_info.setdefault("album_name", data.get("album_name")) 36 | music_info.setdefault("mp3_url", data.get("mp3_url")) 37 | music_info.setdefault("playTime", data.get("playTime")) # 音乐时长 38 | music_info.setdefault("quality", data.get("quality")) 39 | playlist.append(music_info) 40 | return playlist 41 | 42 | def login(self, username, password): #用户登陆 43 | password = hashlib.md5(password.encode('utf-8')).hexdigest() 44 | login_info = self.netease.login(username, password) 45 | #login_info = {u'profile': {u'followed': False, u'remarkName': None, u'expertTags': None, u'userId': 57542828, u'authority': 0, u'userType': 0, u'backgroundImgId': 2002210674180199, u'city': 500105, u'mutual': False, u'avatarUrl': u'http://p4.music.126.net/VnZiScyynLG7atLIZ2YPkw==/18686200114669622.jpg', u'avatarImgIdStr': u'18686200114669622', u'detailDescription': u'', u'province': 500000, u'description': u'', u'avatarImgId_str': u'18686200114669622', u'signature': u'', u'birthday': -2209017600000, u'nickname': u'\u8309\u82b7\u6c34', u'vipType': 0, u'avatarImgId': 18686200114669622, u'gender': 0, u'djStatus': 0, u'accountStatus': 0, u'backgroundImgIdStr': u'2002210674180199', u'backgroundUrl': u'http://p1.music.126.net/VTW4vsN08vwL3uSQqPyHqg==/2002210674180199.jpg', u'defaultAvatar': True, u'authStatus': 0}, u'account': {u'userName': u'0_zhouyaphone@163.com', u'status': 0, u'anonimousUser': False, u'whitelistAuthority': 0, u'baoyueVersion': 0, u'salt': u'', u'createTime': 0, u'tokenVersion': 0, u'vipType': 0, u'ban': 0, u'type': 0, u'id': 57542828, u'donateVersion': 0}, u'code': 200, u'effectTime': 2147483647, u'clientId': u'9505bf08c1e71d06255c860eb9b7dc399042ae3a54428d81b05af2aad65f9b9a2128fa7de6b09db4b64bf3324e151b2186a1ad5be63cc816', u'loginType': 0, u'bindings': [{u'expiresIn': 2147483647, u'tokenJsonStr': u'{"email":"zhouyaphone@163.com"}', u'url': u'', u'type': 0, u'userId': 57542828, u'refreshTime': 0, u'expired': False, u'id': 27817958}]} 46 | if login_info['code'] == 200: 47 | res = u"登陆成功" 48 | #登陆成功保存userId,作为获取用户歌单的依据,userId是唯一的,只要登陆成功,就会保存在userInfo文件中,所以不必每次都登陆 49 | userId = login_info.get('profile').get('userId') 50 | self.userId = userId 51 | file = open("./userInfo", 'w') 52 | file.write(str(userId)) 53 | file.close() 54 | else: 55 | res = u"登陆失败" 56 | return res 57 | 58 | def get_user_playlist(self): #获取用户歌单 59 | playlist = self.netease.user_playlist(self.userId) # 用户歌单 60 | return playlist 61 | 62 | def get_song_list_by_playlist_id(self, playlist_id): 63 | songs = self.netease.playlist_detail(playlist_id) 64 | song_list = self.netease.dig_info(songs, 'songs') 65 | return song_list 66 | 67 | def search_by_name(self, song_name): 68 | data = self.netease.search(song_name) 69 | song_ids = [] 70 | if 'songs' in data['result']: 71 | if 'mp3Url' in data['result']['songs']: 72 | songs = data['result']['songs'] 73 | 74 | else: 75 | for i in range(0, len(data['result']['songs'])): 76 | song_ids.append(data['result']['songs'][i]['id']) 77 | songs = self.netease.songs_detail(song_ids) 78 | song_list = self.netease.dig_info(songs, 'songs') 79 | return song_list 80 | 81 | 82 | if __name__ == '__main__': 83 | myNetease = MyNetease() 84 | myNetease.get_music_list() -------------------------------------------------------------------------------- /neteaseApi/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: UTF-8 3 | ''' 4 | 网易云音乐 Entry 5 | ''' 6 | from __future__ import print_function 7 | from __future__ import unicode_literals 8 | from __future__ import division 9 | from __future__ import absolute_import 10 | #from builtins import str 11 | from future import standard_library 12 | standard_library.install_aliases() 13 | 14 | #import curses 15 | import traceback 16 | import argparse 17 | import sys 18 | 19 | version = "0.2.3.7" 20 | 21 | 22 | def start(): 23 | pass 24 | 25 | 26 | if __name__ == '__main__': 27 | start() 28 | 29 | parser = argparse.ArgumentParser() 30 | parser.add_argument("-v", 31 | "--version", 32 | help="show this version and exit", 33 | action="store_true") 34 | args = parser.parse_args() 35 | if args.version: 36 | print('NetEase-MusicBox installed version:' + version) 37 | if latest != version: 38 | print('NetEase-MusicBox latest version:' + str(latest)) 39 | sys.exit() 40 | -------------------------------------------------------------------------------- /neteaseApi/api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author: omi 4 | # @Date: 2014-08-24 21:51:57 5 | ''' 6 | 网易云音乐 Api 7 | ''' 8 | from __future__ import print_function 9 | from __future__ import unicode_literals 10 | from __future__ import division 11 | from __future__ import absolute_import 12 | from builtins import chr 13 | from builtins import int 14 | from builtins import map 15 | from builtins import open 16 | from builtins import range 17 | from builtins import str 18 | from future import standard_library 19 | standard_library.install_aliases() 20 | 21 | import re 22 | import os 23 | import json 24 | import time 25 | import hashlib 26 | import random 27 | import base64 28 | import binascii 29 | import sys 30 | import crypto 31 | 32 | try: 33 | from Crypto.Cipher import AES 34 | except: 35 | sys.modules['Crypto'] = crypto 36 | from Crypto.Cipher import AES 37 | 38 | from Crypto.Cipher import AES 39 | from http.cookiejar import LWPCookieJar 40 | from bs4 import BeautifulSoup 41 | import requests 42 | 43 | from .config import Config 44 | from .storage import Storage 45 | from .utils import notify 46 | from . import logger 47 | 48 | # 歌曲榜单地址 49 | top_list_all = { 50 | 0: ['云音乐新歌榜', '/discover/toplist?id=3779629'], 51 | 1: ['云音乐热歌榜', '/discover/toplist?id=3778678'], 52 | 2: ['网易原创歌曲榜', '/discover/toplist?id=2884035'], 53 | 3: ['云音乐飙升榜', '/discover/toplist?id=19723756'], 54 | 4: ['云音乐电音榜', '/discover/toplist?id=10520166'], 55 | 5: ['UK排行榜周榜', '/discover/toplist?id=180106'], 56 | 6: ['美国Billboard周榜', '/discover/toplist?id=60198'], 57 | 7: ['KTV嗨榜', '/discover/toplist?id=21845217'], 58 | 8: ['iTunes榜', '/discover/toplist?id=11641012'], 59 | 9: ['Hit FM Top榜', '/discover/toplist?id=120001'], 60 | 10: ['日本Oricon周榜', '/discover/toplist?id=60131'], 61 | 11: ['韩国Melon排行榜周榜', '/discover/toplist?id=3733003'], 62 | 12: ['韩国Mnet排行榜周榜', '/discover/toplist?id=60255'], 63 | 13: ['韩国Melon原声周榜', '/discover/toplist?id=46772709'], 64 | 14: ['中国TOP排行榜(港台榜)', '/discover/toplist?id=112504'], 65 | 15: ['中国TOP排行榜(内地榜)', '/discover/toplist?id=64016'], 66 | 16: ['香港电台中文歌曲龙虎榜', '/discover/toplist?id=10169002'], 67 | 17: ['华语金曲榜', '/discover/toplist?id=4395559'], 68 | 18: ['中国嘻哈榜', '/discover/toplist?id=1899724'], 69 | 19: ['法国 NRJ EuroHot 30周榜', '/discover/toplist?id=27135204'], 70 | 20: ['台湾Hito排行榜', '/discover/toplist?id=112463'], 71 | 21: ['Beatport全球电子舞曲榜', '/discover/toplist?id=3812895'] 72 | } 73 | 74 | default_timeout = 10 75 | 76 | log = logger.getLogger(__name__) 77 | 78 | modulus = ('00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7' 79 | 'b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280' 80 | '104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932' 81 | '575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b' 82 | '3ece0462db0a22b8e7') 83 | nonce = '0CoJUm6Qyw8W8jud' 84 | pubKey = '010001' 85 | 86 | 87 | # 歌曲加密算法, 基于https://github.com/yanunon/NeteaseCloudMusic脚本实现 88 | def encrypted_id(id): 89 | magic = bytearray('3go8&$8*3*3h0k(2)2', 'u8') 90 | song_id = bytearray(id, 'u8') 91 | magic_len = len(magic) 92 | for i, sid in enumerate(song_id): 93 | song_id[i] = sid ^ magic[i % magic_len] 94 | m = hashlib.md5(song_id) 95 | result = m.digest() 96 | result = base64.b64encode(result) 97 | result = result.replace(b'/', b'_') 98 | result = result.replace(b'+', b'-') 99 | return result.decode('utf-8') 100 | 101 | 102 | # 登录加密算法, 基于https://github.com/stkevintan/nw_musicbox脚本实现 103 | def encrypted_request(text): 104 | text = json.dumps(text) 105 | secKey = createSecretKey(16) 106 | encText = aesEncrypt(aesEncrypt(text, nonce), secKey) 107 | encSecKey = rsaEncrypt(secKey, pubKey, modulus) 108 | data = {'params': encText, 'encSecKey': encSecKey} 109 | return data 110 | 111 | 112 | def aesEncrypt(text, secKey): 113 | pad = 16 - len(text) % 16 114 | text = text + chr(pad) * pad 115 | encryptor = AES.new(secKey, 2, '0102030405060708') 116 | ciphertext = encryptor.encrypt(text) 117 | ciphertext = base64.b64encode(ciphertext).decode('utf-8') 118 | return ciphertext 119 | 120 | 121 | def rsaEncrypt(text, pubKey, modulus): 122 | text = text[::-1] 123 | rs = pow(int(binascii.hexlify(text), 16), int(pubKey, 16)) % int(modulus, 16) 124 | return format(rs, 'x').zfill(256) 125 | 126 | 127 | def createSecretKey(size): 128 | return binascii.hexlify(os.urandom(size))[:16] 129 | 130 | 131 | # list去重 132 | def uniq(arr): 133 | arr2 = list(set(arr)) 134 | arr2.sort(key=arr.index) 135 | return arr2 136 | 137 | 138 | # 获取高音质mp3 url 139 | def geturl(song): 140 | quality = Config().get_item('music_quality') 141 | if song['hMusic'] and quality <= 0: 142 | music = song['hMusic'] 143 | quality = 'HD' 144 | play_time = str(music['playTime']) 145 | elif song['mMusic'] and quality <= 1: 146 | music = song['mMusic'] 147 | quality = 'MD' 148 | play_time = str(music['playTime']) 149 | elif song['lMusic'] and quality <= 2: 150 | music = song['lMusic'] 151 | quality = 'LD' 152 | play_time = str(music['playTime']) 153 | else: 154 | play_time = 0 155 | return song['mp3Url'], '', play_time 156 | quality = quality + ' {0}k'.format(music['bitrate'] // 1000) 157 | song_id = str(music['dfsId']) 158 | enc_id = encrypted_id(song_id) 159 | url = 'http://m%s.music.126.net/%s/%s.mp3' % (2, 160 | enc_id, song_id) 161 | return url, quality, play_time 162 | 163 | 164 | def geturl_new_api(song): 165 | br_to_quality = {128000: 'MD 128k', 320000: 'HD 320k'} 166 | alter = NetEase().songs_detail_new_api([song['id']])[0] 167 | url = alter['url'] 168 | quality = br_to_quality.get(alter['br'], '') 169 | return url, quality 170 | 171 | 172 | class NetEase(object): 173 | 174 | def __init__(self): 175 | self.header = { 176 | 'Accept': '*/*', 177 | 'Accept-Encoding': 'gzip,deflate,sdch', 178 | 'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4', 179 | 'Connection': 'keep-alive', 180 | 'Content-Type': 'application/x-www-form-urlencoded', 181 | 'Host': 'music.163.com', 182 | 'Referer': 'http://music.163.com/search/', 183 | 'User-Agent': 184 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36' # NOQA 185 | } 186 | self.cookies = {'appver': '1.5.2'} 187 | self.playlist_class_dict = {} 188 | self.session = requests.Session() 189 | self.storage = Storage() 190 | self.session.cookies = LWPCookieJar(self.storage.cookie_path) 191 | try: 192 | self.session.cookies.load() 193 | cookie = '' 194 | if os.path.isfile(self.storage.cookie_path): 195 | self.file = open(self.storage.cookie_path, 'r') 196 | cookie = self.file.read() 197 | self.file.close() 198 | expire_time = re.compile(r'\d{4}-\d{2}-\d{2}').findall(cookie) 199 | if expire_time: 200 | if expire_time[0] < time.strftime('%Y-%m-%d', time.localtime(time.time())): 201 | self.storage.database['user'] = { 202 | 'username': '', 203 | 'password': '', 204 | 'user_id': '', 205 | 'nickname': '', 206 | } 207 | self.storage.save() 208 | os.remove(self.storage.cookie_path) 209 | except IOError as e: 210 | log.error(e) 211 | self.session.cookies.save() 212 | 213 | def return_toplists(self): 214 | return [l[0] for l in top_list_all.values()] 215 | 216 | def httpRequest(self, 217 | method, 218 | action, 219 | query=None, 220 | urlencoded=None, 221 | callback=None, 222 | timeout=None): 223 | connection = json.loads( 224 | self.rawHttpRequest(method, action, query, urlencoded, callback, timeout) 225 | ) 226 | return connection 227 | 228 | def rawHttpRequest(self, 229 | method, 230 | action, 231 | query=None, 232 | urlencoded=None, 233 | callback=None, 234 | timeout=None): 235 | if method == 'GET': 236 | url = action if query is None else action + '?' + query 237 | connection = self.session.get(url, 238 | headers=self.header, 239 | timeout=default_timeout) 240 | 241 | elif method == 'POST': 242 | connection = self.session.post(action, 243 | data=query, 244 | headers=self.header, 245 | timeout=default_timeout) 246 | 247 | elif method == 'Login_POST': 248 | connection = self.session.post(action, 249 | data=query, 250 | headers=self.header, 251 | timeout=default_timeout) 252 | self.session.cookies.save() 253 | 254 | connection.encoding = 'UTF-8' 255 | return connection.text 256 | 257 | # 登录 258 | def login(self, username, password): 259 | pattern = re.compile(r'^0\d{2,3}\d{7,8}$|^1[34578]\d{9}$') 260 | if pattern.match(username): 261 | return self.phone_login(username, password) 262 | action = 'https://music.163.com/weapi/login?csrf_token=' 263 | text = { 264 | 'username': username, 265 | 'password': password, 266 | 'rememberLogin': 'true' 267 | } 268 | data = encrypted_request(text) 269 | try: 270 | return self.httpRequest('Login_POST', action, data) 271 | except requests.exceptions.RequestException as e: 272 | log.error(e) 273 | return {'code': 501} 274 | 275 | # 手机登录 276 | def phone_login(self, username, password): 277 | action = 'https://music.163.com/weapi/login/cellphone' 278 | text = { 279 | 'phone': username, 280 | 'password': password, 281 | 'rememberLogin': 'true' 282 | } 283 | data = encrypted_request(text) 284 | try: 285 | return self.httpRequest('Login_POST', action, data) 286 | except requests.exceptions.RequestException as e: 287 | log.error(e) 288 | return {'code': 501} 289 | 290 | # 每日签到 291 | def daily_signin(self, type): 292 | action = 'http://music.163.com/weapi/point/dailyTask' 293 | text = {'type': type} 294 | data = encrypted_request(text) 295 | try: 296 | return self.httpRequest('POST', action, data) 297 | except requests.exceptions.RequestException as e: 298 | log.error(e) 299 | return -1 300 | 301 | # 用户歌单 302 | def user_playlist(self, uid, offset=0, limit=100): 303 | action = 'http://music.163.com/api/user/playlist/?offset={}&limit={}&uid={}'.format( # NOQA 304 | offset, limit, uid) 305 | try: 306 | data = self.httpRequest('GET', action) 307 | return data['playlist'] 308 | except (requests.exceptions.RequestException, KeyError) as e: 309 | log.error(e) 310 | return -1 311 | 312 | # 每日推荐歌单 313 | def recommend_playlist(self): 314 | try: 315 | action = 'http://music.163.com/weapi/v1/discovery/recommend/songs?csrf_token=' # NOQA 316 | self.session.cookies.load() 317 | csrf = '' 318 | for cookie in self.session.cookies: 319 | if cookie.name == '__csrf': 320 | csrf = cookie.value 321 | if csrf == '': 322 | return False 323 | action += csrf 324 | req = {'offset': 0, 'total': True, 'limit': 20, 'csrf_token': csrf} 325 | page = self.session.post(action, 326 | data=encrypted_request(req), 327 | headers=self.header, 328 | timeout=default_timeout) 329 | results = json.loads(page.text)['recommend'] 330 | song_ids = [] 331 | for result in results: 332 | song_ids.append(result['id']) 333 | data = map(self.song_detail, song_ids) 334 | return [d[0] for d in data] 335 | except (requests.exceptions.RequestException, ValueError) as e: 336 | log.error(e) 337 | return False 338 | 339 | # 私人FM 340 | def personal_fm(self): 341 | action = 'http://music.163.com/api/radio/get' 342 | try: 343 | data = self.httpRequest('GET', action) 344 | return data['data'] 345 | except requests.exceptions.RequestException as e: 346 | log.error(e) 347 | return -1 348 | 349 | # like 350 | def fm_like(self, songid, like=True, time=25, alg='itembased'): 351 | action = 'http://music.163.com/api/radio/like?alg={}&trackId={}&like={}&time={}'.format( # NOQA 352 | alg, songid, 'true' if like else 'false', time) 353 | 354 | try: 355 | data = self.httpRequest('GET', action) 356 | if data['code'] == 200: 357 | return data 358 | else: 359 | return -1 360 | except requests.exceptions.RequestException as e: 361 | log.error(e) 362 | return -1 363 | 364 | # FM trash 365 | def fm_trash(self, songid, time=25, alg='RT'): 366 | action = 'http://music.163.com/api/radio/trash/add?alg={}&songId={}&time={}'.format( # NOQA 367 | alg, songid, time) 368 | try: 369 | data = self.httpRequest('GET', action) 370 | if data['code'] == 200: 371 | return data 372 | else: 373 | return -1 374 | except requests.exceptions.RequestException as e: 375 | log.error(e) 376 | return -1 377 | 378 | # 搜索单曲(1),歌手(100),专辑(10),歌单(1000),用户(1002) *(type)* 379 | def search(self, s, stype=1, offset=0, total='true', limit=60): 380 | action = 'http://music.163.com/api/search/get' 381 | data = { 382 | 's': s, 383 | 'type': stype, 384 | 'offset': offset, 385 | 'total': total, 386 | 'limit': limit 387 | } 388 | return self.httpRequest('POST', action, data) 389 | 390 | # 新碟上架 http://music.163.com/#/discover/album/ 391 | def new_albums(self, offset=0, limit=50): 392 | action = 'http://music.163.com/api/album/new?area=ALL&offset={}&total=true&limit={}'.format( # NOQA 393 | offset, limit) 394 | try: 395 | data = self.httpRequest('GET', action) 396 | return data['albums'] 397 | except requests.exceptions.RequestException as e: 398 | log.error(e) 399 | return [] 400 | 401 | # 歌单(网友精选碟) hot||new http://music.163.com/#/discover/playlist/ 402 | def top_playlists(self, category='全部', order='hot', offset=0, limit=50): 403 | action = 'http://music.163.com/api/playlist/list?cat={}&order={}&offset={}&total={}&limit={}'.format( # NOQA 404 | category, order, offset, 'true' if offset else 'false', 405 | limit) # NOQA 406 | try: 407 | data = self.httpRequest('GET', action) 408 | return data['playlists'] 409 | except requests.exceptions.RequestException as e: 410 | log.error(e) 411 | return [] 412 | 413 | # 分类歌单 414 | def playlist_classes(self): 415 | action = 'http://music.163.com/discover/playlist/' 416 | try: 417 | data = self.rawHttpRequest('GET', action) 418 | return data 419 | except requests.exceptions.RequestException as e: 420 | log.error(e) 421 | return [] 422 | 423 | # 分类歌单中某一个分类的详情 424 | def playlist_class_detail(self): 425 | pass 426 | 427 | # 歌单详情 428 | def playlist_detail(self, playlist_id): 429 | action = 'http://music.163.com/api/playlist/detail?id={}'.format( 430 | playlist_id) 431 | try: 432 | data = self.httpRequest('GET', action) 433 | return data['result']['tracks'] 434 | except requests.exceptions.RequestException as e: 435 | log.error(e) 436 | return [] 437 | 438 | # 热门歌手 http://music.163.com/#/discover/artist/ 439 | def top_artists(self, offset=0, limit=100): 440 | action = 'http://music.163.com/api/artist/top?offset={}&total=false&limit={}'.format( # NOQA 441 | offset, limit) 442 | try: 443 | data = self.httpRequest('GET', action) 444 | return data['artists'] 445 | except requests.exceptions.RequestException as e: 446 | log.error(e) 447 | return [] 448 | 449 | # 热门单曲 http://music.163.com/discover/toplist?id= 450 | def top_songlist(self, idx=0, offset=0, limit=100): 451 | action = 'http://music.163.com' + top_list_all[idx][1] 452 | try: 453 | connection = requests.get(action, 454 | headers=self.header, 455 | timeout=default_timeout) 456 | connection.encoding = 'UTF-8' 457 | songids = re.findall(r'/song\?id=(\d+)', connection.text) 458 | if songids == []: 459 | return [] 460 | # 去重 461 | songids = uniq(songids) 462 | return self.songs_detail(songids) 463 | except requests.exceptions.RequestException as e: 464 | log.error(e) 465 | return [] 466 | 467 | # 歌手单曲 468 | def artists(self, artist_id): 469 | action = 'http://music.163.com/api/artist/{}'.format(artist_id) 470 | try: 471 | data = self.httpRequest('GET', action) 472 | return data['hotSongs'] 473 | except requests.exceptions.RequestException as e: 474 | log.error(e) 475 | return [] 476 | 477 | def get_artist_album(self, artist_id, offset=0, limit=50): 478 | action = 'http://music.163.com/api/artist/albums/{}?offset={}&limit={}'.format( 479 | artist_id, offset, limit) 480 | try: 481 | data = self.httpRequest('GET', action) 482 | return data['hotAlbums'] 483 | except requests.exceptions.RequestException as e: 484 | log.error(e) 485 | return [] 486 | 487 | # album id --> song id set 488 | def album(self, album_id): 489 | action = 'http://music.163.com/api/album/{}'.format(album_id) 490 | try: 491 | data = self.httpRequest('GET', action) 492 | return data['album']['songs'] 493 | except requests.exceptions.RequestException as e: 494 | log.error(e) 495 | return [] 496 | 497 | def song_comments(self, music_id, offset=0, total='fasle', limit=100): 498 | action = 'http://music.163.com/api/v1/resource/comments/R_SO_4_{}/?rid=R_SO_4_{}&\ 499 | offset={}&total={}&limit={}'.format(music_id, music_id, offset, total, limit) 500 | try: 501 | comments = self.httpRequest('GET', action) 502 | return comments 503 | except requests.exceptions.RequestException as e: 504 | log.error(e) 505 | return [] 506 | 507 | # song ids --> song urls ( details ) 508 | def songs_detail(self, ids, offset=0): 509 | tmpids = ids[offset:] 510 | tmpids = tmpids[0:100] 511 | tmpids = list(map(str, tmpids)) 512 | action = 'http://music.163.com/api/song/detail?ids=[{}]'.format( # NOQA 513 | ','.join(tmpids)) 514 | try: 515 | data = self.httpRequest('GET', action) 516 | 517 | # the order of data['songs'] is no longer the same as tmpids, 518 | # so just make the order back 519 | data['songs'].sort(key=lambda song: tmpids.index(str(song['id']))) 520 | 521 | return data['songs'] 522 | except requests.exceptions.RequestException as e: 523 | log.error(e) 524 | return [] 525 | 526 | def songs_detail_new_api(self, music_ids, bit_rate=320000): 527 | action = 'http://music.163.com/weapi/song/enhance/player/url?csrf_token=' # NOQA 528 | self.session.cookies.load() 529 | csrf = '' 530 | for cookie in self.session.cookies: 531 | if cookie.name == '__csrf': 532 | csrf = cookie.value 533 | if csrf == '': 534 | notify('You Need Login', 1) 535 | action += csrf 536 | data = {'ids': music_ids, 'br': bit_rate, 'csrf_token': csrf} 537 | connection = self.session.post(action, 538 | data=encrypted_request(data), 539 | headers=self.header, ) 540 | result = json.loads(connection.text) 541 | return result['data'] 542 | 543 | # song id --> song url ( details ) 544 | def song_detail(self, music_id): 545 | action = 'http://music.163.com/api/song/detail/?id={}&ids=[{}]'.format( 546 | music_id, music_id) # NOQA 547 | try: 548 | data = self.httpRequest('GET', action) 549 | return data['songs'] 550 | except requests.exceptions.RequestException as e: 551 | log.error(e) 552 | return [] 553 | 554 | # lyric http://music.163.com/api/song/lyric?os=osx&id= &lv=-1&kv=-1&tv=-1 555 | def song_lyric(self, music_id): 556 | action = 'http://music.163.com/api/song/lyric?os=osx&id={}&lv=-1&kv=-1&tv=-1'.format( # NOQA 557 | music_id) 558 | try: 559 | data = self.httpRequest('GET', action) 560 | if 'lrc' in data and data['lrc']['lyric'] is not None: 561 | lyric_info = data['lrc']['lyric'] 562 | else: 563 | lyric_info = '未找到歌词' 564 | return lyric_info 565 | except requests.exceptions.RequestException as e: 566 | log.error(e) 567 | return [] 568 | 569 | def song_tlyric(self, music_id): 570 | action = 'http://music.163.com/api/song/lyric?os=osx&id={}&lv=-1&kv=-1&tv=-1'.format( # NOQA 571 | music_id) 572 | try: 573 | data = self.httpRequest('GET', action) 574 | if 'tlyric' in data and data['tlyric'].get('lyric') is not None: 575 | lyric_info = data['tlyric']['lyric'][1:] 576 | else: 577 | lyric_info = '未找到歌词翻译' 578 | return lyric_info 579 | except requests.exceptions.RequestException as e: 580 | log.error(e) 581 | return [] 582 | 583 | # 今日最热(0), 本周最热(10),历史最热(20),最新节目(30) 584 | def djchannels(self, stype=0, offset=0, limit=50): 585 | action = 'http://music.163.com/discover/djradio?type={}&offset={}&limit={}'.format( # NOQA 586 | stype, offset, limit) 587 | try: 588 | connection = requests.get(action, 589 | headers=self.header, 590 | timeout=default_timeout) 591 | connection.encoding = 'UTF-8' 592 | channelids = re.findall(r'/program\?id=(\d+)', connection.text) 593 | channelids = uniq(channelids) 594 | return self.channel_detail(channelids) 595 | except requests.exceptions.RequestException as e: 596 | log.error(e) 597 | return [] 598 | 599 | # DJchannel ( id, channel_name ) ids --> song urls ( details ) 600 | # 将 channels 整理为 songs 类型 601 | def channel_detail(self, channelids, offset=0): 602 | channels = [] 603 | for i in range(0, len(channelids)): 604 | action = 'http://music.163.com/api/dj/program/detail?id={}'.format( 605 | channelids[i]) 606 | try: 607 | data = self.httpRequest('GET', action) 608 | channel = self.dig_info(data['program']['mainSong'], 'channels') 609 | channels.append(channel) 610 | except requests.exceptions.RequestException as e: 611 | log.error(e) 612 | continue 613 | 614 | return channels 615 | 616 | # 获取版本 617 | def get_version(self): 618 | action = 'https://pypi.python.org/pypi?:action=doap&name=NetEase-MusicBox' # NOQA 619 | try: 620 | data = requests.get(action) 621 | return data.content 622 | except requests.exceptions.RequestException as e: 623 | log.error(e) 624 | return "" 625 | 626 | def dig_info(self, data, dig_type): 627 | temp = [] 628 | if dig_type == 'songs' or dig_type == 'fmsongs': 629 | for i in range(0, len(data)): 630 | url, quality, play_time = geturl(data[i]) 631 | if data[i]['album'] is not None: 632 | album_name = data[i]['album']['name'] 633 | album_id = data[i]['album']['id'] 634 | else: 635 | album_name = '未知专辑' 636 | album_id = '' 637 | 638 | song_info = { 639 | 'song_id': data[i]['id'], 640 | 'artist': [], 641 | 'song_name': data[i]['name'], 642 | 'album_name': album_name, 643 | 'album_id': album_id, 644 | 'mp3_url': url, 645 | 'quality': quality, 646 | 'playTime': play_time 647 | } 648 | if 'artist' in data[i]: 649 | song_info['artist'] = data[i]['artist'] 650 | elif 'artists' in data[i]: 651 | for j in range(0, len(data[i]['artists'])): 652 | song_info['artist'].append(data[i]['artists'][j][ 653 | 'name']) 654 | song_info['artist'] = ', '.join(song_info['artist']) 655 | else: 656 | song_info['artist'] = '未知艺术家' 657 | 658 | temp.append(song_info) 659 | 660 | elif dig_type == 'artists': 661 | artists = [] 662 | for i in range(0, len(data)): 663 | artists_info = { 664 | 'artist_id': data[i]['id'], 665 | 'artists_name': data[i]['name'], 666 | 'alias': ''.join(data[i]['alias']) 667 | } 668 | artists.append(artists_info) 669 | 670 | return artists 671 | 672 | elif dig_type == 'albums': 673 | for i in range(0, len(data)): 674 | albums_info = { 675 | 'album_id': data[i]['id'], 676 | 'albums_name': data[i]['name'], 677 | 'artists_name': data[i]['artist']['name'] 678 | } 679 | temp.append(albums_info) 680 | 681 | elif dig_type == 'top_playlists': 682 | for i in range(0, len(data)): 683 | playlists_info = { 684 | 'playlist_id': data[i]['id'], 685 | 'playlists_name': data[i]['name'], 686 | 'creator_name': data[i]['creator']['nickname'] 687 | } 688 | temp.append(playlists_info) 689 | 690 | elif dig_type == 'channels': 691 | url, quality = geturl(data) 692 | channel_info = { 693 | 'song_id': data['id'], 694 | 'song_name': data['name'], 695 | 'artist': data['artists'][0]['name'], 696 | 'album_name': '主播电台', 697 | 'mp3_url': url, 698 | 'quality': quality 699 | } 700 | temp = channel_info 701 | 702 | elif dig_type == 'playlist_classes': 703 | soup = BeautifulSoup(data, 'lxml') 704 | dls = soup.select('dl.f-cb') 705 | for dl in dls: 706 | title = dl.dt.text 707 | sub = [item.text for item in dl.select('a')] 708 | temp.append(title) 709 | self.playlist_class_dict[title] = sub 710 | 711 | elif dig_type == 'playlist_class_detail': 712 | log.debug(data) 713 | temp = self.playlist_class_dict[data] 714 | 715 | return temp 716 | 717 | 718 | if __name__ == '__main__': 719 | ne = NetEase() 720 | print(geturl_new_api(ne.songs_detail([27902910])[0])) # MD 128k, fallback 721 | print(ne.songs_detail_new_api([27902910])[0]['url']) 722 | print(ne.songs_detail([405079776])[0]['mp3Url']) # old api 723 | print(requests.get(ne.songs_detail([405079776])[0]['mp3Url']).status_code) # 404 724 | -------------------------------------------------------------------------------- /neteaseApi/cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Author: Catofes 3 | # @Date: 2015-08-15 4 | ''' 5 | Class to cache songs into local storage. 6 | ''' 7 | from __future__ import unicode_literals 8 | from __future__ import print_function 9 | from __future__ import division 10 | from __future__ import absolute_import 11 | from builtins import str 12 | from future import standard_library 13 | standard_library.install_aliases() 14 | 15 | import threading 16 | import subprocess 17 | import os 18 | import signal 19 | 20 | from .const import Constant 21 | from .config import Config 22 | from .singleton import Singleton 23 | from .api import NetEase 24 | from . import logger 25 | 26 | log = logger.getLogger(__name__) 27 | 28 | 29 | class Cache(Singleton): 30 | 31 | def __init__(self): 32 | if hasattr(self, '_init'): 33 | return 34 | self._init = True 35 | self.const = Constant() 36 | self.config = Config() 37 | self.download_lock = threading.Lock() 38 | self.check_lock = threading.Lock() 39 | self.downloading = [] 40 | self.aria2c = None 41 | self.wget = None 42 | self.stop = False 43 | self.enable = self.config.get_item('cache') 44 | self.aria2c_parameters = self.config.get_item('aria2c_parameters') 45 | 46 | def _is_cache_successful(self): 47 | def succ(x): 48 | return x and x.returncode == 0 49 | return succ(self.aria2c) or succ(self.wget) 50 | 51 | def _kill_all(self): 52 | def _kill(p): 53 | if p: 54 | os.kill(p.pid, signal.SIGKILL) 55 | 56 | _kill(self.aria2c) 57 | _kill(self.wget) 58 | 59 | def _mkdir(self, name): 60 | try: 61 | os.mkdir(name) 62 | except OSError: 63 | pass 64 | 65 | def start_download(self): 66 | check = self.download_lock.acquire(False) 67 | if not check: 68 | return False 69 | while True: 70 | if self.stop: 71 | break 72 | if not self.enable: 73 | break 74 | self.check_lock.acquire() 75 | if len(self.downloading) <= 0: 76 | self.check_lock.release() 77 | break 78 | data = self.downloading.pop() 79 | self.check_lock.release() 80 | song_id = data[0] 81 | song_name = data[1] 82 | artist = data[2] 83 | url = data[3] 84 | onExit = data[4] 85 | output_path = Constant.download_dir 86 | output_file = str(artist) + ' - ' + str(song_name) + '.mp3' 87 | full_path = os.path.join(output_path, output_file) 88 | 89 | new_url = NetEase().songs_detail_new_api([song_id])[0]['url'] 90 | log.info('Old:{}. New:{}'.format(url, new_url)) 91 | try: 92 | para = ['aria2c', '--auto-file-renaming=false', 93 | '--allow-overwrite=true', '-d', output_path, '-o', 94 | output_file, new_url] 95 | para[1:1] = self.aria2c_parameters 96 | self.aria2c = subprocess.Popen(para, 97 | stdin=subprocess.PIPE, 98 | stdout=subprocess.PIPE, 99 | stderr=subprocess.PIPE) 100 | self.aria2c.wait() 101 | except OSError as e: 102 | log.warning( 103 | '{}.\tAria2c is unavailable, fall back to wget'.format(e)) 104 | 105 | self._mkdir(output_path) 106 | para = ['wget', '-O', full_path, new_url] 107 | self.wget = subprocess.Popen(para, 108 | stdin=subprocess.PIPE, 109 | stdout=subprocess.PIPE, 110 | stderr=subprocess.PIPE) 111 | self.wget.wait() 112 | 113 | if self._is_cache_successful(): 114 | log.debug(str(song_id) + ' Cache OK') 115 | onExit(song_id, full_path) 116 | self.download_lock.release() 117 | 118 | def add(self, song_id, song_name, artist, url, onExit): 119 | self.check_lock.acquire() 120 | self.downloading.append([song_id, song_name, artist, url, onExit]) 121 | self.check_lock.release() 122 | 123 | def quit(self): 124 | self.stop = True 125 | try: 126 | self._kill_all() 127 | except (AttributeError, OSError) as e: 128 | log.error(e) 129 | pass 130 | -------------------------------------------------------------------------------- /neteaseApi/config.py: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | from __future__ import unicode_literals 4 | from __future__ import print_function 5 | from __future__ import division 6 | from __future__ import absolute_import 7 | from builtins import open 8 | from future import standard_library 9 | standard_library.install_aliases() 10 | import json 11 | import os 12 | 13 | from . import logger 14 | from .singleton import Singleton 15 | from .const import Constant 16 | from .utils import utf8_data_to_file 17 | 18 | log = logger.getLogger(__name__) 19 | 20 | 21 | class Config(Singleton): 22 | 23 | def __init__(self): 24 | if hasattr(self, '_init'): 25 | return 26 | self._init = True 27 | self.config_file_path = Constant.config_path 28 | self.default_config = { 29 | 'version': 7, 30 | 'cache': { 31 | 'value': False, 32 | 'default': False, 33 | 'describe': ('A toggle to enable cache function or not. ' 34 | 'Set value to true to enable it.') 35 | }, 36 | 'mpg123_parameters': { 37 | 'value': [], 38 | 'default': [], 39 | 'describe': 'The additional parameters when mpg123 start.' 40 | }, 41 | 'aria2c_parameters': { 42 | 'value': [], 43 | 'default': [], 44 | 'describe': ('The additional parameters when ' 45 | 'aria2c start to download something.') 46 | }, 47 | 'music_quality': { 48 | 'value': 0, 49 | 'default': 0, 50 | 'describe': ('Select the quality of the music. ' 51 | 'May be useful when network is terrible. ' 52 | '0 for high quality, 1 for medium and 2 for low.') 53 | }, 54 | 'global_play_pause': { 55 | 'value': 'p', 56 | 'default': 'p', 57 | 'describe': 'Global keybind for play/pause.' 58 | 'Uses gtk notation for keybinds.' 59 | }, 60 | 'global_next': { 61 | 'value': 'j', 62 | 'default': 'j', 63 | 'describe': 'Global keybind for next song.' 64 | 'Uses gtk notation for keybinds.' 65 | }, 66 | 'global_previous': { 67 | 'value': 'k', 68 | 'default': 'k', 69 | 'describe': 'Global keybind for previous song.' 70 | 'Uses gtk notation for keybinds.' 71 | }, 72 | 'notifier': { 73 | 'value': True, 74 | 'default': True, 75 | 'describe': 'Notifier when switching songs.' 76 | }, 77 | 'translation': { 78 | 'value': True, 79 | 'default': True, 80 | 'describe': 'Foreign language lyrics translation.' 81 | }, 82 | 'osdlyrics': { 83 | 'value': False, 84 | 'default': False, 85 | 'describe': 'Desktop lyrics for musicbox.' 86 | }, 87 | 'osdlyrics_transparent': { 88 | 'value': False, 89 | 'default': False, 90 | 'describe': 'Desktop lyrics transparent bg.' 91 | }, 92 | 'osdlyrics_color': { 93 | 'value': [225, 248, 113], 94 | 'default': [225, 248, 113], 95 | 'describe': 'Desktop lyrics RGB Color.' 96 | }, 97 | 'osdlyrics_font': { 98 | 'value': ['Decorative', 16], 99 | 'default': ['Decorative', 16], 100 | 'describe': 'Desktop lyrics font-family and font-size.' 101 | }, 102 | 'osdlyrics_background': { 103 | 'value': 'rgba(100, 100, 100, 120)', 104 | 'default': 'rgba(100, 100, 100, 120)', 105 | 'describe': 'Desktop lyrics background color.' 106 | }, 107 | 'osdlyrics_on_top': { 108 | 'value': True, 109 | 'default': True, 110 | 'describe': 'Desktop lyrics OnTopHint.' 111 | }, 112 | 'curses_transparency': { 113 | 'value': False, 114 | 'default': False, 115 | 'describe': 'Set true to make curses transparency.' 116 | } 117 | } 118 | self.config = {} 119 | if not os.path.isfile(self.config_file_path): 120 | self.generate_config_file() 121 | try: 122 | f = open(self.config_file_path, 'r') 123 | except IOError: 124 | log.debug('Read config file error.') 125 | return 126 | try: 127 | self.config = json.loads(f.read()) 128 | except ValueError: 129 | log.debug('Load config json data failed.') 130 | return 131 | f.close() 132 | if not self.check_version(): 133 | self.save_config_file() 134 | 135 | def generate_config_file(self): 136 | f = open(self.config_file_path, 'w') 137 | utf8_data_to_file(f, json.dumps(self.default_config, indent=2)) 138 | f.close() 139 | 140 | def save_config_file(self): 141 | f = open(self.config_file_path, 'w') 142 | utf8_data_to_file(f, json.dumps(self.config, indent=2)) 143 | f.close() 144 | 145 | def check_version(self): 146 | if self.config['version'] == self.default_config['version']: 147 | return True 148 | else: 149 | # Should do some update. Like 150 | # if self.database['version'] == 2 : self.database.['version'] = 3 151 | # update database form version 1 to version 2 152 | if self.config['version'] == 1: 153 | self.config['version'] = 2 154 | self.config['global_play_pause'] = { 155 | 'value': 'p', 156 | 'default': 'p', 157 | 'describe': 'Global keybind for play/pause.' 158 | 'Uses gtk notation for keybinds.' 159 | } 160 | self.config['global_next'] = { 161 | 'value': 'j', 162 | 'default': 'j', 163 | 'describe': 'Global keybind for next song.' 164 | 'Uses gtk notation for keybinds.' 165 | } 166 | self.config['global_previous'] = { 167 | 'value': 'k', 168 | 'default': 'k', 169 | 'describe': 'Global keybind for previous song.' 170 | 'Uses gtk notation for keybinds.' 171 | } 172 | elif self.config['version'] == 2: 173 | self.config['version'] = 3 174 | self.config['notifier'] = { 175 | 'value': True, 176 | 'default': True, 177 | 'describe': 'Notifier when switching songs.' 178 | } 179 | elif self.config['version'] == 3: 180 | self.config['version'] = 4 181 | self.config['translation'] = { 182 | 'value': True, 183 | 'default': True, 184 | 'describe': 'Foreign language lyrics translation.' 185 | } 186 | elif self.config['version'] == 4: 187 | self.config['version'] = 5 188 | self.config['osdlyrics'] = { 189 | 'value': False, 190 | 'default': False, 191 | 'describe': 'Desktop lyrics for musicbox.' 192 | } 193 | self.config['osdlyrics_color'] = { 194 | 'value': [225, 248, 113], 195 | 'default': [225, 248, 113], 196 | 'describe': 'Desktop lyrics RGB Color.' 197 | } 198 | self.config['osdlyrics_font'] = { 199 | 'value': ['Decorative', 16], 200 | 'default': ['Decorative', 16], 201 | 'describe': 'Desktop lyrics font-family and font-size.' 202 | } 203 | self.config['osdlyrics_background'] = { 204 | 'value': 'rgba(100, 100, 100, 120)', 205 | 'default': 'rgba(100, 100, 100, 120)', 206 | 'describe': 'Desktop lyrics background color.' 207 | } 208 | self.config['osdlyrics_transparent'] = { 209 | 'value': False, 210 | 'default': False, 211 | 'describe': 'Desktop lyrics transparent bg.' 212 | } 213 | elif self.config['version'] == 5: 214 | self.config['version'] = 6 215 | self.config['osdlyrics_on_top'] = { 216 | 'value': True, 217 | 'default': True, 218 | 'describe': 'Desktop lyrics OnTopHint.' 219 | } 220 | elif self.config['version'] == 6: 221 | self.config['version'] = 7 222 | self.config['curses_transparency'] = { 223 | 'value': False, 224 | 'default': False, 225 | 'describe': 'Set true to make curses transparency.' 226 | } 227 | self.check_version() 228 | return False 229 | 230 | def get_item(self, name): 231 | if name not in self.config.keys(): 232 | if name not in self.default_config.keys(): 233 | return None 234 | return self.default_config[name].get('value') 235 | return self.config[name].get('value') 236 | -------------------------------------------------------------------------------- /neteaseApi/const.py: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | from __future__ import unicode_literals 4 | from __future__ import print_function 5 | from __future__ import division 6 | from __future__ import absolute_import 7 | from future import standard_library 8 | standard_library.install_aliases() 9 | import os 10 | 11 | 12 | class Constant(object): 13 | conf_dir = os.path.join(os.path.expanduser('~'), '.netease-musicbox') 14 | download_dir = os.path.join(conf_dir, 'cached') 15 | config_path = os.path.join(conf_dir, 'config.json') 16 | storage_path = os.path.join(conf_dir, 'database.json') 17 | cookie_path = os.path.join(conf_dir, 'cookie') 18 | log_path = os.path.join(conf_dir, 'musicbox.log') 19 | -------------------------------------------------------------------------------- /neteaseApi/logger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author: omi 4 | # @Date: 2014-08-24 21:51:57 5 | 6 | from __future__ import unicode_literals 7 | from __future__ import print_function 8 | from __future__ import division 9 | from __future__ import absolute_import 10 | from builtins import open 11 | from future import standard_library 12 | standard_library.install_aliases() 13 | import os 14 | import logging 15 | 16 | from . import const 17 | 18 | FILE_NAME = const.Constant.log_path 19 | if os.path.isdir(const.Constant.conf_dir) is False: 20 | os.mkdir(const.Constant.conf_dir) 21 | 22 | with open(FILE_NAME, 'a+') as f: 23 | f.write('#' * 80) 24 | f.write('\n') 25 | 26 | 27 | def getLogger(name): 28 | log = logging.getLogger(name) 29 | log.setLevel(logging.DEBUG) 30 | 31 | # File output handler 32 | fh = logging.FileHandler(FILE_NAME) 33 | fh.setLevel(logging.DEBUG) 34 | fh.setFormatter(logging.Formatter( 35 | '%(asctime)s - %(levelname)s - %(name)s:%(lineno)s: %(message)s')) # NOQA 36 | log.addHandler(fh) 37 | 38 | return log 39 | -------------------------------------------------------------------------------- /neteaseApi/osdlyrics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # osdlyrics.py --- desktop lyrics for musicbox 4 | # Copyright (c) 2015-2016 omi & Contributors 5 | 6 | from __future__ import unicode_literals 7 | from __future__ import print_function 8 | from __future__ import division 9 | from __future__ import absolute_import 10 | from builtins import super 11 | from future import standard_library 12 | standard_library.install_aliases() 13 | import sys 14 | from multiprocessing import Process 15 | 16 | from . import logger 17 | from .config import Config 18 | 19 | log = logger.getLogger(__name__) 20 | 21 | config = Config() 22 | 23 | try: 24 | from PyQt4 import QtGui, QtCore, QtDBus 25 | pyqt_activity = True 26 | except ImportError: 27 | pyqt_activity = False 28 | log.warn("PyQt4 module not installed.") 29 | log.warn("Osdlyrics Not Available.") 30 | 31 | if pyqt_activity: 32 | 33 | class Lyrics(QtGui.QWidget): 34 | 35 | def __init__(self): 36 | super(Lyrics, self).__init__() 37 | self.__dbusAdaptor = LyricsAdapter(self) 38 | self.initUI() 39 | 40 | def initUI(self): 41 | self.setStyleSheet("background:" + config.get_item( 42 | "osdlyrics_background")) 43 | if config.get_item("osdlyrics_transparent"): 44 | self.setAttribute(QtCore.Qt.WA_TranslucentBackground) 45 | self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating) 46 | self.setAttribute(QtCore.Qt.WA_X11DoNotAcceptFocus) 47 | self.setFocusPolicy(QtCore.Qt.NoFocus) 48 | if config.get_item("osdlyrics_on_top"): 49 | self.setWindowFlags(QtCore.Qt.FramelessWindowHint | 50 | QtCore.Qt.WindowStaysOnTopHint | 51 | QtCore.Qt.X11BypassWindowManagerHint) 52 | else: 53 | self.setWindowFlags(QtCore.Qt.FramelessWindowHint) 54 | self.setMinimumSize(600, 50) 55 | self.resize(600, 60) 56 | scn = QtGui.QApplication.desktop().screenNumber( 57 | QtGui.QApplication.desktop().cursor().pos()) 58 | br = QtGui.QApplication.desktop().screenGeometry(scn).bottomRight() 59 | frameGeo = self.frameGeometry() 60 | frameGeo.moveBottomRight(br) 61 | self.move(frameGeo.topLeft()) 62 | self.text = "OSD Lyrics for Musicbox" 63 | self.setWindowTitle("Lyrics") 64 | self.show() 65 | 66 | def mousePressEvent(self, event): 67 | self.mpos = event.pos() 68 | 69 | def mouseMoveEvent(self, event): 70 | if (event.buttons() and QtCore.Qt.LeftButton): 71 | diff = event.pos() - self.mpos 72 | newpos = self.pos() + diff 73 | self.move(newpos) 74 | 75 | def wheelEvent(self, event): 76 | self.resize(self.width() + event.delta(), self.height()) 77 | 78 | def paintEvent(self, event): 79 | qp = QtGui.QPainter() 80 | qp.begin(self) 81 | self.drawText(event, qp) 82 | qp.end() 83 | 84 | def drawText(self, event, qp): 85 | osdlyrics_color = config.get_item("osdlyrics_color") 86 | osdlyrics_font = config.get_item("osdlyrics_font") 87 | font = QtGui.QFont(osdlyrics_font[0], osdlyrics_font[1]) 88 | pen = QtGui.QColor(osdlyrics_color[0], osdlyrics_color[1], 89 | osdlyrics_color[2]) 90 | qp.setFont(font) 91 | qp.setPen(pen) 92 | qp.drawText(event.rect(), QtCore.Qt.AlignCenter | 93 | QtCore.Qt.TextWordWrap, self.text) 94 | 95 | class LyricsAdapter(QtDBus.QDBusAbstractAdaptor): 96 | QtCore.Q_CLASSINFO("D-Bus Interface", "local.musicbox.Lyrics") 97 | QtCore.Q_CLASSINFO( 98 | "D-Bus Introspection", 99 | ' \n' 100 | ' \n' 101 | ' \n' 102 | ' \n' 103 | ' \n') 104 | 105 | def __init__(self, parent): 106 | super(LyricsAdapter, self).__init__(parent) 107 | 108 | @QtCore.pyqtSlot(str) 109 | def refresh_lyrics(self, text): 110 | self.parent().text = text 111 | self.parent().repaint() 112 | 113 | def show_lyrics(): 114 | 115 | app = QtGui.QApplication(sys.argv) 116 | lyrics = Lyrics() 117 | QtDBus.QDBusConnection.sessionBus().registerService('org.musicbox.Bus') 118 | QtDBus.QDBusConnection.sessionBus().registerObject('/', lyrics) 119 | sys.exit(app.exec_()) 120 | 121 | 122 | def show_lyrics_new_process(): 123 | if pyqt_activity and config.get_item("osdlyrics"): 124 | p = Process(target=show_lyrics) 125 | p.daemon = True 126 | p.start() 127 | -------------------------------------------------------------------------------- /neteaseApi/player.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author: omi 4 | # @Date: 2014-07-15 15:48:27 5 | # @Last Modified by: omi 6 | # @Last Modified time: 2015-01-30 18:05:08 7 | ''' 8 | 网易云音乐 Player 9 | ''' 10 | from __future__ import unicode_literals 11 | from __future__ import print_function 12 | from __future__ import division 13 | from __future__ import absolute_import 14 | from builtins import range 15 | from builtins import str 16 | from future import standard_library 17 | standard_library.install_aliases() 18 | # Let's make some noise 19 | 20 | import subprocess 21 | import threading 22 | import time 23 | import os 24 | import random 25 | import re 26 | 27 | from .ui import Ui 28 | from .storage import Storage 29 | from .api import NetEase 30 | from .cache import Cache 31 | from .config import Config 32 | from . import logger 33 | 34 | log = logger.getLogger(__name__) 35 | 36 | 37 | class Player(object): 38 | 39 | def __init__(self): 40 | self.config = Config() 41 | self.ui = Ui() 42 | self.popen_handler = None 43 | # flag stop, prevent thread start 44 | self.playing_flag = False 45 | self.pause_flag = False 46 | self.process_length = 0 47 | self.process_location = 0 48 | self.process_first = False 49 | self.storage = Storage() 50 | self.info = self.storage.database['player_info'] 51 | self.songs = self.storage.database['songs'] 52 | self.playing_id = -1 53 | self.playing_name = '' 54 | self.cache = Cache() 55 | self.notifier = self.config.get_item('notifier') 56 | self.mpg123_parameters = self.config.get_item('mpg123_parameters') 57 | self.end_callback = None 58 | self.playing_song_changed_callback = None 59 | 60 | def popen_recall(self, onExit, popenArgs): 61 | ''' 62 | Runs the given args in subprocess.Popen, and then calls the function 63 | onExit when the subprocess completes. 64 | onExit is a callable object, and popenArgs is a lists/tuple of args 65 | that would give to subprocess.Popen. 66 | ''' 67 | 68 | def runInThread(onExit, arg): 69 | para = ['mpg123', '-R'] 70 | para[1:1] = self.mpg123_parameters 71 | self.popen_handler = subprocess.Popen(para, 72 | stdin=subprocess.PIPE, 73 | stdout=subprocess.PIPE, 74 | stderr=subprocess.PIPE) 75 | self.popen_handler.stdin.write(b'V ' + str(self.info['playing_volume']).encode('utf-8') + b'\n') 76 | if arg: 77 | self.popen_handler.stdin.write(b'L ' + arg.encode('utf-8') + b'\n') 78 | else: 79 | self.next_idx() 80 | onExit() 81 | return 82 | 83 | self.popen_handler.stdin.flush() 84 | 85 | self.process_first = True 86 | while True: 87 | if self.playing_flag is False: 88 | break 89 | 90 | strout = self.popen_handler.stdout.readline().decode('utf-8') 91 | 92 | if re.match('^\@F.*$', strout): 93 | process_data = strout.split(' ') 94 | process_location = float(process_data[4]) 95 | if self.process_first: 96 | self.process_length = process_location 97 | self.process_first = False 98 | self.process_location = 0 99 | else: 100 | self.process_location = self.process_length - process_location # NOQA 101 | continue 102 | elif strout[:2] == '@E': 103 | # get a alternative url from new api 104 | sid = popenArgs['song_id'] 105 | new_url = NetEase().songs_detail_new_api([sid])[0]['url'] 106 | if new_url is None: 107 | log.warning(('Song {} is unavailable ' 108 | 'due to copyright issue.').format(sid)) 109 | break 110 | log.warning( 111 | 'Song {} is not compatible with old api.'.format(sid)) 112 | popenArgs['mp3_url'] = new_url 113 | 114 | self.popen_handler.stdin.write(b'\nL ' + new_url.encode('utf-8') + b'\n') 115 | self.popen_handler.stdin.flush() 116 | self.popen_handler.stdout.readline() 117 | elif strout == '@P 0\n': 118 | self.popen_handler.stdin.write(b'Q\n') 119 | self.popen_handler.stdin.flush() 120 | self.popen_handler.kill() 121 | break 122 | if self.playing_flag: 123 | self.next_idx() 124 | onExit() 125 | return 126 | 127 | def getLyric(): 128 | if 'lyric' not in self.songs[str(self.playing_id)].keys(): 129 | self.songs[str(self.playing_id)]['lyric'] = [] 130 | if len(self.songs[str(self.playing_id)]['lyric']) > 0: 131 | return 132 | netease = NetEase() 133 | lyric = netease.song_lyric(self.playing_id) 134 | if lyric == [] or lyric == '未找到歌词': 135 | return 136 | lyric = lyric.split('\n') 137 | self.songs[str(self.playing_id)]['lyric'] = lyric 138 | return 139 | 140 | def gettLyric(): 141 | if 'tlyric' not in self.songs[str(self.playing_id)].keys(): 142 | self.songs[str(self.playing_id)]['tlyric'] = [] 143 | if len(self.songs[str(self.playing_id)]['tlyric']) > 0: 144 | return 145 | netease = NetEase() 146 | tlyric = netease.song_tlyric(self.playing_id) 147 | if tlyric == [] or tlyric == '未找到歌词翻译': 148 | return 149 | tlyric = tlyric.split('\n') 150 | self.songs[str(self.playing_id)]['tlyric'] = tlyric 151 | return 152 | 153 | def cacheSong(song_id, song_name, artist, song_url): 154 | def cacheExit(song_id, path): 155 | self.songs[str(song_id)]['cache'] = path 156 | 157 | self.cache.add(song_id, song_name, artist, song_url, cacheExit) 158 | self.cache.start_download() 159 | 160 | if 'cache' in popenArgs.keys() and os.path.isfile(popenArgs['cache']): 161 | thread = threading.Thread(target=runInThread, 162 | args=(onExit, popenArgs['cache'])) 163 | else: 164 | thread = threading.Thread(target=runInThread, 165 | args=(onExit, popenArgs['mp3_url'])) 166 | cache_thread = threading.Thread( 167 | target=cacheSong, 168 | args=(popenArgs['song_id'], popenArgs['song_name'], popenArgs[ 169 | 'artist'], popenArgs['mp3_url'])) 170 | cache_thread.start() 171 | thread.start() 172 | lyric_download_thread = threading.Thread(target=getLyric, args=()) 173 | lyric_download_thread.start() 174 | tlyric_download_thread = threading.Thread(target=gettLyric, args=()) 175 | tlyric_download_thread.start() 176 | # returns immediately after the thread starts 177 | return thread 178 | 179 | def get_playing_id(self): 180 | return self.playing_id 181 | 182 | def get_playing_name(self): 183 | return self.playing_name 184 | 185 | def recall(self): 186 | if self.info['idx'] >= len(self.info[ 187 | 'player_list']) and self.end_callback is not None: 188 | log.debug('Callback') 189 | self.end_callback() 190 | if self.info['idx'] < 0 or self.info['idx'] >= len(self.info[ 191 | 'player_list']): 192 | self.info['idx'] = 0 193 | self.stop() 194 | return 195 | self.playing_flag = True 196 | self.pause_flag = False 197 | item = self.songs[self.info['player_list'][self.info['idx']]] 198 | self.ui.build_playinfo(item['song_name'], item['artist'], 199 | item['album_name'], item['quality'], 200 | time.time()) 201 | if self.notifier: 202 | self.ui.notify('Now playing', item['song_name'], 203 | item['album_name'], item['artist']) 204 | self.playing_id = item['song_id'] 205 | self.playing_name = item['song_name'] 206 | self.popen_recall(self.recall, item) 207 | 208 | def generate_shuffle_playing_list(self): 209 | del self.info['playing_list'][:] 210 | for i in range(0, len(self.info['player_list'])): 211 | self.info['playing_list'].append(i) 212 | random.shuffle(self.info['playing_list']) 213 | self.info['ridx'] = 0 214 | 215 | def new_player_list(self, type, title, datalist, offset): 216 | self.info['player_list_type'] = type 217 | self.info['player_list_title'] = title 218 | self.info['idx'] = offset 219 | del self.info['player_list'][:] 220 | del self.info['playing_list'][:] 221 | self.info['ridx'] = 0 222 | for song in datalist: 223 | self.info['player_list'].append(str(song['song_id'])) 224 | if str(song['song_id']) not in self.songs.keys(): 225 | self.songs[str(song['song_id'])] = song 226 | else: 227 | database_song = self.songs[str(song['song_id'])] 228 | if (database_song['song_name'] != song['song_name'] or 229 | database_song['quality'] != song['quality']): 230 | self.songs[str(song['song_id'])] = song 231 | 232 | def append_songs(self, datalist): 233 | for song in datalist: 234 | self.info['player_list'].append(str(song['song_id'])) 235 | if str(song['song_id']) not in self.songs.keys(): 236 | self.songs[str(song['song_id'])] = song 237 | else: 238 | database_song = self.songs[str(song['song_id'])] 239 | cond = any([database_song[k] != song[k] 240 | for k in ('song_name', 'quality', 'mp3_url')]) 241 | if cond: 242 | if 'cache' in self.songs[str(song['song_id'])].keys(): 243 | song['cache'] = self.songs[str(song['song_id'])][ 244 | 'cache'] 245 | self.songs[str(song['song_id'])] = song 246 | if len(datalist) > 0 and self.info['playing_mode'] == 3 or self.info[ 247 | 'playing_mode'] == 4: 248 | self.generate_shuffle_playing_list() 249 | 250 | def play_and_pause(self, idx): 251 | # if same playlists && idx --> same song :: pause/resume it 252 | if self.info['idx'] == idx: 253 | if self.pause_flag: 254 | self.resume() 255 | else: 256 | self.pause() 257 | else: 258 | self.info['idx'] = idx 259 | 260 | # if it's playing 261 | if self.playing_flag: 262 | self.switch() 263 | 264 | # start new play 265 | else: 266 | self.recall() 267 | 268 | # play another 269 | def switch(self): 270 | self.stop() 271 | # wait process be killed 272 | time.sleep(0.1) 273 | self.recall() 274 | 275 | def stop(self): 276 | if self.playing_flag and self.popen_handler: 277 | self.playing_flag = False 278 | self.popen_handler.stdin.write(b'Q\n') 279 | self.popen_handler.stdin.flush() 280 | try: 281 | self.popen_handler.kill() 282 | except OSError as e: 283 | log.error(e) 284 | return 285 | 286 | def pause(self): 287 | if not self.playing_flag and not self.popen_handler: 288 | return 289 | self.pause_flag = True 290 | self.popen_handler.stdin.write(b'P\n') 291 | self.popen_handler.stdin.flush() 292 | 293 | item = self.songs[self.info['player_list'][self.info['idx']]] 294 | self.ui.build_playinfo(item['song_name'], 295 | item['artist'], 296 | item['album_name'], 297 | item['quality'], 298 | time.time(), 299 | pause=True) 300 | 301 | def resume(self): 302 | self.pause_flag = False 303 | self.popen_handler.stdin.write(b'P\n') 304 | self.popen_handler.stdin.flush() 305 | 306 | item = self.songs[self.info['player_list'][self.info['idx']]] 307 | self.ui.build_playinfo(item['song_name'], item['artist'], 308 | item['album_name'], item['quality'], 309 | time.time()) 310 | self.playing_id = item['song_id'] 311 | self.playing_name = item['song_name'] 312 | 313 | def _swap_song(self): 314 | plist = self.info['playing_list'] 315 | now_songs = plist.index(self.info['idx']) 316 | plist[0], plist[now_songs] = plist[now_songs], plist[0] 317 | 318 | def _is_idx_valid(self): 319 | return 0 <= self.info['idx'] < len(self.info['player_list']) 320 | 321 | def _inc_idx(self): 322 | if self.info['idx'] < len(self.info['player_list']): 323 | self.info['idx'] += 1 324 | 325 | def _dec_idx(self): 326 | if self.info['idx'] > 0: 327 | self.info['idx'] -= 1 328 | 329 | def _need_to_shuffle(self): 330 | playing_list = self.info['playing_list'] 331 | ridx = self.info['ridx'] 332 | idx = self.info['idx'] 333 | if ridx >= len(playing_list) or playing_list[ridx] != idx: 334 | return True 335 | else: 336 | return False 337 | 338 | def next_idx(self): 339 | if not self._is_idx_valid(): 340 | self.stop() 341 | return 342 | playlist_len = len(self.info['player_list']) 343 | playinglist_len = len(self.info['playing_list']) 344 | 345 | # Playing mode. 0 is ordered. 1 is orderde loop. 346 | # 2 is single song loop. 3 is single random. 4 is random loop 347 | if self.info['playing_mode'] == 0: 348 | self._inc_idx() 349 | elif self.info['playing_mode'] == 1: 350 | self.info['idx'] = (self.info['idx'] + 1) % playlist_len 351 | elif self.info['playing_mode'] == 2: 352 | self.info['idx'] = self.info['idx'] 353 | elif self.info['playing_mode'] == 3 or self.info['playing_mode'] == 4: 354 | if self._need_to_shuffle(): 355 | self.generate_shuffle_playing_list() 356 | playinglist_len = len(self.info['playing_list']) 357 | # When you regenerate playing list 358 | # you should keep previous song same. 359 | self._swap_song() 360 | 361 | self.info['ridx'] += 1 362 | # Out of border 363 | if self.info['playing_mode'] == 4: 364 | self.info['ridx'] %= playinglist_len 365 | 366 | if self.info['ridx'] >= playinglist_len: 367 | self.info['idx'] = playlist_len 368 | else: 369 | self.info['idx'] = self.info['playing_list'][self.info['ridx']] 370 | else: 371 | self.info['idx'] += 1 372 | if self.playing_song_changed_callback is not None: 373 | self.playing_song_changed_callback() 374 | 375 | def next(self): 376 | self.stop() 377 | time.sleep(0.01) 378 | self.next_idx() 379 | self.recall() 380 | 381 | def prev_idx(self): 382 | if not self._is_idx_valid(): 383 | self.stop() 384 | return 385 | playlist_len = len(self.info['player_list']) 386 | playinglist_len = len(self.info['playing_list']) 387 | # Playing mode. 0 is ordered. 1 is orderde loop. 388 | # 2 is single song loop. 3 is single random. 4 is random loop 389 | if self.info['playing_mode'] == 0: 390 | self._dec_idx() 391 | elif self.info['playing_mode'] == 1: 392 | self.info['idx'] = (self.info['idx'] - 1) % playlist_len 393 | elif self.info['playing_mode'] == 2: 394 | self.info['idx'] = self.info['idx'] 395 | elif self.info['playing_mode'] == 3 or self.info['playing_mode'] == 4: 396 | if self._need_to_shuffle(): 397 | self.generate_shuffle_playing_list() 398 | playinglist_len = len(self.info['playing_list']) 399 | self.info['ridx'] -= 1 400 | if self.info['ridx'] < 0: 401 | if self.info['playing_mode'] == 3: 402 | self.info['ridx'] = 0 403 | else: 404 | self.info['ridx'] %= playinglist_len 405 | self.info['idx'] = self.info['playing_list'][self.info['ridx']] 406 | else: 407 | self.info['idx'] -= 1 408 | if self.playing_song_changed_callback is not None: 409 | self.playing_song_changed_callback() 410 | 411 | def prev(self): 412 | self.stop() 413 | time.sleep(0.01) 414 | self.prev_idx() 415 | self.recall() 416 | 417 | def shuffle(self): 418 | self.stop() 419 | time.sleep(0.01) 420 | self.info['playing_mode'] = 3 421 | self.generate_shuffle_playing_list() 422 | self.info['idx'] = self.info['playing_list'][self.info['ridx']] 423 | self.recall() 424 | 425 | def volume_up(self): 426 | self.info['playing_volume'] = self.info['playing_volume'] + 7 427 | if (self.info['playing_volume'] > 100): 428 | self.info['playing_volume'] = 100 429 | if not self.playing_flag: 430 | return 431 | self.popen_handler.stdin.write(b'V ' + str(self.info[ 432 | 'playing_volume']).encode('utf-8') + b'\n') 433 | self.popen_handler.stdin.flush() 434 | 435 | def volume_down(self): 436 | self.info['playing_volume'] = self.info['playing_volume'] - 7 437 | if (self.info['playing_volume'] < 0): 438 | self.info['playing_volume'] = 0 439 | if not self.playing_flag: 440 | return 441 | 442 | self.popen_handler.stdin.write(b'V ' + str(self.info[ 443 | 'playing_volume']).encode('utf-8') + b'\n') 444 | self.popen_handler.stdin.flush() 445 | 446 | def update_size(self): 447 | self.ui.update_size() 448 | if not 0 <= self.info['idx'] < len(self.info['player_list']): 449 | if self.info['player_list']: 450 | log.error('Index not in range!') 451 | log.debug(self.info) 452 | else: 453 | item = self.songs[self.info['player_list'][self.info['idx']]] 454 | if self.playing_flag: 455 | self.ui.build_playinfo(item['song_name'], item['artist'], 456 | item['album_name'], item['quality'], 457 | time.time()) 458 | if self.pause_flag: 459 | self.ui.build_playinfo(item['song_name'], item['artist'], 460 | item['album_name'], item['quality'], 461 | time.time(), 462 | pause=True) 463 | 464 | def cacheSong1time(self, song_id, song_name, artist, song_url): 465 | def cacheExit(song_id, path): 466 | self.songs[str(song_id)]['cache'] = path 467 | self.cache.enable = False 468 | 469 | self.cache.enable = True 470 | self.cache.add(song_id, song_name, artist, song_url, cacheExit) 471 | self.cache.start_download() 472 | -------------------------------------------------------------------------------- /neteaseApi/scrollstring.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import division 5 | from __future__ import unicode_literals 6 | from __future__ import print_function 7 | from __future__ import absolute_import 8 | from builtins import int 9 | from builtins import chr 10 | from future import standard_library 11 | standard_library.install_aliases() 12 | from time import time 13 | 14 | 15 | class scrollstring(object): 16 | 17 | def __init__(self, content, START): 18 | self.content = content # the true content of the string 19 | self.display = content # the displayed string 20 | self.START = START // 1 # when this instance is created 21 | self.update() 22 | 23 | def update(self): 24 | self.display = self.content 25 | curTime = time() // 1 26 | offset = max(int((curTime - self.START) % len(self.content)) - 1, 0) 27 | while offset > 0: 28 | if self.display[0] > chr(127): 29 | offset -= 1 30 | self.display = self.display[3:] + self.display[:3] 31 | else: 32 | offset -= 1 33 | self.display = self.display[1:] + self.display[:1] 34 | 35 | # self.display = self.content[offset:] + self.content[:offset] 36 | 37 | def __repr__(self): 38 | return self.display 39 | 40 | # determine the display length of a string 41 | 42 | 43 | def truelen(string): 44 | """ 45 | It appears one Asian character takes two spots, but __len__ 46 | counts it as three, so this function counts the dispalyed 47 | length of the string. 48 | 49 | >>> truelen('abc') 50 | 3 51 | >>> truelen('你好') 52 | 4 53 | >>> truelen('1二3') 54 | 4 55 | >>> truelen('') 56 | 0 57 | """ 58 | return len(string) - sum(1 for c in string if c > chr(127)) / 3 59 | -------------------------------------------------------------------------------- /neteaseApi/singleton.py: -------------------------------------------------------------------------------- 1 | class Singleton(object): 2 | """Singleton Class 3 | This is a class to make some class being a Singleton class. 4 | Such as database class or config class. 5 | 6 | usage: 7 | class xxx(Singleton): 8 | def __init__(self): 9 | if hasattr(self, '_init'): 10 | return 11 | self._init = True 12 | other init method 13 | """ 14 | 15 | def __new__(cls, *args, **kwargs): 16 | if not hasattr(cls, '_instance'): 17 | orig = super(Singleton, cls) 18 | cls._instance = orig.__new__(cls, *args, **kwargs) 19 | return cls._instance 20 | -------------------------------------------------------------------------------- /neteaseApi/storage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Author: Catofes 3 | # @Date: 2015-08-15 4 | ''' 5 | Class to stores everything into a json file. 6 | ''' 7 | from __future__ import unicode_literals 8 | from __future__ import print_function 9 | from __future__ import division 10 | from __future__ import absolute_import 11 | from builtins import open 12 | from future import standard_library 13 | standard_library.install_aliases() 14 | import json 15 | 16 | from .const import Constant 17 | from .singleton import Singleton 18 | from .utils import utf8_data_to_file 19 | 20 | 21 | class Storage(Singleton): 22 | 23 | def __init__(self): 24 | ''' 25 | Database stores every info. 26 | 27 | version int 28 | #if value in file is unequal to value defined in this class. 29 | #An database update will be applied. 30 | user dict: 31 | username str 32 | key str 33 | collections list: 34 | collection_info(dict): 35 | collection_name str 36 | collection_type str 37 | collection_describe str 38 | collection_songs list: 39 | song_id(int) 40 | songs dict: 41 | song_id(int) dict: 42 | song_id int 43 | artist str 44 | song_name str 45 | mp3_url str 46 | album_name str 47 | album_id str 48 | quality str 49 | lyric str 50 | tlyric str 51 | player_info dict: 52 | player_list list: 53 | songs_id(int) 54 | playing_list list: 55 | songs_id(int) 56 | playing_mode int 57 | playing_offset int 58 | 59 | 60 | :return: 61 | ''' 62 | if hasattr(self, '_init'): 63 | return 64 | self._init = True 65 | self.version = 4 66 | self.database = { 67 | 'version': 4, 68 | 'user': { 69 | 'username': '', 70 | 'password': '', 71 | 'user_id': '', 72 | 'nickname': '', 73 | }, 74 | 'collections': [[]], 75 | 'songs': {}, 76 | 'player_info': { 77 | 'player_list': [], 78 | 'player_list_type': '', 79 | 'player_list_title': '', 80 | 'playing_list': [], 81 | 'playing_mode': 0, 82 | 'idx': 0, 83 | 'ridx': 0, 84 | 'playing_volume': 60, 85 | } 86 | } 87 | self.storage_path = Constant.storage_path 88 | self.cookie_path = Constant.cookie_path 89 | self.file = None 90 | 91 | def load(self): 92 | try: 93 | self.file = open(self.storage_path, 'r') 94 | self.database = json.loads(self.file.read()) 95 | self.file.close() 96 | except (ValueError, OSError, IOError): 97 | self.__init__() 98 | if not self.check_version(): 99 | self.save() 100 | 101 | def check_version(self): 102 | if self.database['version'] == self.version: 103 | return True 104 | else: 105 | # Should do some update. 106 | if self.database['version'] == 1: 107 | self.database['version'] = 2 108 | self.database['cache'] = False 109 | elif self.database['version'] == 2: 110 | self.database['version'] = 3 111 | self.database.pop('cache') 112 | elif self.database['version'] == 3: 113 | self.database['version'] = 4 114 | self.database['user'] = {'username': '', 115 | 'password': '', 116 | 'user_id': '', 117 | 'nickname': ''} 118 | self.check_version() 119 | return False 120 | 121 | def save(self): 122 | self.file = open(self.storage_path, 'w') 123 | db_str = json.dumps(self.database) 124 | utf8_data_to_file(self.file, db_str) 125 | self.file.close() 126 | -------------------------------------------------------------------------------- /neteaseApi/terminalsize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | from __future__ import unicode_literals 4 | from __future__ import division 5 | from __future__ import absolute_import 6 | from builtins import int 7 | from future import standard_library 8 | standard_library.install_aliases() 9 | import os 10 | import shlex 11 | import struct 12 | import platform 13 | import subprocess 14 | 15 | from . import logger 16 | 17 | log = logger.getLogger(__name__) 18 | 19 | 20 | def get_terminal_size(): 21 | ''' getTerminalSize() 22 | - get width and height of console 23 | - works on linux,os x,windows,cygwin(windows) 24 | originally retrieved from: 25 | http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python # NOQA 26 | ''' 27 | current_os = platform.system() 28 | tuple_xy = None 29 | if current_os == 'Windows': 30 | tuple_xy = _get_terminal_size_windows() 31 | if tuple_xy is None: 32 | tuple_xy = _get_terminal_size_tput() 33 | # needed for window's python in cygwin's xterm! 34 | if current_os in ['Linux', 'Darwin'] or current_os.startswith('CYGWIN'): 35 | tuple_xy = _get_terminal_size_linux() 36 | if tuple_xy is None: 37 | print('default') 38 | tuple_xy = (80, 25) # default value 39 | return tuple_xy 40 | 41 | 42 | def _get_terminal_size_windows(): 43 | try: 44 | from ctypes import windll, create_string_buffer 45 | # stdin handle is -10 46 | # stdout handle is -11 47 | # stderr handle is -12 48 | h = windll.kernel32.GetStdHandle(-12) 49 | csbi = create_string_buffer(22) 50 | res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) 51 | if res: 52 | (bufx, bufy, curx, cury, wattr, left, top, right, bottom, maxx, 53 | maxy) = struct.unpack('hhhhHhhhhhh', csbi.raw) 54 | sizex = right - left + 1 55 | sizey = bottom - top + 1 56 | return sizex, sizey 57 | except Exception as e: 58 | log.error(e) 59 | pass 60 | 61 | 62 | def _get_terminal_size_tput(): 63 | # get terminal width 64 | # src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window # NOQA 65 | try: 66 | cols = int(subprocess.check_call(shlex.split('tput cols'))) 67 | rows = int(subprocess.check_call(shlex.split('tput lines'))) 68 | return (cols, rows) 69 | except Exception as e: 70 | log.error(e) 71 | pass 72 | 73 | 74 | def _get_terminal_size_linux(): 75 | def ioctl_GWINSZ(fd): 76 | try: 77 | import fcntl 78 | import termios 79 | cr = struct.unpack('hh', 80 | fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) 81 | return cr 82 | except Exception as e: 83 | log.error(e) 84 | pass 85 | 86 | cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) 87 | if not cr: 88 | try: 89 | fd = os.open(os.ctermid(), os.O_RDONLY) 90 | cr = ioctl_GWINSZ(fd) 91 | os.close(fd) 92 | except Exception as e: 93 | log.error(e) 94 | pass 95 | if not cr: 96 | try: 97 | cr = (os.environ['LINES'], os.environ['COLUMNS']) 98 | except Exception as e: 99 | log.error(e) 100 | return None 101 | return int(cr[1]), int(cr[0]) 102 | 103 | 104 | if __name__ == '__main__': 105 | sizex, sizey = get_terminal_size() 106 | print('width =', sizex, 'height =', sizey) 107 | -------------------------------------------------------------------------------- /neteaseApi/ui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Author: omi 4 | # @Date: 2014-08-24 21:51:57 5 | ''' 6 | 网易云音乐 Ui 7 | ''' 8 | from __future__ import division 9 | from __future__ import unicode_literals 10 | from __future__ import print_function 11 | from __future__ import absolute_import 12 | from builtins import range 13 | from builtins import str 14 | from builtins import int 15 | from future import standard_library 16 | standard_library.install_aliases() 17 | import hashlib 18 | import re 19 | import curses 20 | 21 | from .api import NetEase 22 | from .scrollstring import truelen, scrollstring 23 | from .storage import Storage 24 | from .config import Config 25 | from .utils import notify 26 | from . import logger 27 | from . import terminalsize 28 | 29 | log = logger.getLogger(__name__) 30 | 31 | try: 32 | import dbus 33 | dbus_activity = True 34 | except ImportError: 35 | dbus_activity = False 36 | log.warn('Qt dbus module is not installed.') 37 | log.warn('Osdlyrics is not available.') 38 | 39 | 40 | def break_str(s, start, max_len=80): 41 | l = len(s) 42 | i, x = 0, max_len 43 | res = [] 44 | while i < l: 45 | res.append(s[i:i + max_len]) 46 | i += x 47 | return '\n{}'.format(' ' * start).join(res) 48 | 49 | 50 | class Ui(object): 51 | 52 | def __init__(self): 53 | self.screen = curses.initscr() 54 | self.screen.timeout(100) # the screen refresh every 100ms 55 | # charactor break buffer 56 | curses.cbreak() 57 | self.screen.keypad(1) 58 | self.netease = NetEase() 59 | 60 | curses.start_color() 61 | if Config().get_item('curses_transparency'): 62 | curses.use_default_colors() 63 | curses.init_pair(1, curses.COLOR_GREEN, -1) 64 | curses.init_pair(2, curses.COLOR_CYAN, -1) 65 | curses.init_pair(3, curses.COLOR_RED, -1) 66 | curses.init_pair(4, curses.COLOR_YELLOW, -1) 67 | else: 68 | curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK) 69 | curses.init_pair(2, curses.COLOR_CYAN, curses.COLOR_BLACK) 70 | curses.init_pair(3, curses.COLOR_RED, curses.COLOR_BLACK) 71 | curses.init_pair(4, curses.COLOR_YELLOW, curses.COLOR_BLACK) 72 | # term resize handling 73 | size = terminalsize.get_terminal_size() 74 | self.x = max(size[0], 10) 75 | self.y = max(size[1], 25) 76 | self.startcol = int(float(self.x) / 5) 77 | self.indented_startcol = max(self.startcol - 3, 0) 78 | self.update_space() 79 | self.lyric = '' 80 | self.now_lyric = '' 81 | self.tlyric = '' 82 | self.storage = Storage() 83 | self.config = Config() 84 | self.newversion = False 85 | 86 | def addstr(self, *args): 87 | if len(args) == 1: 88 | self.screen.addstr(args[0]) 89 | else: 90 | self.screen.addstr(args[0], args[1], args[2].encode('utf-8'), *args[3:]) 91 | 92 | def notify(self, summary, song, album, artist): 93 | if summary != 'disable': 94 | body = '%s\nin %s by %s' % (song, album, artist) 95 | content = summary + ': ' + body 96 | notify(content) 97 | 98 | def build_playinfo(self, 99 | song_name, 100 | artist, 101 | album_name, 102 | quality, 103 | start, 104 | pause=False): 105 | curses.noecho() 106 | # refresh top 2 line 107 | self.screen.move(1, 1) 108 | self.screen.clrtoeol() 109 | self.screen.move(2, 1) 110 | self.screen.clrtoeol() 111 | if pause: 112 | self.addstr(1, self.indented_startcol, 113 | '_ _ z Z Z ' + quality, curses.color_pair(3)) 114 | else: 115 | self.addstr(1, self.indented_startcol, 116 | '♫ ♪ ♫ ♪ ' + quality, curses.color_pair(3)) 117 | 118 | self.addstr( 119 | 1, min(self.indented_startcol + 18, self.x - 1), 120 | song_name + self.space + artist + ' < ' + album_name + ' >', 121 | curses.color_pair(4)) 122 | 123 | self.screen.refresh() 124 | 125 | def build_process_bar(self, now_playing, total_length, playing_flag, 126 | pause_flag, playing_mode): 127 | if (self.storage.database['player_info']['idx'] >= 128 | len(self.storage.database['player_info']['player_list'])): 129 | return 130 | curses.noecho() 131 | self.screen.move(3, 1) 132 | self.screen.clrtoeol() 133 | self.screen.move(4, 1) 134 | self.screen.clrtoeol() 135 | if not playing_flag: 136 | return 137 | if total_length <= 0: 138 | total_length = 1 139 | if now_playing > total_length or now_playing <= 0: 140 | now_playing = 0 141 | process = '[' 142 | for i in range(0, 33): 143 | if i < now_playing / total_length * 33: 144 | if (i + 1) > now_playing / total_length * 33: 145 | if not pause_flag: 146 | process += '>' 147 | continue 148 | process += '=' 149 | else: 150 | process += ' ' 151 | process += '] ' 152 | now_minute = int(now_playing / 60) 153 | if now_minute > 9: 154 | now_minute = str(now_minute) 155 | else: 156 | now_minute = '0' + str(now_minute) 157 | now_second = int(now_playing - int(now_playing / 60) * 60) 158 | if now_second > 9: 159 | now_second = str(now_second) 160 | else: 161 | now_second = '0' + str(now_second) 162 | total_minute = int(total_length / 60) 163 | if total_minute > 9: 164 | total_minute = str(total_minute) 165 | else: 166 | total_minute = '0' + str(total_minute) 167 | total_second = int(total_length - int(total_length / 60) * 60) 168 | if total_second > 9: 169 | total_second = str(total_second) 170 | else: 171 | total_second = '0' + str(total_second) 172 | process += '(' + now_minute + ':' + now_second + '/' + total_minute + ':' + total_second + ')' # NOQA 173 | if playing_mode == 0: 174 | process = '顺序播放 ' + process 175 | elif playing_mode == 1: 176 | process = '顺序循环 ' + process 177 | elif playing_mode == 2: 178 | process = '单曲循环 ' + process 179 | elif playing_mode == 3: 180 | process = '随机播放 ' + process 181 | elif playing_mode == 4: 182 | process = '随机循环 ' + process 183 | else: 184 | pass 185 | self.addstr(3, self.startcol - 2, process, curses.color_pair(1)) 186 | song = self.storage.database['songs'][ 187 | self.storage.database['player_info']['player_list'][ 188 | self.storage.database['player_info']['idx']]] 189 | if 'lyric' not in song.keys() or len(song['lyric']) <= 0: 190 | self.now_lyric = '暂无歌词 ~>_<~ \n' 191 | if dbus_activity and self.config.get_item('osdlyrics'): 192 | self.now_playing = song['song_name'] + ' - ' + song[ 193 | 'artist'] + '\n' 194 | 195 | else: 196 | key = now_minute + ':' + now_second 197 | for line in song['lyric']: 198 | if key in line: 199 | if 'tlyric' not in song.keys() or len(song['tlyric']) <= 0: 200 | self.now_lyric = line 201 | else: 202 | self.now_lyric = line 203 | for tline in song['tlyric']: 204 | if key in tline and self.config.get_item( 205 | 'translation'): 206 | self.now_lyric = tline + ' || ' + self.now_lyric # NOQA 207 | self.now_lyric = re.sub('\[.*?\]', '', self.now_lyric) 208 | if dbus_activity and self.config.get_item('osdlyrics'): 209 | try: 210 | bus = dbus.SessionBus().get_object('org.musicbox.Bus', '/') 211 | if self.now_lyric == '暂无歌词 ~>_<~ \n': 212 | bus.refresh_lyrics(self.now_playing, 213 | dbus_interface='local.musicbox.Lyrics') 214 | else: 215 | bus.refresh_lyrics(self.now_lyric, 216 | dbus_interface='local.musicbox.Lyrics') 217 | except Exception as e: 218 | log.error(e) 219 | pass 220 | self.addstr(4, self.startcol - 2, str(self.now_lyric), 221 | curses.color_pair(3)) 222 | self.screen.refresh() 223 | 224 | def build_loading(self): 225 | self.addstr(7, self.startcol, '享受高品质音乐,loading...', 226 | curses.color_pair(1)) 227 | self.screen.refresh() 228 | 229 | # start is the timestamp of this function being called 230 | def build_menu(self, datatype, title, datalist, offset, index, step, 231 | start): 232 | # keep playing info in line 1 233 | curses.noecho() 234 | self.screen.move(5, 1) 235 | self.screen.clrtobot() 236 | self.addstr(5, self.startcol, title, curses.color_pair(1)) 237 | 238 | if len(datalist) == 0: 239 | self.addstr(8, self.startcol, '这里什么都没有 -,-') 240 | 241 | else: 242 | if datatype == 'main': 243 | for i in range(offset, min(len(datalist), offset + step)): 244 | if i == index: 245 | self.addstr(i - offset + 9, 246 | self.indented_startcol, 247 | '-> ' + str(i) + '. ' + datalist[i], 248 | curses.color_pair(2)) 249 | else: 250 | self.addstr(i - offset + 9, self.startcol, 251 | str(i) + '. ' + datalist[i]) 252 | 253 | elif datatype == 'songs' or datatype == 'fmsongs': 254 | iter_range = min(len(datalist), offset + step) 255 | for i in range(offset, iter_range): 256 | # this item is focus 257 | if i == index: 258 | self.addstr(i - offset + 8, 0, 259 | ' ' * self.startcol) 260 | lead = '-> ' + str(i) + '. ' 261 | self.addstr(i - offset + 8, 262 | self.indented_startcol, lead, 263 | curses.color_pair(2)) 264 | name = '{}{}{} < {} >'.format( 265 | datalist[i]['song_name'], self.space, 266 | datalist[i]['artist'], datalist[i]['album_name']) 267 | 268 | # the length decides whether to scoll 269 | if truelen(name) < self.x - self.startcol - 1: 270 | self.addstr( 271 | i - offset + 8, 272 | self.indented_startcol + len(lead), name, 273 | curses.color_pair(2)) 274 | else: 275 | name = scrollstring(name + ' ', start) 276 | self.addstr( 277 | i - offset + 8, 278 | self.indented_startcol + len(lead), str(name), 279 | curses.color_pair(2)) 280 | else: 281 | self.addstr(i - offset + 8, 0, 282 | ' ' * self.startcol) 283 | self.addstr( 284 | i - offset + 8, self.startcol, 285 | '{}. {}{}{} < {} >'.format( 286 | i, datalist[i]['song_name'], self.space, 287 | datalist[i]['artist'], 288 | datalist[i]['album_name'])[:int(self.x * 2)]) 289 | 290 | self.addstr(iter_range - offset + 8, 0, ' ' * self.x) 291 | 292 | elif datatype == 'comments': 293 | # 被选中的评论在最下方显示全部字符,其余评论仅显示一行 294 | for i in range(offset, min(len(datalist), offset + step)): 295 | maxlength = min(int(1.8 * self.startcol), len(datalist[i])) 296 | if i == index: 297 | try: 298 | self.addstr( 299 | 20, self.indented_startcol, 300 | '-> ' + str(i) + '. ' + 301 | break_str(datalist[i], self.indented_startcol, maxlength), 302 | curses.color_pair(2)) 303 | except: 304 | self.addstr( 305 | 20, self.indented_startcol, 306 | '-> ' + str(i) + '. ' + 'This comment is invalid', 307 | curses.color_pair(2)) 308 | else: 309 | self.addstr( 310 | i - offset + 9, self.startcol, 311 | str(i) + '. ' + datalist[i][:maxlength]) 312 | 313 | elif datatype == 'artists': 314 | for i in range(offset, min(len(datalist), offset + step)): 315 | if i == index: 316 | self.addstr( 317 | i - offset + 9, self.indented_startcol, 318 | '-> ' + str(i) + '. ' + datalist[i]['artists_name'] + 319 | self.space + str(datalist[i]['alias']), 320 | curses.color_pair(2)) 321 | else: 322 | self.addstr( 323 | i - offset + 9, self.startcol, 324 | str(i) + '. ' + datalist[i]['artists_name'] + 325 | self.space + datalist[i][ 326 | 'alias']) 327 | 328 | elif datatype == 'artist_info': 329 | for i in range(offset, min(len(datalist), offset + step)): 330 | if i == index: 331 | self.addstr( 332 | i - offset + 9, self.indented_startcol, 333 | '-> ' + str(i) + '. ' + datalist[i]['item'], 334 | curses.color_pair(2)) 335 | else: 336 | self.addstr( 337 | i - offset + 9, self.startcol, 338 | str(i) + '. ' + datalist[i]['item']) 339 | 340 | elif datatype == 'albums': 341 | for i in range(offset, min(len(datalist), offset + step)): 342 | if i == index: 343 | self.addstr( 344 | i - offset + 9, self.indented_startcol, 345 | '-> ' + str(i) + '. ' + datalist[i]['albums_name'] + 346 | self.space + datalist[i]['artists_name'], curses.color_pair(2)) 347 | else: 348 | self.addstr( 349 | i - offset + 9, self.startcol, 350 | str(i) + '. ' + datalist[i]['albums_name'] + 351 | self.space + datalist[i][ 352 | 'artists_name']) 353 | 354 | elif datatype == 'playlists': 355 | for i in range(offset, min(len(datalist), offset + step)): 356 | if i == index: 357 | self.addstr( 358 | i - offset + 9, self.indented_startcol, 359 | '-> ' + str(i) + '. ' + datalist[i]['title'], 360 | curses.color_pair(2)) 361 | else: 362 | self.addstr( 363 | i - offset + 9, self.startcol, 364 | str(i) + '. ' + datalist[i]['title']) 365 | 366 | elif datatype == 'top_playlists': 367 | for i in range(offset, min(len(datalist), offset + step)): 368 | if i == index: 369 | self.addstr( 370 | i - offset + 9, self.indented_startcol, '-> ' + 371 | str(i) + '. ' + datalist[i]['playlists_name'] + 372 | self.space + datalist[i]['creator_name'], 373 | curses.color_pair(2)) 374 | else: 375 | self.addstr( 376 | i - offset + 9, self.startcol, 377 | str(i) + '. ' + datalist[i]['playlists_name'] + 378 | self.space + datalist[i][ 379 | 'creator_name']) 380 | 381 | elif datatype == 'toplists': 382 | for i in range(offset, min(len(datalist), offset + step)): 383 | if i == index: 384 | self.addstr(i - offset + 9, 385 | self.indented_startcol, 386 | '-> ' + str(i) + '. ' + datalist[i], 387 | curses.color_pair(2)) 388 | else: 389 | self.addstr(i - offset + 9, self.startcol, 390 | str(i) + '. ' + datalist[i]) 391 | 392 | elif datatype in ('playlist_classes', 'playlist_class_detail'): 393 | for i in range(offset, min(len(datalist), offset + step)): 394 | if i == index: 395 | self.addstr(i - offset + 9, 396 | self.indented_startcol, 397 | '-> ' + str(i) + '. ' + datalist[i], 398 | curses.color_pair(2)) 399 | else: 400 | self.addstr(i - offset + 9, self.startcol, 401 | str(i) + '. ' + datalist[i]) 402 | 403 | elif datatype == 'djchannels': 404 | for i in range(offset, min(len(datalist), offset + step)): 405 | if i == index: 406 | self.addstr( 407 | i - offset + 8, self.indented_startcol, 408 | '-> ' + str(i) + '. ' + datalist[i]['song_name'], 409 | curses.color_pair(2)) 410 | else: 411 | self.addstr( 412 | i - offset + 8, self.startcol, 413 | str(i) + '. ' + datalist[i]['song_name']) 414 | 415 | elif datatype == 'search': 416 | self.screen.move(6, 1) 417 | self.screen.clrtobot() 418 | self.screen.timeout(-1) 419 | self.addstr(8, self.startcol, '选择搜索类型:', 420 | curses.color_pair(1)) 421 | for i in range(offset, min(len(datalist), offset + step)): 422 | if i == index: 423 | self.addstr( 424 | i - offset + 10, self.indented_startcol, 425 | '-> ' + str(i) + '.' + datalist[i - 1], 426 | curses.color_pair(2)) 427 | else: 428 | self.addstr(i - offset + 10, self.startcol, 429 | str(i) + '.' + datalist[i - 1]) 430 | self.screen.timeout(100) 431 | 432 | elif datatype == 'help': 433 | for i in range(offset, min(len(datalist), offset + step)): 434 | if i == index: 435 | self.addstr( 436 | i - offset + 9, self.indented_startcol, 437 | '-> {}. \'{}{} {}'.format( 438 | i, (datalist[i][0] + '\'').ljust(11), 439 | datalist[i][1], datalist[i][2]), 440 | curses.color_pair(2)) 441 | else: 442 | self.addstr( 443 | i - offset + 9, self.startcol, 444 | '{}. \'{}{} {}'.format( 445 | i, (datalist[i][0] + '\'').ljust(11), 446 | datalist[i][1], datalist[i][2])) 447 | 448 | self.addstr( 449 | 20, 6, 'NetEase-MusicBox 基于Python,所有版权音乐来源于网易,本地不做任何保存') 450 | self.addstr(21, 10, 451 | '按 [G] 到 Github 了解更多信息,帮助改进,或者Star表示支持~~') 452 | self.addstr(22, self.startcol, 453 | 'Build with love to music by omi') 454 | 455 | self.screen.refresh() 456 | 457 | def build_search(self, stype): 458 | self.screen.timeout(-1) 459 | netease = self.netease 460 | if stype == 'songs': 461 | song_name = self.get_param('搜索歌曲:') 462 | if song_name == '/return': 463 | return [] 464 | else: 465 | try: 466 | data = netease.search(song_name, stype=1) 467 | song_ids = [] 468 | if 'songs' in data['result']: 469 | if 'mp3Url' in data['result']['songs']: 470 | songs = data['result']['songs'] 471 | 472 | # if search song result do not has mp3Url 473 | # send ids to get mp3Url 474 | else: 475 | for i in range(0, len(data['result']['songs'])): 476 | song_ids.append(data['result']['songs'][i][ 477 | 'id']) 478 | songs = netease.songs_detail(song_ids) 479 | return netease.dig_info(songs, 'songs') 480 | except Exception as e: 481 | log.error(e) 482 | return [] 483 | 484 | elif stype == 'artists': 485 | artist_name = self.get_param('搜索艺术家:') 486 | if artist_name == '/return': 487 | return [] 488 | else: 489 | try: 490 | data = netease.search(artist_name, stype=100) 491 | if 'artists' in data['result']: 492 | artists = data['result']['artists'] 493 | return netease.dig_info(artists, 'artists') 494 | except Exception as e: 495 | log.error(e) 496 | return [] 497 | 498 | elif stype == 'albums': 499 | albums_name = self.get_param('搜索专辑:') 500 | if albums_name == '/return': 501 | return [] 502 | else: 503 | try: 504 | data = netease.search(albums_name, stype=10) 505 | if 'albums' in data['result']: 506 | albums = data['result']['albums'] 507 | return netease.dig_info(albums, 'albums') 508 | except Exception as e: 509 | log.error(e) 510 | return [] 511 | 512 | elif stype == 'search_playlist': 513 | search_playlist = self.get_param('搜索网易精选集:') 514 | if search_playlist == '/return': 515 | return [] 516 | else: 517 | try: 518 | data = netease.search(search_playlist, stype=1000) 519 | if 'playlists' in data['result']: 520 | playlists = data['result']['playlists'] 521 | return netease.dig_info(playlists, 'top_playlists') 522 | except Exception as e: 523 | log.error(e) 524 | return [] 525 | 526 | return [] 527 | 528 | def build_login(self): 529 | self.build_login_bar() 530 | local_account = self.get_account() 531 | local_password = hashlib.md5(self.get_password().encode('utf-8')).hexdigest() 532 | login_info = self.netease.login(local_account, local_password) 533 | account = [local_account, local_password] 534 | if login_info['code'] != 200: 535 | x = self.build_login_error() 536 | if x == ord('1'): 537 | return self.build_login() 538 | else: 539 | return -1 540 | else: 541 | return [login_info, account] 542 | 543 | def build_login_bar(self): 544 | curses.noecho() 545 | self.screen.move(4, 1) 546 | self.screen.clrtobot() 547 | self.addstr(5, self.startcol, '请输入登录信息(支持手机登陆)', 548 | curses.color_pair(1)) 549 | self.addstr(8, self.startcol, '账号:', curses.color_pair(1)) 550 | self.addstr(9, self.startcol, '密码:', curses.color_pair(1)) 551 | self.screen.move(8, 24) 552 | self.screen.refresh() 553 | 554 | def build_login_error(self): 555 | self.screen.move(4, 1) 556 | self.screen.timeout(-1) # disable the screen timeout 557 | self.screen.clrtobot() 558 | self.addstr(8, self.startcol, '艾玛,登录信息好像不对呢 (O_O)#', 559 | curses.color_pair(1)) 560 | self.addstr(10, self.startcol, '[1] 再试一次') 561 | self.addstr(11, self.startcol, '[2] 稍后再试') 562 | self.addstr(14, self.startcol, '请键入对应数字:', curses.color_pair(2)) 563 | self.screen.refresh() 564 | x = self.screen.getch() 565 | self.screen.timeout(100) # restore the screen timeout 566 | return x 567 | 568 | def get_account(self): 569 | self.screen.timeout(-1) # disable the screen timeout 570 | curses.echo() 571 | account = self.screen.getstr(8, self.startcol + 6, 60) 572 | self.screen.timeout(100) # restore the screen timeout 573 | return account.decode('utf-8') 574 | 575 | def get_password(self): 576 | self.screen.timeout(-1) # disable the screen timeout 577 | curses.noecho() 578 | password = self.screen.getstr(9, self.startcol + 6, 60) 579 | self.screen.timeout(100) # restore the screen timeout 580 | return password.decode('utf-8') 581 | 582 | def get_param(self, prompt_string): 583 | # keep playing info in line 1 584 | curses.echo() 585 | self.screen.move(4, 1) 586 | self.screen.clrtobot() 587 | self.addstr(5, self.startcol, prompt_string, 588 | curses.color_pair(1)) 589 | self.screen.refresh() 590 | info = self.screen.getstr(10, self.startcol, 60) 591 | if info == '': 592 | return '/return' 593 | elif info.strip() is '': 594 | return self.get_param(prompt_string) 595 | else: 596 | return info 597 | 598 | def update_size(self): 599 | # get terminal size 600 | size = terminalsize.get_terminal_size() 601 | self.x = max(size[0], 10) 602 | self.y = max(size[1], 25) 603 | 604 | # update intendations 605 | curses.resizeterm(self.y, self.x) 606 | self.startcol = int(float(self.x) / 5) 607 | self.indented_startcol = max(self.startcol - 3, 0) 608 | self.update_space() 609 | self.screen.clear() 610 | self.screen.refresh() 611 | 612 | def update_space(self): 613 | if self.x > 140: 614 | self.space = ' - ' 615 | elif self.x > 80: 616 | self.space = ' - ' 617 | else: 618 | self.space = ' - ' 619 | self.screen.refresh() 620 | -------------------------------------------------------------------------------- /neteaseApi/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # utils.py --- utils for musicbox 4 | # Copyright (c) 2015-2016 omi & Contributors 5 | 6 | from __future__ import unicode_literals 7 | from __future__ import print_function 8 | from __future__ import division 9 | from __future__ import absolute_import 10 | from builtins import str 11 | from future import standard_library 12 | standard_library.install_aliases() 13 | import platform 14 | import os 15 | 16 | 17 | def utf8_data_to_file(f, data): 18 | if hasattr(data, 'decode'): 19 | f.write(data.decode('utf-8')) 20 | else: 21 | f.write(data) 22 | 23 | 24 | def notify_command_osx(msg, msg_type, t=None): 25 | command = '/usr/bin/osascript -e "display notification \\\"{}\\\" {} with title \\\"musicbox\\\""' 26 | sound = 'sound name \\\"/System/Library/Sounds/Ping.aiff\\\"' if msg_type else '' 27 | return command.format(msg, sound) 28 | 29 | 30 | def notify_command_linux(msg, t=None): 31 | command = '/usr/bin/notify-send "' + msg + '"' 32 | if t: 33 | command += ' -t ' + str(t) 34 | command += ' -h int:transient:1' 35 | return command 36 | 37 | 38 | def notify(msg, msg_type=0, t=None): 39 | "Show system notification with duration t (ms)" 40 | if platform.system() == 'Darwin': 41 | command = notify_command_osx(msg, msg_type, t) 42 | else: 43 | command = notify_command_linux(msg, t) 44 | os.system(command.encode('utf-8')) 45 | 46 | 47 | if __name__ == "__main__": 48 | notify("I'm test 1", msg_type=1, t=1000) 49 | notify("I'm test 2", msg_type=0, t=1000) 50 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | from WxNeteaseMusic import WxNeteaseMusic 3 | import itchat 4 | 5 | wnm = WxNeteaseMusic() 6 | @itchat.msg_register(itchat.content.TEXT) 7 | def mp3_player(msg): 8 | text = msg['Text'] 9 | res = wnm.msg_handler(text) 10 | return res 11 | 12 | itchat.auto_login(enableCmdQR=False) 13 | itchat.run(debug=True) 14 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | from WxNeteaseMusic import WxNeteaseMusic 3 | import itchat 4 | 5 | wnm = WxNeteaseMusic() 6 | @itchat.msg_register(itchat.content.TEXT) 7 | def mp3_player(msg): 8 | text = msg['Text'] 9 | res = wnm.msg_handler(text) 10 | return res 11 | 12 | itchat.auto_login() 13 | itchat.run(debug=True) 14 | -------------------------------------------------------------------------------- /userInfo: -------------------------------------------------------------------------------- 1 | 57542828 --------------------------------------------------------------------------------