mdisc 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e7f5547a564f2f00c77ae5e07ab4f343553ad39d
4
+ data.tar.gz: ae58b382a6e761fbf450c7f9777f74aae550d04b
5
+ SHA512:
6
+ metadata.gz: 05887319f06a3465625493599c567434b43a663085902e42fb1b6bdd2feba05e29722ebc9f72577c8ebc488dc76d66fda73633efd2889eb3b4d656c2a874b3a3
7
+ data.tar.gz: b58337cc0d810e8e08bf7ce9cd8b8abc4e199346e818ba2357a0147d6908e0aae38fdbd8fe8fe85b64534024baed77a103fda2ff3c62c67b88b5d241516e6779
data/.gitignore ADDED
@@ -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 mdisc.gemspec
4
+ gemspec
5
+
6
+ gem 'curses'
7
+ gem 'open4'
8
+ gem 'unirest'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Rick Yu
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.
data/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # Mdisc
2
+
3
+ Mdisc, built with Ruby 2.1, is a command line music player that wirelessly plugs in Netease music(http://music.163.com).
4
+
5
+ ## Installation
6
+
7
+ My MacBook Pro is running Yosemite(OS X 10.10), Mdisc runs smoothly with Ruby 2.1.
8
+
9
+ You're lucky if you use Mac OS X. Just follow:
10
+
11
+ ```
12
+ $ brew install mpg123
13
+ $ gem install mdisc
14
+ ```
15
+
16
+ After finishing the installation, open your terminal and input `mdisc`. Music's coming!
17
+
18
+ Sorry, I do not test Mdisc in Linux. If you try it in Linux and catch some problem, please issue me. Thanks!
19
+
20
+ ## Shortcuts
21
+
22
+ | Key | Explanation | 中文释义 |
23
+ | :---|:---------------------|:---------------------|
24
+ | j | Down | 向下 |
25
+ | k | Up | 向上 |
26
+ | h | Back | 后退 |
27
+ | l | Forward | 前进或播放高亮选中歌曲 |
28
+ | [ | Prev Song | 上一首歌曲 |
29
+ | ] | Next Song | 下一首歌曲 |
30
+ | ' ' | Play / Pause | 播放 / 暂停当前进行的歌曲|
31
+ | u | Prev Page | 上一页列表 |
32
+ | d | Next Page | 下一页列表 |
33
+ | f | Search | 搜索 |
34
+ | m | Main Menu | 主菜单 |
35
+ | p | Present Playlist | 当前播放列表 |
36
+ | s | Star | 收藏歌曲、精选歌单、专辑 |
37
+ | t | Playlist | 收藏精选歌单 |
38
+ | c | Collection | 收藏歌曲列表 |
39
+ | a | Album | 收藏专辑 |
40
+ | z | DJ Channels | 收藏 DJ 节目 |
41
+ | r | Remove Present Entry | 删除当前曲目 |
42
+ | q | Quit | 退出 |
43
+
44
+ ## Attention
45
+
46
+ Mdisc will make a new directory `~/.mdisc` and touch a file to store user's data for the first time.
47
+
48
+ ## Big Thanks
49
+
50
+ [NetEase-MusicBox](https://github.com/bluetomlee/NetEase-MusicBox)
51
+
52
+ [网易云音乐API分析](https://github.com/yanunon/NeteaseCloudMusic/wiki/网易云音乐API分析)
53
+
54
+ Their great projects inspire me. Thanks!
55
+
56
+ ## License
57
+
58
+ MIT License.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/bin/mdisc ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('../../lib/mdisc', __FILE__)
4
+
5
+ main = Menu.new
6
+ main.start
data/lib/mdisc.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "mdisc/version"
2
+ require File.expand_path('../mdisc/menu', __FILE__)
3
+ require File.expand_path('../mdisc/api', __FILE__)
4
+ require File.expand_path('../mdisc/player', __FILE__)
5
+ require File.expand_path('../mdisc/ui', __FILE__)
data/lib/mdisc/api.rb ADDED
@@ -0,0 +1,247 @@
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)
27
+ connection =
28
+ if method == 'GET'
29
+ url = (query.nil? ? action : "#{action}?#{query}")
30
+ Unirest.get(url, headers: @header)
31
+ elsif method == 'POST'
32
+ Unirest.post(action, headers: @header, parameters: query)
33
+ end
34
+
35
+ connection.body
36
+ end
37
+
38
+ # Log in
39
+ def login(username, password)
40
+ action = "http://music.163.com/api/login/"
41
+ query = {
42
+ "username" => username,
43
+ "password" => Digest::MD5.hexdigest(password),
44
+ "rememberLogin" => "true"
45
+ }
46
+ begin
47
+ return http_request('POST', action, query)
48
+ rescue => e
49
+ return {"code" => 501}
50
+ end
51
+ end
52
+
53
+ # User's playlists
54
+ def user_playlists(uid, offset = 0, limit = 100)
55
+ action = "http://music.163.com/api/user/playlist/?offset=#{offset}&limit=#{limit}&uid=#{uid}"
56
+ data = http_request('GET', action)
57
+ data['playlist']
58
+ end
59
+
60
+ # Search song(1),artist(100),album(10),playlist(1000),user(1002)
61
+ def search(s, stype = 1, offset = 0, limit = 100)
62
+ action = "http://music.163.com/api/search/get/web"
63
+ query = {
64
+ "s" => s,
65
+ "type" => stype,
66
+ "offset" => offset,
67
+ "total" => true,
68
+ "limit" => limit
69
+ }
70
+ http_request('POST', action, query)
71
+ end
72
+
73
+ # New albums
74
+ # http://music.163.com/#/discover/album/
75
+ def new_albums(offset=0, limit=50)
76
+ action = "http://music.163.com/api/album/new?area=ALL&offset=#{offset}&total=true&limit=#{limit}"
77
+ data = http_request('GET', action)
78
+ data['albums']
79
+ end
80
+
81
+ # Top playlists
82
+ # hot||new http://music.163.com/#/discover/playlist/
83
+
84
+ # '全部' => '%E5%85%A8%E9%83%A8'
85
+ def top_playlists(category = '%E5%85%A8%E9%83%A8', order = 'hot', offset = 0, limit = 100)
86
+ flag = (offset > 0 ? true : false)
87
+ action = "http://music.163.com/api/playlist/list?cat=#{category}&order=#{order}&offset=#{offset}&total=#{flag}&limit=#{limit}"
88
+ data = http_request('GET', action)
89
+ return data['playlists']
90
+ end
91
+
92
+ # Playlist's details
93
+ def playlist_detail(playlist_id)
94
+ action = "http://music.163.com/api/playlist/detail?id=#{playlist_id}"
95
+ data = http_request('GET', action)
96
+ return data['result']['tracks']
97
+ end
98
+
99
+ # Top artists
100
+ # http://music.163.com/#/discover/artist/
101
+ def top_artists(offset = 0, limit = 100)
102
+ action = "http://music.163.com/api/artist/top?offset=#{offset}&total=false&limit=#{limit}"
103
+ data = http_request('GET', action)
104
+ return data['artists']
105
+ end
106
+
107
+ # Top songlist
108
+ # http://music.163.com/#/discover/toplist 100
109
+ def top_songlist
110
+ action = "http://music.163.com/discover/toplist"
111
+ connection = http_request('GET', action)
112
+ songids = connection.scan(/\/song\?id=(\d+)/)
113
+ return [] if songids == []
114
+ return songs_detail(songids.uniq)
115
+ end
116
+
117
+ # Songs to which a artist belongs.
118
+ def artists(artist_id)
119
+ action = "http://music.163.com/api/artist/#{artist_id}"
120
+ data = http_request('GET', action)
121
+ return data['hotSongs']
122
+ end
123
+
124
+ # album id -> song id set
125
+ def album(album_id)
126
+ action = "http://music.163.com/api/album/#{album_id}"
127
+ data = http_request('GET', action)
128
+ return data['album']['songs']
129
+ end
130
+
131
+ # song ids -> song urls (details)
132
+ def songs_detail(ids, offset=0)
133
+ tmpids = ids[offset, 100]
134
+ action = "http://music.163.com/api/song/detail?ids=[#{tmpids.join(',')}]"
135
+ data = http_request('GET', action)
136
+ return data['songs']
137
+ end
138
+
139
+ # song id -> song url (details)
140
+ def song_detail(music_id)
141
+ id = music_id.join(',')
142
+ action = "http://music.163.com/api/song/detail/?id=#{id}&ids=[#{id}]"
143
+ data = http_request('GET', action)
144
+ return data['songs']
145
+ end
146
+
147
+ # DJ channels: hot today(0), week(10), history(20), new(30)
148
+ def djchannels(stype = 0, offset = 0, limit = 50)
149
+ action = "http://music.163.com/discover/djchannel?type=#{stype}&offset=#{offset}&limit=#{limit}"
150
+ connection = http_request('GET', action)
151
+ channelids = connection.scan(/\/dj\?id=(\d+)/) || []
152
+ return [] if channelids.empty?
153
+ return channel_detail(channelids.uniq)
154
+ end
155
+
156
+ # DJchannel (id, channel_name) ids -> song urls (details)
157
+ # channels -> songs
158
+ def channel_detail(channelids)
159
+ channels = []
160
+
161
+ # ["xxxxxx"] -> "xxxxxx"
162
+ channelids.each do |c|
163
+ action = "http://music.163.com/api/dj/program/detail?id=#{c.join('')}"
164
+ begin
165
+ data = http_request('GET', action)
166
+ channel = dig_info(data['program']['mainSong'], 'channels')
167
+ channels.push(channel)
168
+ rescue => e
169
+ next
170
+ end
171
+ end
172
+
173
+ channels
174
+ end
175
+
176
+ def dig_info(data, dig_type)
177
+ tmp = []
178
+ case dig_type
179
+ when 'songs'
180
+ data.each do |song|
181
+ song_info = {
182
+ "song_id" => song['id'],
183
+ "artist" => [],
184
+ "song_name" => song['name'],
185
+ "album_name" => song['album']['name'],
186
+ "mp3_url" => song['mp3Url']
187
+ }
188
+
189
+ if song.include? 'artist'
190
+ song_info['artist'] = song['artist'].join('')
191
+ elsif song.include? 'artists'
192
+ song['artists'].each do |artist|
193
+ song_info['artist'].push(artist['name'].strip)
194
+ end
195
+ song_info['artist'].join(',')
196
+ else
197
+ song_info['artist'] = '未知艺术家'
198
+ end
199
+
200
+ song_info['artist'] = song_info['artist'].join(',')
201
+ tmp.push song_info
202
+ end
203
+
204
+ when 'artists'
205
+ data.each do |artist|
206
+ artists_info = {
207
+ "artist_id" => artist['id'],
208
+ "artists_name" => artist['name'],
209
+ "alias" => artist['alias'].join('')
210
+ }
211
+ tmp.push artists_info
212
+ end
213
+
214
+ when 'albums'
215
+ data.each do |album|
216
+ albums_info = {
217
+ "album_id" => album['id'],
218
+ "albums_name" => album['name'],
219
+ "artists_name" => album['artist']['name']
220
+ }
221
+ tmp.push albums_info
222
+ end
223
+
224
+ when 'playlists'
225
+ data.each do |playlist|
226
+ playlists_info = {
227
+ "playlist_id" => playlist['id'],
228
+ "playlists_name" => playlist['name'],
229
+ "creator_name" => playlist['creator']['nickname']
230
+ }
231
+ tmp.push playlists_info
232
+ end
233
+
234
+ when 'channels'
235
+ channel_info = {
236
+ "song_id" => data['id'],
237
+ "song_name" => data['name'],
238
+ "artist" => data['artists'][0]['name'],
239
+ "album_name" => 'DJ节目',
240
+ "mp3_url" => data['mp3Url']
241
+ }
242
+ tmp.push channel_info
243
+ end
244
+
245
+ tmp
246
+ end
247
+ end
data/lib/mdisc/menu.rb ADDED
@@ -0,0 +1,435 @@
1
+ require 'curses'
2
+ require 'json'
3
+
4
+ SHORTCUT = [
5
+ ['j', 'Down ', '向下'],
6
+ ['k', 'Up ', '向上'],
7
+ ['h', 'Back ', '后退'],
8
+ ['l', 'Forward ', '前进或播放高亮选中歌曲'],
9
+ ['[', 'Prev song ', '上一首歌曲'],
10
+ [']', 'Next song ', '下一首歌曲'],
11
+ [' ', 'Play / Pause', '播放 / 暂停当前进行的歌曲'],
12
+ ['u', 'Prev page ', '上一页列表'],
13
+ ['d', 'Next page ', '下一页列表'],
14
+ ['f', 'Search ', '搜索'],
15
+ ['m', 'Menu ', '主菜单'],
16
+ ['p', 'Present ', '当前播放列表'],
17
+ ['s', 'Star ', '收藏歌曲、精选歌单、专辑'],
18
+ ['t', 'Playlist ', '收藏精选歌单'],
19
+ ['c', 'Collection ', '收藏歌曲列表'],
20
+ ['a', 'Album ', '收藏专辑'],
21
+ ['z', 'DJ Channels ', '收藏 DJ 节目'],
22
+ ['r', 'Remove ', '删除当前曲目'],
23
+ ['q', 'Quit ', '退出']
24
+ ]
25
+
26
+ class Menu
27
+ attr_accessor :player, :ui, :netease, :screen
28
+
29
+ def initialize
30
+ self.player = Player.new
31
+ self.ui = Ui.new
32
+ self.netease = NetEase.new
33
+ self.screen = ui.screen
34
+ @datatype = 'main'
35
+ @title = '网易云音乐'
36
+ @datalist = %w(排行榜 精选歌单 艺术家 新碟上架 我的歌单 DJ节目 本地收藏 搜索 帮助)
37
+ @offset = 0
38
+ @index = 0
39
+ @present_songs = []
40
+ @step = 20
41
+ @stack = []
42
+ @userid = nil
43
+ @username = nil
44
+ @collection = []
45
+ @playlists = []
46
+ @account = {}
47
+
48
+ @wait = 0.1
49
+ @carousel = ->(left, right, x){x < left ? right : (x > right ? left : x)}
50
+
51
+ read_data
52
+ end
53
+
54
+ def start
55
+ ui.build_menu(@datatype, @title, @datalist, @offset, @index, @step)
56
+ @stack.push([@datatype, @title, @datalist, @offset, @index, @step])
57
+
58
+ loop do
59
+ datatype = @datatype
60
+ title = @title
61
+ datalist = @datalist
62
+ offset = @offset
63
+ idx = index = @index
64
+ step = @step
65
+ stack = @stack
66
+ key = screen.getch
67
+ screen.refresh
68
+
69
+ case key
70
+
71
+ # Quit
72
+ when 'q'
73
+ break
74
+
75
+ # Up
76
+ when 'k'
77
+ @index = @carousel[@offset, [datalist.size, offset+step].min - 1, idx - 1]
78
+
79
+ # Down
80
+ when 'j'
81
+ @index = @carousel[@offset, [datalist.size, offset+step].min - 1, idx + 1]
82
+
83
+ # Previous page
84
+ when 'u'
85
+ next if offset == 0
86
+ @offset = @offset - step
87
+ @index = (index - step).divmod(step)[0] * step
88
+
89
+ # Next page
90
+ when 'd'
91
+ next if offset + step >= datalist.size
92
+ @offset = @offset + step
93
+ @index = (index + step).divmod(step)[0] * step
94
+
95
+ # Forward
96
+ when 'l'
97
+ next if @datatype == 'help'
98
+ if @datatype == 'songs' || @datatype == 'djchannels'
99
+ player.play(@datatype, datalist, @index, true)
100
+ @present_songs = [datatype, title, datalist, offset, index]
101
+ else
102
+ ui.build_loading
103
+ dispatch_enter(idx)
104
+ @index = 0
105
+ @offset = 0
106
+ end
107
+
108
+ # Back
109
+ when 'h'
110
+ next if @stack.size == 1
111
+ up = stack.pop
112
+ @datatype, @title, @datalist, @offset, @index = up[0], up[1], up[2], up[3], up[4]
113
+
114
+ # Search
115
+ when 'f'
116
+ search
117
+
118
+ # Next song
119
+ when ']'
120
+ player.next
121
+ sleep @wait
122
+
123
+ # Previous song
124
+ when '['
125
+ player.prev
126
+ sleep @wait
127
+
128
+ # Play or pause a song.
129
+ when ' '
130
+ player.play(datatype, datalist, idx)
131
+ sleep @wait
132
+
133
+ # Load present playlist.
134
+ when 'p'
135
+ next if @present_songs.empty?
136
+ @stack.push([datatype, title, datalist, offset, index])
137
+ @datatype, @title, @datalist, @offset, @index = @present_songs[0], @present_songs[1], @present_songs[2], @present_songs[3], @present_songs[4]
138
+
139
+ # Star a song, a playlist or an album.
140
+ when 's'
141
+ next if datalist.empty?
142
+ if datatype == 'songs'
143
+ @collection.push(datalist[idx]).uniq!
144
+ elsif datatype == 'playlists'
145
+ @playlists.push(datalist[idx]).uniq!
146
+ elsif datatype == 'albums'
147
+ @albums.push(datalist[idx]).uniq!
148
+ elsif datatype == 'djchannels'
149
+ @djs.push(datalist[idx]).uniq!
150
+ end
151
+
152
+ # Load favorite playlists.
153
+ when 't'
154
+ @stack.push([datatype, title, datalist, offset, index])
155
+ @datatype = 'playlists'
156
+ @title = '网易云音乐 > 收藏精选歌单'
157
+ @datalist = @playlists
158
+ @offset = 0
159
+ @index = 0
160
+
161
+ # Load favorite songs.
162
+ when 'c'
163
+ @stack.push([datatype, title, datalist, offset, index])
164
+ @datatype = 'songs'
165
+ @title = '网易云音乐 > 收藏歌曲列表'
166
+ @datalist = @collection
167
+ @offset = 0
168
+ @index = 0
169
+
170
+ # Load favorite albums
171
+ when 'a'
172
+ @stack.push([datatype, title, datalist, offset, index])
173
+ @datatype = 'albums'
174
+ @title = '网易云音乐 > 收藏专辑'
175
+ @datalist = @albums
176
+ @offset = 0
177
+ @index = 0
178
+
179
+ # Load favorite dj channels
180
+ when 'z'
181
+ @stack.push([datatype, title, datalist, offset, index])
182
+ @datatype = 'djchannels'
183
+ @title = '网易云音乐 > 收藏 DJ 节目'
184
+ @datalist = @djs
185
+ @offset = 0
186
+ @index = 0
187
+
188
+ # Remove an entry from the present list.
189
+ when 'r'
190
+ if (datatype != 'main') && !datalist.empty?
191
+ @datalist.delete_at(idx)
192
+ @index = @carousel[@offset, [datalist.size, offset+step].min - 1, idx]
193
+ end
194
+
195
+ # Main menu.
196
+ when 'm'
197
+ if datatype != 'main'
198
+ @stack.push([datatype, title, datalist, offset, index])
199
+ @datatype, @title, @datalist = @stack[0][0], @stack[0][1], @stack[0][2]
200
+ @offset = 0
201
+ @index = 0
202
+ end
203
+
204
+ end
205
+
206
+ write_data
207
+ ui.build_menu(@datatype, @title, @datalist, @offset, @index, @step)
208
+ end
209
+
210
+ player.stop
211
+ exit
212
+ end
213
+
214
+ def dispatch_enter(idx)
215
+ # netease = @netease
216
+ datatype = @datatype
217
+ title = @title
218
+ datalist = @datalist
219
+ offset = @offset
220
+ index = @index
221
+ @stack.push([datatype, title, datalist, offset, index])
222
+
223
+ case datatype
224
+ when 'main'
225
+ choice_channel idx
226
+
227
+ # Hot songs to which a artist belongs.
228
+ when 'artists'
229
+ artist_id = datalist[idx]['artist_id']
230
+ songs = netease.artists(artist_id)
231
+ @datatype = 'songs'
232
+ @datalist = netease.dig_info(songs, 'songs')
233
+ @title += " > #{datalist[idx]['aritsts_name']}"
234
+
235
+ # All songs to which an album belongs.
236
+ when 'albums'
237
+ album_id = datalist[idx]['album_id']
238
+ songs = netease.album(album_id)
239
+ @datatype = 'songs'
240
+ @datalist = netease.dig_info(songs, 'songs')
241
+ @title += " > #{datalist[idx]['albums_name']}"
242
+
243
+ # All songs to which a playlist belongs.
244
+ when 'playlists'
245
+ playlist_id = datalist[idx]['playlist_id']
246
+ songs = netease.playlist_detail(playlist_id)
247
+ @datatype = 'songs'
248
+ @datalist = netease.dig_info(songs, 'songs')
249
+ @title += " > #{datalist[idx]['playlists_name']}"
250
+ end
251
+ end
252
+
253
+ def choice_channel(idx)
254
+ # netease = @netease
255
+
256
+ case idx
257
+
258
+ # Top
259
+ when 0
260
+ songs = netease.top_songlist
261
+ @datalist = netease.dig_info(songs, 'songs')
262
+ @title += ' > 排行榜'
263
+ @datatype = 'songs'
264
+
265
+ # Playlists
266
+ when 1
267
+ playlists = netease.top_playlists
268
+ @datalist = netease.dig_info(playlists, 'playlists')
269
+ @title += ' > 精选歌单'
270
+ @datatype = 'playlists'
271
+
272
+ # Artist
273
+ when 2
274
+ artists = netease.top_artists
275
+ @datalist = netease.dig_info(artists, 'artists')
276
+ @title += ' > 艺术家'
277
+ @datatype = 'artists'
278
+
279
+ # New album
280
+ when 3
281
+ albums = netease.new_albums
282
+ @datalist = netease.dig_info(albums, 'albums')
283
+ @title += ' > 新碟上架'
284
+ @datatype = 'albums'
285
+
286
+ # My playlist
287
+ when 4
288
+ # Require user's account before fetching his playlists.
289
+ if !@userid
290
+ user_info = netease.login(@account[0], @account[1]) unless @account.empty?
291
+
292
+ if @account == {} || user_info['code'] != 200
293
+ data = ui.build_login
294
+ return if data == -1
295
+ user_info, @account = data[0], data[1]
296
+ end
297
+
298
+ @username = user_info['profile']['nickname']
299
+ @userid = user_info['account']['id']
300
+ end
301
+
302
+ # Fetch this user's all playlists while he logs in successfully.
303
+ my_playlist = netease.user_playlists(@userid)
304
+ @datalist = netease.dig_info(my_playlist, 'playlists')
305
+ @datatype = 'playlists'
306
+ @title += " > #{@username} 的歌单"
307
+
308
+ # DJ channels
309
+ when 5
310
+ @datatype = 'djchannels'
311
+ @title += ' > DJ 节目'
312
+ @datalist = netease.djchannels
313
+
314
+ # Favorite things.
315
+ when 6
316
+ favorite
317
+
318
+ # Search
319
+ when 7
320
+ search
321
+
322
+ # Help
323
+ when 8
324
+ @datatype = 'help'
325
+ @title += ' > 帮助'
326
+ @datalist = SHORTCUT
327
+ end
328
+
329
+ @offset = 0
330
+ @index = 0
331
+ end
332
+
333
+ def favorite
334
+ # ui = @ui
335
+ x = ui.build_favorite_menu
336
+
337
+ if (1..4).include? x.to_i
338
+ @stack.push([@datatype, @title, @datalist, @offset, @index])
339
+ @index = 0
340
+ @offset = 0
341
+ end
342
+
343
+ case x
344
+
345
+ when '1'
346
+ @datatype = 'songs'
347
+ @datalist = @collection
348
+ @title += ' > 收藏歌曲'
349
+
350
+ when '2'
351
+ @datatype = 'playlists'
352
+ @datalist = @playlists
353
+ @title += ' > 收藏歌单'
354
+
355
+ when '3'
356
+ @datatype = 'albums'
357
+ @datalist = @albums
358
+ @title += ' > 收藏专辑'
359
+
360
+ when '4'
361
+ @datatype = 'djchannels'
362
+ @datalist = @djs
363
+ @title += ' > 收藏 DJ 节目'
364
+
365
+ end
366
+ end
367
+
368
+ def search
369
+ # ui = @ui
370
+ x = ui.build_search_menu
371
+
372
+ if (1..4).include? x.to_i
373
+ @stack.push([@datatype, @title, @datalist, @offset, @index])
374
+ @index = 0
375
+ @offset = 0
376
+ end
377
+
378
+ case x
379
+
380
+ when '1'
381
+ @datatype = 'songs'
382
+ @datalist = ui.build_search('songs')
383
+ @title = '歌曲搜索列表'
384
+
385
+ when '2'
386
+ @datatype = 'artists'
387
+ @datalist = ui.build_search('artists')
388
+ @title = '艺术家搜索列表'
389
+
390
+ when '3'
391
+ @datatype = 'albums'
392
+ @datalist = ui.build_search('albums')
393
+ @title = '专辑搜索列表'
394
+
395
+ when '4'
396
+ @datatype = 'playlists'
397
+ @datalist = ui.build_search('playlists')
398
+ @title = '精选歌单搜索列表'
399
+ end
400
+ end
401
+
402
+ private
403
+
404
+ def check_mdisc_dir
405
+ Dir.mkdir File.expand_path("~/.mdisc") unless Dir.exist? File.expand_path("~/.mdisc")
406
+ end
407
+
408
+ def read_data
409
+ check_mdisc_dir
410
+ user_file = File.expand_path("~/.mdisc/flavor.json")
411
+ return unless File.exist? user_file
412
+
413
+ data = JSON.parse(File.read(user_file))
414
+ @account = data['account'] || {}
415
+ @collection = data['collection'] || []
416
+ @playlists = data['playlists'] || []
417
+ @albums = data['albums'] || []
418
+ @djs = data['djs'] || []
419
+ end
420
+
421
+ def write_data
422
+ user_file = File.expand_path("~/.mdisc/flavor.json")
423
+ data = {
424
+ :account => @account,
425
+ :collection => @collection,
426
+ :playlists => @playlists,
427
+ :albums => @albums,
428
+ :djs => @djs
429
+ }
430
+
431
+ File.open(user_file, 'w') do |f|
432
+ f.write(JSON.generate(data))
433
+ end
434
+ end
435
+ end
@@ -0,0 +1,99 @@
1
+ require 'open4'
2
+
3
+ class Player
4
+ attr_accessor :ui
5
+
6
+ def initialize
7
+ self.ui = Ui.new
8
+ @datatype = 'songs'
9
+ @mpg123_thread = nil
10
+ @mpg123_pid = nil
11
+ @playing_flag = false
12
+ @pause_flag = false
13
+ @songs = []
14
+ @idx = 0
15
+ @wait = 0.5
16
+ @carousel = ->(left, right, x){x < left ? right : (x > right ? left : x)}
17
+ end
18
+
19
+ def recall
20
+ @playing_flag = true
21
+ @pause_flag = false
22
+
23
+ item = @songs[@idx]
24
+ ui.build_playinfo(item['song_name'], item['artist'])
25
+
26
+ @thread = Thread.new do
27
+ @mp3id, stdin, stdout, stderr = Open4::popen4('mpg123', item['mp3_url'])
28
+ Process::waitpid2 @mp3id
29
+
30
+ if @playing_flag
31
+ @idx = @carousel[0, @songs.size - 1, @idx + 1]
32
+ recall
33
+ end
34
+ end
35
+ end
36
+
37
+ def play(datatype, songs, idx, switch_flag = false)
38
+ @datatype = datatype
39
+
40
+ if !switch_flag
41
+ @pause_flag ? resume : pause if @playing_flag
42
+ elsif switch_flag
43
+ @songs = songs
44
+ @idx = idx
45
+ @playing_flag ? switch : recall
46
+ end
47
+ end
48
+
49
+ def switch
50
+ stop
51
+ sleep @wait
52
+ recall
53
+ end
54
+
55
+ def stop
56
+ return unless @playing_flag
57
+ return unless @thread
58
+ return unless @mp3id
59
+
60
+ @playing_flag = false
61
+ # kill this process and thread
62
+ Process.kill(:SIGKILL, @mp3id)
63
+ Thread.kill @thread
64
+ end
65
+
66
+ def pause
67
+ @pause_flag = true
68
+ # send SIGSTOP to pipe
69
+ Process.kill(:SIGSTOP, @mp3id)
70
+
71
+ item = @songs[@idx]
72
+ ui.build_playinfo(item['song_name'], item['artist'], true)
73
+ end
74
+
75
+ def resume
76
+ @pause_flag = false
77
+ # send SIGCONT to pipe
78
+ Process.kill(:SIGCONT, @mp3id)
79
+
80
+ item = @songs[@idx]
81
+ ui.build_playinfo(item['song_name'], item['artist'])
82
+ end
83
+
84
+ def next
85
+ stop
86
+ sleep @wait
87
+
88
+ @idx = @carousel[0, @songs.size - 1, @idx + 1]
89
+ recall
90
+ end
91
+
92
+ def prev
93
+ stop
94
+ sleep @wait
95
+
96
+ @idx = @carousel[0, @songs.size - 1, @idx - 1]
97
+ recall
98
+ end
99
+ end
data/lib/mdisc/ui.rb ADDED
@@ -0,0 +1,307 @@
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_accessor :netease, :screen
31
+
32
+ SCREEN_HEIGHT = 40
33
+ SCREEN_WIDTH = 80
34
+ SCREEN_TOP = 0
35
+ SCREEN_LEFT = 0
36
+
37
+ PLAYER_X = 5
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 - 2
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
+ self.screen = Curses::Window.new(SCREEN_HEIGHT, SCREEN_WIDTH, 0, 0)
58
+
59
+ self.netease = NetEase.new
60
+ end
61
+
62
+ def build_playinfo(song_name, artist, pause = false)
63
+ if pause
64
+ putstr(screen, PLAYER_STATUS_Y, PLAYER_NOTE_X, '■', Curses.color_pair(3))
65
+ else
66
+ putstr(screen, PLAYER_STATUS_Y, PLAYER_NOTE_X, '▶', Curses.color_pair(3))
67
+ end
68
+
69
+ sn = pretty_format(song_name, 0, 32)
70
+ at = pretty_format(artist, 0, 28)
71
+ info = "#{sn} - #{at}"
72
+ putstr(screen, PLAYER_STATUS_Y, PLAYER_X, info, Curses.color_pair(4))
73
+ screen.refresh
74
+ end
75
+
76
+ def build_loading
77
+ clear_to_bottom(screen, PLAYER_CONTENT_Y,SCREEN_HEIGHT)
78
+ putstr(screen, PLAYER_CONTENT_Y, PLAYER_X, 'loading...', Curses.color_pair(1))
79
+ screen.refresh
80
+ end
81
+
82
+ def build_menu(datatype, title, datalist, offset, index, step)
83
+ title = pretty_format(title, 0, 52)
84
+
85
+ clear_to_bottom(screen, PLAYER_CONTENT_Y,SCREEN_HEIGHT)
86
+ putstr(screen, PLAYER_TITLE_Y, PLAYER_X, title, Curses.color_pair(1))
87
+
88
+ if datalist.size == 0
89
+ putstr(screen, PLAYER_CONTENT_Y, PLAYER_X, '没有内容 Orz')
90
+ else
91
+ case datatype
92
+ when 'main'
93
+ (offset...[datalist.length, offset + step].min).each do |i|
94
+ if i == index
95
+ info = "♩ #{i}. #{datalist[i]}"
96
+ putstr(screen, i-offset+PLAYER_CONTENT_Y, PLAYER_POINTER_X, info, Curses.color_pair(2))
97
+ else
98
+ info = "#{i}. #{datalist[i]}"
99
+ putstr(screen, i-offset+PLAYER_CONTENT_Y, PLAYER_X, info)
100
+ end
101
+ end
102
+
103
+ putstr(screen, PLAYER_INFO_Y, PLAYER_X, 'Crafted with ❤ by cosmtrek', Curses.color_pair(3))
104
+
105
+ when 'songs'
106
+ (offset...[datalist.length, offset + step].min).each do |i|
107
+ sn = pretty_format(datalist[i]['song_name'], 0, 32)
108
+ at = pretty_format(datalist[i]['artist'], 0, 28)
109
+
110
+ if i == index
111
+ info = "♩ #{i}. #{sn} - #{at}"
112
+ putstr(screen, i-offset+PLAYER_CONTENT_Y, PLAYER_POINTER_X, info, Curses.color_pair(2))
113
+ else
114
+ info = "#{i}. #{sn} - #{at}"
115
+ putstr(screen, i-offset+PLAYER_CONTENT_Y, PLAYER_X, info)
116
+ end
117
+ end
118
+
119
+ when 'artists'
120
+ (offset...[datalist.length, offset + step].min).each do |i|
121
+ an = pretty_format(datalist[i]['artists_name'], 0, 32)
122
+ if i == index
123
+ info = "♩ #{i}. #{an}"
124
+ putstr(screen, i-offset+PLAYER_CONTENT_Y, PLAYER_POINTER_X, info, Curses.color_pair(2))
125
+ else
126
+ info = "#{i}. #{an}"
127
+ putstr(screen, i-offset+PLAYER_CONTENT_Y, PLAYER_X, info)
128
+ end
129
+ end
130
+
131
+ when 'albums'
132
+ (offset...[datalist.length, offset + step].min).each do |i|
133
+ al = pretty_format(datalist[i]['albums_name'], 0, 32)
134
+ an = pretty_format(datalist[i]['artists_name'], 0, 28)
135
+ if i == index
136
+ info = "♩ #{i}. #{al} - #{an}"
137
+ putstr(screen, i-offset+PLAYER_CONTENT_Y, PLAYER_POINTER_X, info, Curses.color_pair(2))
138
+ else
139
+ info = "#{i}. #{al} - #{an}"
140
+ putstr(screen, i-offset+PLAYER_CONTENT_Y, PLAYER_X, info)
141
+ end
142
+ end
143
+
144
+ when 'playlists'
145
+ (offset...[datalist.length, offset + step].min).each do |i|
146
+ pn = pretty_format(datalist[i]['playlists_name'], 0, 32);
147
+ cn = pretty_format(datalist[i]['creator_name'], 0, 28);
148
+ if i == index
149
+ info = "♩ #{i}. #{pn} - #{cn}"
150
+ putstr(screen, i-offset+PLAYER_CONTENT_Y, PLAYER_POINTER_X, info, Curses.color_pair(2))
151
+ else
152
+ info = "#{i}. #{pn} - #{cn}"
153
+ putstr(screen, i-offset+PLAYER_CONTENT_Y, PLAYER_X, info)
154
+ end
155
+ end
156
+
157
+ when 'djchannels'
158
+ (offset...[datalist.length, offset + step].min).each do |i|
159
+ sn = pretty_format(datalist[i][0]['song_name'], 0, 32)
160
+ if i == index
161
+ info = "♩ #{i}. #{sn}"
162
+ putstr(screen, i-offset+PLAYER_CONTENT_Y, PLAYER_POINTER_X, info, Curses.color_pair(2))
163
+ else
164
+ info = "#{i}. #{sn}"
165
+ putstr(screen, i-offset+PLAYER_CONTENT_Y, PLAYER_X, info)
166
+ end
167
+ end
168
+
169
+ when 'help'
170
+ (offset...[datalist.length, offset + step].min).each do |i|
171
+ info = "#{i}. #{datalist[i][0]} #{datalist[i][1]} #{datalist[i][2]}"
172
+ putstr(screen, i-offset+PLAYER_CONTENT_Y, PLAYER_X, info)
173
+ end
174
+ end
175
+ end
176
+
177
+ end
178
+
179
+ def build_search(stype)
180
+ case stype
181
+ when 'songs'
182
+ song_name = get_param('搜索歌曲:')
183
+ data = netease.search(song_name, stype = 1)
184
+ song_ids = []
185
+ if data['result'].include? 'songs'
186
+ if data['result']['songs'].include? 'mp3Url'
187
+ songs = data['result']['songs']
188
+ else
189
+ (0...data['result']['songs'].size).each do |i|
190
+ song_ids.push data['result']['songs'][i]['id']
191
+ end
192
+ songs = netease.songs_detail(song_ids)
193
+ end
194
+ return netease.dig_info(songs, 'songs')
195
+ end
196
+
197
+ when 'artists'
198
+ artist_name = get_param('搜索艺术家:')
199
+ data = netease.search(artist_name, stype = 100)
200
+ if data['result'].include? 'artists'
201
+ artists = data['result']['artists']
202
+ return netease.dig_info(artists, 'artists')
203
+ end
204
+
205
+ when 'albums'
206
+ artist_name = get_param('搜索专辑:')
207
+ data = netease.search(artist_name, stype = 10)
208
+ if data['result'].include? 'albums'
209
+ albums = data['result']['albums']
210
+ return netease.dig_info(albums, 'albums')
211
+ end
212
+
213
+ when 'playlists'
214
+ artist_name = get_param('搜索精选歌单:')
215
+ data = netease.search(artist_name, stype = 1000)
216
+ if data['result'].include? 'playlists'
217
+ playlists = data['result']['playlists']
218
+ return netease.dig_info(playlists, 'playlists')
219
+ end
220
+
221
+ end
222
+ end
223
+
224
+ def build_favorite_menu
225
+ clear_to_bottom(screen, PLAYER_CONTENT_Y,SCREEN_HEIGHT)
226
+ putstr(screen, PLAYER_CONTENT_Y, PLAYER_X, '选择收藏条目类型:', Curses.color_pair(1))
227
+ putstr(screen, PLAYER_CONTENT_Y + 1, PLAYER_X, '1 - 歌曲')
228
+ putstr(screen, PLAYER_CONTENT_Y + 2, PLAYER_X, '2 - 精选歌单')
229
+ putstr(screen, PLAYER_CONTENT_Y + 3, PLAYER_X, '3 - 专辑')
230
+ putstr(screen, PLAYER_CONTENT_Y + 4, PLAYER_X, '4 - DJ 节目')
231
+ putstr(screen, PLAYER_CONTENT_Y + 6, PLAYER_X, '请键入对应数字:', Curses.color_pair(2))
232
+ screen.refresh
233
+ screen.getch
234
+ end
235
+
236
+ def build_search_menu
237
+ clear_to_bottom(screen, PLAYER_CONTENT_Y,SCREEN_HEIGHT)
238
+ putstr(screen, PLAYER_CONTENT_Y, PLAYER_X, '选择搜索类型:', Curses.color_pair(1))
239
+ putstr(screen, PLAYER_CONTENT_Y + 1, PLAYER_X, '1 - 歌曲')
240
+ putstr(screen, PLAYER_CONTENT_Y + 2, PLAYER_X, '2 - 艺术家')
241
+ putstr(screen, PLAYER_CONTENT_Y + 3, PLAYER_X, '3 - 专辑')
242
+ putstr(screen, PLAYER_CONTENT_Y + 4, PLAYER_X, '4 - 精选歌单')
243
+ putstr(screen, PLAYER_CONTENT_Y + 6, PLAYER_X, '请键入对应数字:', Curses.color_pair(2))
244
+ screen.refresh
245
+ screen.getch
246
+ end
247
+
248
+ def build_login
249
+ params = get_param('请输入登录信息:(e.g. foobar@163.com foobar)')
250
+ account = params.split(' ')
251
+ return build_login if account.size != 2
252
+
253
+ login_info = netease.login(account[0], account[1])
254
+ if login_info['code'] != 200
255
+ x = build_login_error
256
+ return x == '1' ? build_login : -1
257
+ else
258
+ return [login_info, account]
259
+ end
260
+ end
261
+
262
+ def build_login_error
263
+ clear_to_bottom(screen, PLAYER_CONTENT_Y,SCREEN_HEIGHT)
264
+ putstr(screen, PLAYER_CONTENT_Y + 1, PLAYER_X, 'oh,出现错误 Orz', Curses.color_pair(2))
265
+ putstr(screen, PLAYER_CONTENT_Y + 2, PLAYER_X, '1 - 再试一次')
266
+ putstr(screen, PLAYER_CONTENT_Y + 3, PLAYER_X, '2 - 稍后再试')
267
+ putstr(screen, PLAYER_CONTENT_Y + 5, PLAYER_X, '请键入对应数字:', Curses.color_pair(2))
268
+ screen.refresh
269
+ screen.getch
270
+ end
271
+
272
+ def get_param(prompt_str)
273
+ clear_to_bottom(screen, PLAYER_CONTENT_Y,SCREEN_HEIGHT)
274
+ putstr(screen, PLAYER_CONTENT_Y, PLAYER_X, prompt_str, Curses.color_pair(1))
275
+ screen.setpos(PLAYER_CONTENT_Y + 2, PLAYER_X)
276
+ params = screen.getstr
277
+ if params.strip.nil?
278
+ return get_param(prompt_str)
279
+ else
280
+ return params
281
+ end
282
+ end
283
+
284
+ private
285
+
286
+ def putstr(screen, y, x, string, color = Curses.color_pair(0))
287
+ screen.setpos(y, x)
288
+ screen.clrtoeol
289
+ screen.attrset(color)
290
+ screen.addstr(string)
291
+ end
292
+
293
+ def clear_to_bottom(screen, top, bottom)
294
+ (top..bottom).each do |i|
295
+ screen.setpos(i, 0)
296
+ screen.clrtoeol
297
+ end
298
+ end
299
+
300
+ def pretty_format(info, start, length)
301
+ if info.size >= length
302
+ "#{info[start, length]}..."
303
+ else
304
+ info
305
+ end
306
+ end
307
+ end
@@ -0,0 +1,3 @@
1
+ module Mdisc
2
+ VERSION = "0.1.1"
3
+ end
data/mdisc.gemspec ADDED
@@ -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 'mdisc/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mdisc"
8
+ spec.version = Mdisc::VERSION
9
+ spec.authors = ["Rick Yu"]
10
+ spec.email = ["cosmtrek@gmail.com"]
11
+ spec.summary = %q{A local music player based on Netease music.}
12
+ spec.description = %q{}
13
+ spec.homepage = "https://github.com/cosmtrek/mdisc"
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: mdisc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Rick Yu
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-18 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
+ - cosmtrek@gmail.com
44
+ executables:
45
+ - mdisc
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".gitignore"
50
+ - Gemfile
51
+ - LICENSE.txt
52
+ - README.md
53
+ - Rakefile
54
+ - bin/mdisc
55
+ - lib/mdisc.rb
56
+ - lib/mdisc/api.rb
57
+ - lib/mdisc/menu.rb
58
+ - lib/mdisc/player.rb
59
+ - lib/mdisc/ui.rb
60
+ - lib/mdisc/version.rb
61
+ - mdisc.gemspec
62
+ homepage: https://github.com/cosmtrek/mdisc
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.2.2
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: A local music player based on Netease music.
86
+ test_files: []