shoutout 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # shoutout
1
+ # shoutout [![Build Status](https://travis-ci.org/DouweM/shoutout.png?branch=master)](https://travis-ci.org/DouweM/shoutout)
2
2
 
3
3
  A Ruby library for easily getting metadata from Shoutcast-compatible audio streaming servers.
4
4
 
@@ -19,60 +19,68 @@ gem "shoutout"
19
19
  ```ruby
20
20
  require "shoutout"
21
21
 
22
- shoutout = Shoutout.new("http://82.201.100.5:8000/radio538")
22
+ # You can open a connection to the stream that will live for the duration of the block using `.open`:
23
+ Shoutout::Stream.open("http://82.201.100.5:8000/radio538") do |stream|
24
+ # Use `stream` as described below.
25
+ end
26
+
27
+ # If you want a little more control over opening and closing the connection, just use `.new`:
28
+ stream = Shoutout::Stream.new("http://82.201.100.5:8000/radio538")
23
29
 
24
- # Explicitly open a connection with the server. You're responsible for closing this connection using `#disconnect`.
25
- shoutout.connect
30
+ # You can then explicitly open a connection to the stream.
31
+ # You're responsible for closing it using `#disconnect` when you're done.
32
+ stream.connect
26
33
 
27
- # If you call any of the reader methods below without having explicitly opened a connection,
28
- # one will be opened and closed around reading the information implicitly.
34
+ # If you call any of the reader methods below without having opened a connection using `#connect`
35
+ # or `.open`, one will be opened and closed around reading the information implicitly.
29
36
  # This is convenient if you're only looking for one piece of information, but it is of course
30
37
  # very inefficient if you're going to do multiple reads.
31
38
 
32
39
  # Stream info
33
- shoutout.name # => "RADIO538"
34
- shoutout.description # => "ARE YOU IN"
35
- shoutout.genre # => "Various"
36
- shoutout.notice # => nil in this case, but this could very well have a value for your stream
37
- shoutout.content_type # => "audio/mpeg"
38
- shoutout.bitrate # => 128
39
- shoutout.public? # => true
40
- shoutout.audio_info # => { :samplerate => 44100, :bitrate => 128, :channels => 2 }
40
+ stream.name # => "RADIO538"
41
+ stream.description # => "ARE YOU IN"
42
+ stream.genre # => "Various"
43
+ stream.notice # => nil in this case, but this could very well have a value for your stream
44
+ stream.content_type # => "audio/mpeg"
45
+ stream.bitrate # => 128
46
+ stream.public? # => true
47
+ stream.audio_info # => { :samplerate => 44100, :bitrate => 128, :channels => 2 }
41
48
 
42
49
  # Current metadata
43
- shoutout.metadata # => { "StreamTitle" => "ARMIN VAN BUUREN - THIS IS WHAT IT FEELS LIKE", "StreamUrl" => "http://www.radio538.nl" }
50
+ stream.metadata # => { "StreamTitle" => "Armin van Buuren - This Is What It Feels Like", "StreamUrl" => "http://www.radio538.nl" }
44
51
 
45
52
  # The Metadata object is a Hash that has been extended with the following features:
46
- shoutout.metadata[:stream_title] # => "ARMIN VAN BUUREN - THIS IS WHAT IT FEELS LIKE"
47
- shoutout.metadata[:stream_url] # => "http://www.radio538.nl"
48
- shoutout.metadata.now_playing # => "ARMIN VAN BUUREN - THIS IS WHAT IT FEELS LIKE"
49
- shoutout.metadata.website # => "http://www.radio538.nl"
50
- shoutout.metadata.artist # => "ARMIN VAN BUUREN"
51
- shoutout.metadata.song # => "THIS IS WHAT IT FEELS LIKE"
53
+ stream.metadata[:stream_title] # => "Armin van Buuren - This Is What It Feels Like"
54
+ stream.metadata[:stream_url] # => "http://www.radio538.nl"
55
+ stream.metadata.now_playing # => "Armin van Buuren - This Is What It Feels Like"
56
+ stream.metadata.website # => "http://www.radio538.nl"
57
+ stream.metadata.artist # => "Armin van Buuren"
58
+ stream.metadata.song # => "This Is What It Feels Like"
52
59
 
53
- # Conveniently, `#now_playing` and `#website` are also available on the Shoutout instance:
54
- shoutout.now_playing # => "ARMIN VAN BUUREN - THIS IS WHAT IT FEELS LIKE"
55
- shoutout.website # => "http://www.radio538.nl"
60
+ # Conveniently, `#now_playing` and `#website` are also available on the stream object:
61
+ stream.now_playing # => "Armin van Buuren - This Is What It Feels Like"
62
+ stream.website # => "http://www.radio538.nl"
56
63
 
57
64
  # For convenience, all of the reader methods above are also available as class methods:
58
- Shoutout.now_playing("http://82.201.100.5:8000/radio538") # => "ARMIN VAN BUUREN - THIS IS WHAT IT FEELS LIKE"
59
- # Just like the equivalent `Shoutout.new("http://82.201.100.5:8000/radio538").now_playing`,
65
+ Shoutout::Stream.now_playing("http://82.201.100.5:8000/radio538") # => "Armin van Buuren - This Is What It Feels Like"
66
+ # Just like the equivalent `Shoutout::Stream.new("http://82.201.100.5:8000/radio538").now_playing`,
60
67
  # this will automatically open and close a connection around reading the information.
61
68
 
62
- # You can have a block called every time the metadata changes:
63
- shoutout.metadata_change do |metadata|
69
+ # You can be notified every time the metadata changes:
70
+ stream.metadata_change do |metadata|
64
71
  puts "Now playing: #{metadata.song} by #{metadata.artist}"
72
+ # => Now playing: This Is What It Feels Like by Armin van Buuren
65
73
  end
66
74
  # Of course, this only works with an explicitly opened connection.
67
75
 
68
76
  # If you're done setting up but want the program to keep listening for metadata, say so:
69
- shoutout.listen
77
+ stream.listen
70
78
  # Note that listening will only end when the connection is lost or the end of file is reached,
71
79
  # so anything that comes after this call will only then be executed.
72
80
  # This will generally be the last call in your program.
73
81
 
74
82
  # If we don't want to wait around and listen, just let the program exit or disconnect explicitly:
75
- shoutout.disconnect
83
+ stream.disconnect
76
84
  ```
77
85
 
78
86
  ## Examples
@@ -6,173 +6,4 @@ require "shoutout/util"
6
6
  require "shoutout/metadata"
7
7
  require "shoutout/headers"
8
8
  require "shoutout/quick_access"
9
-
10
- class Shoutout
11
- include QuickAccess
12
-
13
- attr_reader :url
14
-
15
- class << self
16
- def open(url)
17
- shoutcast = new(url)
18
-
19
- begin
20
- shoutcast.connect
21
- yield shoutcast
22
- ensure
23
- shoutcast.disconnect
24
- end
25
- end
26
-
27
- def metadata(url)
28
- new(url).metadata
29
- end
30
- end
31
-
32
- def initialize(url)
33
- @url = url
34
- end
35
-
36
- def connected?
37
- @connected
38
- end
39
-
40
- def connect
41
- return false if @connected
42
-
43
- uri = URI.parse(@url)
44
- @socket = TCPSocket.new(uri.host, uri.port)
45
- @socket.puts "GET #{uri.path} HTTP/1.0"
46
- @socket.puts "Icy-MetaData: 1"
47
- @socket.puts
48
-
49
- # Read status line
50
- status_line = @socket.gets
51
- status_code = status_line.match(/\AHTTP\/([0-9]\.[0-9]) ([0-9]{3})/)[2].to_i
52
-
53
- @connected = true
54
-
55
- read_headers
56
-
57
- if status_code >= 300 && status_code < 400 && headers[:location]
58
- disconnect
59
-
60
- @url = URI.join(uri, headers[:location]).to_s
61
-
62
- return connect
63
- end
64
-
65
- unless status_code >= 200 && status_code < 300
66
- disconnect
67
-
68
- return false
69
- end
70
-
71
- unless metadata_interval
72
- disconnect
73
-
74
- return false
75
- end
76
-
77
- @read_metadata_thread = Thread.new(&method(:read_metadata))
78
-
79
- true
80
- end
81
-
82
- def disconnect
83
- return false unless @connected
84
-
85
- @connected = false
86
-
87
- @socket.close if @socket && !@socket.closed?
88
- @socket = nil
89
-
90
- true
91
- end
92
-
93
- def listen
94
- return unless @connected
95
-
96
- @read_metadata_thread.join
97
- @last_metadata_change_thread.join if @last_metadata_change_thread
98
- end
99
-
100
- def metadata
101
- return @metadata if defined?(@metadata)
102
-
103
- original_metadata_change_block = @metadata_change_block
104
-
105
- received = false
106
- metadata_change do |new_metadata|
107
- received = true
108
- end
109
-
110
- already_connected = @connected
111
- connect unless already_connected
112
-
113
- sleep 0.015 until received
114
-
115
- disconnect unless already_connected
116
-
117
- metadata_change(&original_metadata_change_block) if original_metadata_change_block
118
-
119
- @metadata
120
- end
121
-
122
- def metadata_change(&block)
123
- @metadata_change_block = block
124
-
125
- report_metadata_change(@metadata) if defined?(@metadata)
126
-
127
- true
128
- end
129
-
130
- private
131
- def read_headers
132
- raw_headers = ""
133
- while line = @socket.gets
134
- break if line.chomp == ""
135
- raw_headers << line
136
- end
137
- @headers = Headers.parse(raw_headers)
138
- end
139
-
140
- def read_metadata
141
- while @connected
142
- # Skip audio data
143
- data = @socket.read(metadata_interval) || raise(EOFError)
144
-
145
- data = @socket.read(1) || raise(EOFError)
146
- metadata_length = data.unpack("c")[0] * 16
147
- next if metadata_length == 0
148
-
149
- data = @socket.read(metadata_length) || raise(EOFError)
150
- raw_metadata = data.unpack("A*")[0]
151
- @metadata = Metadata.parse(raw_metadata)
152
-
153
- report_metadata_change(@metadata)
154
- end
155
- rescue Errno::EBADF, IOError => e
156
- # Connection lost
157
- disconnect
158
- end
159
-
160
- def report_metadata_change(metadata)
161
- @last_metadata_change_thread = Thread.new(metadata, @last_metadata_change_thread) do |metadata, last_metadata_change_thread|
162
- last_metadata_change_thread.join if last_metadata_change_thread
163
-
164
- @metadata_change_block.call(metadata) if @metadata_change_block
165
- end
166
- end
167
-
168
- def headers
169
- return @headers if defined?(@headers)
170
-
171
- # Connected but no headers? I give up.
172
- return [] if @connected
173
-
174
- connect && disconnect
175
-
176
- @headers
177
- end
178
- end
9
+ require "shoutout/stream"
@@ -1,4 +1,4 @@
1
- class Shoutout
1
+ module Shoutout
2
2
  class Headers < Hash
3
3
  def self.parse(raw_headers)
4
4
  headers = {}
@@ -1,4 +1,4 @@
1
- class Shoutout
1
+ module Shoutout
2
2
  class Metadata < Hash
3
3
  def self.parse(raw_metadata)
4
4
  metadata = {}
@@ -51,12 +51,17 @@ class Shoutout
51
51
  end
52
52
 
53
53
  def artist
54
- now_playing.split(" - ")[0]
54
+ artist_and_song[0]
55
55
  end
56
56
 
57
57
  def song
58
- now_playing.split(" - ")[1]
58
+ artist_and_song[1]
59
59
  end
60
+
61
+ private
62
+ def artist_and_song
63
+ @artist_and_song ||= now_playing.split(" - ", 2)
64
+ end
60
65
  end
61
66
 
62
67
  include QuickAccess
@@ -1,4 +1,4 @@
1
- class Shoutout
1
+ module Shoutout
2
2
  module QuickAccess
3
3
  def self.included(base)
4
4
  base.extend(ClassMethods)
@@ -0,0 +1,171 @@
1
+ module Shoutout
2
+ class Stream
3
+ include QuickAccess
4
+
5
+ attr_reader :url
6
+
7
+ class << self
8
+ def open(url)
9
+ stream = new(url)
10
+
11
+ begin
12
+ stream.connect
13
+ yield stream
14
+ ensure
15
+ stream.disconnect
16
+ end
17
+ end
18
+
19
+ def metadata(url)
20
+ new(url).metadata
21
+ end
22
+ end
23
+
24
+ def initialize(url)
25
+ @url = url
26
+ end
27
+
28
+ def connected?
29
+ @connected
30
+ end
31
+
32
+ def connect
33
+ return false if @connected
34
+
35
+ uri = URI.parse(@url)
36
+ @socket = TCPSocket.new(uri.host, uri.port)
37
+ @socket.puts "GET #{uri.path} HTTP/1.0"
38
+ @socket.puts "Icy-MetaData: 1"
39
+ @socket.puts
40
+
41
+ # Read status line
42
+ status_line = @socket.gets
43
+ status_code = status_line.match(/\AHTTP\/([0-9]\.[0-9]) ([0-9]{3})/)[2].to_i
44
+
45
+ @connected = true
46
+
47
+ read_headers
48
+
49
+ if status_code >= 300 && status_code < 400 && headers[:location]
50
+ disconnect
51
+
52
+ @url = URI.join(uri, headers[:location]).to_s
53
+
54
+ return connect
55
+ end
56
+
57
+ unless status_code >= 200 && status_code < 300
58
+ disconnect
59
+
60
+ return false
61
+ end
62
+
63
+ unless metadata_interval
64
+ disconnect
65
+
66
+ return false
67
+ end
68
+
69
+ @read_metadata_thread = Thread.new(&method(:read_metadata))
70
+
71
+ true
72
+ end
73
+
74
+ def disconnect
75
+ return false unless @connected
76
+
77
+ @connected = false
78
+
79
+ @socket.close if @socket && !@socket.closed?
80
+ @socket = nil
81
+
82
+ true
83
+ end
84
+
85
+ def listen
86
+ return unless @connected
87
+
88
+ @read_metadata_thread.join
89
+ @last_metadata_change_thread.join if @last_metadata_change_thread
90
+ end
91
+
92
+ def metadata
93
+ return @metadata if defined?(@metadata)
94
+
95
+ original_metadata_change_block = @metadata_change_block
96
+
97
+ received = false
98
+ metadata_change do |new_metadata|
99
+ received = true
100
+ end
101
+
102
+ already_connected = @connected
103
+ connect unless already_connected
104
+
105
+ sleep 0.015 until received
106
+
107
+ disconnect unless already_connected
108
+
109
+ metadata_change(&original_metadata_change_block) if original_metadata_change_block
110
+
111
+ @metadata
112
+ end
113
+
114
+ def metadata_change(&block)
115
+ @metadata_change_block = block
116
+
117
+ report_metadata_change(@metadata) if defined?(@metadata)
118
+
119
+ true
120
+ end
121
+
122
+ private
123
+ def read_headers
124
+ raw_headers = ""
125
+ while line = @socket.gets
126
+ break if line.chomp == ""
127
+ raw_headers << line
128
+ end
129
+ @headers = Headers.parse(raw_headers)
130
+ end
131
+
132
+ def read_metadata
133
+ while @connected
134
+ # Skip audio data
135
+ data = @socket.read(metadata_interval) || raise(EOFError)
136
+
137
+ data = @socket.read(1) || raise(EOFError)
138
+ metadata_length = data.unpack("c")[0] * 16
139
+ next if metadata_length == 0
140
+
141
+ data = @socket.read(metadata_length) || raise(EOFError)
142
+ raw_metadata = data.unpack("A*")[0]
143
+ @metadata = Metadata.parse(raw_metadata)
144
+
145
+ report_metadata_change(@metadata)
146
+ end
147
+ rescue Errno::EBADF, IOError => e
148
+ # Connection lost
149
+ disconnect
150
+ end
151
+
152
+ def report_metadata_change(metadata)
153
+ @last_metadata_change_thread = Thread.new(metadata, @last_metadata_change_thread) do |metadata, last_metadata_change_thread|
154
+ last_metadata_change_thread.join if last_metadata_change_thread
155
+
156
+ @metadata_change_block.call(metadata) if @metadata_change_block
157
+ end
158
+ end
159
+
160
+ def headers
161
+ return @headers if defined?(@headers)
162
+
163
+ # Connected but no headers? I give up.
164
+ return [] if @connected
165
+
166
+ connect && disconnect
167
+
168
+ @headers
169
+ end
170
+ end
171
+ end
@@ -1,4 +1,4 @@
1
- class Shoutout
1
+ module Shoutout
2
2
  module Util
3
3
  def self.camelize(term)
4
4
  term = term.sub(/^[a-z\d]*/) { $&.capitalize }
@@ -1,3 +1,3 @@
1
- class Shoutout
2
- VERSION = "0.0.1"
1
+ module Shoutout
2
+ VERSION = "0.0.2"
3
3
  end
@@ -3,7 +3,7 @@ require "spec_helper"
3
3
  describe Shoutout::QuickAccess do
4
4
 
5
5
  let(:url) { "http://82.201.100.5:8000/radio538" }
6
- subject { Shoutout.new(url) }
6
+ subject { Shoutout::Stream.new(url) }
7
7
 
8
8
  describe "#audio_info" do
9
9
 
@@ -28,15 +28,15 @@ describe Shoutout::QuickAccess do
28
28
  describe ".name" do
29
29
 
30
30
  it "creates a new instance" do
31
- Shoutout.should_receive(:new).with(url).and_return(double("shoutout").as_null_object)
31
+ Shoutout::Stream.should_receive(:new).with(url).and_return(double("stream").as_null_object)
32
32
 
33
- Shoutout.name(url)
33
+ Shoutout::Stream.name(url)
34
34
  end
35
35
 
36
36
  it "calls #name on the opened connection" do
37
- Shoutout.any_instance.should_receive(:name)
37
+ Shoutout::Stream.any_instance.should_receive(:name)
38
38
 
39
- Shoutout.name(url)
39
+ Shoutout::Stream.name(url)
40
40
  end
41
41
  end
42
42
  end
@@ -1,11 +1,10 @@
1
1
  require "spec_helper"
2
2
 
3
- describe Shoutout do
4
-
3
+ describe Shoutout::Stream do
5
4
  let(:url) { "http://82.201.100.5:8000/radio538" }
6
5
  let(:uri) { URI.parse(url) }
7
6
  subject! { described_class.new(url) }
8
- let(:response_data) { File.read(File.expand_path("../fixtures/ok_response", __FILE__)) }
7
+ let(:response_data) { File.read(File.expand_path("../../fixtures/ok_response", __FILE__)) }
9
8
  let(:socket) { FakeTCPSocket.new(response_data) }
10
9
 
11
10
  before(:each) do
@@ -105,7 +104,7 @@ describe Shoutout do
105
104
 
106
105
  context "when the response indicates a redirect" do
107
106
 
108
- let(:redirect_response_data) { File.read(File.expand_path("../fixtures/redirect_response", __FILE__)) }
107
+ let(:redirect_response_data) { File.read(File.expand_path("../../fixtures/redirect_response", __FILE__)) }
109
108
  let(:redirect_socket) { FakeTCPSocket.new(redirect_response_data) }
110
109
 
111
110
  before(:each) do
@@ -138,7 +137,7 @@ describe Shoutout do
138
137
 
139
138
  context "when the response indicates an error" do
140
139
 
141
- let(:response_data) { File.read(File.expand_path("../fixtures/error_response", __FILE__)) }
140
+ let(:response_data) { File.read(File.expand_path("../../fixtures/error_response", __FILE__)) }
142
141
 
143
142
  it "closes the connection" do
144
143
  subject.should_receive(:disconnect).twice.and_call_original
@@ -155,7 +154,7 @@ describe Shoutout do
155
154
 
156
155
  context "when the response is unsupported" do
157
156
 
158
- let(:response_data) { File.read(File.expand_path("../fixtures/unsupported_response", __FILE__)) }
157
+ let(:response_data) { File.read(File.expand_path("../../fixtures/unsupported_response", __FILE__)) }
159
158
 
160
159
  it "closes the connection" do
161
160
  subject.should_receive(:disconnect).twice.and_call_original
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shoutout
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -13,7 +13,7 @@ date: 2013-09-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
16
- requirement: &70299652575840 !ruby/object:Gem::Requirement
16
+ requirement: &70322661963000 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *70299652575840
24
+ version_requirements: *70322661963000
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rspec
27
- requirement: &70299652591920 !ruby/object:Gem::Requirement
27
+ requirement: &70322661962580 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,7 +32,7 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *70299652591920
35
+ version_requirements: *70322661962580
36
36
  description: A Ruby library for easily getting metadata from Shoutcast-compatible
37
37
  audio streaming servers
38
38
  email: douwe@selenight.nl
@@ -43,6 +43,7 @@ files:
43
43
  - lib/shoutout/headers.rb
44
44
  - lib/shoutout/metadata.rb
45
45
  - lib/shoutout/quick_access.rb
46
+ - lib/shoutout/stream.rb
46
47
  - lib/shoutout/util.rb
47
48
  - lib/shoutout/version.rb
48
49
  - lib/shoutout.rb
@@ -57,8 +58,8 @@ files:
57
58
  - spec/shoutout/headers_spec.rb
58
59
  - spec/shoutout/metadata_spec.rb
59
60
  - spec/shoutout/quick_access_spec.rb
61
+ - spec/shoutout/stream_spec.rb
60
62
  - spec/shoutout/util_spec.rb
61
- - spec/shoutout_spec.rb
62
63
  - spec/spec_helper.rb
63
64
  - spec/support/fake_tcp_socket.rb
64
65
  homepage: https://github.com/DouweM/shoutout
@@ -76,7 +77,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
76
77
  version: '0'
77
78
  segments:
78
79
  - 0
79
- hash: 848854510650494207
80
+ hash: 401984402802077394
80
81
  required_rubygems_version: !ruby/object:Gem::Requirement
81
82
  none: false
82
83
  requirements:
@@ -85,7 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
85
86
  version: '0'
86
87
  segments:
87
88
  - 0
88
- hash: 848854510650494207
89
+ hash: 401984402802077394
89
90
  requirements: []
90
91
  rubyforge_project:
91
92
  rubygems_version: 1.8.6
@@ -100,7 +101,7 @@ test_files:
100
101
  - spec/shoutout/headers_spec.rb
101
102
  - spec/shoutout/metadata_spec.rb
102
103
  - spec/shoutout/quick_access_spec.rb
104
+ - spec/shoutout/stream_spec.rb
103
105
  - spec/shoutout/util_spec.rb
104
- - spec/shoutout_spec.rb
105
106
  - spec/spec_helper.rb
106
107
  - spec/support/fake_tcp_socket.rb