douban.fm 0.1.1 → 0.2.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.
- data/README.md +41 -3
- data/bin/douban.fm +170 -85
- data/lib/douban.fm/douban_fm.rb +63 -6
- data/lib/douban.fm/version.rb +1 -1
- metadata +6 -6
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Play music from douban.fm.
|
4
4
|
|
5
|
-
__Note that this project is still on-going, so I
|
5
|
+
__Note that this project is still on-going, so I will only try my best to stablize the interface.__
|
6
6
|
|
7
7
|
## Installation
|
8
8
|
|
@@ -20,9 +20,47 @@ Or install it yourself as:
|
|
20
20
|
|
21
21
|
## Usage
|
22
22
|
|
23
|
-
|
23
|
+
<pre>
|
24
|
+
$ douban.fm -h
|
24
25
|
|
25
|
-
|
26
|
+
Usage: douban.fm [OPTIONS]
|
27
|
+
-u, --user email douban.fm account name, normally an email address
|
28
|
+
if not provided, will play anonymous playlist
|
29
|
+
-p, --password [password] douban.fm account password
|
30
|
+
if not provided, will be asked
|
31
|
+
-m, --mpd do not play by it own, send playlist to Music Player Daemon
|
32
|
+
-c, --channel channel which channel to play
|
33
|
+
if not provided, channel 0 will be selected but who knows what it is
|
34
|
+
-l, --list list all available channels
|
35
|
+
-v, --verbose verbose mode
|
36
|
+
-h, --help show this message
|
37
|
+
</pre>
|
38
|
+
|
39
|
+
Basically there are two ways to play music
|
40
|
+
|
41
|
+
* to play direclty by `mpg123`
|
42
|
+
|
43
|
+
Under this mode, music will be played by forking `mpg123` directly. Sorry currently there is no way to configure which music player to use.
|
44
|
+
|
45
|
+
* `douban.fm` will play anonymous playlist of channel 0
|
46
|
+
* `douban.fm -c 1` will play channel 1
|
47
|
+
* `douban.fm -u xxx@xxx.com -p xxx` will play private playlist
|
48
|
+
* `douban.fm -u xxx@xxx.com -p xxx -c 1` will play channel 1 but with your account signed in
|
49
|
+
* `douban.fm -u xxx@xx.com -p` will play private playlist but will ask for your password to sign in
|
50
|
+
|
51
|
+
* to play by [Music Player Daemon](http://mpd.wikia.com/wiki/Music_Player_Daemon_Wiki)
|
52
|
+
|
53
|
+
Under this mode, URL of music will be sent to MPD which will actually play. Whenever there are less than _10_ songs in MPD playlist, more will be retrieved from douban.fm.
|
54
|
+
|
55
|
+
It is fantastic to use MPD since there are quite many [clients](http://mpd.wikia.com/wiki/Clients) to use. I am now just use my iPhone with [MPoD2](http://mpd.wikia.com/wiki/Client:MPoD2).
|
56
|
+
|
57
|
+
Thanks for @tdsparrow pointing out MPD, otherwise I might have been doing some really stupid things.
|
58
|
+
|
59
|
+
* `douban.fm -m` will play anonymous playlist of channel 0
|
60
|
+
* `douban.fm -m -c 1` will play channel 1
|
61
|
+
* `douban.fm -m -u xxx@xxx.com -p xxx` will play private playlist
|
62
|
+
* `douban.fm -m -u xxx@xxx.com -p xxx -c 1` will play channel 1 but with your account signed in
|
63
|
+
* `douban.fm -m -u xxx@xx.com -p` will play private playlist but will ask for your password to sign in
|
26
64
|
|
27
65
|
## Contributing
|
28
66
|
|
data/bin/douban.fm
CHANGED
@@ -4,128 +4,213 @@ require 'douban.fm'
|
|
4
4
|
require 'optparse'
|
5
5
|
require 'ostruct'
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
class DoubanFMCLI
|
8
|
+
def parse
|
9
|
+
options = OpenStruct.new
|
10
|
+
options.verbose = false
|
11
|
+
|
12
|
+
opts = OptionParser.new do |opts|
|
13
|
+
opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} [OPTIONS]"
|
14
|
+
|
15
|
+
opts.on('-u', '--user email',
|
16
|
+
'douban.fm account name, normally an email address',
|
17
|
+
'if not provided, will play anonymous playlist') do |email|
|
18
|
+
options.email = email
|
19
|
+
end
|
10
20
|
|
11
|
-
|
12
|
-
|
21
|
+
opts.on('-p', '--password [password]',
|
22
|
+
'douban.fm account password',
|
23
|
+
'if not provided, will be asked') do |password|
|
24
|
+
options.password = password
|
25
|
+
end
|
13
26
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
options.email = email
|
18
|
-
end
|
27
|
+
opts.on('-m', '--mpd', 'do not play by it own, send playlist to Music Player Daemon') do
|
28
|
+
options.mpd = true
|
29
|
+
end
|
19
30
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
31
|
+
opts.on('-r', '--remote remote', 'mpd remote host, in format of <IP>:<Port>') do |remote|
|
32
|
+
parts = remote.split(':')
|
33
|
+
if parts.length != 2
|
34
|
+
puts opts
|
35
|
+
exit
|
36
|
+
end
|
25
37
|
|
26
|
-
|
27
|
-
|
28
|
-
|
38
|
+
options.remote_host = parts[0]
|
39
|
+
options.remote_port = parts[1].to_i
|
40
|
+
end
|
29
41
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
42
|
+
opts.on('-c', '--channel channel',
|
43
|
+
'which channel to play',
|
44
|
+
'if not provided, channel 0 will be selected but who knows what it is') do |channel|
|
45
|
+
options.channel = channel
|
46
|
+
end
|
35
47
|
|
36
|
-
|
37
|
-
|
38
|
-
|
48
|
+
opts.on('-l', '--list', 'list all available channels') do
|
49
|
+
options.list = true
|
50
|
+
end
|
51
|
+
|
52
|
+
opts.on('-i', '--interaction', 'start an http server for interaction') do
|
53
|
+
options.interaction = true
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on('-v', '--verbose', 'verbose mode') do
|
57
|
+
options.verbose = true
|
58
|
+
end
|
39
59
|
|
40
|
-
|
41
|
-
|
60
|
+
opts.on_tail('-h', '--help', 'show this message') do
|
61
|
+
puts opts
|
62
|
+
exit
|
63
|
+
end
|
42
64
|
end
|
43
65
|
|
44
|
-
opts.
|
45
|
-
|
66
|
+
opts.parse!
|
67
|
+
|
68
|
+
options
|
69
|
+
end
|
70
|
+
|
71
|
+
def main
|
72
|
+
options = parse
|
73
|
+
|
74
|
+
if not options.interaction.nil? and options.mpd.nil?
|
75
|
+
puts 'only mpd mode supports interaction'
|
46
76
|
exit
|
47
77
|
end
|
48
|
-
end
|
49
78
|
|
50
|
-
|
79
|
+
if options.verbose
|
80
|
+
logger = DoubanFM::ConsoleLogger.new
|
81
|
+
else
|
82
|
+
logger = DoubanFM::DummyLogger.new
|
83
|
+
end
|
51
84
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
85
|
+
if options.list
|
86
|
+
@@douban_fm = DoubanFM::DoubanFM.new(logger)
|
87
|
+
@@douban_fm.fetch_channels
|
88
|
+
@@douban_fm.channels['channels'].sort_by { |i| i['channel_id'] }.each do |channel|
|
89
|
+
channel_id = channel['channel_id']
|
90
|
+
puts "#{channel_id}.#{' ' * (4 - channel_id.to_s.length)}#{channel['name']}"
|
91
|
+
end
|
57
92
|
|
58
|
-
|
59
|
-
douban_fm = DoubanFM::DoubanFM.new(logger)
|
60
|
-
douban_fm.fetch_channels
|
61
|
-
douban_fm.channels['channels'].sort_by { |i| i['channel_id'] }.each do |channel|
|
62
|
-
channel_id = channel['channel_id']
|
63
|
-
puts "#{channel_id}.#{' ' * (4 - channel_id.to_s.length)}#{channel['name']}"
|
93
|
+
exit
|
64
94
|
end
|
65
95
|
|
66
|
-
|
67
|
-
|
96
|
+
if options.channel.nil?
|
97
|
+
options.channel = 0
|
98
|
+
end
|
68
99
|
|
69
|
-
|
70
|
-
|
71
|
-
|
100
|
+
if options.email.nil?
|
101
|
+
@@douban_fm = DoubanFM::DoubanFM.new(logger)
|
102
|
+
|
103
|
+
logger.log('play anonymous playlist')
|
104
|
+
else
|
105
|
+
if options.password.nil? or options.password.empty?
|
106
|
+
require 'highline/import'
|
107
|
+
options.password = ask("Enter password: ") { |q| q.echo = false }
|
108
|
+
end
|
72
109
|
|
73
|
-
|
74
|
-
|
110
|
+
@@douban_fm = DoubanFM::DoubanFM.new(logger, options.email, options.password)
|
111
|
+
@@douban_fm.login
|
75
112
|
|
76
|
-
|
77
|
-
else
|
78
|
-
if options.password.nil? or options.password.empty?
|
79
|
-
require 'highline/import'
|
80
|
-
options.password = ask("Enter password: ") { |q| q.echo = false }
|
113
|
+
logger.log("login as user [#{options.email}]")
|
81
114
|
end
|
82
115
|
|
83
|
-
douban_fm
|
84
|
-
douban_fm.login
|
116
|
+
@@douban_fm.select_channel(options.channel)
|
85
117
|
|
86
|
-
logger.log("
|
87
|
-
|
118
|
+
logger.log("select channel #{options.channel}")
|
119
|
+
|
120
|
+
if options.mpd.nil?
|
121
|
+
play_proc = proc do |waiting|
|
122
|
+
if waiting
|
123
|
+
begin
|
124
|
+
logger.log('fetch next playlist')
|
88
125
|
|
89
|
-
|
126
|
+
@@douban_fm.fetch_next_playlist
|
127
|
+
rescue
|
128
|
+
logger.log('session expired, relogin')
|
90
129
|
|
91
|
-
|
130
|
+
@@douban_fm.login
|
92
131
|
|
93
|
-
|
94
|
-
play_proc = proc do |waiting|
|
95
|
-
if waiting
|
96
|
-
begin
|
97
|
-
logger.log('fetch next playlist')
|
132
|
+
logger.log('fetch next playlist')
|
98
133
|
|
99
|
-
|
100
|
-
|
101
|
-
logger.log('session expired, relogin')
|
134
|
+
@@douban_fm.fetch_next_playlist
|
135
|
+
end
|
102
136
|
|
103
|
-
|
137
|
+
logger.log('play current playlist')
|
104
138
|
|
105
|
-
|
139
|
+
@@douban_fm.play do |waiting|
|
140
|
+
play_proc.call(waiting)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
106
144
|
|
107
|
-
|
145
|
+
play_proc.call(true)
|
146
|
+
else
|
147
|
+
unless options.interaction.nil?
|
148
|
+
require 'webrick'
|
149
|
+
require 'webrick/httpstatus'
|
150
|
+
require 'json'
|
151
|
+
|
152
|
+
servletClass = Class.new(WEBrick::HTTPServlet::AbstractServlet) do
|
153
|
+
class << self
|
154
|
+
def remote_host=(host)
|
155
|
+
@@remote_host = host
|
156
|
+
end
|
157
|
+
|
158
|
+
def remote_port=(port)
|
159
|
+
@@remote_port = port
|
160
|
+
end
|
161
|
+
end
|
162
|
+
@@remote_host = nil
|
163
|
+
@@remote_port = nil
|
164
|
+
|
165
|
+
def do_GET request, response
|
166
|
+
path = request.path[1..-1].split('/')
|
167
|
+
case path[0]
|
168
|
+
when 'channels'
|
169
|
+
@@douban_fm.fetch_channels
|
170
|
+
response.body = JSON.generate(@@douban_fm.channels)
|
171
|
+
raise WEBrick::HTTPStatus::OK
|
172
|
+
when 'channel'
|
173
|
+
unless path[1].nil?
|
174
|
+
# there is a race but not a big deal
|
175
|
+
@@douban_fm.select_channel(path[1].to_i)
|
176
|
+
@@douban_fm.clear_mpd_playlist(@@remote_host, @@remote_port)
|
177
|
+
raise WEBrick::HTTPStatus::OK
|
178
|
+
else
|
179
|
+
raise WEBrick::HTTPStatus::NotFound
|
180
|
+
end
|
181
|
+
else
|
182
|
+
raise WEBrick::HTTPStatus::NotFound
|
183
|
+
end
|
184
|
+
end
|
108
185
|
end
|
109
186
|
|
110
|
-
|
187
|
+
servletClass.remote_host = options.remote_host
|
188
|
+
servletClass.remote_port = options.remote_port
|
111
189
|
|
112
|
-
|
113
|
-
|
190
|
+
Thread.new do
|
191
|
+
server = WEBrick::HTTPServer.new(:Port => 3000)
|
192
|
+
|
193
|
+
%w[INT TERM].each do |signal|
|
194
|
+
trap(signal) do
|
195
|
+
server.shutdown
|
196
|
+
Thread.main.kill # die hard
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
server.mount("/", servletClass)
|
201
|
+
server.start
|
114
202
|
end
|
115
203
|
end
|
116
|
-
end
|
117
204
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
sleep 10
|
205
|
+
# there are a lot more chances to get stack overflow in this mode,
|
206
|
+
# so can't use the proc way
|
207
|
+
while true
|
208
|
+
@@douban_fm.add_to_mpd(options.remote_host, options.remote_port)
|
209
|
+
sleep 10
|
210
|
+
end
|
125
211
|
end
|
126
212
|
end
|
127
213
|
end
|
128
214
|
|
129
|
-
main
|
130
|
-
|
215
|
+
DoubanFMCLI.new.main
|
131
216
|
sleep
|
data/lib/douban.fm/douban_fm.rb
CHANGED
@@ -97,7 +97,17 @@ module DoubanFM
|
|
97
97
|
end
|
98
98
|
|
99
99
|
def add_to_mpd(host = 'localhost', port = 6600)
|
100
|
+
if host.nil?
|
101
|
+
host = 'localhost'
|
102
|
+
end
|
103
|
+
if port.nil?
|
104
|
+
port = 6600
|
105
|
+
end
|
106
|
+
|
100
107
|
mpd = MPD.new(host, port)
|
108
|
+
|
109
|
+
@logger.log("connecting to mpd at #{host}:#{port}")
|
110
|
+
|
101
111
|
mpd.connect
|
102
112
|
|
103
113
|
# remove after played
|
@@ -143,14 +153,36 @@ module DoubanFM
|
|
143
153
|
fetch_next_playlist
|
144
154
|
end
|
145
155
|
|
146
|
-
|
156
|
+
add_current_playlist_to_mpd(mpd)
|
157
|
+
end
|
147
158
|
|
148
|
-
|
149
|
-
|
159
|
+
mpd.disconnect
|
160
|
+
end
|
150
161
|
|
151
|
-
|
152
|
-
|
153
|
-
|
162
|
+
def clear_mpd_playlist(host = 'localhost', port = 6600)
|
163
|
+
if host.nil?
|
164
|
+
host = 'localhost'
|
165
|
+
end
|
166
|
+
if port.nil?
|
167
|
+
port = 6600
|
168
|
+
end
|
169
|
+
|
170
|
+
mpd = MPD.new(host, port)
|
171
|
+
|
172
|
+
@logger.log("connecting to mpd at #{host}:#{port}")
|
173
|
+
|
174
|
+
mpd.connect
|
175
|
+
|
176
|
+
@logger.log('crop or clear current playlist')
|
177
|
+
|
178
|
+
status = mpd.status
|
179
|
+
mpd.clear # this will stop mpd if it is playing
|
180
|
+
|
181
|
+
fetch_next_playlist
|
182
|
+
add_current_playlist_to_mpd(mpd)
|
183
|
+
|
184
|
+
if status[:state] == :play
|
185
|
+
mpd.play
|
154
186
|
end
|
155
187
|
|
156
188
|
mpd.disconnect
|
@@ -166,5 +198,30 @@ module DoubanFM
|
|
166
198
|
end
|
167
199
|
end
|
168
200
|
end
|
201
|
+
|
202
|
+
private
|
203
|
+
|
204
|
+
def add_current_playlist_to_mpd(mpd)
|
205
|
+
@logger.log("add more songs to mpd")
|
206
|
+
|
207
|
+
@current_playlist['song'].each do |song|
|
208
|
+
@logger.log("send [#{song['url'].gsub('\\', '')}] to mpd")
|
209
|
+
|
210
|
+
mpd.add(song['url'].gsub('\\', ''))
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# currently not used
|
215
|
+
def crop(mpd, status)
|
216
|
+
total = status[:playlistlength]
|
217
|
+
current = status[:song]
|
218
|
+
|
219
|
+
total.downto(current + 2) do |i|
|
220
|
+
mpd.delete(i)
|
221
|
+
end
|
222
|
+
current.downto(1) do |i|
|
223
|
+
mpd.delete(i)
|
224
|
+
end
|
225
|
+
end
|
169
226
|
end
|
170
227
|
end
|
data/lib/douban.fm/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: douban.fm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-01-
|
12
|
+
date: 2013-01-18 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: ruby-mpd
|
16
|
-
requirement: &
|
16
|
+
requirement: &70103937077020 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - =
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 0.1.5
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70103937077020
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: highline
|
27
|
-
requirement: &
|
27
|
+
requirement: &70103937076420 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - =
|
@@ -32,7 +32,7 @@ dependencies:
|
|
32
32
|
version: 1.6.15
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70103937076420
|
36
36
|
description: douban.fm
|
37
37
|
email:
|
38
38
|
- hxliang1982@gmail.com
|