douban.fm 0.1.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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 won't promise anything.__
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
- * to play your private playlist
23
+ <pre>
24
+ $ douban.fm -h
24
25
 
25
- `douban.fm <email> <password>`
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
 
@@ -4,128 +4,213 @@ require 'douban.fm'
4
4
  require 'optparse'
5
5
  require 'ostruct'
6
6
 
7
- def main
8
- options = OpenStruct.new
9
- options.verbose = false
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
- opts = OptionParser.new do |opts|
12
- opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} [OPTIONS]"
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
- opts.on('-u', '--user email',
15
- 'douban.fm account name, normally an email address',
16
- 'if not provided, will play anonymous playlist') do |email|
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
- opts.on('-p', '--password [password]',
21
- 'douban.fm account password',
22
- 'if not provided, will be asked') do |password|
23
- options.password = password
24
- end
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
- opts.on('-m', '--mpd', 'do not play by it own, send playlist to Music Player Daemon') do
27
- options.mpd = true
28
- end
38
+ options.remote_host = parts[0]
39
+ options.remote_port = parts[1].to_i
40
+ end
29
41
 
30
- opts.on('-c', '--channel channel',
31
- 'which channel to play',
32
- 'if not provided, channel 0 will be selected but who knows what it is') do |channel|
33
- options.channel = channel
34
- end
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
- opts.on('-l', '--list', 'list all available channels') do
37
- options.list = true
38
- end
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
- opts.on('-v', '--verbose', 'verbose mode') do
41
- options.verbose = true
60
+ opts.on_tail('-h', '--help', 'show this message') do
61
+ puts opts
62
+ exit
63
+ end
42
64
  end
43
65
 
44
- opts.on_tail('-h', '--help', 'show this message') do
45
- puts opts
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
- opts.parse!
79
+ if options.verbose
80
+ logger = DoubanFM::ConsoleLogger.new
81
+ else
82
+ logger = DoubanFM::DummyLogger.new
83
+ end
51
84
 
52
- if options.verbose
53
- logger = DoubanFM::ConsoleLogger.new
54
- else
55
- logger = DoubanFM::DummyLogger.new
56
- end
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
- if options.list
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
- exit
67
- end
96
+ if options.channel.nil?
97
+ options.channel = 0
98
+ end
68
99
 
69
- if options.channel.nil?
70
- options.channel = 0
71
- end
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
- if options.email.nil?
74
- douban_fm = DoubanFM::DoubanFM.new(logger)
110
+ @@douban_fm = DoubanFM::DoubanFM.new(logger, options.email, options.password)
111
+ @@douban_fm.login
75
112
 
76
- logger.log('play anonymous playlist')
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 = DoubanFM::DoubanFM.new(logger, options.email, options.password)
84
- douban_fm.login
116
+ @@douban_fm.select_channel(options.channel)
85
117
 
86
- logger.log("login as user [#{options.email}]")
87
- end
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
- douban_fm.select_channel(options.channel)
126
+ @@douban_fm.fetch_next_playlist
127
+ rescue
128
+ logger.log('session expired, relogin')
90
129
 
91
- logger.log("select channel #{options.channel}")
130
+ @@douban_fm.login
92
131
 
93
- if options.mpd.nil?
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
- douban_fm.fetch_next_playlist
100
- rescue
101
- logger.log('session expired, relogin')
134
+ @@douban_fm.fetch_next_playlist
135
+ end
102
136
 
103
- douban_fm.login
137
+ logger.log('play current playlist')
104
138
 
105
- logger.log('fetch next playlist')
139
+ @@douban_fm.play do |waiting|
140
+ play_proc.call(waiting)
141
+ end
142
+ end
143
+ end
106
144
 
107
- douban_fm.fetch_next_playlist
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
- logger.log('play current playlist')
187
+ servletClass.remote_host = options.remote_host
188
+ servletClass.remote_port = options.remote_port
111
189
 
112
- douban_fm.play do |waiting|
113
- play_proc.call(waiting)
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
- play_proc.call(true)
119
- else
120
- # there are a lot more chances to get stack overflow in this mode,
121
- # so can't use the proc way
122
- while true
123
- douban_fm.add_to_mpd
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
@@ -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
- @logger.log("add more songs to mpd")
156
+ add_current_playlist_to_mpd(mpd)
157
+ end
147
158
 
148
- @current_playlist['song'].each do |song|
149
- @logger.log("send [#{song['url'].gsub('\\', '')}] to mpd")
159
+ mpd.disconnect
160
+ end
150
161
 
151
- # douban_fm_playlist.add(song['url'].gsub('\\', ''))
152
- mpd.add(song['url'].gsub('\\', ''))
153
- end
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
@@ -1,3 +1,3 @@
1
1
  module DoubanFM
2
- VERSION = '0.1.1'
2
+ VERSION = '0.2.1'
3
3
  end
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.1.1
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-17 00:00:00.000000000 Z
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: &70256513065900 !ruby/object:Gem::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: *70256513065900
24
+ version_requirements: *70103937077020
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: highline
27
- requirement: &70256513062600 !ruby/object:Gem::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: *70256513062600
35
+ version_requirements: *70103937076420
36
36
  description: douban.fm
37
37
  email:
38
38
  - hxliang1982@gmail.com