Not crap after all...
[amarok.git] / src / amarok_proxy.rb
blob7984ffcf74cc4f1f3dd03f53bf24aebb40aca3dd
1 #!/usr/bin/env ruby
3 # Proxy server for Last.fm and DAAP. Relays the stream from the server to localhost, and
4 # converts the protocol to http on the fly.
6 # (c) 2006 Paul Cifarelli <paul@cifarelli.net>
7 # (c) 2006 Mark Kretschmann <markey@web.de>
8 # (c) 2006 Michael Fellinger <manveru@weez-int.com>
9 # (c) 2006 Ian Monroe <ian@monroe.nu>
10 # (c) 2006 Martin Ellis <martin.ellis@kdemail.net>
11 # (c) 2006 Alexandre Oliveira <aleprj@gmail.net>
12 # (c) 2006 Tom Kaitchuck <tkaitchuck@comcast.net>
14 # License: GNU General Public License V2
16 # Amarok listens to stderr and recognizes these magic strings, do not remove them:
17 # "AMAROK_PROXY: startup", "AMAROK_PROXY: SYNC"
20 require 'socket'
21 require "uri"
22 $stdout.sync = true
24 class Proxy
25   ENDL = "\r\n"
27   def initialize( port, remote_url, engine, proxy )
28     @engine = engine
30     myputs( "running with port: #{port} and url: #{remote_url} and engine: #{engine}" )
32     # Open the amarok-facing socket
33     # amarok: the server port on the localhost to which the engine will connect.
34     amarok = TCPServer.new( port )
35     myputs( "startup" )
37     # amaroks: server socket for above.
38     amaroks = amarok.accept
40     # uri: from amarok, identifies the source of the music
41     uri = URI.parse( remote_url )
42     myputs("host " << uri.host << " ")
43     myputs( port )
45     # Now we have the source of the music, determine the HTTP request that
46     # needs to be made to the remote server (or remote proxy).  It will
47     # be of the form "GET ... HTTP/1.x".  It will include the
48     # http://hostname/ part if, and only if, we're using a remote proxy.
49     get = get_request( uri, !proxy.nil? )
51     #Check for proxy
52     begin
53       proxy_uri = URI.parse( proxy )
54       serv = TCPSocket.new( proxy_uri.host, proxy_uri.port )
55     rescue
56       serv = TCPSocket.new( uri.host, uri.port )
57     end
59     serv.sync = true
60     myputs( "running with port: #{uri.port} and host: #{uri.host}" )
62     # Read the GET request from the engine
63     amaroks_get = amaroks.readline
64     myputs( amaroks_get.inspect )
66     myputs( get.inspect )
67     myputs( "#{amaroks_get} but sending #{get}" )
68     serv.puts( get )
70     # Copy the HTTP REQUEST headers from the amarok engine to the
71     # remote server, and signal end of headers.
72     myputs( "COPY from amarok -> serv" )
73     cp_to_empty_outward( amaroks, serv )
74     safe_write( serv, "\r\n\r\n" )
76     # Copy the HTTP RESPONSE headers from the server back to the
77     # amarok engine.
78     myputs( "COPY from serv -> amarok" )
79     cp_to_empty_inward( serv, amaroks )
81     if @engine == 'gst10-engine'
82       3.times do
83         myputs( "gst10-engine waiting for reconnect" )
84         sleep 1
85         break if amaroks.eof
86       end
87       amaroks = amarok.accept
88       safe_write( amaroks, "HTTP/1.0 200 OK\r\n\r\n" )
89       amaroks.each_line do |data|
90         myputs( data )
91         data.chomp!
92         break if data.empty?
93       end
94     end
96     # Now stream the music!
97     myputs( "Before cp_all()" )
98     cp_all_inward( serv, amaroks )
100     if @engine == 'helix-engine' && amaroks.eof
101       myputs( "EOF Detected, reconnecting" )
102       amaroks = amarok.accept
103       cp_all_inward( serv, amaroks )
104     end
105   end
107   def safe_write( output, data )
108     begin
109         output.write data
110     rescue
111       myputs( "error from output.write, #{$!}" )
112       myputs( $!.backtrace.inspect )
113       break
114     end
115   end
117   def cp_to_empty_outward( income, output )
118     myputs "cp_to_empty_outward( income => #{income.inspect}, output => #{output.inspect}"
119     income.each_line do |data|
120       if data =~ /User-Agent: xine\/([0-9.]+)/
121         version = $1.split(".").collect { |v| v.to_i }
122         myputs("Found xine user agent version #{version.join(".")}")
123         @xineworkaround = ( version[0] <= 1 && version[1] <= 1 && version[2] <= 2 )
124       end
125       myputs( data )
126       data.chomp!
127       safe_write( output, data )
128       myputs( "data sent.")
129       return if data.empty?
130     end
131   end
133   def desync (data)
134       if data.gsub!( "SYNC", "" )
135         myputs( "SYNC" )
136       end
137   end
139   def cp_to_empty_inward( income, output )
140     myputs( "cp_to_empty_inward( income => #{income.inspect}, output => #{output.inspect}" )
141     income.each_line do |data|
142       myputs( data )
143       safe_write( output, data )
144       return if data.chomp == ""
145     end
146   end
148   def cp_all_inward( income, output )
149     myputs( "cp_all( income => #{income.inspect}, output => #{output.inspect}" )
150     if self.is_a?( LastFM ) and @xineworkaround
151       myputs( "Using buffer fill workaround." )
152       filler = Array.new( 4096, 0 )
153       safe_write( output, filler ) # HACK: Fill xine's buffer so that xine_open() won't block
154     end
155     if @engine == 'helix-engine'
156       data = income.read( 1024 )
157     else
158       data = income.read( 4 )
159     end
160     desync( data )
161     holdover = ""
162     loop do
163       begin
164         safe_write( output, data )
165       rescue
166         myputs( "error from o.write, #{$!}" )
167         break
168       end
169       newdata = income.read( 1024 )
171       data = holdover + newdata[0..-5]
172       holdover = newdata[-4..-1]
173       desync( data )
175       break if newdata == nil
176     end
177   end
180 class LastFM < Proxy
181 # Last.fm protocol:
182 # Stream consists of pure MP3 files concatenated, with the string "SYNC" in between, which
183 # marks a track change. The proxy notifies Amarok on track change.
185   def get_request( remote_uri, via_proxy )
186     # remote_uri - the URI of the stream we want
187     # via_proxy - true iff we're going through another proxy
188     if via_proxy then
189       url = remote_uri.to_s
190     else
191       url = "#{remote_uri.path || '/'}?#{remote_uri.query}"
192     end
193     get = "GET #{url} HTTP/1.0" + ENDL
194     get += "Host: #{remote_uri.host}:#{remote_uri.port}" + ENDL + ENDL
195   end
199 class DaapProxy < Proxy
200   def initialize( port, remote_url, engine, hash, request_id, proxy )
201     @hash = hash
202     @requestId = request_id
203     super( port, remote_url, engine, proxy )
204   end
206   def get_request( remote_uri, via_proxy )
207     # via_proxy ignored for now
208     get = "GET #{remote_uri.path || '/'}?#{remote_uri.query} HTTP/1.0" + ENDL
209     get += "Accept: */*" + ENDL
210     get += "User-Agent: iTunes/4.6 (Windows; N)" + ENDL
211     get += "Client-DAAP-Version: 3.0" + ENDL
212     get += "Client-DAAP-Validation: #{@hash}" + ENDL
213     get += "Client-DAAP-Access-Index: 2" + ENDL
214     get += "Client-DAAP-Request-ID: #{@requestId}" + ENDL
215     get += "Host: #{remote_uri.host}:#{remote_uri.port}" + ENDL + ENDL
216     get
217   end
220 def myputs( string )
221    $stdout.puts( "AMAROK_PROXY: #{string}" )
224 begin
225   myputs( ARGV )
226   if( ARGV[0] == "--lastfm" ) then
227     option, port, remote_url, engine, proxy = ARGV
228     LastFM.new( port, remote_url, engine, proxy )
229   else
230     option, port, remote_url, engine, hash, request_id, proxy = ARGV
231     DaapProxy.new( port, remote_url, engine, hash, request_id, proxy )
232   end
233 rescue
234   myputs( $!.to_s )
235   myputs( $!.backtrace.inspect )
238 puts( "exiting" )