mplug163 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5143da5494049d78f55d76a98a1b91b79655c641
4
+ data.tar.gz: 4694b7e9a9316f81ff7e90c834679cc8ac7c7410
5
+ SHA512:
6
+ metadata.gz: fb56f92dea87f05ac93efca90321ebd325f2d4c257904a18c0e15ca3d31dfde7455d4ec433cd80de737b3136458478a84a48bc8d147810ee39e8c6e299fdb1e9
7
+ data.tar.gz: 72c48c42edc713db334409dfbe2cc4999554bc1338da24860a08281f04296f218bed5af08d84f44e39b4330ff4d0ece19394db41f18f15754fe190a1304e54d6
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mplug163.gemspec
4
+ gemspec
5
+
6
+ gem 'curses'
7
+ gem 'open4'
8
+ gem 'unirest'
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Ripple Yui
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,39 @@
1
+ # Mplug163
2
+
3
+ A local music player based on Netease music.
4
+
5
+ ## Requirements
6
+
7
+ ```bash
8
+ brew install mpg123
9
+ ```
10
+
11
+ ## Installation
12
+
13
+ ```ruby
14
+ gem install mplug163
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ Open your terminal and input `mplug163`. Music's coming!
20
+
21
+ ## Big Thanks
22
+
23
+ [NetEase-MusicBox](https://github.com/bluetomlee/NetEase-MusicBox)
24
+
25
+ [网易云音乐API分析](https://github.com/yanunon/NeteaseCloudMusic/wiki/网易云音乐API分析)
26
+
27
+ Their great projects inspire me. Thanks!
28
+
29
+ ## License
30
+
31
+ MIT License.
32
+
33
+ ## Contributing
34
+
35
+ 1. Fork it ( https://github.com/[my-github-username]/mplug163/fork )
36
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
37
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
38
+ 4. Push to the branch (`git push origin my-new-feature`)
39
+ 5. Create a new Pull Request
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('../../lib/mplug163', __FILE__)
4
+
5
+ main = Menu.new
6
+ main.start
@@ -0,0 +1,4 @@
1
+ require File.expand_path('../mplug163/menu', __FILE__)
2
+ require File.expand_path('../mplug163/api', __FILE__)
3
+ require File.expand_path('../mplug163/player', __FILE__)
4
+ require File.expand_path('../mplug163/ui', __FILE__)
@@ -0,0 +1,286 @@
1
+ require 'unirest'
2
+ require 'json'
3
+ require 'digest'
4
+
5
+ class NetEase
6
+ def initialize
7
+ @header = {
8
+ "Accept" => "*/*",
9
+ "Accept-Encoding" => "gzip,deflate,sdch",
10
+ "Accept-Language" => "zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4",
11
+ "Connection" => "keep-alive",
12
+ "Content-Type" => "application/x-www-form-urlencoded",
13
+ "Host" => "music.163.com",
14
+ "Referer" => "http://music.163.com/",
15
+ "User-Agent" => "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"
16
+ }
17
+
18
+ @cookies = {
19
+ "appver" => "2.0.2"
20
+ }
21
+
22
+ @default_timeout = 10
23
+ Unirest.timeout @default_timeout
24
+ end
25
+
26
+ def http_request(method, action, query=nil, urlencoded=nil, callback=nil, timeout=nil)
27
+ if method == 'GET'
28
+ url = (query == nil ? action : "#{action}?#{query}")
29
+ connection = Unirest.get(url, headers: @header)
30
+ elsif method == 'POST'
31
+ connection = Unirest.post(action, headers: @header, parameters: query)
32
+ end
33
+
34
+ connection.body
35
+ end
36
+
37
+ # 登录
38
+ def login(username, password)
39
+ action = "http://music.163.com/api/login/"
40
+ query = {
41
+ "username" => username,
42
+ "password" => Digest::MD5.hexdigest(password),
43
+ "rememberLogin" => "true"
44
+ }
45
+ begin
46
+ return http_request('POST', action, query)
47
+ rescue Exception => e
48
+ return {"code" => 501}
49
+ end
50
+ end
51
+
52
+ # 用户歌单
53
+ def user_playlists(uid, offset=0, limit=100)
54
+ action = "http://music.163.com/api/user/playlist/?offset=#{offset}&limit=#{limit}&uid=#{uid}"
55
+ begin
56
+ data = http_request('GET', action)
57
+ return data['playlist']
58
+ rescue Exception => e
59
+ return []
60
+ end
61
+ end
62
+
63
+ # 搜索单曲(1),歌手(100),专辑(10),歌单(1000),用户(1002)
64
+ def search(s, stype=1, offset=0, total='true', limit=100)
65
+ action = "http://music.163.com/api/search/get/web"
66
+ query = {
67
+ "s" => s,
68
+ "type" => stype,
69
+ "offset" => offset,
70
+ "total" => true,
71
+ "limit" => limit
72
+ }
73
+ http_request('POST', action, query)
74
+ end
75
+
76
+ # 新碟上架 http://music.163.com/#/discover/album/
77
+ def new_albums(offset=0, limit=50)
78
+ action = "http://music.163.com/api/album/new?area=ALL&offset=#{offset}&total=true&limit=#{limit}"
79
+ begin
80
+ data = http_request('GET', action)
81
+ return data['albums']
82
+ rescue Exception => e
83
+ return []
84
+ end
85
+ end
86
+
87
+ # 歌单(网友精选碟) hot||new http://music.163.com/#/discover/playlist/
88
+
89
+ # '全部' => '%E5%85%A8%E9%83%A8'
90
+ def top_playlists(category='%E5%85%A8%E9%83%A8', order='hot', offset=0, limit=50)
91
+ flag = (offset > 0 ? true : false)
92
+ action = "http://music.163.com/api/playlist/list?cat=#{category}&order=#{order}&offset=#{offset}&total=#{flag}&limit=#{limit}"
93
+ begin
94
+ data = http_request('GET', action)
95
+ return data['playlists']
96
+ rescue Exception => e
97
+ return []
98
+ end
99
+ end
100
+
101
+ # 歌单详情
102
+ def playlist_detail(playlist_id)
103
+ action = "http://music.163.com/api/playlist/detail?id=#{playlist_id}"
104
+ begin
105
+ data = http_request('GET', action)
106
+ return data['result']['tracks']
107
+ rescue Exception => e
108
+ return []
109
+ end
110
+ end
111
+
112
+ # 热门歌手 http://music.163.com/#/discover/artist/
113
+ def top_artists(offset=0, limit=100)
114
+ action = "http://music.163.com/api/artist/top?offset=#{offset}&total=false&limit=#{limit}"
115
+ begin
116
+ data = http_request('GET', action)
117
+ return data['artists']
118
+ rescue Exception => e
119
+ return []
120
+ end
121
+ end
122
+
123
+ # 热门单曲 http://music.163.com/#/discover/toplist 50
124
+ def top_songlist(offset=0, limit=100)
125
+ action = "http://music.163.com/discover/toplist"
126
+ begin
127
+ connection = http_request('GET', action)
128
+ songids = connection.scan(/\/song\?id=(\d+)/)
129
+ return [] if songids == []
130
+ return songs_detail(songids.uniq)
131
+ rescue Exception => e
132
+ return []
133
+ end
134
+ end
135
+
136
+ # 歌手单曲
137
+ def artists(artist_id)
138
+ action = "http://music.163.com/api/artist/#{artist_id}"
139
+ begin
140
+ data = http_request('GET', action)
141
+ return data['hotSongs']
142
+ rescue Exception => e
143
+ return []
144
+ end
145
+ end
146
+
147
+ # album id -> song id set
148
+ def album(album_id)
149
+ action = "http://music.163.com/api/album/#{album_id}"
150
+ begin
151
+ data = http_request('GET', action)
152
+ return data['album']['songs']
153
+ rescue Exception => e
154
+ return []
155
+ end
156
+ end
157
+
158
+ # song ids -> song urls (details)
159
+ def songs_detail(ids, offset=0)
160
+ tmpids = ids[offset, 100]
161
+ action = "http://music.163.com/api/song/detail?ids=[#{tmpids.join(',')}]"
162
+ begin
163
+ data = http_request('GET', action)
164
+ return data['songs']
165
+ rescue Exception => e
166
+ return []
167
+ end
168
+ end
169
+
170
+ # song id -> song url (details)
171
+ def song_detail(music_id)
172
+ id = music_id.join(',')
173
+ action = "http://music.163.com/api/song/detail/?id=#{id}&ids=[#{id}]"
174
+ begin
175
+ data = http_request('GET', action)
176
+ return data['songs']
177
+ rescue Exception => e
178
+ return []
179
+ end
180
+ end
181
+
182
+ # 今日最热(0), 本周最热(10),历史最热(20),最新节目(30)
183
+ def djchannels(stype=0, offset=0, limit=30)
184
+ action = "http://music.163.com/discover/djchannel?type=#{stype}&offset=#{offset}&limit=#{limit}"
185
+ begin
186
+ connection = http_request('GET', action)
187
+ channelids = connection.scan(/\/dj\?id=(\d+)/) || []
188
+ return [] if channelids.empty?
189
+ return channel_detail(channelids.uniq)
190
+ rescue Exception => e
191
+ return []
192
+ end
193
+ end
194
+
195
+ # DJchannel (id, channel_name) ids -> song urls (details)
196
+ # channels 整理为 songs 类型
197
+ def channel_detail(channelids, offset=0)
198
+ channels = []
199
+
200
+ # ["xxxxxx"] -> "xxxxxx"
201
+ channelids.each do |c|
202
+ action = "http://music.163.com/api/dj/program/detail?id=#{c.join('')}"
203
+ begin
204
+ data = http_request('GET', action)
205
+ channel = dig_info(data['program']['mainSong'], 'channels')
206
+ channels.push(channel)
207
+ rescue Exception => e
208
+ next
209
+ end
210
+ end
211
+
212
+ channels
213
+ end
214
+
215
+ def dig_info(data, dig_type)
216
+ tmp = []
217
+ case dig_type
218
+ when 'songs'
219
+ data.each do |song|
220
+ song_info = {
221
+ "song_id" => song['id'],
222
+ "artist" => [],
223
+ "song_name" => song['name'],
224
+ "album_name" => song['album']['name'],
225
+ "mp3_url" => song['mp3Url']
226
+ }
227
+
228
+ if song.include? 'artist'
229
+ song_info['artist'] = song['artist'].join('')
230
+ elsif song.include? 'artists'
231
+ song['artists'].each do |artist|
232
+ song_info['artist'].push(artist['name'].strip)
233
+ end
234
+ song_info['artist'].join(',')
235
+ else
236
+ song_info['artist'] = '未知艺术家'
237
+ end
238
+
239
+ song_info['artist'] = song_info['artist'].join(',')
240
+ tmp.push song_info
241
+ end
242
+
243
+ when 'artists'
244
+ data.each do |artist|
245
+ artists_info = {
246
+ "artist_id" => artist['id'],
247
+ "artists_name" => artist['name'],
248
+ "alias" => artist['alias'].join('')
249
+ }
250
+ tmp.push artists_info
251
+ end
252
+
253
+ when 'albums'
254
+ data.each do |album|
255
+ albums_info = {
256
+ "album_id" => album['id'],
257
+ "albums_name" => album['name'],
258
+ "artists_name" => album['artist']['name']
259
+ }
260
+ tmp.push albums_info
261
+ end
262
+
263
+ when 'playlists'
264
+ data.each do |playlist|
265
+ playlists_info = {
266
+ "playlist_id" => playlist['id'],
267
+ "playlists_name" => playlist['name'],
268
+ "creator_name" => playlist['creator']['nickname']
269
+ }
270
+ tmp.push playlists_info
271
+ end
272
+
273
+ when 'channels'
274
+ channel_info = {
275
+ "song_id" => data['id'],
276
+ "song_name" => data['name'],
277
+ "artist" => data['artists'][0]['name'],
278
+ "album_name" => 'DJ节目',
279
+ "mp3_url" => data['mp3Url']
280
+ }
281
+ tmp.push channel_info
282
+ end
283
+
284
+ tmp
285
+ end
286
+ end
@@ -0,0 +1,389 @@
1
+ require 'curses'
2
+ require 'json'
3
+
4
+ SHORTCUT = [
5
+ ['j', 'Down ', '下移'],
6
+ ['k', 'Up ', '上移'],
7
+ ['h', 'Back ', '后退'],
8
+ ['l', 'Forward ', '前进'],
9
+ ['u', 'Prev page ', '上一页'],
10
+ ['d', 'Next page ', '下一页'],
11
+ ['f', 'Search ', '快速搜索'],
12
+ ['[', 'Prev song ', '上一曲'],
13
+ [']', 'Next song ', '下一曲'],
14
+ [' ', 'Play/Pause', '播放/暂停'],
15
+ ['m', 'Menu ', '主菜单'],
16
+ ['p', 'Present ', '当前播放列表'],
17
+ ['a', 'Add ', '添加曲目到打碟'],
18
+ ['z', 'DJ list ', '打碟列表'],
19
+ ['s', 'Star ', '添加到收藏'],
20
+ ['c', 'Collection', '收藏列表'],
21
+ ['r', 'Remove ', '删除当前条目'],
22
+ ['q', 'Quit ', '退出']
23
+ ]
24
+
25
+ class Menu
26
+ def initialize
27
+ @datatype = 'main'
28
+ @title = '网易云音乐'
29
+ @datalist = ['排行榜', '艺术家', '新碟上架', '精选歌单', '我的歌单', 'DJ节目', '打碟', '收藏', '搜索', '帮助']
30
+ @offset = 0
31
+ @index = 0
32
+ @present_songs = []
33
+ @player = Player.new
34
+ @ui = Ui.new
35
+ @netease = NetEase.new
36
+ @screen = @ui.screen
37
+ @step = 10
38
+ @stack = []
39
+ @djstack = []
40
+ @userid = nil
41
+ @username = nil
42
+ @collection = []
43
+ @account = {}
44
+
45
+ @wait = 0.1
46
+ @carousel = ->(left, right, x){x < left ? right : (x > right ? left : x)}
47
+
48
+ read_data
49
+ end
50
+
51
+ def start
52
+ @ui.build_menu(@datatype, @title, @datalist, @offset, @index, @step)
53
+ @stack.push([@datatype, @title, @datalist, @offset, @index, @step])
54
+
55
+ loop do
56
+ datatype = @datatype
57
+ title = @title
58
+ datalist = @datalist
59
+ offset = @offset
60
+ idx = index = @index
61
+ step = @step
62
+ stack = @stack
63
+ djstack = @djstack
64
+ key = @screen.getch
65
+ @screen.refresh
66
+
67
+ case key
68
+
69
+ # 退出
70
+ when 'q'
71
+ break
72
+
73
+ # 上移
74
+ when 'k'
75
+ @index = @carousel[@offset, [datalist.size, offset+step].min - 1, idx - 1]
76
+
77
+ # 下移
78
+ when 'j'
79
+ @index = @carousel[@offset, [datalist.size, offset+step].min - 1, idx + 1]
80
+
81
+ # 向上翻页
82
+ when 'u'
83
+ next if offset == 0
84
+ @offset = @offset - step
85
+ @index = (index - step).divmod(step)[0] * step
86
+
87
+ # 向下翻页
88
+ when 'd'
89
+ next if offset + step >= datalist.size
90
+ @offset = @offset + step
91
+ @index = (index + step).divmod(step)[0] * step
92
+
93
+ # 前进
94
+ when 'l'
95
+ next if @datatype == 'songs' || @datatype == 'djchannels' || @datatype == 'help'
96
+ @ui.build_loading
97
+ dispatch_enter(idx)
98
+ @index = 0
99
+ @offset = 0
100
+
101
+ # 回退
102
+ when 'h'
103
+ next if @stack.size == 1
104
+ up = stack.pop
105
+ @datatype, @title, @datalist, @offset, @index = up[0], up[1], up[2], up[3], up[4]
106
+
107
+ # 搜索
108
+ when 'f'
109
+ search
110
+
111
+ # 下一曲
112
+ when ']'
113
+ @player.next
114
+ sleep @wait
115
+
116
+ # 上一曲
117
+ when '['
118
+ @player.prev
119
+ sleep @wait
120
+
121
+ # 播放、暂停
122
+ when ' '
123
+ if datatype == 'songs'
124
+ @present_songs = ['songs', title, datalist, offset, index]
125
+ elsif datatype == 'djchannels'
126
+ @present_songs = ['djchannels', title, datalist, offset, index]
127
+ end
128
+ @player.play(datatype, datalist, idx)
129
+ sleep @wait
130
+
131
+ # 加载当前播放列表
132
+ when 'p'
133
+ next if @present_songs.empty?
134
+ @stack.push([datatype, title, datalist, offset, index])
135
+ @datatype, @title, @datalist, @offset, @index = @present_songs[0], @present_songs[1], @present_songs[2], @present_songs[3], @present_songs[4]
136
+
137
+ # 添加到打碟歌单
138
+ when 'a'
139
+ if datatype == 'songs' && !datalist.empty?
140
+ @djstack.push(datalist[idx])
141
+ end
142
+
143
+ # 加载打碟歌单
144
+ when 'z'
145
+ @stack.push([datatype, title, datalist, offset, index])
146
+ @datatype = 'songs'
147
+ @title = '网易云音乐 > 打碟'
148
+ @datalist = @djstack
149
+ @offset = 0
150
+ @index = 0
151
+
152
+ # 添加到收藏歌曲
153
+ when 's'
154
+ if (datatype == 'songs' || datatype == 'djchannels') && !datalist.empty?
155
+ @collection.push(datalist[idx]).uniq!
156
+ end
157
+
158
+ # 加载收藏歌曲
159
+ when 'c'
160
+ @stack.push([datatype, title, datalist, offset, index])
161
+ @datatype = 'songs'
162
+ @title = '网易云音乐 > 打碟'
163
+ @datalist = @collection
164
+ @offset = 0
165
+ @index = 0
166
+
167
+ # 从当前列表移除
168
+ when 'r'
169
+ if (datatype != 'main') && !datalist.empty?
170
+ @datalist.delete_at(idx)
171
+ @index = @carousel[@offset, [datalist.size, offset+step].min - 1, idx]
172
+ end
173
+
174
+ # 主菜单
175
+ when 'm'
176
+ if datatype != 'main'
177
+ @stack.push([datatype, title, datalist, offset, index])
178
+ @datatype, @title, @datalist = @stack[0][0], @stack[0][1], @stack[0][2]
179
+ @offset = 0
180
+ @index = 0
181
+ end
182
+
183
+ end
184
+
185
+ write_data
186
+ @ui.build_menu(@datatype, @title, @datalist, @offset, @index, @step)
187
+ end
188
+
189
+ @player.stop
190
+ exit
191
+ end
192
+
193
+ def dispatch_enter(idx)
194
+ netease = @netease
195
+ datatype = @datatype
196
+ title = @title
197
+ datalist = @datalist
198
+ offset = @offset
199
+ index = @index
200
+ @stack.push([datatype, title, datalist, offset, index])
201
+
202
+ case datatype
203
+ when 'main'
204
+ choice_channel idx
205
+
206
+ # 该艺术家的热门歌曲
207
+ when 'artists'
208
+ artist_id = datalist[idx]['artist_id']
209
+ songs = netease.artists(artist_id)
210
+ @datatype = 'songs'
211
+ @datalist = netease.dig_info(songs, 'songs')
212
+ @title += " > #{datalist[idx]['aritsts_name']}"
213
+
214
+ # 该专辑包含的歌曲
215
+ when 'albums'
216
+ album_id = datalist[idx]['album_id']
217
+ songs = netease.album(album_id)
218
+ @datatype = 'songs'
219
+ @datalist = netease.dig_info(songs, 'songs')
220
+ @title += " > #{datalist[idx]['albums_name']}"
221
+
222
+ # 该歌单包含的歌曲
223
+ when 'playlists'
224
+ playlist_id = datalist[idx]['playlist_id']
225
+ songs = netease.playlist_detail(playlist_id)
226
+ @datatype = 'songs'
227
+ @datalist = netease.dig_info(songs, 'songs')
228
+ @title += " > #{datalist[idx]['playlists_name']}"
229
+ end
230
+ end
231
+
232
+ def choice_channel(idx)
233
+ netease = @netease
234
+
235
+ case idx
236
+
237
+ # 排行榜
238
+ when 0
239
+ songs = netease.top_songlist
240
+ @datalist = netease.dig_info(songs, 'songs')
241
+ @title += ' > 排行榜'
242
+ @datatype = 'songs'
243
+
244
+ # 艺术家
245
+ when 1
246
+ artists = netease.top_artists
247
+ @datalist = netease.dig_info(artists, 'artists')
248
+ @title += ' > 艺术家'
249
+ @datatype = 'artists'
250
+
251
+ # 新碟上架
252
+ when 2
253
+ albums = netease.new_albums
254
+ @datalist = netease.dig_info(albums, 'albums')
255
+ @title += ' > 新碟上架'
256
+ @datatype = 'albums'
257
+
258
+ # 精选歌单
259
+ when 3
260
+ playlists = netease.top_playlists
261
+ @datalist = netease.dig_info(playlists, 'playlists')
262
+ @title += ' > 精选歌单'
263
+ @datatype = 'playlists'
264
+
265
+ # 我的歌单
266
+ when 4
267
+ # 未登录
268
+ if !@userid
269
+ if !@account.empty?
270
+ user_info = netease.login(@account[0], @account[1])
271
+ end
272
+
273
+ if @account == {} || user_info['code'] != 200
274
+ data = @ui.build_login
275
+ return if data == -1
276
+ user_info, @account = data[0], data[1]
277
+ end
278
+
279
+ @username = user_info['profile']['nickname']
280
+ @userid = user_info['account']['id']
281
+ end
282
+
283
+ # 读取登录之后的用户歌单
284
+ my_playlist = netease.user_playlists(@userid)
285
+ @datalist = netease.dig_info(my_playlist, 'playlists')
286
+ @datatype = 'playlists'
287
+ @title += " > #{@username} 的歌单"
288
+
289
+ # DJ节目
290
+ when 5
291
+ @datatype = 'djchannels'
292
+ @title += ' > DJ节目'
293
+ @datalist = netease.djchannels
294
+
295
+ # 打碟
296
+ when 6
297
+ @datatype = 'songs'
298
+ @title += ' > 打碟'
299
+ @datalist = @djstack
300
+
301
+ # 收藏
302
+ when 7
303
+ @datatype = 'songs'
304
+ @title += ' > 收藏'
305
+ @datalist = @collection
306
+
307
+ # 搜索
308
+ when 8
309
+ search
310
+
311
+ # 帮助
312
+ when 9
313
+ @datatype = 'help'
314
+ @title += ' > 帮助'
315
+ @datalist = SHORTCUT
316
+ end
317
+
318
+ @offset = 0
319
+ @index = 0
320
+ end
321
+
322
+ def search
323
+ ui = @ui
324
+ x = ui.build_search_menu
325
+
326
+ if (1...5).include? x.to_i
327
+ @stack.push([@datatype, @title, @datalist, @offset, @index])
328
+ @index = 0
329
+ @offset = 0
330
+ end
331
+
332
+ case x
333
+
334
+ when '1'
335
+ @datatype = 'songs'
336
+ @datalist = ui.build_search('songs')
337
+ @title = '歌曲搜索列表'
338
+
339
+ when '2'
340
+ @datatype = 'artists'
341
+ @datalist = ui.build_search('artists')
342
+ @title = '艺术家搜索列表'
343
+
344
+ when '3'
345
+ @datatype = 'albums'
346
+ @datalist = ui.build_search('albums')
347
+ @title = '专辑搜索列表'
348
+
349
+ when '4'
350
+ @datatype = 'playlists'
351
+ @datalist = ui.build_search('playlists')
352
+ @title = '精选歌单搜索列表'
353
+ end
354
+ end
355
+
356
+ private
357
+
358
+ def check_mplug163_dir
359
+ Dir.mkdir File.expand_path("~/.mplug163") unless Dir.exists? File.expand_path("~/.mplug163")
360
+
361
+ end
362
+
363
+ def read_data
364
+ check_mplug163_dir
365
+ user_file = File.expand_path("~/.mplug163/flavor.json")
366
+ if File.exists? user_file
367
+ begin
368
+ data = JSON.parse(File.read(user_file))
369
+ @collection = data['collection']
370
+ @account = data['account']
371
+ rescue Exception => e
372
+ @collection = []
373
+ @account = {}
374
+ end
375
+ end
376
+ end
377
+
378
+ def write_data
379
+ user_file = File.expand_path("~/.mplug163/flavor.json")
380
+ data = {
381
+ :account => @account,
382
+ :collection => @collection
383
+ }
384
+
385
+ File.open(user_file, 'w') do |f|
386
+ f.write(JSON.generate(data))
387
+ end
388
+ end
389
+ end
@@ -0,0 +1,99 @@
1
+ require 'open4'
2
+
3
+ class Player
4
+ def initialize
5
+ @ui = Ui.new
6
+ @datatype = 'songs'
7
+ @mpg123_thread = nil
8
+ @mpg123_pid = nil
9
+ @playing_flag = false
10
+ @pause_flag = false
11
+ @songs = []
12
+ @idx = 0
13
+ @wait = 0.5
14
+ @carousel = ->(left, right, x){x < left ? right : (x > right ? left : x)}
15
+ end
16
+
17
+ def recall
18
+ @playing_flag = true
19
+ @pause_flag = false
20
+
21
+ item = @songs[@idx]
22
+ @ui.build_playinfo(item['song_name'], item['artist'], item['album_name'])
23
+
24
+ @thread = Thread.new do
25
+ @mp3id, stdin, stdout, stderr = Open4::popen4('mpg123', item['mp3_url'])
26
+ Process::waitpid2 @mp3id
27
+
28
+ if @playing_flag
29
+ @idx = @carousel[0, @songs.size-1, @idx+1]
30
+ recall
31
+ end
32
+ end
33
+ end
34
+
35
+ def play(datatype, songs, idx)
36
+ @datatype = datatype
37
+
38
+ if datatype == 'songs' || datatype == 'djchannels'
39
+ if @idx == idx && @songs == songs
40
+ @pause_flag ? resume : pause
41
+ else
42
+ if datatype == 'songs' || datatype == 'djchannels'
43
+ @songs = songs
44
+ @idx = idx
45
+ end
46
+ @playing_flag ? switch : recall
47
+ end
48
+ else
49
+ if @playing_flag
50
+ @pause_flag ? resume : pause
51
+ end
52
+ end
53
+ end
54
+
55
+ def switch
56
+ stop
57
+ sleep @wait
58
+ recall
59
+ end
60
+
61
+ def stop
62
+ if @playing_flag && @thread && @mp3id
63
+ @playing_flag = false
64
+ # kill this process and thread
65
+ Process.kill(:SIGKILL, @mp3id)
66
+ Thread.kill @thread
67
+ end
68
+ end
69
+
70
+ def pause
71
+ @pause_flag = true
72
+ # send SIGSTOP to pipe
73
+ Process.kill(:SIGSTOP, @mp3id)
74
+ item = @songs[@idx]
75
+ @ui.build_playinfo(item['song_name'], item['artist'], item['album_name'], true)
76
+ end
77
+
78
+ def resume
79
+ @pause_flag = false
80
+ # send SIGCONT to pipe
81
+ Process.kill(:SIGCONT, @mp3id)
82
+ item = @songs[@idx]
83
+ @ui.build_playinfo(item['song_name'], item['artist'], item['album_name'])
84
+ end
85
+
86
+ def next
87
+ stop
88
+ sleep @wait
89
+ @idx = @carousel[0, @songs.size-1, @idx+1]
90
+ recall
91
+ end
92
+
93
+ def prev
94
+ stop
95
+ sleep @wait
96
+ @idx = @carousel[0, @songs.size-1, @idx-1]
97
+ recall
98
+ end
99
+ end
@@ -0,0 +1,299 @@
1
+ require 'curses'
2
+
3
+ # Player UI
4
+ #
5
+ # SCREEN_TOP, SCREEN_LEFT
6
+ # |-----|-------------SCREEN_WIDTH-----------------------|
7
+ # | PLAYER_X |
8
+ # | |------------------------------------------------|
9
+ # | |PLAYER_TITLE_Y |
10
+ # | |------------------------------------------------|
11
+ # | |PLAYER_STATUS_Y |
12
+ # | | |
13
+ # | |------------------------------------------------|
14
+ # | |PLAYER_CONTENT_Y |
15
+ # | | |
16
+ # | | |
17
+ # | | |SCREEN_HEIGHT
18
+ # | | |
19
+ # | | |
20
+ # | | |
21
+ # | | |
22
+ # | | |
23
+ # | | |
24
+ # | | |
25
+ # | |------------------------------------------------|
26
+ # | |PLAYER_INFO_Y |
27
+ # |-----|------------------------------------------------|
28
+
29
+ class Ui
30
+ attr_reader :screen
31
+
32
+ SCREEN_HEIGHT = 40
33
+ SCREEN_WIDTH = 80
34
+ SCREEN_TOP = 0
35
+ SCREEN_LEFT = 0
36
+
37
+ PLAYER_X = 10
38
+ PLAYER_TITLE_Y = 4
39
+ PLAYER_STATUS_Y = 5
40
+ PLAYER_CONTENT_Y = 7
41
+ PLAYER_INFO_Y = 19
42
+
43
+ PLAYER_NOTE_X = PLAYER_X - 6
44
+ PLAYER_POINTER_X = PLAYER_X - 3
45
+
46
+ def initialize
47
+ Curses.init_screen
48
+ Curses.start_color
49
+ Curses.cbreak
50
+ Curses.stdscr.keypad(true)
51
+ Curses.init_pair(1, Curses::COLOR_BLUE, Curses::COLOR_BLACK)
52
+ Curses.init_pair(2, Curses::COLOR_CYAN, Curses::COLOR_BLACK)
53
+ Curses.init_pair(3, Curses::COLOR_RED, Curses::COLOR_BLACK)
54
+ Curses.init_pair(4, Curses::COLOR_MAGENTA, Curses::COLOR_BLACK)
55
+
56
+ # height, width, top, left
57
+ @screen = Curses::Window.new(SCREEN_HEIGHT, SCREEN_WIDTH, 0, 0)
58
+
59
+ @netease = NetEase.new
60
+ end
61
+
62
+ def build_playinfo(song_name, artist, album_name, pause=false)
63
+ if pause
64
+ putstr(@screen, PLAYER_STATUS_Y, PLAYER_NOTE_X, 'z Zzz', Curses.color_pair(3))
65
+ else
66
+ putstr(@screen, PLAYER_STATUS_Y, PLAYER_NOTE_X, '♫ ♪ ♫ ', Curses.color_pair(3))
67
+ end
68
+
69
+ song_info = "#{song_name} - #{artist} <#{album_name}>"
70
+ putstr(@screen, PLAYER_STATUS_Y, PLAYER_X, song_info, Curses.color_pair(4))
71
+ @screen.refresh
72
+ end
73
+
74
+ def build_loading
75
+ clear_to_bottom(@screen, PLAYER_CONTENT_Y, SCREEN_HEIGHT)
76
+ putstr(@screen, PLAYER_CONTENT_Y, PLAYER_X, 'loading...', Curses.color_pair(1))
77
+ @screen.refresh
78
+ end
79
+
80
+ def build_menu(datatype, title, datalist, offset, index, step)
81
+ clear_to_bottom(@screen, PLAYER_CONTENT_Y, SCREEN_HEIGHT)
82
+ putstr(@screen, PLAYER_TITLE_Y, PLAYER_X, title, Curses.color_pair(1))
83
+
84
+ if datalist.size == 0
85
+ putstr(@screen, PLAYER_CONTENT_Y, PLAYER_X, '还没有内容 - -||')
86
+ else
87
+ case datatype
88
+ when 'main'
89
+ (offset...[datalist.length, offset+step].min).each do |i|
90
+ if i == index
91
+ line_info = "-> #{i}.#{datalist[i]}"
92
+ putstr(@screen, i-offset+PLAYER_CONTENT_Y, PLAYER_POINTER_X, line_info, Curses.color_pair(2))
93
+ else
94
+ line_info = "#{i}.#{datalist[i]}"
95
+ putstr(@screen, i-offset+PLAYER_CONTENT_Y, PLAYER_X, line_info)
96
+ end
97
+ end
98
+
99
+ putstr(@screen, PLAYER_INFO_Y, PLAYER_X, 'Mplug163 built by Ripple Yui', Curses.color_pair(3))
100
+
101
+ when 'songs'
102
+ (offset...[datalist.length, offset+step].min).each do |i|
103
+ if i == index
104
+ info = "-> #{i}.#{datalist[i]['song_name']} - #{datalist[i]['artist']} <#{datalist[i]['album_name']}>"
105
+ line_info = pretty_format(info, 0, 63)
106
+ putstr(@screen, i-offset+PLAYER_CONTENT_Y, PLAYER_POINTER_X, line_info, Curses.color_pair(2))
107
+ else
108
+ info = "#{i}.#{datalist[i]['song_name']} - #{datalist[i]['artist']} <#{datalist[i]['album_name']}>"
109
+ line_info = pretty_format(info, 0, 60)
110
+ putstr(@screen, i-offset+PLAYER_CONTENT_Y, PLAYER_X, line_info)
111
+ end
112
+ end
113
+
114
+ when 'artists'
115
+ (offset...[datalist.length, offset+step].min).each do |i|
116
+ if i == index
117
+ line_info = "-> #{i}.#{datalist[i]['artists_name']} - #{datalist[i]['artist']} #{datalist[i]['alias']}"
118
+ putstr(@screen, i-offset+PLAYER_CONTENT_Y, PLAYER_POINTER_X, line_info, Curses.color_pair(2))
119
+ else
120
+ line_info = "#{i}.#{datalist[i]['artists_name']} - #{datalist[i]['artist']} #{datalist[i]['alias']}"
121
+ putstr(@screen, i-offset+PLAYER_CONTENT_Y, PLAYER_X, line_info)
122
+ end
123
+ end
124
+
125
+ when 'albums'
126
+ (offset...[datalist.length, offset+step].min).each do |i|
127
+ if i == index
128
+ line_info = "-> #{i}.#{datalist[i]['albums_name']} - #{datalist[i]['artists_name']}"
129
+ putstr(@screen, i-offset+PLAYER_CONTENT_Y, PLAYER_POINTER_X, line_info, Curses.color_pair(2))
130
+ else
131
+ line_info = "#{i}.#{datalist[i]['albums_name']} - #{datalist[i]['artists_name']}"
132
+ putstr(@screen, i-offset+PLAYER_CONTENT_Y, PLAYER_X, line_info)
133
+ end
134
+ end
135
+
136
+ when 'playlists'
137
+ (offset...[datalist.length, offset+step].min).each do |i|
138
+ if i == index
139
+ line_info = "-> #{i}.#{datalist[i]['playlists_name']} - #{datalist[i]['creator_name']}"
140
+ putstr(@screen, i-offset+PLAYER_CONTENT_Y, PLAYER_POINTER_X, line_info, Curses.color_pair(2))
141
+ else
142
+ line_info = "#{i}.#{datalist[i]['playlists_name']} - #{datalist[i]['creator_name']}"
143
+ putstr(@screen, i-offset+PLAYER_CONTENT_Y, PLAYER_X, line_info)
144
+ end
145
+ end
146
+
147
+ when 'djchannels'
148
+ (offset...[datalist.length, offset+step].min).each do |i|
149
+ if i == index
150
+ line_info = "-> #{i}.#{datalist[i][0]['song_name']}"
151
+ putstr(@screen, i-offset+PLAYER_CONTENT_Y, PLAYER_POINTER_X, line_info, Curses.color_pair(2))
152
+ else
153
+ line_info = "#{i}.#{datalist[i][0]['song_name']}"
154
+ putstr(@screen, i-offset+PLAYER_CONTENT_Y, PLAYER_X, line_info)
155
+ end
156
+ end
157
+
158
+ when 'help'
159
+ (offset...[datalist.length, offset+step].min).each do |i|
160
+ line_info = "#{i}.#{datalist[i][0]} #{datalist[i][1]} #{datalist[i][2]}"
161
+ putstr(@screen, i-offset+PLAYER_CONTENT_Y, PLAYER_X, line_info)
162
+ end
163
+ end
164
+ end
165
+
166
+ end
167
+
168
+ def build_search(stype)
169
+ case stype
170
+ when 'songs'
171
+ song_name = get_param('搜索歌曲:')
172
+ begin
173
+ data = @netease.search(song_name, stype=1)
174
+ song_ids = []
175
+ if data['result'].include? 'songs'
176
+ if data['result']['songs'].include? 'mp3Url'
177
+ songs = data['result']['songs']
178
+ else
179
+ (0...data['result']['songs'].size).each do |i|
180
+ song_ids.push data['result']['songs'][i]['id']
181
+ end
182
+ songs = @netease.songs_detail(song_ids)
183
+ end
184
+ return @netease.dig_info(songs, 'songs')
185
+ end
186
+ rescue Exception => e
187
+ return []
188
+ end
189
+
190
+ when 'artists'
191
+ artist_name = get_param('搜索艺术家:')
192
+ begin
193
+ data = @netease.search(artist_name, stype=100)
194
+ if data['result'].include? 'artists'
195
+ artists = data['result']['artists']
196
+ return @netease.dig_info(artists, 'artists')
197
+ end
198
+ rescue Exception => e
199
+ return []
200
+ end
201
+
202
+ when 'albums'
203
+ artist_name = get_param('搜索专辑:')
204
+ begin
205
+ data = @netease.search(artist_name, stype=10)
206
+ if data['result'].include? 'albums'
207
+ albums = data['result']['albums']
208
+ return @netease.dig_info(albums, 'albums')
209
+ end
210
+ rescue Exception => e
211
+ return []
212
+ end
213
+
214
+ when 'playlists'
215
+ artist_name = get_param('搜索网易精选集:')
216
+ begin
217
+ data = @netease.search(artist_name, stype=1000)
218
+ if data['result'].include? 'playlists'
219
+ playlists = data['result']['playlists']
220
+ return @netease.dig_info(playlists, 'playlists')
221
+ end
222
+ rescue Exception => e
223
+ return []
224
+ end
225
+ end
226
+ end
227
+
228
+ def build_search_menu
229
+ clear_to_bottom(@screen, PLAYER_CONTENT_Y, SCREEN_HEIGHT)
230
+ putstr(@screen, PLAYER_CONTENT_Y, PLAYER_X, '选择搜索类型:', Curses.color_pair(1))
231
+ putstr(@screen, PLAYER_CONTENT_Y + 1, PLAYER_X, '[1] 歌曲')
232
+ putstr(@screen, PLAYER_CONTENT_Y + 2, PLAYER_X, '[2] 艺术家')
233
+ putstr(@screen, PLAYER_CONTENT_Y + 3, PLAYER_X, '[3] 专辑')
234
+ putstr(@screen, PLAYER_CONTENT_Y + 4, PLAYER_X, '[4] 网易精选集')
235
+ putstr(@screen, PLAYER_CONTENT_Y + 6, PLAYER_X, '请键入对应数字:', Curses.color_pair(2))
236
+ @screen.refresh
237
+ x = @screen.getch
238
+ end
239
+
240
+ def build_login
241
+ params = get_param('请输入登录信息: (e.g. foobar@163.com foobar)')
242
+ account = params.split(' ')
243
+ return build_login if account.size != 2
244
+
245
+ login_info = @netease.login(account[0], account[1])
246
+ if login_info['code'] != 200
247
+ x = build_login_error
248
+ return x == '1' ? build_login : -1
249
+ else
250
+ return [login_info, account]
251
+ end
252
+ end
253
+
254
+ def build_login_error
255
+ clear_to_bottom(@screen, PLAYER_CONTENT_Y, SCREEN_HEIGHT)
256
+ putstr(@screen, PLAYER_CONTENT_Y + 1, PLAYER_X, 'oh,登录信息好像不对 (O_O)#', Curses.color_pair(1))
257
+ putstr(@screen, PLAYER_CONTENT_Y + 2, PLAYER_X, '[1] 再试一次')
258
+ putstr(@screen, PLAYER_CONTENT_Y + 3, PLAYER_X, '[2] 稍后再试')
259
+ putstr(@screen, PLAYER_CONTENT_Y + 5, PLAYER_X, '请键入对应数字:', Curses.color_pair(2))
260
+ @screen.refresh
261
+ x = @screen.getch
262
+ end
263
+
264
+ def get_param(prompt_str)
265
+ clear_to_bottom(@screen, PLAYER_CONTENT_Y, SCREEN_HEIGHT)
266
+ putstr(@screen, PLAYER_CONTENT_Y, PLAYER_X, prompt_str, Curses.color_pair(1))
267
+ @screen.setpos(PLAYER_CONTENT_Y + 2, PLAYER_X)
268
+ params = @screen.getstr
269
+ if params.strip == nil
270
+ return get_param(prompt_str)
271
+ else
272
+ return params
273
+ end
274
+ end
275
+
276
+ private
277
+
278
+ def putstr(screen, y, x, string, color=Curses.color_pair(0))
279
+ screen.setpos(y, x)
280
+ screen.clrtoeol
281
+ screen.attrset(color)
282
+ screen.addstr(string)
283
+ end
284
+
285
+ def clear_to_bottom(screen, top, bottom)
286
+ (top..bottom).each do |i|
287
+ screen.setpos(i, 0)
288
+ screen.clrtoeol
289
+ end
290
+ end
291
+
292
+ def pretty_format(info, start, length)
293
+ if info.size >= length
294
+ "#{info[start, length]}..."
295
+ else
296
+ info
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,3 @@
1
+ module Mplug163
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mplug163/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mplug163"
8
+ spec.version = Mplug163::VERSION
9
+ spec.authors = ["Ripple Yui"]
10
+ spec.email = ["rippleyui@gmail.com"]
11
+ spec.summary = %q{A local music player based on Netease music.}
12
+ spec.description = %q{}
13
+ spec.homepage = "https://github.com/rippleyui/mplug163"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.7"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mplug163
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ripple Yui
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ description: ''
42
+ email:
43
+ - rippleyui@gmail.com
44
+ executables:
45
+ - mplug163
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".gitignore"
50
+ - Gemfile
51
+ - LICENSE.txt
52
+ - README.md
53
+ - Rakefile
54
+ - bin/mplug163
55
+ - lib/mplug163.rb
56
+ - lib/mplug163/api.rb
57
+ - lib/mplug163/menu.rb
58
+ - lib/mplug163/player.rb
59
+ - lib/mplug163/ui.rb
60
+ - lib/mplug163/version.rb
61
+ - mplug163.gemspec
62
+ homepage: https://github.com/rippleyui/mplug163
63
+ licenses:
64
+ - MIT
65
+ metadata: {}
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubyforge_project:
82
+ rubygems_version: 2.4.1
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: A local music player based on Netease music.
86
+ test_files: []