File Coverage

blib/lib/TiVo/Calypso.pm
Criterion Covered Total %
statement 24 544 4.4
branch 0 178 0.0
condition 0 173 0.0
subroutine 8 44 18.1
pod n/a
total 32 939 3.4


line stmt bran cond sub pod time code
1             package TiVo::Calypso;
2              
3 1     1   22878 use 5.006_001;
  1         4  
  1         55  
4              
5             our $VERSION = '1.3.5';
6              
7             ## Currently requires these additional modules for full functionality:
8             ##
9             ## Storable
10             ## IO::File
11             ## MP3::Info
12             ## Digest::MD5
13             ## Encode
14             ## LWP::Simple
15              
16             # Constants for use in QueryServer message
17 1     1   6 use constant VERSION => '1';
  1         2  
  1         75  
18 1     1   6 use constant INTVERSION => $VERSION;
  1         12  
  1         53  
19 1     1   11 use constant INTNAME => 'TiVoServer BC';
  1         2  
  1         47  
20 1     1   6 use constant ORGANIZATION => 'TiVo, Inc.';
  1         2  
  1         40  
21 1     1   5 use constant COMMENT => 'Modifications by Scott Schneider, sschneid at gmail dot com';
  1         2  
  1         2849  
22              
23             # Global expiration time (in hours)
24             my $expire_hours = 48;
25              
26             ## Generic, overridable interface to dynamic class data
27             ##
28             ## Autoload will catch any method beginning with an underscore ( _ )
29             ## and convert the method name to a key value, which is used to
30             ## access the object's internal DATA hash. Methods written to
31             ## override interactions with a given key should use lvalue
32             ## syntax to maintain compatibility with other module internals.
33              
34             sub AUTOLOAD : lvalue {
35 0     0     my $self = shift;
36 0           my $param = $AUTOLOAD;
37              
38 0           $param =~ s/^.*:://;
39              
40 0 0         return unless $param =~ /^_(.+)$/;
41              
42 0           $self->{'DATA'}->{ uc($1) };
43             }
44              
45             ## TiVo::Calypso->_uri_unescape( $ )
46             ##
47             ## Decodes URI strings per RFC 2396
48              
49             sub _uri_unescape {
50 0     0     my $self = shift;
51 0           my $str = shift;
52              
53 0           $str =~ s/\+/ /g;
54 0           $str =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
  0            
55              
56 0           return $str;
57             }
58              
59             ## TiVo::Calypso->_uri_escape( $ )
60             ##
61             ## Encodes URI strings per RFC 2396
62              
63             sub _uri_escape {
64 0     0     my $self = shift;
65 0   0       my $str = shift || return undef;
66              
67 0           $str =~ s/([^A-Za-z0-9\+\-\/_.!~*'() ])/sprintf("%%%02X", ord($1))/eg;
  0            
68 0           $str =~ s/ /+/g;
69              
70 0           return $str;
71             }
72              
73             ## TiVo::Calypso->_servicename( $ )
74             ##
75             ## Returns the service name (first element of object path) of the object
76             ## or passed argument
77              
78             sub _servicename {
79 0     0     my $self = shift;
80 0   0       my $path = shift || $self->_Object || return undef;
81              
82 0           $path =~ /^(\/[^\/]*)/;
83              
84 0           return $1;
85             }
86              
87             ## TiVo::Calypso->_basename( $ )
88             ##
89             ## Returns the basename (filename) of the object's internal Path
90             ## or passed argument
91              
92             sub _basename {
93 0     0     my $self = shift;
94 0   0       my $path = shift || $self->_Path || return undef;
95              
96 0           my @path_parts = split( /\//, $path );
97              
98 0           return pop @path_parts;
99             }
100              
101             ## TiVo::Calypso->_query_container
102             ##
103             ## Returns a data structure (suitable for use with xml_out) which
104             ## describes this object in response to a QueryContainer command
105              
106             sub _query_container {
107 0     0     my $self = shift;
108 0           my $params = shift;
109              
110 0   0       my $script_name = $params->_EnvScriptName || "";
111              
112 0   0       my $details = {
113             'Item' => [
114             {
115             'Details' => {
116             'Title' => $self->_Title || $self->_basename,
117             'ContentType' => $self->_ContentType,
118             'SourceFormat' => $self->_SourceFormat
119             }
120             },
121             {
122             'Links' =>
123             { 'Content' => { 'Url' => $script_name . $self->_Url } }
124             }
125             ]
126             };
127              
128 0           return $details;
129             }
130              
131             ##############################################################################
132             # TiVo::Calypso::Server
133             # The core server object for processing requests
134             ##############################################################################
135             package TiVo::Calypso::Server;
136             @ISA = ('TiVo::Calypso');
137              
138             ## TiVo::Calypso::Server->new( % )
139             ##
140             ## Constructor for TiVo::Calypso::Server. Accepts parameters via arguement
141             ## hash.
142             ## SERVER_NAME
143             ## CACHE_DIR
144              
145             sub new {
146 0     0     my $class = shift;
147              
148 0           my $self = {};
149 0           bless $self, $class;
150              
151 0           my %params = (@_);
152              
153 0   0       $self->_Name = $params{'SERVER_NAME'} || 'TiVo Server';
154 0           $self->_CacheDir = $params{'CACHE_DIR'};
155              
156 0           $self->_Services = {};
157              
158 0           return $self;
159             }
160              
161             ## TiVo::Calypso::Server->load_cache( $ )
162             ##
163             ## Loads the requested object from the external cache.
164              
165             sub load_cache {
166 0     0     my $self = shift;
167 0   0       my $path = shift || return undef;
168              
169 0   0       my $cache_dir = $self->_CacheDir || return undef;
170              
171 0           require Storable;
172 0           require Digest::MD5;
173              
174 0           my $cache_name = Digest::MD5::md5_hex($path);
175 0           my $cache_path = "$cache_dir/$cache_name";
176 0           my $rval = eval { Storable::retrieve("$cache_path") };
  0            
177              
178             # Check if it is expired.
179 0           my $expiration = $self->expire( $rval, $path, $cache_path );
180              
181 0           return $rval;
182             }
183              
184             ## TiVo::Calypso::Server->expire( $ )
185             ##
186             ## Checks the external cache vs. the source file for the
187             ## object to determine if the object is expired. If the
188             ## object is expired, it marks it as expired.
189              
190             sub expire {
191 0     0     my $self = shift;
192 0   0       my $item = shift || return 1;
193 0   0       my $path = shift || return 1;
194 0   0       my $cache_path = shift || return 1;
195              
196             # If the file exists, and if it was retrieved, check
197             # if it is expired.
198 0           my $rval = 1; # Assume it's expired
199 0 0         if ( defined($item) ) {
200 0           my $service = $self->_servicename($path);
201              
202 0 0         return 1 unless defined $self->_Services->{$service};
203 0           $service = $self->_Services->{$service};
204              
205 0           my $real_path = $service->obj_to_path($path);
206              
207             # Check file times.
208 0           my @st_cache = stat($cache_path);
209 0           my $ctime = $st_cache[9];
210              
211 0 0         if ( !-r $real_path ) {
212              
213             # Path not found. Expire item.
214 0           $item->_Expired = 1;
215             }
216             else {
217              
218             # Path exists, compare times to see if we need
219             # to expire it.
220 0           my @st_orig = stat($real_path);
221 0           my $otime = $st_orig[9];
222              
223 0           my $timenow = time();
224 0           my $xtime = $timenow - ( $expire_hours * 60 * 60 );
225              
226             # If it's expired, mark it as so. It's expired if either
227             # the actual file is newer than the cached file/directory,
228             # or the cache file is older than the allowed expiration
229             # duration (the current time - $expire_hours ).
230 0 0 0       if ( $otime > $ctime || $ctime < $xtime ) {
231              
232             # Expire item from cache and remove.
233 0           $item->_Expired = 1;
234              
235 0           unlink $cache_path;
236             }
237             else {
238 0           $item->_Expired = 0;
239             }
240             }
241              
242 0           $rval = $item->_Expired;
243             }
244              
245 0           return $rval;
246             }
247              
248             ## TiVo::Calypso::Server->store_cache( $ )
249             ##
250             ## Stores the given object in the external cache.
251              
252             sub store_cache {
253 0     0     my $self = shift;
254 0   0       my $object = shift || return undef;
255              
256 0   0       my $cache_dir = $self->_CacheDir || return undef;
257              
258 0           require Storable;
259 0           require Digest::MD5;
260              
261 0           my $cache_name = Digest::MD5::md5_hex( $object->_Object );
262              
263 0           my $rval = eval { Storable::store( $object, "$cache_dir/$cache_name" ); };
  0            
264              
265 0           return $rval;
266             }
267              
268             ## TiVo::Calypso::Server->freeze( $ )
269             ##
270             ## Stores the given Object in memory and passes it to the server's current
271             ## external cache functions
272              
273             sub freeze {
274 0     0     my $self = shift;
275 0   0       my $object = shift || return undef;
276              
277 0           return $self->store_cache($object);
278             }
279              
280             ## TiVo::Calypso::Server->thaw( $ )
281             ##
282             ## Returns the requested Object from Cache, creating it when necessary
283              
284             sub thaw {
285 0     0     my $self = shift;
286 0   0       my $path = shift || return undef;
287              
288 0           my ($item);
289              
290 0 0         $item = $self->load_cache($path) unless $path eq '/Shuffle';
291              
292 0 0 0       if ( !defined($item) || $item->_Expired == 1 ) {
293 0           $item = $self->create_object($path);
294              
295 0           $self->freeze($item);
296             }
297              
298 0           return $item;
299             }
300              
301             ## TiVo::Calypso::Server->create_object( $ )
302             ##
303             ## Creates a new Item or Container object using the full filesystem
304             ## path provided.
305              
306             sub create_object {
307 0     0     my $self = shift;
308 0   0       my $path = shift || return undef;
309              
310 0           my ($item);
311              
312             # Check for '/' special condition
313 0 0         if ( $path eq '/' ) {
    0          
    0          
314 0   0       $item = TiVo::Calypso::Container::Server->new(
315             SERVICE => "/",
316             TITLE => $self->_Name
317             )
318             || return undef;
319              
320 0           my @contents =
321 0           map { $self->_Services->{$_} } keys %{ $self->_Services };
  0            
322              
323 0           $item->_Contents = \@contents;
324             }
325             elsif ( $path eq '/Shuffle' ) {
326 0           my $service = '/Music';
327              
328 0 0         return undef unless defined $self->_Services->{$service};
329 0           $service = $self->_Services->{$service};
330              
331 0           $path = $service->obj_to_path($path);
332              
333 0   0       $item = TiVo::Calypso::Container->new(
334             PATH => $path,
335             SERVICE => $service
336             )
337             || return undef;
338             }
339             elsif ( $path =~ /\/Browse\// ) {
340 0           my $service = '/Music';
341              
342 0 0         return undef unless defined $self->_Services->{$service};
343 0           $service = $self->_Services->{$service};
344              
345 0           $path = $service->obj_to_path($path);
346              
347 0 0         return undef if grep { /^\.\.$/ } split( /\//, $path );
  0            
348              
349 0 0         my $letter = $1 if $path =~ /.+?\/Browse\/(.*)/;
350 0 0         $path = $1 if $path =~ /(.+?)\/Browse.*/;
351              
352 0 0         opendir( DIR, $path ) || return undef;
353              
354 0           while ( defined( my $file = readdir DIR ) ) {
355 0 0         next if $file =~ /^\./;
356              
357 0 0         if ( defined $server ) {
358 0           my $object_path = $self->_Object . "/" . $file;
359 0   0       my $child = $server->thaw($object_path) || next;
360              
361 0           push( @contents, $child );
362             }
363             else {
364 0 0         next unless $file =~ /^$letter/;
365              
366 0           my $full_path = $path . "/" . $file;
367              
368 0 0         if ( -d $full_path ) {
    0          
369 0   0       my $child = TiVo::Calypso::Container->new(
370             PATH => $full_path,
371             SERVICE => $service
372             )
373             || next;
374              
375 0           push( @contents, $child );
376             }
377             elsif ( -r $full_path ) {
378 0           my @parts = split( /\./, $full_path );
379 0           my $suffix = uc( pop @parts );
380              
381 0           my $class = "TiVo::Calypso::Item::$suffix";
382             my $child =
383 0   0       eval { $class->new( $full_path, $self->_Service ); }
384             || next;
385              
386 0           push( @contents, $child );
387             }
388             }
389             }
390              
391 0           closedir(DIR);
392              
393 0           $self->_Contents = \@contents;
394 0 0         $server->freeze($self) if defined $server;
395              
396 0   0       $item = TiVo::Calypso::Container->new(
397             SERVICE => "/Music/Browse/$letter",
398             TITLE => "/Music/Browse/$letter"
399             )
400             || return undef;
401              
402 0           $item->_Contents = \@contents;
403             }
404              
405             # Perform filesystem scan
406             else {
407 0           my $service = $self->_servicename($path);
408              
409 0 0         return undef unless defined $self->_Services->{$service};
410 0           $service = $self->_Services->{$service};
411              
412 0           $path = $service->obj_to_path($path);
413              
414 0 0         return undef if grep { /^\.\.$/ } split( /\//, $path );
  0            
415              
416             # Create a directory container
417 0 0         if ( -d $path ) {
    0          
418 0   0       $item = TiVo::Calypso::Container->new(
419             PATH => $path,
420             SERVICE => $service
421             )
422             || return undef;
423             }
424              
425             # Create a file item
426             elsif ( -r $path ) {
427 0           my @parts = split( /\./, $path );
428 0           my $suffix = uc( pop @parts );
429              
430 0           my $class = "TiVo::Calypso::Item::$suffix";
431 0   0       $item = eval { $class->new( $path, $service ); } || return undef;
432             }
433             }
434              
435 0   0       return $item || undef;
436             }
437              
438             ## TiVo::Calypso::Server->add_service( $ )
439             ##
440             ## Adds a TiVo::Calypso::Container object to the service list for this server.
441              
442             sub add_service {
443 0     0     my $self = shift;
444 0   0       my $service = shift || return undef;
445              
446 0           $self->_Services->{ $service->_Object } = $service;
447 0           $self->freeze($service);
448              
449 0           return $self->_Services->{ $service->_Object };
450             }
451              
452             ## TiVo::Calypso::Server->request( $ $ $ )
453             ##
454             ## Processes a client request and returns the output from the appropriate
455             ## command method. The return value is a list: first element
456             ## is a scalar containing the mime-type of the returned data, second
457             ## element is a reference to the data itself. Both scalar refs and
458             ## IO::File refs may be returned, so the calling application must check
459             ## for and support both types.
460              
461             sub request {
462 0     0     my $self = shift;
463 0   0       my $params = shift || return undef;
464              
465             # Use a passed TiVo::Calypso::Request object if given or
466             # create a TiVo::Calypso::Request object from arguments if needed
467 0 0         if ( ( ref $params ) !~ /^TiVo::Calypso::Request/ ) {
468              
469             # See TiVo::Calypso::Request for the proper syntax of these arguments
470 0           my $script_name = $params;
471 0           my $path_info = shift;
472 0           my $query_string = shift;
473              
474 0           $params = TiVo::Calypso::Request->new( $script_name, $path_info, $query_string );
475             }
476              
477             # File transfer requested? (binary output)
478 0 0 0       if ( defined( $params->_EnvPathInfo ) && $params->_EnvPathInfo ) {
479 0           my $path_info = $self->_uri_unescape( $params->_EnvPathInfo );
480              
481 0   0       my $item = $self->thaw($path_info) || return undef;
482              
483 0 0         $self->scrobble($item) if $item->_Service->_Scrobble;
484              
485 0           my ( $headers, $ref ) = $item->send( $params, $self );
486              
487 0           my $isDirty = $item->_Dirty;
488              
489 0 0         if ( $isDirty == 1 ) {
490 0           $item->_Dirty = 0;
491 0           $self->freeze($item);
492             }
493              
494 0           return ( $headers, $ref );
495             }
496              
497             # Command given? (XML output)
498             else {
499 0   0       my $command = uc( $params->_Command ) || 'QUERYCONTAINER';
500              
501             # Create and eval the method name dynamically
502 0           my $method = "command_$command";
503 0           my $response = eval { $self->$method($params); };
  0            
504              
505             # Call command_UNKNOWN if the eval failed
506 0 0         if ( !defined $response ) {
507 0           $response = $self->command_UNKNOWN($@);
508             }
509              
510             # Set the default mime-type to be returned
511 0           my $mime_type = 'text/xml';
512              
513             # Check to see if clint requested a different format
514 0 0         if ( defined( $params->_Format ) ) {
515 0           $mime_type = $params->_Format;
516              
517             # If text/html was requested, simply display the xml as plaintext
518 0 0         if ( $mime_type eq 'text/html' ) {
519 0           $mime_type = 'text/plain';
520             }
521             }
522              
523 0   0       my $xml = $self->xml_out($response) || return undef;
524              
525             # Wrap XML with header and footer
526 0           my $return = "\n";
527 0           $return .= $xml;
528 0           $return .= "\n";
529              
530 0           my $headers = {
531             'Content-Type' => $mime_type,
532             'Content-Length' => length $return
533             };
534              
535 0           return ( $headers, \$return );
536             }
537              
538 0           my $response = $self->command_QUERYCONTAINER($params);
539              
540 0           return undef;
541             }
542              
543             sub scrobble {
544 0     0     my $self = shift;
545 0   0       my $item = shift || return undef;
546              
547 0           require Digest::MD5;
548              
549 1     1   1184 use Encode;
  1         11879  
  1         80  
550 1     1   658 use LWP::Simple;
  1         62557  
  1         8  
551              
552 0           my ( $sec, $min, $hour, $day, $month, $year ) =
553             (localtime)[ 0, 1, 2, 3, 4, 5 ];
554 0           my $utc_date = sprintf(
555             "%04d-%02d-%02d %02d:%02d:%02d",
556             ( $year + 1900 ),
557             ( $month + 1 ),
558             $day, ( $hour + 6 ),
559             $min, $sec
560             );
561              
562 0           my $handshake =
563             $item->_Service->_ScrobblePostURL
564             . '/?hs=true'
565             . '&p=1.1'
566             . '&c=tst'
567             . '&v=1.0'
568             . '&u='
569             . $item->_Service->_ScrobbleU;
570              
571 0           my ( $update, $challenge, $post_url, $interval ) = split /\n/,
572             get($handshake);
573              
574 0           my $password_md5 = Digest::MD5::md5_hex( $item->_Service->_ScrobbleP );
575 0           my $md5_password_digest =
576             Digest::MD5::md5_hex( $password_md5 . $challenge );
577              
578 0           for ( $item->_Service->_ScrobbleU,
579             $item->_Artist, $item->_Title, $item->_Album, $item->_Duration,
580             $utc_date )
581             {
582 0           $_ = encode( 'utf8', $_ );
583             }
584              
585 0           my $scrobblepost =
586             $post_url
587             . '?u=' . $item->_Service->_ScrobbleU
588             . '&s=' . $md5_password_digest
589             . '&a[0]=' . $item->_Artist
590             . '&t[0]=' . $item->_Title
591             . '&b[0]=' . $item->_Album
592             . '&m[0]=' . ''
593             . '&l[0]=' . $item->_Duration
594             . '&i[0]=' . $utc_date;
595              
596 0           my @response = split /\n/, get($scrobblepost);
597              
598 0           return;
599             }
600              
601             ## TiVo::Calypso::Server->xml_out( $ [$] )
602             ##
603             ## Converts a referenced hash/array data structure to XML. Use array
604             ## references to pass keys when order of the resulting XML tags
605             ## is important. Keys passed in a hash reference will have no
606             ## predictable ordering.
607              
608             sub xml_out {
609 0     0     my $self = shift;
610 0   0       my $data = shift || return undef;
611 0   0       my $indent = shift || 0;
612              
613 0           my $return;
614              
615             my @keys;
616              
617 0           my $data_type = ref $data;
618              
619             # Process each key if the passed reference was a hash
620 0 0         if ( $data_type eq 'HASH' ) {
    0          
621 0           foreach my $key ( keys %$data ) {
622              
623             # Force undef values to empty strings before printing
624 0 0         $data->{$key} = "" unless defined( $data->{$key} );
625              
626 0           my $key_type = ref( $data->{$key} );
627              
628             # Recurse again if the child key is another hash
629 0 0         if ( $key_type eq 'HASH' ) {
    0          
630 0           $return .= ' ' x $indent . "<$key>\n";
631 0   0       $return .= $self->xml_out( $data->{$key}, $indent + 2 ) || "";
632 0           $return .= ' ' x $indent . "\n";
633             }
634              
635             # Recurse on each element if the child key is an array
636             elsif ( $key_type eq 'ARRAY' ) {
637 0           $return .= ' ' x $indent . "<$key>\n";
638 0           foreach my $item ( @{ $data->{$key} } ) {
  0            
639 0   0       $return .= $self->xml_out( $item, $indent + 2 ) || "";
640             }
641 0           $return .= ' ' x $indent . "\n";
642             }
643              
644             # Assume the child is a text node otherwise, and print
645             else {
646 0           $return .=
647             ' ' x $indent . "<$key>" . $data->{$key} . "\n";
648             }
649             }
650             }
651              
652             # Recurse on each element if the passed ref is an array
653             elsif ( $data_type eq 'ARRAY' ) {
654 0           foreach my $item (@$data) {
655 0           $return .= $self->xml_out( $item, $indent );
656             }
657             }
658              
659             # What's this? Print it and hope for the best
660             else {
661 0           $return .= "$data\n";
662             }
663              
664 0           return $return;
665             }
666              
667             ## TiVo::Calypso::Server->command_QUERYSERVER( $ )
668             ##
669             ## Generates response to QueryServer command
670             ## Expects to be passed a TiVo::Calypso::Request object
671             ## Returns data structure suitable for use with xml_out
672              
673             sub command_QUERYSERVER {
674 0     0     my $self = shift;
675 0           my $params = shift;
676              
677 0   0       my $return = {
      0        
678             'TiVoServer' => {
679             'Version' => $self->VERSION,
680             'InternalVersion' => $self->INTVERSION,
681             'InternalName' => $self->INTNAME,
682             'Organization' => $self->_Organization || $self->ORGANIZATION,
683             'Comment' => $self->_Comment || $self->COMMENT
684             }
685             };
686              
687 0           return $return;
688             }
689              
690             ## TiVo::Calypso::Server->command_QUERYCONTAINER( $ )
691             ##
692             ## Generates response to QueryContainer command
693             ## Expects to be passed a TiVo::Calypso::Request object
694             ## Returns data structure suitable for use with xml_out
695              
696             sub command_QUERYCONTAINER {
697 0     0     my $self = shift;
698 0           my $params = shift;
699              
700 0           my $container = $params->_Container;
701              
702             # Return service containers unless otherwise requested
703 0 0         $container = '/' unless defined $container;
704              
705 0           my ($object);
706              
707 0   0       $object = $self->thaw($container) || return undef;
708              
709 0           my @list;
710              
711 0 0 0       if ( defined( $params->_Recurse ) && uc( $params->_Recurse ) eq 'YES' ) {
712              
713             # Explode the content list and get a recursive flat list of objects
714 0           @list = @{ $object->explode($self) };
  0            
715             }
716             else {
717              
718             # Take the top-level list of objects and remove any subfolder list refs
719              
720 0           @list = @{ $object->contents($self) };
  0            
721              
722 0           @list = grep { ref($_) ne 'ARRAY' } @list;
  0            
723              
724             # We'll always perform the default Sort of Type,Title
725 0 0         unless ( $params->_Container eq '/Shuffle' ) {
726 0 0 0       @list = sort {
727 0           return -1
728             if ( ref $a ) =~ /^TiVo::Calypso::Container/
729             && ( ref $b ) =~ /^TiVo::Calypso::Item/;
730 0 0 0       return 1
731             if ( ref $b ) =~ /^TiVo::Calypso::Container/
732             && ( ref $a ) =~ /^TiVo::Calypso::Item/;
733              
734 0           return uc( $a->_Path ) cmp uc( $b->_Path );
735             } @list;
736             }
737             }
738              
739             =n/a
740             # Filters are, at this time, broken. -ss
741              
742             # Apply any requested filters
743             if ( defined( $params->_Filter ) ) {
744             my %types;
745             my @filters;
746              
747             if ( $params->_Filter =~ /,/ ) {
748             @filters = split( /,/, $params->_Filter );
749             }
750             else {
751             @filters = ( $params->_Filter );
752             }
753              
754             # Construct a list of every possible matching type instead
755             # of matching against each object's SourceFormat individually
756             my $possible_types = $object->_Service->_MediaTypes;
757             $possible_types->{'FOLDER'} = 'x-container/folder';
758              
759             foreach my $filter (@filters) {
760             my ( $major, $minor ) = split( /\//, $filter );
761              
762             $major = $major || '*';
763             $minor = $minor || '*';
764              
765             # Compare the filter to each supported MediaType for this service
766             foreach my $supported ( keys %$possible_types ) {
767             my ( $s_major, $s_minor ) =
768             split( /\//, $possible_types->{$supported} );
769              
770             if ( ( $major eq $s_major || $major eq '*' )
771             && ( $minor eq $s_minor || $minor eq '*' ) )
772             {
773             $types{"$s_major/$s_minor"} = 1;
774             }
775             }
776             }
777              
778             @list = grep { defined( $types{ $_->_SourceFormat } ) } @list;
779             }
780             =cut
781              
782 0           my $total_duration = 0;
783              
784             # Check for any audio files that passed the Filter and sum their Duration
785 0           foreach (@list) {
786 0 0         if ( defined( $_->_Duration ) ) {
787 0           $total_duration += $_->_Duration;
788             }
789             }
790              
791             # Perform any requested sorts. Currently incomplete, only supports Random
792             # and Type,Title
793 0 0         if ( defined( $params->_SortOrder ) ) {
794 0 0         if ( uc( $params->_SortOrder ) eq 'RANDOM' ) {
795              
796             # Remove RandomStart from the object list before sorting
797 0           my $start;
798 0 0         if ( defined( $params->_RandomStart ) ) {
799 0           my $prefix = $params->_EnvScriptname;
800              
801 0           my $short_start = $params->_RandomStart;
802 0           $short_start =~ s/^$prefix//;
803              
804 0           foreach my $i ( 0 .. $#list ) {
805 0 0         next unless defined $list[$i]->_Url;
806 0 0         next unless $list[$i]->_Url eq $short_start;
807              
808 0           $start = splice( @list, $i, 1 );
809 0           last;
810             }
811              
812             }
813              
814 0 0         srand( $params->_RandomSeed ) if defined $params->_RandomSeed;
815              
816 0           my $i;
817 0           for ( $i = @list ; --$i ; ) {
818 0           my $j = int rand( $i + 1 );
819 0 0         next if $i == $j;
820 0           @list[ $i, $j ] = @list[ $j, $i ];
821             }
822              
823             # Reattach RandomStart as the first object
824 0 0         unshift( @list, $start ) if defined $start;
825             }
826             }
827              
828 0   0       my $count = scalar @list || 0;
829              
830             # Anchor defaults to first item
831 0           my $anchor_pos = 0;
832              
833 0 0         if ( defined( $params->_AnchorItem ) ) {
834 0           my $prefix = $params->_EnvScriptname;
835              
836 0           my $short_anchor = $params->_AnchorItem;
837 0           $short_anchor =~ s/^$prefix//;
838              
839 0           foreach my $i ( 0 .. $#list ) {
840 0 0         next unless defined $list[$i]->_Url;
841 0 0         next unless $list[$i]->_Url eq $short_anchor;
842              
843 0           $anchor_pos = $i + 1;
844 0           last;
845             }
846              
847             # Adjust the anchor position if a positive or negative offset is given
848 0 0         if ( defined( $params->_AnchorOffset ) ) {
849 0   0       my $anchor_offset = $params->_AnchorOffset || 0;
850 0           $anchor_pos += $anchor_offset;
851             }
852              
853             }
854              
855             # Trim return list, if requested
856 0 0         if ( defined( $params->_ItemCount ) ) {
857 0           my $count = $params->_ItemCount;
858              
859             # Wrap the pointer if a negative count is requested
860 0 0         if ( $count < 0 ) {
861 0           $count *= -1;
862              
863             # Jump to end of list if no Anchor is provided
864 0 0         if ( defined( $params->_AnchorItem ) ) {
865 0           $anchor_pos -= $count + 1;
866             }
867             else {
868 0           $anchor_pos = $#list - $count + 1;
869             }
870             }
871              
872             # Check for under/overflow
873 0 0 0       if ( $anchor_pos >= 0 && $anchor_pos <= $#list ) {
874 0           @list = splice( @list, $anchor_pos, $count );
875             }
876             else {
877 0           $anchor_pos = 0;
878 0           undef @list;
879 0           undef $params->_AnchorItem;
880 0           undef $params->_AnchorOffset;
881 0           undef $params->_ItemCount;
882 0           return $self->command_QUERYCONTAINER( $params );
883             }
884             }
885              
886             # Build description of each item to be returned
887 0           my @children;
888 0           foreach my $child (@list) {
889 0           push( @children, $child->_query_container($params) );
890             }
891              
892 0   0       my $return = {
      0        
      0        
893             'TiVoContainer' => [
894             {
895             'Details' => {
896             'Title' => $object->_Title,
897             'ContentType' => $object->_ContentType
898             || 'x-container/folder',
899             'SourceFormat' => $object->_SourceFormat
900             || 'x-container/folder',
901             'TotalItems' => $count,
902             'TotalDuration' => $total_duration
903             }
904             },
905             { 'ItemStart' => $anchor_pos },
906             { 'ItemCount' => scalar @children || 0 },
907             \@children
908             ]
909             };
910              
911 0           return $return;
912             }
913              
914             ## TiVo::Calypso::Server->command_UNKNOWN( $ )
915             ##
916             ## Generates response to Unknown commands
917             ## Expects to be passed a TiVo::Calypso::Request object
918             ## Returns data structure suitable for use with xml_out
919              
920             sub command_UNKNOWN {
921 0     0     my $self = shift;
922 0           my $params = shift;
923              
924 0           return {};
925             }
926              
927             ##############################################################################
928             # TiVo::Calypso::Container
929             # Attaches TiVo methods to a particular directory
930             ##############################################################################
931             package TiVo::Calypso::Container;
932             @ISA = ('TiVo::Calypso');
933              
934             ## TiVo::Calypso::Container->new( % )
935             ##
936             ## Generic TiVo::Calypso::Container constructor
937             ## Accepts parameters via an argument hash.
938             ## Expects to be passed a full pathname and either a string describing
939             ## the service prefix (if this container is to be a service) or another
940             ## TiVo::Calypso::Container object (if this container is to be a subdirectory
941             ## of an existing service).
942              
943             sub new {
944 0     0     my $class = shift;
945              
946 0           my $self = {};
947 0           bless $self, $class;
948              
949 0           my %params = (@_);
950              
951 0   0       my $service = $params{'SERVICE'} || return undef;
952 0           $self->_Path = $params{'PATH'};
953              
954             # This container is a subdirectory
955 0 0         if ( ( ref $service ) =~ /^TiVo::Calypso::Container/ ) {
956 0   0       $self->_Object = $service->path_to_obj( $self->_Path ) || return undef;
957 0           $self->_Service = $service;
958             }
959              
960             # This container is a service container
961             else {
962 0           $self->_Object = $service;
963 0           $self->_Service = $self;
964             }
965              
966             # Set folder title, if provided
967 0           $self->_Title = $params{'TITLE'};
968              
969             # Defaults common to all Containers
970 0           $self->_SourceFormat = 'x-container/folder';
971 0           $self->_Url =
972             '?Command=QueryContainer&Container='
973             . $self->_uri_escape( $self->_Object );
974              
975 0           $self->_Expired = 0;
976              
977             # Call class-specific init method
978 0 0         $self->init(%params) || return undef;
979              
980 0           return $self;
981             }
982              
983             ## TiVo::Calypso::Container->init( )
984             ##
985             ## Generic TiVo::Calypso::Container initialization
986              
987             sub init {
988 0     0     my $self = shift;
989              
990 0           $self->_ContentType = 'x-container/folder';
991              
992 0   0       $self->_Title = $self->_Title || $self->_basename;
993              
994 0           return 1;
995             }
996              
997             ## TiVo::Calypso::Container->path_to_obj( $ )
998             ##
999             ## Converts the given pathname to an object path relative to the
1000             ## current service
1001              
1002             sub path_to_obj {
1003 0     0     my $self = shift;
1004 0   0       my $path = shift || return undef;
1005              
1006 0           my $service_p = $self->_Path;
1007 0           my $service_o = $self->_Object;
1008              
1009 0           $path =~ s/^$service_p/$service_o/;
1010              
1011 0           return $path;
1012             }
1013              
1014             ## TiVo::Calypso::Container->obj_to_path( $ )
1015             ##
1016             ## Converts the given object path (relative to the current service) to
1017             ## a full filesystem pathname
1018              
1019             sub obj_to_path {
1020 0     0     my $self = shift;
1021 0   0       my $path = shift || return undef;
1022              
1023 0           my $service_p = $self->_Path;
1024 0           my $service_o = $self->_Object;
1025              
1026 0           $path =~ s/^$service_o/$service_p/;
1027              
1028 0           return $path;
1029             }
1030              
1031             ## TiVo::Calypso::Container->contents( $ )
1032             ##
1033             ## Returns the contents of a TiVo::Calypso::Container directory as a list ref
1034             ## of Item and Container objects.
1035              
1036             sub contents {
1037 0     0     my $self = shift;
1038 0           my $server = shift;
1039              
1040 0 0         return $self->_Contents if defined $self->_Contents;
1041              
1042 0           my @contents;
1043              
1044 0           local *DIR;
1045 0 0         if ( $self->_Path eq '/Shuffle' ) {
    0          
1046 0           my ( @artists, @songs );
1047              
1048 0 0         opendir( DIR, $server->_Services->{'/Shuffle'}->_Path ) || return undef;
1049              
1050 0           while ( defined( my $file = readdir DIR ) ) {
1051 0 0         next if $file =~ /^\./;
1052              
1053 0           push @artists, $file;
1054             }
1055              
1056 0           closedir(DIR);
1057              
1058 0           srand();
1059              
1060 0           for (1) {
1061 0           my ( @albums, @songlist );
1062              
1063 0           my $artist = $artists[ rand @artists ];
1064              
1065 0 0         opendir( DIR,
1066             $server->_Services->{'/Shuffle'}->_Path . '/' . $artist )
1067             || return undef;
1068              
1069 0           while ( defined( my $file = readdir DIR ) ) {
1070 0 0         next if $file =~ /^\./;
1071              
1072 0           push @albums, $artist . '/' . $file;
1073             }
1074              
1075 0           closedir(DIR);
1076              
1077 0           my $album = $albums[ rand @albums ];
1078              
1079 0 0         opendir( DIR,
1080             $server->_Services->{'/Shuffle'}->_Path . '/' . $album )
1081             || return undef;
1082              
1083 0           while ( defined( my $file = readdir DIR ) ) {
1084 0 0         next if $file =~ /^\./;
1085              
1086 0           push @songlist, $album . '/' . $file;
1087              
1088             }
1089              
1090 0           closedir(DIR);
1091              
1092 0           push @songs, $songlist[ rand @songlist ];
1093             }
1094              
1095 0           foreach my $song (@songs) {
1096 0           my @parts = split( /\./, $song );
1097 0           my $suffix = uc( pop @parts );
1098              
1099 0           my $class = "TiVo::Calypso::Item::$suffix";
1100 0   0       my $child = eval {
1101             $class->new(
1102             $server->_Services->{'/Shuffle'}->_Path . '/' . $song,
1103             $self->_Service );
1104             } || next;
1105              
1106 0           push @contents, $child;
1107             }
1108             }
1109             elsif ( $self->_Path eq $server->_Services->{'/Music'}->_Path ) {
1110 0           foreach (qw/ * A B C D E F G H I J K L M N O P Q R S T U V W X Y Z /) {
1111 0   0       my $child = TiVo::Calypso::Container->new(
1112             PATH => $self->_Path . "/Browse/" . $_,
1113             SERVICE => $self->_Service
1114             )
1115             || next;
1116              
1117 0           push( @contents, $child );
1118             }
1119             }
1120             else {
1121 0 0         opendir( DIR, $self->_Path ) || return undef;
1122              
1123 0           while ( defined( my $file = readdir DIR ) ) {
1124 0 0         next if $file =~ /^\./;
1125              
1126 0 0         if ( defined $server ) {
1127              
1128 0           my $object_path = $self->_Object . "/" . $file;
1129 0   0       my $child = $server->thaw($object_path) || next;
1130              
1131 0           push( @contents, $child );
1132              
1133             }
1134             else {
1135              
1136 0           my $full_path = $self->_Path . "/" . $file;
1137              
1138 0 0         if ( -d $full_path ) {
    0          
1139              
1140 0   0       my $child = TiVo::Calypso::Container->new(
1141             PATH => $full_path,
1142             SERVICE => $self->_Service
1143             )
1144             || next;
1145              
1146 0           push( @contents, $child );
1147              
1148             }
1149             elsif ( -r $full_path ) {
1150              
1151 0           my @parts = split( /\./, $full_path );
1152 0           my $suffix = uc( pop @parts );
1153              
1154 0           my $class = "TiVo::Calypso::Item::$suffix";
1155             my $child =
1156 0   0       eval { $class->new( $full_path, $self->_Service ); }
1157             || next;
1158              
1159 0           push( @contents, $child );
1160             }
1161              
1162             }
1163              
1164             }
1165              
1166 0           closedir(DIR);
1167             }
1168              
1169             # Cache the new information we just built
1170 0           $self->_Contents = \@contents;
1171 0 0         $server->freeze($self) if defined $server;
1172              
1173 0           return \@contents;
1174             }
1175              
1176             ## TiVo::Calypso::Container->explode( $ )
1177             ##
1178             ## Converts the single-directory Container and Item list format of an
1179             ## object's contents() to a recursive list of all Containers and Items.
1180              
1181             sub explode {
1182 0     0     my $self = shift;
1183 0           my $server = shift;
1184              
1185 0           my $list = $self->contents($server);
1186              
1187 0 0 0       @$list = sort {
1188 0           return -1
1189             if ( ref $a ) =~ /^TiVo::Calypso::Container/ && ( ref $b ) =~ /^TiVo::Calypso::Item/;
1190 0 0 0       return 1
1191             if ( ref $b ) =~ /^TiVo::Calypso::Container/ && ( ref $a ) =~ /^TiVo::Calypso::Item/;
1192 0           return uc( $a->_Path ) cmp uc( $b->_Path );
1193              
1194             #$ return uc($a->_Album) cmp uc($b->_Album) ||
1195             #$ $a->_Track <=> $b->_Track ||
1196             #$ $a->_Path <=> $b->_Path ||
1197             #$ uc($a->_Title) cmp uc($b->_Title);
1198             } @$list;
1199              
1200 0           my @return;
1201              
1202 0           foreach my $item (@$list) {
1203              
1204 0 0         if ( ( ref $item ) =~ /^TiVo::Calypso::Container/ ) {
1205              
1206             # Fetch the most current copy of this item from Cache
1207 0   0       $item = $server->thaw( $item->_Object ) || next;
1208              
1209 0           push( @return, $item );
1210 0           push( @return, @{ $item->explode($server) } );
  0            
1211              
1212             }
1213             else {
1214              
1215 0           push( @return, $item );
1216              
1217             }
1218              
1219             }
1220              
1221 0           return \@return;
1222             }
1223              
1224             package TiVo::Calypso::Container::Server;
1225             @ISA = ("TiVo::Calypso::Container");
1226              
1227             ## TiVo::Calypso::Container::Server->init( )
1228             ##
1229             ## Defines a Server psuedo-container which overrides the generic init
1230             ## method. Sets content types unique to a Server container;
1231              
1232             sub init {
1233 0     0     my $self = shift;
1234              
1235 0           $self->_Object = "/";
1236 0           $self->_Service = "/";
1237              
1238 0           $self->_ContentType = 'x-container/tivo-server';
1239              
1240 0   0       $self->_Title = $self->_Title || "TiVo Server";
1241              
1242 0           return 1;
1243             }
1244              
1245             # TiVo::Calypso::Container extension
1246             package TiVo::Calypso::Container::Music;
1247             @ISA = ("TiVo::Calypso::Container");
1248              
1249             ## TiVo::Calypso::Container::Music->init( )
1250             ##
1251             ## Defines a Music container which overrides the generic init
1252             ## method. Sets content and media types unique to a 'Music'
1253             ## container.
1254              
1255             sub init {
1256 0     0     my $self = shift;
1257              
1258 0           my %params = (@_);
1259              
1260 0           $self->_ContentType = 'x-container/tivo-music';
1261              
1262             # Media types accepted for this container.
1263             # When creating a handler for a new media type, be sure to
1264             # register it with the appropriate service via:
1265             # $service->_MediaTypes->{'NewSuffix'} = 'mime/type';
1266              
1267 0           $self->_MediaTypes = { 'MP3' => 'audio/mpeg' };
1268              
1269 0   0       $self->_Title = $self->_Title || "Music";
1270              
1271 0 0         if ( $params{'SCROBBLER'} ) {
1272 0           $self->_Scrobble = 1;
1273 0           $self->_ScrobblePostUrl = $params{'SCROBBLER'}->{'POSTURL'};
1274 0           $self->_ScrobbleU = $params{'SCROBBLER'}->{'USERNAME'};
1275 0           $self->_ScrobbleP = $params{'SCROBBLER'}->{'PASSWORD'};
1276             }
1277              
1278 0           return 1;
1279             }
1280              
1281             ##############################################################################
1282             # TiVo::Calypso::Item # Attaches TiVo methods to a particular file
1283             ##############################################################################
1284             package TiVo::Calypso::Item;
1285             @ISA = ('TiVo::Calypso');
1286              
1287             ## TiVo::Calypso::Item->new( $ $ )
1288             ##
1289             ## Constructor for generic TiVo::Calypso::Item
1290             ## Expects to be passed a full pathname and a TiVo::Calypso::Container service
1291             ## to pull container information from
1292              
1293             sub new {
1294 0     0     my $class = shift;
1295              
1296 0           my $self = {};
1297 0           bless $self, $class;
1298              
1299 0   0       $self->_Path = shift || return undef;
1300 0   0       $self->_Service = shift || return undef;
1301              
1302             # use the file suffix to determine file type
1303 0           my @parts = split( /\./, $self->_Path );
1304 0           my $suffix = uc( pop @parts );
1305              
1306             # Skip this file if the service doesn't claim to support it
1307 0 0         return undef unless defined $self->_Service->_MediaTypes;
1308              
1309 0   0       $self->_SourceFormat = $self->_Service->_MediaTypes->{$suffix}
1310             || return undef;
1311              
1312 0   0       $self->_Object = $self->_Service->path_to_obj( $self->_Path )
1313             || return undef;
1314 0           $self->_Url = $self->_uri_escape( $self->_Object );
1315              
1316             # Contruct ContentType from SourceFormat
1317 0           my $content_type = $self->_SourceFormat;
1318 0           $content_type =~ s/\/.*$/\/\*/;
1319              
1320 0           $self->_ContentType = $content_type;
1321              
1322 0           $self->_Dirty = 0;
1323              
1324             # Call class-specific init method
1325 0 0         $self->init || return undef;
1326              
1327 0           return $self;
1328             }
1329              
1330             ##
1331             ## TiVo::Calypso::Item->init( )
1332             ##
1333             ## Generic TiVo::Calypso::Item initialization
1334             ##
1335             sub init {
1336 0     0     my $self = shift;
1337              
1338 0           return 1;
1339             }
1340              
1341             ## TiVo::Calypso::Item->send( )
1342             ##
1343             ## Generic TiVo::Calypso::Item file transfer
1344              
1345             sub send {
1346 0     0     my $self = shift;
1347              
1348 0           require IO::File;
1349              
1350 0           my $handle = IO::File->new( $self->_Path );
1351              
1352 0           my $headers = {
1353             'Content-Type' => $self->_SourceFormat,
1354             'Content-Length' => $self->_SourceSize
1355             };
1356              
1357 0           return ( $headers, $handle );
1358             }
1359              
1360             # TiVo::Calypso::Item extension
1361             package TiVo::Calypso::Item::MP3;
1362             @ISA = ('TiVo::Calypso::Item');
1363              
1364             ## TiVo::Calypso::Item::MP3->init( )
1365             ##
1366             ## Overrides generic init method for TiVo::Calypso::Item and includes MP3
1367             ## specific fields
1368              
1369             sub init {
1370 0     0     my $self = shift;
1371              
1372             # use the file suffix to determine file type
1373 0           my @parts = split( /\./, $self->_Path );
1374 0           my $suffix = uc( pop @parts );
1375              
1376             # Assume MP3 for lack of anything better.
1377 0           require MP3::Info;
1378              
1379 0           my $tag = MP3::Info::get_mp3tag( $self->_Path );
1380 0           my $info = MP3::Info::get_mp3info( $self->_Path );
1381              
1382 0 0         return undef unless defined $info;
1383              
1384 0   0       $self->_SourceBitRate = sprintf( "%d", $info->{'BITRATE'} * 1000 ) || 0;
1385 0   0       $self->_SourceSampleRate = sprintf( "%d", $info->{'FREQUENCY'} * 1000 )
1386             || 0;
1387 0   0       $self->_Duration = sprintf( "%d", ( $info->{'SECS'} * 1000 ) ) || 0;
1388              
1389 0   0       $self->_Genre = $tag->{'GENRE'} || "";
1390 0   0       $self->_Artist = $tag->{'ARTIST'} || "";
1391 0   0       $self->_Album = $tag->{'ALBUM'} || "";
1392 0   0       $self->_Year = $tag->{'YEAR'} || "";
1393 0   0       $self->_Title = $tag->{'TITLE'} || $self->_basename;
1394              
1395             # Get timestamps and size if the file referenced by Path exists
1396 0 0         if ( stat( $self->_Path ) ) {
1397 0           $self->_SourceSize = -s $self->_Path;
1398              
1399 0           my $change_date = ( stat(_) )[9];
1400 0           my $access_date = ( stat(_) )[8];
1401              
1402 0           $change_date = sprintf( "0x%x", $change_date );
1403 0           $access_date = sprintf( "0x%x", $access_date );
1404              
1405             # *nix does not seem to have a portable "creation date" stamp.
1406             # Using last change date, instead.
1407 0           $self->_CreationDate = $change_date;
1408 0           $self->_LastChangeDate = $change_date;
1409 0           $self->_LastAccessDate = $access_date;
1410             }
1411              
1412 0           return 1;
1413             }
1414              
1415             ## TiVo::Calypso::Item::MP3->_query_container
1416             ##
1417             ## Returns a data structure suitable for use with xml_out which
1418             ## describes this object in response to a QueryContainer command
1419              
1420             sub _query_container {
1421 0     0     my $self = shift;
1422 0           my $params = shift;
1423              
1424 0   0       my $script_name = $params->_EnvScriptName || "";
1425              
1426 0           my $details = {
1427             'Item' => [
1428             {
1429             'Details' => {
1430             'Title' => $self->_Title,
1431             'ContentType' => $self->_ContentType,
1432             'SourceFormat' => $self->_SourceFormat,
1433             'ArtistName' => $self->_Artist,
1434             'SongTitle' => $self->_Title,
1435             'AlbumTitle' => $self->_Album,
1436             'MusicGenre' => $self->_Genre,
1437             'Duration' => $self->_Duration
1438             }
1439             },
1440             {
1441             'Links' => {
1442             'Content' => {
1443             'Url' => $script_name . $self->_Url,
1444             'Seekable' => 'Yes'
1445             }
1446             }
1447             }
1448             ]
1449             };
1450              
1451 0           return $details;
1452             }
1453              
1454             ## TiVo::Calypso::Item::MP3->send( $ )
1455             ##
1456             ## TiVo::Calypso::Item send extension supporting MP3 seeking
1457              
1458             sub send {
1459 0     0     my $self = shift;
1460 0           my $params = shift;
1461              
1462 0           require IO::File;
1463              
1464 0           my $handle = IO::File->new( $self->_Path );
1465 0           my $length = $self->_SourceSize;
1466              
1467 0 0         if ( defined $params->_Seek ) {
1468 0           my $seek_ms = $params->_Seek;
1469 0           my $seek_offset =
1470             sprintf( "%d", ( $seek_ms / $self->_Duration ) * $self->_SourceSize );
1471              
1472 0           seek( $handle, $seek_offset, 0 );
1473              
1474 0           $length = $length - $seek_offset;
1475             }
1476              
1477 0           my $headers = {
1478             'Content-Type' => $self->_SourceFormat,
1479             'Content-Length' => $length,
1480             'TivoAccurateDuration' => $self->_Duration
1481             };
1482              
1483 0           return ( $headers, $handle );
1484             }
1485              
1486             ##############################################################################
1487             # TiVo::Calypso::Request
1488             # Stores information about a given command request which needs to be
1489             # passed from object to object
1490             ##############################################################################
1491             package TiVo::Calypso::Request;
1492             @ISA = ('TiVo::Calypso');
1493              
1494             ## TiVo::Calypso::Request->new( $ $ $ )
1495             ##
1496             ## Constructor for TiVo::Calypso::Request.
1497             ## Expects to be passed three strings:
1498             ##
1499             ## Script Name: The path and name of the CGI/server as requested in the URI
1500             ## This is the same string provided by webserver in the
1501             ## $SCRIPT_NAME environment variable
1502             ## Path Info: The path information appended after the CGI/server in
1503             ## the URI, but before the paramater list.
1504             ## This is the same string provided by webserver in the
1505             ## $PATH_INFO environment variable
1506             ## Query String The key/value query string appended to the end of the URI
1507             ## This is the same string provided by webserver in the
1508             ## $QUERY_STRING environment variable
1509              
1510             sub new {
1511 0     0     my $class = shift;
1512              
1513 0           my $self = {};
1514 0           bless $self, $class;
1515              
1516 0           $self->_EnvScriptName = shift;
1517 0           $self->_EnvPathInfo = shift;
1518 0           $self->_EnvQueryString = shift;
1519              
1520             # Parse the query_string, if provided
1521 0 0         if ( defined( $self->_EnvQueryString ) ) {
1522 0           $self->parse( $self->_EnvQueryString );
1523             }
1524              
1525 0           return $self;
1526             }
1527              
1528             ## TiVo::Calypso::Request->parse( $ )
1529             ##
1530             ## Trim, split, and decode a standard CGI query string. The key/value
1531             ## pairs are stored in the object's internal DATA hash
1532              
1533             sub parse {
1534 0     0     my $self = shift;
1535 0           my $query = shift;
1536              
1537             # Skip the query if it doesn't contain anything useful
1538 0 0 0       if ( defined($query) && $query =~ /[=&]/ ) {
1539              
1540             # remove everything before the '?' and replace '+' with a space
1541 0           $query =~ s/.*\?//;
1542 0           $query =~ s/\+/ /g;
1543              
1544 0           my @pairs = split( /&/, $query );
1545              
1546 0           foreach my $pair (@pairs) {
1547 0           my ( $key, $value ) = split( /=/, $pair, 2 );
1548              
1549 0 0         if ( defined($key) ) {
1550              
1551             # Escape each key and value before storing
1552 0           $key = $self->_uri_unescape($key);
1553 0           $self->{'DATA'}->{ uc($key) } = $self->_uri_unescape($value);
1554             }
1555             }
1556             }
1557             }
1558              
1559             1;