xiami_radio 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +0 -0
- data/bin/xiami_radio +38 -0
- data/lib/xiami_radio/client.rb +69 -0
- data/lib/xiami_radio/downloader.rb +55 -0
- data/lib/xiami_radio/notice.rb +19 -0
- data/lib/xiami_radio/player.rb +72 -0
- data/lib/xiami_radio/radio.rb +41 -0
- data/lib/xiami_radio/track.rb +89 -0
- data/lib/xiami_radio/user.rb +80 -0
- data/lib/xiami_radio/version.rb +5 -0
- data/lib/xiami_radio/view/player.rb +90 -0
- data/lib/xiami_radio.rb +20 -0
- metadata +128 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f93c045ffdd8ba47df5d6396621207607e4ddb6b
|
4
|
+
data.tar.gz: bdd9094e9627de4774fe49d3459bb3b510f60c5a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8d51e8774338f469d4396d33aabfb93b3f2ed37bb5adcc860e41affae13245785f1c9ab9f46b8d3c778fbe7db3e10bb78b0d851447a888de1a2cd0a976137957
|
7
|
+
data.tar.gz: d35701ac4c45494d0e2541b8b307b0df193edd247a15713a6c417c243392a8c1fcefd181b07a4af8cba5ce17549d4ad2c4c006a3a116cc40f4673f9e65e93f31
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2017 IvanChou
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
File without changes
|
data/bin/xiami_radio
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#coding=utf-8
|
3
|
+
|
4
|
+
require 'xiami_radio'
|
5
|
+
require 'io/console'
|
6
|
+
|
7
|
+
user = XiamiRadio::User.new
|
8
|
+
|
9
|
+
email = unless user.login?
|
10
|
+
puts 'Plz keep empty if you don\'t want to login'
|
11
|
+
print 'Email: '
|
12
|
+
gets.chomp
|
13
|
+
end.to_s
|
14
|
+
pwd = unless user.login? || email.empty?
|
15
|
+
print 'Password: '
|
16
|
+
STDIN.noecho(&:gets).chomp
|
17
|
+
end.to_s
|
18
|
+
user.login_by_email email, pwd unless email.empty? || pwd.empty?
|
19
|
+
|
20
|
+
puts 'Which radio do you want to listen?'
|
21
|
+
puts '1/Enter - 虾米猜 2 - 个人电台 3 - 其他'
|
22
|
+
|
23
|
+
radio_option = case gets.chomp[0]
|
24
|
+
when nil
|
25
|
+
{type: 8, oid: 0}
|
26
|
+
when '1'
|
27
|
+
{type: 8, oid: 0}
|
28
|
+
when '2'
|
29
|
+
user.login? ? {type: 4, oid: user.user_id} : {type: 8, oid: 0}
|
30
|
+
else
|
31
|
+
print 'type: '
|
32
|
+
type = gets.chomp[0]
|
33
|
+
print 'oid: '
|
34
|
+
oid = gets.chomp
|
35
|
+
{type: (type || 8).to_i, oid: oid.to_i}
|
36
|
+
end
|
37
|
+
|
38
|
+
XiamiRadio::Player.play user.radio radio_option
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
require 'nori'
|
4
|
+
require 'http-cookie'
|
5
|
+
|
6
|
+
module XiamiRadio
|
7
|
+
# There is a client as you saw
|
8
|
+
class Client
|
9
|
+
HOST = 'http://www.xiami.com'.freeze
|
10
|
+
LOGIN_HOST = 'https://login.xiami.com'.freeze
|
11
|
+
HEADERS = {
|
12
|
+
'Accept' => '*/*',
|
13
|
+
'Accept-Encoding' => '*',
|
14
|
+
'Accept-Language' => 'en-US,en;q=0.8,zh;q=0.6,zh-TW;q=0.4',
|
15
|
+
'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
attr_accessor :user, :headers, :http
|
19
|
+
|
20
|
+
def initialize(headers: {}, user: nil, host: HOST)
|
21
|
+
@user = user || User.new
|
22
|
+
@headers = HEADERS.merge headers
|
23
|
+
@uri = URI.parse host
|
24
|
+
|
25
|
+
# @http = Net::HTTP.new(@uri.host, @uri.port, '127.0.0.1', '8888')
|
26
|
+
@http = Net::HTTP.new @uri.host, @uri.port
|
27
|
+
@http.use_ssl = @uri.scheme == 'https'
|
28
|
+
end
|
29
|
+
|
30
|
+
def uri(**args)
|
31
|
+
@uri.class.build scheme: @uri.scheme, host: @uri.host, **args
|
32
|
+
end
|
33
|
+
|
34
|
+
def get(uri, format: :json, headers: {})
|
35
|
+
request uri, format, headers do |_headers|
|
36
|
+
@http.start { |http| http.request(Net::HTTP::Get.new uri, _headers) }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def post(uri, form_data, format: :json, headers: {})
|
41
|
+
request uri, format, headers do |_headers|
|
42
|
+
req = Net::HTTP::Post.new uri, _headers
|
43
|
+
req.set_form_data form_data
|
44
|
+
@http.start { |http| http.request req }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def request(uri, format, headers, &block)
|
51
|
+
_headers = @headers.merge headers
|
52
|
+
_headers.merge! 'Cookie' => HTTP::Cookie.cookie_value(user.cookie_jar.cookies uri)
|
53
|
+
_headers.merge! 'X-Requested-With' => 'XMLHttpRequest' if %i(json xml).include? format
|
54
|
+
|
55
|
+
res = block.call _headers
|
56
|
+
|
57
|
+
res.get_fields('set-cookie')&.each { |value| user.cookie_jar.parse value, uri }
|
58
|
+
case format
|
59
|
+
when :json then JSON.parse(res.body, symbolize_names: true)
|
60
|
+
when :xml then nori.parse res.body
|
61
|
+
else res
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def nori
|
66
|
+
@nori ||= Nori.new(:convert_tags_to => -> (tag) { tag.snakecase.to_sym })
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module XiamiRadio
|
4
|
+
class Downloader
|
5
|
+
class << self
|
6
|
+
def circulator
|
7
|
+
@circulator ||= Queue.new
|
8
|
+
%w(甲 乙).map(&@circulator.method(:push)) if @circulator.empty?
|
9
|
+
@circulator
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :track, :progress, :total, :file
|
14
|
+
|
15
|
+
def initialize(track)
|
16
|
+
@track = track
|
17
|
+
@uri = URI @track.location
|
18
|
+
end
|
19
|
+
|
20
|
+
def filename
|
21
|
+
File.join XiamiRadio::TMP_DIR, self.class.circulator.pop(true)
|
22
|
+
end
|
23
|
+
|
24
|
+
def progress
|
25
|
+
(@progress.to_f / @total.to_f).round(2) unless @progress.nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
def start
|
29
|
+
@thread = Thread.start do
|
30
|
+
Net::HTTP.get_response @uri do |res|
|
31
|
+
if res.code == '302'
|
32
|
+
@uri = URI res.header['Location']
|
33
|
+
start
|
34
|
+
else
|
35
|
+
@progress, @total = 0, res.header['Content-Length'].to_i
|
36
|
+
@file = File.open(filename, 'w')
|
37
|
+
res.read_body do |chunk|
|
38
|
+
@file << chunk
|
39
|
+
@progress += chunk.size
|
40
|
+
@file.close unless @progress < @total
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
sleep 0.1 until @progress.to_i > 0
|
46
|
+
end
|
47
|
+
|
48
|
+
def stop
|
49
|
+
@thread&.exit
|
50
|
+
@thread = nil
|
51
|
+
File.delete(@file)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module XiamiRadio
|
2
|
+
class Notice
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def push(msg, expired = 3)
|
6
|
+
queue << { content: msg, expired_at: Time.now.to_i + expired }
|
7
|
+
end
|
8
|
+
|
9
|
+
def shift
|
10
|
+
queue.shift until queue.empty? || queue.first[:expired_at] > Time.now.to_i
|
11
|
+
queue.first&.fetch(:content, nil)
|
12
|
+
end
|
13
|
+
|
14
|
+
def queue
|
15
|
+
@queue ||= []
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'audite'
|
2
|
+
|
3
|
+
module XiamiRadio
|
4
|
+
# There is a player as you saw
|
5
|
+
class Player
|
6
|
+
|
7
|
+
attr_reader :track, :next_track, :radio, :user
|
8
|
+
|
9
|
+
def initialize(radio:, _playlist: nil)
|
10
|
+
@radio, @user = radio, radio.client.user
|
11
|
+
@player = Audite.new
|
12
|
+
@view = View::Player.new self
|
13
|
+
|
14
|
+
@player.events.on :position_change, &method(:position_change)
|
15
|
+
@player.events.on :complete, &method(:complete)
|
16
|
+
@view.listen_on
|
17
|
+
end
|
18
|
+
|
19
|
+
def play
|
20
|
+
@track = @radio.next_track
|
21
|
+
@player.queue @track.file_path
|
22
|
+
@player.set_current_song
|
23
|
+
@player.start_stream
|
24
|
+
@player.thread.join
|
25
|
+
end
|
26
|
+
|
27
|
+
def next
|
28
|
+
if @next_track.nil?
|
29
|
+
@next_track = @radio.next_track
|
30
|
+
@player.queue @next_track.file_path
|
31
|
+
end
|
32
|
+
@track, @next_track = @next_track, nil
|
33
|
+
@player.request_next_song
|
34
|
+
end
|
35
|
+
|
36
|
+
def rewind
|
37
|
+
@player.rewind
|
38
|
+
end
|
39
|
+
|
40
|
+
def forward
|
41
|
+
@player.forward
|
42
|
+
end
|
43
|
+
|
44
|
+
def toggle
|
45
|
+
@player.toggle
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def position_change(position)
|
51
|
+
@view.refresh position
|
52
|
+
|
53
|
+
if @next_track.nil? && position / @track.duration > 0.7
|
54
|
+
Thread.start do
|
55
|
+
@track.record
|
56
|
+
@next_track = @radio.next_track
|
57
|
+
@player.queue @next_track.file_path
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def complete
|
63
|
+
@track, @next_track = @next_track, nil
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.play(radio)
|
67
|
+
player = new radio: radio
|
68
|
+
player.play
|
69
|
+
player
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module XiamiRadio
|
2
|
+
# There is a radio as you saw
|
3
|
+
class Radio
|
4
|
+
TRACKS_PATH = '/radio/xml/type/%{type}/id/%{oid}'
|
5
|
+
RADIO_PATH = '/radio/play/type/%{type}/oid/%{oid}'
|
6
|
+
|
7
|
+
XIAMI_CAI = {type: 8, oid: 0}.freeze
|
8
|
+
|
9
|
+
attr_reader :type, :oid, :client, :nori, :play_queue
|
10
|
+
|
11
|
+
def initialize(type:, oid:, user: nil)
|
12
|
+
@type = type
|
13
|
+
@oid = oid
|
14
|
+
@client = user&.client || Client.new
|
15
|
+
@nori = Nori.new(:convert_tags_to => -> (tag) { tag.snakecase.to_sym })
|
16
|
+
@play_queue = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_new_playlist
|
20
|
+
tracks = client.get(uri, format: :xml, headers: headers_referer).dig(:play_list, :track_list, :track)
|
21
|
+
tracks.map { |track| Track.new track, radio: self }
|
22
|
+
end
|
23
|
+
|
24
|
+
def next_track
|
25
|
+
@play_queue += get_new_playlist if @play_queue.size < 2
|
26
|
+
@play_queue.shift
|
27
|
+
end
|
28
|
+
|
29
|
+
def headers_referer
|
30
|
+
referer = client.uri(path: RADIO_PATH % {type: type, oid: oid}).to_s
|
31
|
+
{'Referer': referer}
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def uri
|
37
|
+
client.uri path: TRACKS_PATH % {type: type, oid: oid},
|
38
|
+
query: URI.encode_www_form(v: Time.now.to_i)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module XiamiRadio
|
2
|
+
class Track
|
3
|
+
attr_reader :info, :title, :song_id, :album_name, :artist, :radio, :client
|
4
|
+
|
5
|
+
def initialize(track, radio:)
|
6
|
+
@info = track
|
7
|
+
@title, @song_id, @album_name, @artist = track.values_at(:title, :song_id, :album_name, :artist)
|
8
|
+
@radio = radio
|
9
|
+
@client = radio.client
|
10
|
+
end
|
11
|
+
|
12
|
+
def location(hd: false)
|
13
|
+
hd ? decode_location(hd_location) : decode_location(@info[:location])
|
14
|
+
end
|
15
|
+
|
16
|
+
def duration
|
17
|
+
@info[:length].to_f > 1 ? @info[:length].to_f : (@info[:length].to_f * 1_000_000)
|
18
|
+
end
|
19
|
+
|
20
|
+
def duration=(duration)
|
21
|
+
@info[:length] = duration
|
22
|
+
end
|
23
|
+
|
24
|
+
def grade?
|
25
|
+
@info[:grade].to_i == 1
|
26
|
+
end
|
27
|
+
|
28
|
+
def reason
|
29
|
+
@info[:reason] ||= {content: '来自电台推送'}
|
30
|
+
OpenStruct.new @info[:reason]
|
31
|
+
end
|
32
|
+
|
33
|
+
def downloader
|
34
|
+
@downloader ||= Downloader.new self
|
35
|
+
end
|
36
|
+
|
37
|
+
def file_path
|
38
|
+
@info[:file_path] ||= begin
|
39
|
+
downloader.start if downloader.file.nil?
|
40
|
+
downloader.file.path
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def record
|
45
|
+
uri = client.uri path: '/count/playrecord',
|
46
|
+
query: URI.encode_www_form(sid: song_id, type: 1, ishq: 1)
|
47
|
+
client.get(uri, headers: radio.headers_referer, format: :js)
|
48
|
+
end
|
49
|
+
|
50
|
+
def fav
|
51
|
+
uri = client.uri path: '/song/fav',
|
52
|
+
query: URI.encode_www_form(ids: song_id, _xiamitoken: client.user.xiami_token)
|
53
|
+
res = client.get(uri, headers: radio.headers_referer, format: :js)
|
54
|
+
|
55
|
+
return '操作失败 (╯‵□′)╯︵┻━┻' unless res.code == '200'
|
56
|
+
grade = /player_collected\('(\d)','(\d+)'\)/.match(res.body)[1]
|
57
|
+
grade == '1' ? '已添加到音乐库' : '已从音乐库中移除'
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def decode_location(location)
|
63
|
+
key, tmp_url = location[0].to_i, location[1..location.length]
|
64
|
+
fr, ll, lu, true_url = tmp_url.length.divmod(key), [], [], ''
|
65
|
+
|
66
|
+
key.times do |i|
|
67
|
+
ll << (fr[1] > 0 ? fr[0] + 1 : fr[0])
|
68
|
+
lu << tmp_url[0,ll[i]]
|
69
|
+
tmp_url = tmp_url[ll[i]..tmp_url.length]
|
70
|
+
fr[1] -= 1
|
71
|
+
end
|
72
|
+
|
73
|
+
ll[0].times do |i|
|
74
|
+
lu.each do |piece|
|
75
|
+
piece[i] && true_url << piece[i]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
URI.decode(true_url).gsub('^', '0')
|
80
|
+
end
|
81
|
+
|
82
|
+
def hd_location
|
83
|
+
@info[:hd_location] ||= begin
|
84
|
+
uri = client.uri path: "/song/gethqsong/sid/#{song_id}"
|
85
|
+
client.get(uri, headers: radio.headers_referer)[:location]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'http-cookie'
|
2
|
+
|
3
|
+
module XiamiRadio
|
4
|
+
# There is a user as you saw
|
5
|
+
class User
|
6
|
+
attr_reader :nick_name, :level, :user_id, :sign, :is_vip
|
7
|
+
attr_accessor :cookie_jar
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@cookie_jar = HTTP::CookieJar.new
|
11
|
+
@cookie_jar.load cookie_file if File.exist? cookie_file
|
12
|
+
get_user_info
|
13
|
+
end
|
14
|
+
|
15
|
+
def radio(**args)
|
16
|
+
XiamiRadio::Radio.new user: self, **args
|
17
|
+
end
|
18
|
+
|
19
|
+
def login?
|
20
|
+
!user_id.to_s.empty?
|
21
|
+
end
|
22
|
+
|
23
|
+
def login_by_email(email, password)
|
24
|
+
page_uri = login_client.uri(path: '/member/login')
|
25
|
+
form_uri = login_client.uri(path: '/passport/login')
|
26
|
+
login_client.get page_uri, format: :html
|
27
|
+
form_data = {
|
28
|
+
_xiamitoken: xiami_token,
|
29
|
+
done: 'http%3A%2F%2Fwww.xiami.com%2F%2F',
|
30
|
+
verifycode: '',
|
31
|
+
account: email,
|
32
|
+
pw: password,
|
33
|
+
submit: '登录'
|
34
|
+
}
|
35
|
+
login_client.post form_uri, form_data, headers: {
|
36
|
+
'Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8',
|
37
|
+
'Referer' => page_uri.to_s
|
38
|
+
}
|
39
|
+
@cookie_jar.save cookie_file
|
40
|
+
get_user_info
|
41
|
+
end
|
42
|
+
|
43
|
+
def login_by_member_auth(member_auth)
|
44
|
+
uri = client.uri path: '/index/home'
|
45
|
+
@cookie_jar.parse "member_auth=#{member_auth}; path=/; domain=.xiami.com", uri
|
46
|
+
client.get uri, format: :head
|
47
|
+
@cookie_jar.save cookie_file
|
48
|
+
get_user_info
|
49
|
+
end
|
50
|
+
|
51
|
+
def xiami_token
|
52
|
+
cookie_jar.cookies.select { |c| c.name == '_xiamitoken' }.first&.value
|
53
|
+
end
|
54
|
+
|
55
|
+
def client
|
56
|
+
@client ||= Client.new user: self
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def login_client
|
62
|
+
@l_client ||= Client.new user: self, host: Client::LOGIN_HOST
|
63
|
+
end
|
64
|
+
|
65
|
+
def get_user_info
|
66
|
+
update client.get(client.uri path: '/index/home').dig(:data, :userInfo)
|
67
|
+
Notice.push "欢迎归来 #{nick_name},当前已连续签到 #{sign[:persist_num]} 天", 10 if login?
|
68
|
+
end
|
69
|
+
|
70
|
+
def update(attrs)
|
71
|
+
return false unless attrs.is_a? Hash
|
72
|
+
@nick_name, @level, @user_id, @sign, @is_vip = attrs.values_at :nick_name, :level, :user_id, :sign, :isVip
|
73
|
+
true
|
74
|
+
end
|
75
|
+
|
76
|
+
def cookie_file
|
77
|
+
File.join XiamiRadio::TMP_DIR, '己'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'curses'
|
2
|
+
|
3
|
+
module XiamiRadio::View
|
4
|
+
class Player
|
5
|
+
attr_reader :player
|
6
|
+
|
7
|
+
def initialize(player)
|
8
|
+
@player = player
|
9
|
+
Curses.init_screen
|
10
|
+
Curses.noecho
|
11
|
+
Curses.stdscr.keypad(true)
|
12
|
+
Curses.start_color
|
13
|
+
Curses.use_default_colors
|
14
|
+
Curses.init_pair(Curses::COLOR_CYAN,Curses::COLOR_CYAN,Curses::COLOR_BLACK)
|
15
|
+
Curses.init_pair(Curses::COLOR_RED,Curses::COLOR_RED,Curses::COLOR_BLACK)
|
16
|
+
Curses.clear
|
17
|
+
end
|
18
|
+
|
19
|
+
def refresh(position)
|
20
|
+
Curses.clear
|
21
|
+
render_title_line player.track
|
22
|
+
render_progress_line (position / player.track.duration), player.track.downloader.progress
|
23
|
+
render_info_line player.track, position
|
24
|
+
render_msg_line
|
25
|
+
Curses.refresh
|
26
|
+
end
|
27
|
+
|
28
|
+
def listen_on
|
29
|
+
Thread.start do
|
30
|
+
while (key = Curses.getch)
|
31
|
+
case key
|
32
|
+
when Curses::KEY_LEFT
|
33
|
+
player.rewind
|
34
|
+
when Curses::KEY_RIGHT
|
35
|
+
player.forward
|
36
|
+
when Curses::KEY_DOWN
|
37
|
+
player.next
|
38
|
+
when 'l'
|
39
|
+
XiamiRadio::Notice.push player.track.fav
|
40
|
+
when ' '
|
41
|
+
player.toggle
|
42
|
+
else #
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def render_title_line(track)
|
51
|
+
Curses.setpos(0, 0)
|
52
|
+
Curses.addstr('Now is playing ')
|
53
|
+
Curses.attron(Curses.color_pair(Curses::COLOR_RED)|Curses::A_NORMAL){
|
54
|
+
Curses.addstr(" #{track.title} - #{track.artist} ")
|
55
|
+
}
|
56
|
+
if @player.user.login?
|
57
|
+
track.grade? ? Curses.addstr(' 已收藏 ') : Curses.addstr(' 按"L"加入收藏 ')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def render_progress_line(play_rate, dawnload_rate)
|
62
|
+
p = (play_rate * Curses.cols).round
|
63
|
+
d = (dawnload_rate * Curses.cols).round - p
|
64
|
+
Curses.setpos(1, 0)
|
65
|
+
Curses.addstr('_' * p + '#' * d + ' ' * (Curses.cols - p - d))
|
66
|
+
end
|
67
|
+
|
68
|
+
def render_info_line(track, position)
|
69
|
+
Curses.setpos(2, 0)
|
70
|
+
Curses.addstr("#{track.reason.content}")
|
71
|
+
Curses.addstr(": #{track.reason.title} ") unless track.reason.title.nil?
|
72
|
+
Curses.addstr("- #{track.reason.artist} ") unless track.reason.artist.nil?
|
73
|
+
Curses.addstr(" 播放进度: ")
|
74
|
+
Curses.attron(Curses.color_pair(Curses::COLOR_CYAN)|Curses::A_NORMAL){
|
75
|
+
Curses.addstr(" #{sec_2_min position} / #{sec_2_min track.duration} ")
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
def render_msg_line(msg = XiamiRadio::Notice.shift)
|
80
|
+
return if msg.nil?
|
81
|
+
Curses.setpos(3, 0)
|
82
|
+
Curses.addstr(msg)
|
83
|
+
end
|
84
|
+
|
85
|
+
def sec_2_min(sec)
|
86
|
+
Time.at(sec).utc.strftime('%M:%S')
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
data/lib/xiami_radio.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'xiami_radio/client'
|
2
|
+
require 'xiami_radio/downloader'
|
3
|
+
require 'xiami_radio/notice'
|
4
|
+
require 'xiami_radio/player'
|
5
|
+
require 'xiami_radio/radio'
|
6
|
+
require 'xiami_radio/track'
|
7
|
+
require 'xiami_radio/user'
|
8
|
+
require 'xiami_radio/view/player'
|
9
|
+
|
10
|
+
require 'tmpdir'
|
11
|
+
|
12
|
+
module XiamiRadio
|
13
|
+
TMP_DIR = File.join(Dir.tmpdir, 'xiami_radio').freeze
|
14
|
+
|
15
|
+
def self.mktmpdir
|
16
|
+
Dir.mkdir TMP_DIR, 0700 unless Dir.exist? TMP_DIR
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
XiamiRadio.mktmpdir
|
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: xiami_radio
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ivan Chou
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-10-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: audite
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: curses
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: http-cookie
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: nori
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.6'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.6'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: byebug
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '9.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '9.0'
|
83
|
+
description: a command-line tool to help you listen Xiami radio via a geek way
|
84
|
+
email:
|
85
|
+
- me@ichou.cn
|
86
|
+
executables:
|
87
|
+
- xiami_radio
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- MIT-LICENSE
|
92
|
+
- README.md
|
93
|
+
- bin/xiami_radio
|
94
|
+
- lib/xiami_radio.rb
|
95
|
+
- lib/xiami_radio/client.rb
|
96
|
+
- lib/xiami_radio/downloader.rb
|
97
|
+
- lib/xiami_radio/notice.rb
|
98
|
+
- lib/xiami_radio/player.rb
|
99
|
+
- lib/xiami_radio/radio.rb
|
100
|
+
- lib/xiami_radio/track.rb
|
101
|
+
- lib/xiami_radio/user.rb
|
102
|
+
- lib/xiami_radio/version.rb
|
103
|
+
- lib/xiami_radio/view/player.rb
|
104
|
+
homepage: https://github.com/IvanChou/xiami_radio
|
105
|
+
licenses:
|
106
|
+
- MIT
|
107
|
+
metadata: {}
|
108
|
+
post_install_message:
|
109
|
+
rdoc_options: []
|
110
|
+
require_paths:
|
111
|
+
- lib
|
112
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
requirements: []
|
123
|
+
rubyforge_project:
|
124
|
+
rubygems_version: 2.6.11
|
125
|
+
signing_key:
|
126
|
+
specification_version: 4
|
127
|
+
summary: Xiami radio player with command-line on ruby
|
128
|
+
test_files: []
|