airplay 0.2.9 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. data/Gemfile +5 -0
  2. data/Gemfile.lock +68 -0
  3. data/HUGS +15 -0
  4. data/LICENSE +20 -0
  5. data/README.md +50 -72
  6. data/Rakefile +6 -9
  7. data/SPEC.md +34 -0
  8. data/airplay.gemspec +28 -16
  9. data/bin/air +27 -0
  10. data/examples/demo.rb +35 -0
  11. data/examples/image.rb +9 -0
  12. data/examples/video.rb +24 -0
  13. data/lib/airplay.rb +38 -17
  14. data/lib/airplay/browser.rb +68 -0
  15. data/lib/airplay/cli.rb +104 -0
  16. data/lib/airplay/configuration.rb +29 -0
  17. data/lib/airplay/connection.rb +114 -0
  18. data/lib/airplay/connection/authentication.rb +37 -0
  19. data/lib/airplay/connection/persistent.rb +44 -0
  20. data/lib/airplay/device.rb +81 -0
  21. data/lib/airplay/device/features.rb +43 -0
  22. data/lib/airplay/device/info.rb +22 -0
  23. data/lib/airplay/devices.rb +46 -0
  24. data/lib/airplay/logger.rb +20 -0
  25. data/lib/airplay/playable.rb +24 -0
  26. data/lib/airplay/protocol.rb +9 -72
  27. data/lib/airplay/protocol/app.rb +52 -0
  28. data/lib/airplay/protocol/message.rb +8 -0
  29. data/lib/airplay/protocol/playback_info.rb +49 -0
  30. data/lib/airplay/protocol/player.rb +170 -0
  31. data/lib/airplay/protocol/reverse.rb +69 -0
  32. data/lib/airplay/protocol/slideshow.rb +66 -0
  33. data/lib/airplay/protocol/timers.rb +25 -0
  34. data/lib/airplay/protocol/viewer.rb +56 -0
  35. data/lib/airplay/structure.rb +7 -0
  36. data/lib/airplay/viewable.rb +16 -0
  37. data/test/fixtures/cassettes/airplay/listing_slideshow_features.yml +201 -0
  38. data/test/fixtures/cassettes/airplay/play_an_entire_video.yml +100 -0
  39. data/test/fixtures/cassettes/airplay/sending_a_video.yml +71 -0
  40. data/test/fixtures/cassettes/airplay/sending_an_image.yml +26439 -0
  41. data/test/fixtures/cassettes/airplay/stop_any_transmission.yml +8851 -0
  42. data/test/fixtures/files/logo.png +0 -0
  43. data/test/fixtures/files/transition_0.png +0 -0
  44. data/test/fixtures/files/transition_1.png +0 -0
  45. data/test/fixtures/files/transition_2.png +0 -0
  46. data/test/fixtures/files/transition_3.png +0 -0
  47. data/test/integration/discovery_test.rb +13 -0
  48. data/test/integration/features_test.rb +14 -0
  49. data/test/integration/send_media_test.rb +37 -0
  50. data/test/integration/slideshow_test.rb +26 -0
  51. data/test/test_helper.rb +42 -0
  52. data/test/unit/node_test.rb +17 -0
  53. data/test/unit/protocol_test.rb +44 -0
  54. metadata +247 -77
  55. data/.gitignore +0 -2
  56. data/.travis.yml +0 -6
  57. data/examples/jobs.jpg +0 -0
  58. data/examples/send_image.rb +0 -5
  59. data/examples/send_video.rb +0 -7
  60. data/lib/airplay/client.rb +0 -60
  61. data/lib/airplay/protocol/image.rb +0 -41
  62. data/lib/airplay/protocol/media.rb +0 -64
  63. data/lib/airplay/protocol/scrub.rb +0 -37
  64. data/lib/airplay/server.rb +0 -2
  65. data/lib/airplay/server/browser.rb +0 -35
  66. data/lib/airplay/server/node.rb +0 -7
  67. data/test/authentication.rb +0 -13
  68. data/test/discovery.rb +0 -27
  69. data/test/fixtures/cassettes/airplay/authenticate_all_the_things_.yml +0 -139
  70. data/test/fixtures/cassettes/airplay/control_a_video_being_played_in_apple_tv.yml +0 -242
  71. data/test/fixtures/cassettes/airplay/get_current_scrub_from_apple_tv.yml +0 -69
  72. data/test/fixtures/cassettes/airplay/go_to_a_given_position_in_the_video.yml +0 -157
  73. data/test/fixtures/cassettes/airplay/send_audio_to_apple_tv.yml +0 -33
  74. data/test/fixtures/cassettes/airplay/send_image_to_apple_tv.yml +0 -661
  75. data/test/fixtures/cassettes/airplay/send_image_to_apple_tv_with_effects.yml +0 -591
  76. data/test/fixtures/cassettes/airplay/send_video_to_apple_tv.yml +0 -33
  77. data/test/fixtures/image.gif +0 -0
  78. data/test/fixtures/image2.gif +0 -0
  79. data/test/helper.rb +0 -31
  80. data/test/images.rb +0 -31
  81. data/test/media.rb +0 -47
  82. data/test/scrub.rb +0 -31
@@ -0,0 +1,170 @@
1
+ require "uri"
2
+ require "forwardable"
3
+ require "micromachine"
4
+ require "celluloid"
5
+ require "cfpropertylist"
6
+
7
+ require "airplay/connection"
8
+ require "airplay/protocol/timers"
9
+ require "airplay/protocol/playback_info"
10
+
11
+ module Airplay::Protocol
12
+ # Public: The class that handles all the video playback
13
+ #
14
+ class Player
15
+ extend Forwardable
16
+ include Celluloid
17
+
18
+ def_delegators :@machine, :state, :on
19
+
20
+ def initialize(device)
21
+ @device = device
22
+ end
23
+
24
+ # Public: Plays a given url or file.
25
+ # Creates a new persistent connection to ensure that
26
+ # the socket will be kept alive
27
+ #
28
+ # file_or_url - The url or file to be reproduced
29
+ # options - Optional starting time
30
+ #
31
+ def play(file_or_url, options = {})
32
+ start_the_machine
33
+ check_for_playback_status
34
+
35
+ media_url = case true
36
+ when File.exists?(file_or_url)
37
+ when !!(file_or_url =~ URI::regexp)
38
+ file_or_url
39
+ else
40
+ raise Errno::ENOENT, file_or_url
41
+ end
42
+
43
+ content = {
44
+ "Content-Location" => media_url,
45
+ "Start-Position" => options.fetch(:time, 0.0)
46
+ }
47
+
48
+ data = content.map { |k, v| "#{k}: #{v}" }.join("\r\n")
49
+
50
+ response = persistent.async.post("/play", data + "\r\n", {
51
+ "Content-Type" => "text/parameters"
52
+ })
53
+
54
+ timers.reset
55
+ end
56
+
57
+ # Public: Handles the progress of the playback, the given &block get's
58
+ # executed every second while the video is played.
59
+ #
60
+ # &block - Block to be executed in every playable second.
61
+ #
62
+ def progress(callback)
63
+ timers << every(1) do
64
+ callback.call(info) if playing?
65
+ end
66
+ end
67
+
68
+ # Public: Shows the current playback time if a video is being played.
69
+ #
70
+ # Returns a hash with the :duration and current :position
71
+ #
72
+ def scrub
73
+ return unless playing?
74
+ response = connection.get("/scrub")
75
+ parts = response.body.split("\n")
76
+ Hash[parts.collect { |v| v.split(": ") }]
77
+ end
78
+
79
+ # Public: checks current playback information
80
+ #
81
+ # Returns a hash with the playback information
82
+ #
83
+ def info
84
+ response = connection.get("/playback-info").response
85
+ plist = CFPropertyList::List.new(data: response.body)
86
+ hash = CFPropertyList.native_types(plist.value)
87
+ PlaybackInfo.new(hash)
88
+ end
89
+
90
+ # Public: Resumes a paused video
91
+ #
92
+ def resume
93
+ connection.async.post("/rate?value=1")
94
+ end
95
+
96
+ # Public: Pauses a playing video
97
+ #
98
+ def pause
99
+ connection.async.post("/rate?value=0")
100
+ end
101
+
102
+ # Public: Stops the video
103
+ #
104
+ def stop
105
+ connection.post("/stop")
106
+ end
107
+
108
+ def playing?; state == :playing end
109
+ def paused?; state == :paused end
110
+ def played?; state == :played end
111
+ def stopped?; state == :stopped end
112
+
113
+ # Public: Locks the execution until the video gets fully played
114
+ #
115
+ def wait
116
+ sleep 0.1 while !played? || stopped?
117
+ cleanup
118
+ end
119
+
120
+ private
121
+
122
+ def timers
123
+ @_timers ||= Timers.new
124
+ end
125
+
126
+ def connection
127
+ @_connection ||= Airplay::Connection.new(@device)
128
+ end
129
+
130
+ def persistent
131
+ @_persistent ||= Airplay::Connection.new(@device, keep_alive: true)
132
+ end
133
+
134
+ def cleanup
135
+ timers.cancel
136
+ persistent.close
137
+ end
138
+
139
+ def check_for_playback_status
140
+ timers << every(1) do
141
+ case true
142
+ when info.stopped? then @machine.trigger(:stopped) if playing?
143
+ when info.played? then @machine.trigger(:played) if playing?
144
+ when info.playing? then @machine.trigger(:playing) if !playing?
145
+ when info.paused? then @machine.trigger(:paused) if playing?
146
+ end
147
+ end
148
+ end
149
+
150
+ # Private: Get ready the state machine
151
+ #
152
+ def start_the_machine
153
+ @machine = MicroMachine.new(:stopped)
154
+
155
+ @machine.on(:stopped) { cleanup }
156
+ @machine.on(:played) { cleanup }
157
+
158
+ @machine.when(:loading, :stopped => :loading)
159
+ @machine.when(:playing, {
160
+ :paused => :playing,
161
+ :loading => :playing,
162
+ :stopped => :playing
163
+ })
164
+
165
+ @machine.when(:paused, :loading => :paused, :playing => :paused)
166
+ @machine.when(:stopped, :playing => :stopped, :paused => :stopped)
167
+ @machine.when(:played, :playing => :played, :paused => :played)
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,69 @@
1
+ require "celluloid"
2
+ require "net/ptth"
3
+ require "net/http"
4
+ require "airplay/protocol/app"
5
+ require "airplay/connection/authentication"
6
+
7
+ module Airplay::Protocol
8
+ # Public: Handles the reverse connection
9
+ #
10
+ class Reverse
11
+ include Celluloid
12
+
13
+ attr_accessor :callbacks
14
+ attr_reader :state
15
+
16
+ def initialize(device, purpose = "event")
17
+ @logger = Airplay::Logger.new("airplay::protocol::reverse")
18
+
19
+ @ptth = Net::PTTH.new("http://#{device.address}")
20
+ @ptth.set_debug_output = @logger
21
+ @state = "disconnected"
22
+ @purpose = purpose
23
+ @ptth.app = Airplay.app
24
+
25
+ @callbacks = []
26
+
27
+ async.pipeline
28
+ @ptth.app.pipeline = self.async
29
+ end
30
+
31
+ # Public: Disconnects the current connection
32
+ #
33
+ def disconnect
34
+ @state = "disconnected"
35
+ @ptth.close
36
+ end
37
+
38
+ # Public: Connects to the reverse resource and starts the switching
39
+ #
40
+ def connect
41
+ @logger.info "Connecting with purpose: #{@purpose}"
42
+
43
+ request = Net::HTTP::Post.new("/reverse")
44
+
45
+ request["X-Apple-Purpose"] = @purpose
46
+ request["X-Apple-Device-ID"] = "0x581faa7c9754"
47
+
48
+ if Airplay.active.password?
49
+ authentication = Airplay::Connection::Authentication.new(@ptth)
50
+ request = authentication.sign(request)
51
+ end
52
+
53
+ @ptth.request(request)
54
+ @state = "connected"
55
+ end
56
+
57
+ # Public: Pipelines all the incomming messages to the callback ppol
58
+ #
59
+ def pipeline
60
+ loop do
61
+ message = receive { |msg| msg.is_a? Message }
62
+ @logger.debug "Incomming message from the event pipeline: #{message}"
63
+ @callbacks.each do |callback|
64
+ callback.call(message.content)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,66 @@
1
+ module Airplay::Protocol
2
+ # Public: Handles the slideshows
3
+ #
4
+ class Slideshow
5
+ def initialize
6
+ @images = []
7
+ end
8
+
9
+ # Public: Lists all the slideshow features for the active device
10
+ #
11
+ # Returns the available themes hash
12
+ #
13
+ def features
14
+ @_features ||= begin
15
+ response = Airplay.connection.get("/slideshow-features", {
16
+ "Accept-Language" => "English"
17
+ })
18
+
19
+ plist = CFPropertyList::List.new(data: response.body)
20
+ native_plist = CFPropertyList.native_types(plist.value)
21
+
22
+ native_plist["themes"]
23
+ end
24
+ end
25
+
26
+ # Public: Adds images to the slideshow pool
27
+ #
28
+ # images - An array of images to be added
29
+ #
30
+ def <<(*images)
31
+ images.each { |image| @images << image }
32
+ end
33
+
34
+ # Public: Gets an images given a position on the list
35
+ #
36
+ # position - The position on the list
37
+ #
38
+ def [](position)
39
+ @images[position]
40
+ end
41
+
42
+ # Public: Pays the current slideshow
43
+ #
44
+ def play
45
+ @reverse = Airplay::Protocol::Reverse.new(Airplay.active, "slideshow")
46
+ @reverse.async.connect
47
+
48
+ content = {
49
+ settings: {
50
+ slideDuration: 3,
51
+ theme: "Dissolve".upcase
52
+ },
53
+ state: "playing"
54
+ }
55
+
56
+ plist = CFPropertyList::List.new
57
+ plist.value = CFPropertyList.guess(content)
58
+ binary = plist.to_str(2)
59
+
60
+ response = Airplay.connection.async.put("/slideshows/1", binary, {
61
+ "Content-Type" => "text/x-apple-plist+xml"
62
+ })
63
+
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,25 @@
1
+ module Airplay::Protocol
2
+ class Timers
3
+ include Enumerable
4
+ extend Forwardable
5
+
6
+ def_delegators :@timers, :each, :size, :empty?
7
+
8
+ def initialize
9
+ @timers = []
10
+ end
11
+
12
+ def <<(timer)
13
+ @timers << timer
14
+ end
15
+
16
+ def reset
17
+ @timers.each { |t| t.reset }
18
+ @timers = []
19
+ end
20
+
21
+ def cancel
22
+ @timers.each { |t| t.cancel }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,56 @@
1
+ require "open-uri"
2
+
3
+ module Airplay::Protocol
4
+ # Public: The class to handle image broadcast to a device
5
+ #
6
+ class Viewer
7
+ TRANSITIONS = %w(None Dissolve SlideLeft SlideRight)
8
+
9
+ def initialize(device)
10
+ @device = device
11
+ @logger = Airplay::Logger.new("airplay::protocol::image")
12
+ end
13
+
14
+ # Public: Broadcasts the content to the device
15
+ #
16
+ def view(media_or_io, options = {})
17
+ content = get_content(media_or_io)
18
+ transition = options.fetch(:transition, "None")
19
+
20
+ @logger.info "Fetched content (#{content.bytesize} bytes)"
21
+ @logger.debug "PUT /photo with transition: #{transition}"
22
+
23
+ response = connection.put("/photo", content, {
24
+ "Content-Length" => content.bytesize.to_s,
25
+ "X-Apple-Transition" => transition
26
+ })
27
+
28
+ response.response.status == 200
29
+ end
30
+
31
+ def transitions; TRANSITIONS end
32
+
33
+ private
34
+
35
+ def connection
36
+ @_connection ||= Airplay::Connection.new(@device)
37
+ end
38
+
39
+ # Private: Gets the content of the possible media_or_io
40
+ #
41
+ # media_or_io - The url, file, path or read compatible source
42
+ #
43
+ def get_content(media_or_io)
44
+ case true
45
+ when File.exists?(File.expand_path(media_or_io))
46
+ File.read(media_or_io)
47
+ when !!(media_or_io =~ URI::regexp)
48
+ open(media_or_io).read
49
+ when media_or_io.is_a?(String)
50
+ media_or_io
51
+ when media_or_io.respond_to?(:read)
52
+ media_or_io.read
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,7 @@
1
+ # Public: Stupid class to easily create and initialize a Struct from a hash
2
+ #
3
+ class Structure < Struct
4
+ def self.create(hash)
5
+ new(*members.map { |member| hash[member.to_sym] })
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ require "forwardable"
2
+ require "airplay/protocol/viewer"
3
+
4
+ module Airplay
5
+ module Viewable
6
+ extend Forwardable
7
+
8
+ def_delegators :viewer, :view, :transitions
9
+
10
+ private
11
+
12
+ def viewer
13
+ @_viewer ||= Airplay::Protocol::Viewer.new(self)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,201 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: http://192.168.100.205:7000/slideshow-features
6
+ body:
7
+ encoding: US-ASCII
8
+ base64_string: ''
9
+ headers:
10
+ user-agent:
11
+ - MediaControl/1.0
12
+ x-apple-session-id:
13
+ - bee61160-51f2-0130-f43c-705681a9dd8d
14
+ accept-language:
15
+ - English
16
+ connection:
17
+ - keep-alive
18
+ keep-alive:
19
+ - 30
20
+ response:
21
+ status:
22
+ code: 200
23
+ message: OK
24
+ headers:
25
+ date:
26
+ - Tue, 05 Feb 2013 16:49:51 GMT
27
+ content-type:
28
+ - text/x-apple-plist+xml
29
+ content-length:
30
+ - '1479'
31
+ body:
32
+ encoding: US-ASCII
33
+ base64_string: ! 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NU
34
+
35
+ WVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VO
36
+
37
+ IiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4w
38
+
39
+ LmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+dGhl
40
+
41
+ bWVzPC9rZXk+Cgk8YXJyYXk+CgkJPGRpY3Q+CgkJCTxrZXk+a2V5PC9rZXk+
42
+
43
+ CgkJCTxzdHJpbmc+RGlzc29sdmU8L3N0cmluZz4KCQkJPGtleT5uYW1lPC9r
44
+
45
+ ZXk+CgkJCTxzdHJpbmc+RGlzc29sdmU8L3N0cmluZz4KCQk8L2RpY3Q+CgkJ
46
+
47
+ PGRpY3Q+CgkJCTxrZXk+a2V5PC9rZXk+CgkJCTxzdHJpbmc+U3dpcGVSaWdo
48
+
49
+ dDwvc3RyaW5nPgoJCQk8a2V5Pm5hbWU8L2tleT4KCQkJPHN0cmluZz5Td2lw
50
+
51
+ ZSBSaWdodDwvc3RyaW5nPgoJCTwvZGljdD4KCQk8ZGljdD4KCQkJPGtleT5r
52
+
53
+ ZXk8L2tleT4KCQkJPHN0cmluZz5Td2lwZUxlZnQ8L3N0cmluZz4KCQkJPGtl
54
+
55
+ eT5uYW1lPC9rZXk+CgkJCTxzdHJpbmc+U3dpcGUgTGVmdDwvc3RyaW5nPgoJ
56
+
57
+ CTwvZGljdD4KCQk8ZGljdD4KCQkJPGtleT5rZXk8L2tleT4KCQkJPHN0cmlu
58
+
59
+ Zz5SaXBwbGU8L3N0cmluZz4KCQkJPGtleT5uYW1lPC9rZXk+CgkJCTxzdHJp
60
+
61
+ bmc+UmlwcGxlPC9zdHJpbmc+CgkJPC9kaWN0PgoJCTxkaWN0PgoJCQk8a2V5
62
+
63
+ PmtleTwva2V5PgoJCQk8c3RyaW5nPlBhZ2VDdXJsPC9zdHJpbmc+CgkJCTxr
64
+
65
+ ZXk+bmFtZTwva2V5PgoJCQk8c3RyaW5nPlBhZ2UgQ3VybDwvc3RyaW5nPgoJ
66
+
67
+ CTwvZGljdD4KCQk8ZGljdD4KCQkJPGtleT5rZXk8L2tleT4KCQkJPHN0cmlu
68
+
69
+ Zz5Db3B5TWFjaGluZTwvc3RyaW5nPgoJCQk8a2V5Pm5hbWU8L2tleT4KCQkJ
70
+
71
+ PHN0cmluZz5Db3B5IE1hY2hpbmU8L3N0cmluZz4KCQk8L2RpY3Q+CgkJPGRp
72
+
73
+ Y3Q+CgkJCTxrZXk+a2V5PC9rZXk+CgkJCTxzdHJpbmc+QmFyc1N3aXBlPC9z
74
+
75
+ dHJpbmc+CgkJCTxrZXk+bmFtZTwva2V5PgoJCQk8c3RyaW5nPkJhcnMgU3dp
76
+
77
+ cGU8L3N0cmluZz4KCQk8L2RpY3Q+CgkJPGRpY3Q+CgkJCTxrZXk+a2V5PC9r
78
+
79
+ ZXk+CgkJCTxzdHJpbmc+RGlzaW50ZWdyYXRlPC9zdHJpbmc+CgkJCTxrZXk+
80
+
81
+ bmFtZTwva2V5PgoJCQk8c3RyaW5nPkRpc2ludGVncmF0ZTwvc3RyaW5nPgoJ
82
+
83
+ CTwvZGljdD4KCQk8ZGljdD4KCQkJPGtleT5rZXk8L2tleT4KCQkJPHN0cmlu
84
+
85
+ Zz5Nb2Q8L3N0cmluZz4KCQkJPGtleT5uYW1lPC9rZXk+CgkJCTxzdHJpbmc+
86
+
87
+ TW9kPC9zdHJpbmc+CgkJPC9kaWN0PgoJCTxkaWN0PgoJCQk8a2V5PmtleTwv
88
+
89
+ a2V5PgoJCQk8c3RyaW5nPkZsYXNoPC9zdHJpbmc+CgkJCTxrZXk+bmFtZTwv
90
+
91
+ a2V5PgoJCQk8c3RyaW5nPkZsYXNoPC9zdHJpbmc+CgkJPC9kaWN0PgoJCTxk
92
+
93
+ aWN0PgoJCQk8a2V5PmtleTwva2V5PgoJCQk8c3RyaW5nPk5vbmU8L3N0cmlu
94
+
95
+ Zz4KCQkJPGtleT5uYW1lPC9rZXk+CgkJCTxzdHJpbmc+Tm9uZTwvc3RyaW5n
96
+
97
+ PgoJCTwvZGljdD4KCTwvYXJyYXk+CjwvZGljdD4KPC9wbGlzdD4K
98
+
99
+ '
100
+ http_version: '1.1'
101
+ recorded_at: Tue, 05 Feb 2013 18:49:51 GMT
102
+ - request:
103
+ method: get
104
+ uri: http://192.168.100.179:7000/slideshow-features
105
+ body:
106
+ encoding: US-ASCII
107
+ base64_string: ''
108
+ headers:
109
+ user-agent:
110
+ - MediaControl/1.0
111
+ x-apple-session-id:
112
+ - 74bc36c0-5213-0130-f46a-705681a9dd8d
113
+ accept-language:
114
+ - English
115
+ connection:
116
+ - keep-alive
117
+ keep-alive:
118
+ - 30
119
+ response:
120
+ status:
121
+ code: 200
122
+ message: OK
123
+ headers:
124
+ date:
125
+ - Tue, 05 Feb 2013 20:44:00 GMT
126
+ content-type:
127
+ - text/x-apple-plist+xml
128
+ content-length:
129
+ - '1479'
130
+ body:
131
+ encoding: US-ASCII
132
+ base64_string: ! 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NU
133
+
134
+ WVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VO
135
+
136
+ IiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4w
137
+
138
+ LmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+dGhl
139
+
140
+ bWVzPC9rZXk+Cgk8YXJyYXk+CgkJPGRpY3Q+CgkJCTxrZXk+a2V5PC9rZXk+
141
+
142
+ CgkJCTxzdHJpbmc+RGlzc29sdmU8L3N0cmluZz4KCQkJPGtleT5uYW1lPC9r
143
+
144
+ ZXk+CgkJCTxzdHJpbmc+RGlzc29sdmU8L3N0cmluZz4KCQk8L2RpY3Q+CgkJ
145
+
146
+ PGRpY3Q+CgkJCTxrZXk+a2V5PC9rZXk+CgkJCTxzdHJpbmc+U3dpcGVSaWdo
147
+
148
+ dDwvc3RyaW5nPgoJCQk8a2V5Pm5hbWU8L2tleT4KCQkJPHN0cmluZz5Td2lw
149
+
150
+ ZSBSaWdodDwvc3RyaW5nPgoJCTwvZGljdD4KCQk8ZGljdD4KCQkJPGtleT5r
151
+
152
+ ZXk8L2tleT4KCQkJPHN0cmluZz5Td2lwZUxlZnQ8L3N0cmluZz4KCQkJPGtl
153
+
154
+ eT5uYW1lPC9rZXk+CgkJCTxzdHJpbmc+U3dpcGUgTGVmdDwvc3RyaW5nPgoJ
155
+
156
+ CTwvZGljdD4KCQk8ZGljdD4KCQkJPGtleT5rZXk8L2tleT4KCQkJPHN0cmlu
157
+
158
+ Zz5SaXBwbGU8L3N0cmluZz4KCQkJPGtleT5uYW1lPC9rZXk+CgkJCTxzdHJp
159
+
160
+ bmc+UmlwcGxlPC9zdHJpbmc+CgkJPC9kaWN0PgoJCTxkaWN0PgoJCQk8a2V5
161
+
162
+ PmtleTwva2V5PgoJCQk8c3RyaW5nPlBhZ2VDdXJsPC9zdHJpbmc+CgkJCTxr
163
+
164
+ ZXk+bmFtZTwva2V5PgoJCQk8c3RyaW5nPlBhZ2UgQ3VybDwvc3RyaW5nPgoJ
165
+
166
+ CTwvZGljdD4KCQk8ZGljdD4KCQkJPGtleT5rZXk8L2tleT4KCQkJPHN0cmlu
167
+
168
+ Zz5Db3B5TWFjaGluZTwvc3RyaW5nPgoJCQk8a2V5Pm5hbWU8L2tleT4KCQkJ
169
+
170
+ PHN0cmluZz5Db3B5IE1hY2hpbmU8L3N0cmluZz4KCQk8L2RpY3Q+CgkJPGRp
171
+
172
+ Y3Q+CgkJCTxrZXk+a2V5PC9rZXk+CgkJCTxzdHJpbmc+QmFyc1N3aXBlPC9z
173
+
174
+ dHJpbmc+CgkJCTxrZXk+bmFtZTwva2V5PgoJCQk8c3RyaW5nPkJhcnMgU3dp
175
+
176
+ cGU8L3N0cmluZz4KCQk8L2RpY3Q+CgkJPGRpY3Q+CgkJCTxrZXk+a2V5PC9r
177
+
178
+ ZXk+CgkJCTxzdHJpbmc+RGlzaW50ZWdyYXRlPC9zdHJpbmc+CgkJCTxrZXk+
179
+
180
+ bmFtZTwva2V5PgoJCQk8c3RyaW5nPkRpc2ludGVncmF0ZTwvc3RyaW5nPgoJ
181
+
182
+ CTwvZGljdD4KCQk8ZGljdD4KCQkJPGtleT5rZXk8L2tleT4KCQkJPHN0cmlu
183
+
184
+ Zz5Nb2Q8L3N0cmluZz4KCQkJPGtleT5uYW1lPC9rZXk+CgkJCTxzdHJpbmc+
185
+
186
+ TW9kPC9zdHJpbmc+CgkJPC9kaWN0PgoJCTxkaWN0PgoJCQk8a2V5PmtleTwv
187
+
188
+ a2V5PgoJCQk8c3RyaW5nPkZsYXNoPC9zdHJpbmc+CgkJCTxrZXk+bmFtZTwv
189
+
190
+ a2V5PgoJCQk8c3RyaW5nPkZsYXNoPC9zdHJpbmc+CgkJPC9kaWN0PgoJCTxk
191
+
192
+ aWN0PgoJCQk8a2V5PmtleTwva2V5PgoJCQk8c3RyaW5nPk5vbmU8L3N0cmlu
193
+
194
+ Zz4KCQkJPGtleT5uYW1lPC9rZXk+CgkJCTxzdHJpbmc+Tm9uZTwvc3RyaW5n
195
+
196
+ PgoJCTwvZGljdD4KCTwvYXJyYXk+CjwvZGljdD4KPC9wbGlzdD4K
197
+
198
+ '
199
+ http_version: '1.1'
200
+ recorded_at: Tue, 05 Feb 2013 22:44:00 GMT
201
+ recorded_with: VCR 2.4.0