mdisc 0.1.1

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.
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: []