mplug163 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []