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 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