├── .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 |
10 |
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 | 
95 |
96 | 如果还不清楚的话,我还拍了个小视频,放在了优酷上,[请点击这里](http://v.youku.com/v_show/id_XMjUxODk5MDQxNg==.html)。
97 |
98 | [](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
--------------------------------------------------------------------------------