diff --git a/fhem/FHEM/37_plex.pm b/fhem/FHEM/37_plex.pm index 5ac990707..0e2a7d4b4 100644 --- a/fhem/FHEM/37_plex.pm +++ b/fhem/FHEM/37_plex.pm @@ -39,13 +39,12 @@ plex_Initialize($) $hash->{ReadFn} = "plex_Read"; $hash->{DefFn} = "plex_Define"; - $hash->{NOTIFYDEV} = "global"; $hash->{NotifyFn} = "plex_Notify"; $hash->{UndefFn} = "plex_Undefine"; $hash->{SetFn} = "plex_Set"; $hash->{GetFn} = "plex_Get"; $hash->{AttrFn} = "plex_Attr"; - $hash->{AttrList} = "disable:1,0 responder:1,0 ignoredClients ignoredServers user password"; + $hash->{AttrList} = "disable:1,0 httpPort responder:1,0 ignoredClients ignoredServers user password"; } ##################################### @@ -123,6 +122,8 @@ plex_Define($$) $hash->{fhemHostname} = hostname(); $hash->{fhemIP} = plex_getLocalIP(); + $hash->{NOTIFYDEV} = "global"; + if( $init_done ) { plex_getToken($hash); plex_startDiscovery($hash); @@ -142,10 +143,19 @@ sub plex_Notify($$) { my ($hash,$dev) = @_; + my $name = $hash->{NAME}; return if($dev->{NAME} ne "global"); return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}})); + if( my $token = ReadingsVal($name, '.token', undef) ) { + Log3 $name, 3, "$name: restoring token from reading"; + + $hash->{token} = $token; + + plex_sendApiCmd($hash, "https://plex.tv/pms/servers.xml", "myPlex:servers" ); + plex_sendApiCmd($hash, "https://plex.tv/devices.xml", "myPlex:devices" ); + } plex_getToken($hash); plex_startDiscovery($hash); plex_startTimelineListener($hash); @@ -176,7 +186,7 @@ plex_sendDiscover($) } - RemoveInternalTimer($hash); + RemoveInternalTimer($hash, "plex_sendDiscover"); if( $hash->{interval} ) { InternalTimer(gettimeofday()+$hash->{interval}, "plex_sendDiscover", $hash, 0); @@ -557,7 +567,7 @@ plex_refreshSubscriptions($) plex_sendSubscription($hash, $ip); } - RemoveInternalTimer($hash); + RemoveInternalTimer($hash,"plex_refreshSubscriptions"); if( $hash->{interval} ) { InternalTimer(gettimeofday()+$hash->{interval}, "plex_refreshSubscriptions", $hash, 0); } @@ -720,7 +730,10 @@ plex_startTimelineListener($) plex_stopTimelineListener($hash); - if( my $socket = IO::Socket::INET->new(LocalPort=>0, Listen=>10, Blocking=>0, ReuseAddr=>1, ReusePort=>defined(&ReusePort)?1:0) ) { + return undef if( AttrVal($name, "disable", 0 ) == 1 ); + + my $port = AttrVal($name, 'httpPort', 0); + if( my $socket = IO::Socket::INET->new(LocalPort=>$port, Listen=>10, Blocking=>0, ReuseAddr=>1, ReusePort=>defined(&ReusePort)?1:0) ) { my $chash = plex_newChash( $hash, $socket, {NAME=>"$name:timelineListener", STATE=>'accepting'} ); @@ -876,6 +889,9 @@ plex_Set($$@) return undef; + } elsif( $cmd eq 'smapiRegister' ) { + return plex_publishToSonos($name, 'PLEX', $params[0]); + } $list .= 'playlistCreate playlistAdd playlistRemove '; @@ -1454,9 +1470,13 @@ plex_Get($$@) } return plex_deviceList($hash, $cmd ); + + } elsif( $cmd eq 'pin' ) { + return plex_getPinForToken($hash); + } - $list .= 'clients:noArg servers:noArg '; + $list .= 'clients:noArg servers:noArg pin:noArg '; } if( my $entry = plex_serverOf($hash, $cmd, !$hash->{machineIdentifier}) ) { @@ -1530,6 +1550,9 @@ plex_Get($$@) } elsif( $cmd eq 'clients' ) { return plex_deviceList($hash, 'clients' ); + } elsif( $cmd eq 'pin' ) { + return plex_getPinForToken($hash); + } elsif( $cmd eq 'm3u' || $cmd eq 'pls' ) { return "usage: $cmd " if( !$param ); @@ -1548,9 +1571,8 @@ plex_Get($$@) } - $list .= 'ls search sessions:noArg detail onDeck:noArg recentlyAdded:noArg playlists:noArg '; - $list .= 'servers:noArg ' if( $list !~ m/\bservers\b/ ); + $list .= 'servers:noArg pin:noArg ' if( $list !~ m/\bservers\b/ ); } @@ -1743,6 +1765,83 @@ plex_getToken($) return undef; } sub +plex_getPinForToken($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + RemoveInternalTimer($hash, "plex_getTokenOfPin"); + + my $url = 'https://plex.tv/pins.xml'; + + Log3 $name, 4, "$name: requesting $url"; + + my $param = { + url => $url, + method => 'POST', + timeout => 5, + noshutdown => 0, + hash => $hash, + key => 'getPinForToken', + header => { 'X-Plex-Provides' => 'controller', + 'X-Plex-Client-Identifier' => $hash->{id}, + 'X-Plex-Platform' => $^O, + #'X-Plex-Device' => 'FHEM', + 'X-Plex-Device-Name' => $hash->{fhemHostname}, + 'X-Plex-Product' => 'FHEM', + 'X-Plex-Version' => '0.0', }, + }; + + $param->{cl} = $hash->{CL} if( ref($hash->{CL}) eq 'HASH' ); + + $param->{callback} = \&plex_parseHttpAnswer; + my($err,$data) = HttpUtils_NonblockingGet( $param ); + + Log3 $name, 2, "$name: http request ($url) failed: $err" if( $err ); + + return undef; +} +sub +plex_getTokenOfPin($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + RemoveInternalTimer($hash, "plex_getTokenOfPin"); + + Log3 $name, 2, "$name: no PIN" if( !$hash->{PIN} ); + + return undef if( !$hash->{PIN} ); + return undef if( !$hash->{PIN_ID} ); + + my $url = "https://plex.tv/pins/$hash->{PIN_ID}.xml"; + + Log3 $name, 4, "$name: requesting $url"; + + my $param = { + url => $url, + method => 'GET', + timeout => 5, + noshutdown => 0, + hash => $hash, + key => 'tokenOfPin', + header => { 'X-Plex-Provides' => 'controller', + 'X-Plex-Client-Identifier' => $hash->{id}, + 'X-Plex-Platform' => $^O, + #'X-Plex-Device' => 'FHEM', + 'X-Plex-Device-Name' => $hash->{fhemHostname}, + 'X-Plex-Product' => 'FHEM', + 'X-Plex-Version' => '0.0', }, + }; + + $param->{callback} = \&plex_parseHttpAnswer; + my($err,$data) = HttpUtils_NonblockingGet( $param ); + + Log3 $name, 2, "$name: http request ($url) failed: $err" if( $err ); + + return undef; +} +sub plex_sendApiCmd($$$;$) { my ($hash,$url,$key,$blocking) = @_; @@ -2080,6 +2179,21 @@ plex_hash2header($) return $header; } sub +plex_hash2form($) +{ + my ($hash) = @_; + + return $hash if( ref($hash) ne 'HASH' ); + + my $form; + foreach my $key (keys %{$hash}) { + $form .= "&" if( $form ); + $form .= "$key=".urlEncode($hash->{$key}); + } + + return $form; +} +sub plex_discovered($$$$) { my ($hash, $type, $ip, $entry) = @_; @@ -2311,6 +2425,453 @@ plex_parseTimeline($$$) readingsEndUpdate($chash, 1); } sub +plex_getDataForSMAPI($$$) +{ + my ($hash,$server,$key) = @_; + my $name = $hash->{NAME}; + + my ($seconds) = gettimeofday(); + foreach my $key ( keys %{$hash->{helper}{SMAPIcache}} ) { + delete $hash->{helper}{SMAPIcache}{$key} if( $seconds - $hash->{helper}{SMAPIcache}{$key}{timestamp} > 10 ); + } + + my $xml; + if( !$hash->{helper}{SMAPIcache}{$key} ) { +Log 1, "get: $key"; + if( $key =~ m'^/library' ) { + $xml = plex_sendApiCmd( $hash, "http://$server->{address}:$server->{port}$key", '#raw', 1 ); + + } else { + $xml = plex_sendApiCmd( $hash, "http://$server->{address}:$server->{port}/library/sections$key", '#raw', 1 ); + + return undef if( !$xml || ref($xml) ne 'HASH' ); + if( $key eq '' && $xml->{Directory} ) { + my $section; + foreach my $item (@{$xml->{Directory}}) { + if( $item->{type} && $item->{type} eq 'artist' ) { + if( $section ) { + $section = undef; + last; + } else { + $section = $item->{key}; + } + } + } + + if( $section ) { + Log3 $name, 4, "$name: found only one music section, using this as root"; + $xml = plex_sendApiCmd( $hash, "http://$server->{address}:$server->{port}/library/sections/$section", '#raw', 1 ); + + } else { + Log3 $name, 4, "$name: found multiple music sections"; + + } + } + } + + return undef if( !$xml || ref($xml) ne 'HASH' ); + if( $xml->{Directory} ) { + for(my $i = int(@{$xml->{Directory}}); $i >= 0; --$i) { + my $item = $xml->{Directory}[$i]; + + # at the toplevel only care about music sections + if( !$key && $item->{type} && $item->{type} ne 'artist' ) { + splice @{$xml->{Directory}}, $i, 1; + --$xml->{size}; + next; + } + # ignore search nodes + if( $item->{key} =~ /^search/ ) { + splice @{$xml->{Directory}}, $i, 1; + --$xml->{size}; + next; + } + } + } + + my ($seconds) = gettimeofday(); + $hash->{helper}{SMAPIcache}{$key} = { value => $xml, timestamp => $seconds }; + + } else { +Log 1, "cached: $key"; + + my ($seconds) = gettimeofday(); + $hash->{helper}{SMAPIcache}{$key}{value}{timestamp} = $seconds; + + $xml = $hash->{helper}{SMAPIcache}{$key}{value} + } + Log3 $name, 5, "$name: got:". Dumper $xml; + + return $xml; +} +sub +plex_metadataResponseForSMAPI($$$$$) +{ + my ($hash,$request,$server,$key,$xml) = @_; + my $name = $hash->{NAME}; + + return undef if( !$request || ref($request) ne 'HASH' ); + return undef if( !$server || ref($server) ne 'HASH' ); + return undef if( !$xml || ref($xml) ne 'HASH' ); + + my $type; + if( $request->{getMetadata} ) { + $type = 'getMetadata'; + } elsif( $request->{getExtendedMetadata} ) { + $type = 'getExtendedMetadata'; + } else { + return undef; + } + + my $index = $request->{$type}{index}; + my $count = $request->{$type}{count}; + + my $body; + $body .= ''; + $body .= ' '; + $body .= ' <'.$type.'Response xmlns="http://www.sonos.com/Services/1.1">'; + $body .= ' <'.$type.'Result>'; + my $i = 0; + my $total = $xml->{size}; + $total = 0 if( !$total ); + if( $xml->{Directory} ) { + foreach my $item (@{$xml->{Directory}}) { + if( $i < $index ) { + ++$i; + next; + } + + my $title = $item->{titleSort}; + $title = $item->{title};# if( !$title ); + + $title =~ s/&/&/g; + + $body .= ''; + $body .= " $title"; + $body .= " $item->{key}" if( $item->{key} =~ '^/' ); + $body .= " $key/$item->{key}" if( $item->{key} !~ '^/' ); + $body .= " http://$server->{address}:$server->{port}$item->{thumb}" if( $item->{thumb} ); + $body .= ' true' if( $xml->{size} > 20 ); + $body .= ' true'; + if( $item->{type} eq 'album' ) { + $body .= 'true'; + $body .= 'album'; + } elsif( $item->{type} eq 'artist' ) { + $body .= 'true'; + $body .= 'artist'; + } elsif( $item->{type} eq 'genre' ) { + $body .= 'true'; + $body .= 'genre'; + } else { + $body .= 'collection'; + } + $body .= ''; + + last if( ++$i >= $index + $count ); + } + + } elsif( $xml->{Track} ) { + foreach my $item (@{$xml->{Track}}) { + if( $i < $index ) { + ++$i; + next; + } + + $item->{title} =~ s/&/&/g; + $item->{parentTitle} =~ s/&/&/g; + $item->{grandparentTitle} =~ s/&/&/g; + + $body .= ''; + $body .= " $item->{title}"; + $body .= " $item->{key}" if( $item->{key} =~ '^/' ); + $body .= " $key/$item->{key}" if( $item->{key} !~ '^/' ); + $body .= ' audio/mp3'; + $body .= ' track'; + $body .= ' '; + $body .= " $item->{parentTitle}"; + $body .= " $item->{parentKey}"; + $body .= " $item->{grandparentTitle}"; + $body .= " $item->{grandparentKey}"; + $body .= " $item->{index}"; + $body .= " ". int($item->{duration}/1000) .""; + $body .= " http://$server->{address}:$server->{port}$item->{parentThumb}" if( $item->{parentThumb} ); + $body .= ' '; + $body .= ''; + + last if( ++$i >= $index + $count ); + } + } + $body .= " $total"; + $body .= " $index"; + $body .= " ". ($i-$index) .""; + $body .= ' '; + $body .= ' '; + $body .= ' '; + $body .= ''; +#Log 1, $body; + + my $ret = "HTTP/1.1 200 OK\r\n"; + $ret .= plex_hash2header( { 'Connection' => 'Close', + 'Content-Type' => 'text/xml; charset=utf-8', + 'Content-Length' => length($body), + } ); + $ret .= "\r\n"; + $ret .= $body; + +#Log 1, $ret; + return $ret; +} +sub +plex_getScrollindicesForSMAPI($$) +{ + my ($hash,$xml) = @_; + my $name = $hash->{NAME}; + + my $indices =''; + my $last; + my $i = 0; + if( $xml->{Directory} ) { + foreach my $item (@{$xml->{Directory}}) { + my $title = $item->{titleSort}; + $title = $item->{title} if( !$title ); + + my $current = uc(substr($title, 0, 1)); + + if( $current =~ /[A-Z]/ && (!$last || $current ne $last) ) { + $indices .= ',' if( $indices ); + $indices .= "$current,$i"; + + $last = $current; + } + + ++$i; + } + } + + return $indices; +} + +sub +plex_handleSMAPI($$) +{ + my ($hash,$msg) = @_; + my $name = $hash->{NAME}; + + my $handled; + + my $server = plex_serverOf($hash, $hash->{machineIdentifier}, !$hash->{machineIdentifier}); + if( !$server ) { + Log3 $name, 2, "$name: no server found for SMAPI request"; + return undef; + } + + if( $msg =~ m/^(.*?)\r?\n\r?\n(.*)$/s ) { + my $header = $1; + my $body = $2; +#Log 1, $header; +#Log 1, $body; + + if( my $xml = eval { XMLin( $body, KeyAttr => {}, ForceArray => 0 ); } ) { + if( my $body = $xml->{'s:Body'} ) { + Log3 $name, 4, "$name: got soap request:". Dumper $body; + + if( $body->{getMetadata} ) { + $handled = 1; + +#Log 1, Dumper $body; + my $key = $body->{getMetadata}{id}; + $key = '' if( $key eq 'root' ); + $key = "/$key" if( $key && $key !~ '^/' ); + + my $xml = plex_getDataForSMAPI($hash, $server, $key); +#Log 1, Dumper $xml; + + return plex_metadataResponseForSMAPI($hash, $body, $server, $key, $xml); + + } elsif( $body->{getExtendedMetadata} ) { + $handled = 1; + +#Log 1, Dumper $body; + my $key = $body->{getExtendedMetadata}{id}; + $key = "" if( $key eq 'root' ); + $key = "/$key" if( $key && $key !~ '^/' ); + + my $xml = plex_getDataForSMAPI($hash, $server, $key); + + return plex_metadataResponseForSMAPI($hash, $body, $server, $key, $xml); + + } elsif( $body->{getScrollIndices} ) { + $handled = 1; + + if( my $key = $body->{getScrollIndices}{id} ) { + $key = "/$key" if( $key && $key !~ '^/' ); + + my $xml = plex_getDataForSMAPI($hash, $server, $key); + return undef if( !$xml || ref($xml) ne 'HASH' ); + + my $body; + $body .= ''; + $body .= ' '; + $body .= ' '; + $body .= ' '; + $body .= plex_getScrollindicesForSMAPI($hash,$xml); + $body .= ' '; + $body .= ' '; + $body .= ' '; + $body .= ''; + + my $ret = "HTTP/1.1 200 OK\r\n"; + $ret .= plex_hash2header( { 'Connection' => 'Close', + 'Content-Type' => 'text/xml; charset=utf-8', + 'Content-Length' => length($body), + } ); + $ret .= "\r\n"; + $ret .= $body; + +#Log 1, $ret; + return $ret; + } + + } elsif( $body->{getMediaMetadata} ) { + $handled = 1; + + if( my $key = $body->{getMediaMetadata}{id} ) { + $key = "/$key" if( $key && $key !~ '^/' ); + + my $xml = plex_getDataForSMAPI($hash, $server, $key); + return undef if( !$xml || ref($xml) ne 'HASH' ); + + my $body; + $body .= ''; + $body .= ' '; + $body .= ' '; + $body .= ' '; + if( $xml->{Track} ) { + foreach my $item (@{$xml->{Track}}) { + $item->{title} =~ s/&/&/g; + $item->{parentTitle} =~ s/&/&/g; + $item->{grandparentTitle} =~ s/&/&/g; + + $body .= "$item->{title}"; + $body .= "$item->{key}" if( $item->{key} =~ '^/' ); + $body .= "$key/$item->{key}" if( $item->{key} !~ '^/' ); + $body .= 'audio/mp3'; + $body .= 'track'; + $body .= ''; + $body .= " $item->{parentTitle}"; + $body .= " $item->{parentKey}"; + $body .= " $item->{grandparentTitle}"; + $body .= " $item->{grandparentKey}"; + $body .= " $item->{index}"; + $body .= " ". int($item->{duration}/1000) .""; + $body .= " http://$server->{address}:$server->{port}$item->{parentThumb}" if( $item->{parentThumb} ); + $body .= ''; + } + } + $body .= ' '; + $body .= ' '; + $body .= ' '; + $body .= ''; + + my $ret = "HTTP/1.1 200 OK\r\n"; + $ret .= plex_hash2header( { 'Connection' => 'Close', + 'Content-Type' => 'text/xml; charset=utf-8', + 'Content-Length' => length($body), + } ); + $ret .= "\r\n"; + $ret .= $body; + +#Log 1, $ret; + return $ret; + } + + } elsif( $body->{getMediaURI} ) { + $handled = 1; + + if( my $key = $body->{getMediaURI}{id} ) { + my $xml = plex_getDataForSMAPI($hash, $server, $key); + return undef if( !$xml || ref($xml) ne 'HASH' ); + + my $body; + $body .= ''; + $body .= ' '; + $body .= ' '; + $body .= ' '; + if( $xml->{Track} ) { + foreach my $item (@{$xml->{Track}}) { + if( $item->{Media} && $item->{Media}[0]{Part} ) { + $body .= "http://$server->{address}:$server->{port}$item->{Media}[0]{Part}[0]{key}"; + #$body .= "&X-Plex-Token=$hash->{token}" if( $hash->{token} ); + last; + } + } + } + $body .= ' '; + if( $hash->{token} ) { + $body .= ''; + $body .= ' '; + $body .= '
X-Plex-Token
'; + $body .= " $hash->{token}"; + $body .= '
'; + $body .= '
'; + } + $body .= ' '; + $body .= '
'; + $body .= '
'; + + my $ret = "HTTP/1.1 200 OK\r\n"; + $ret .= plex_hash2header( { 'Connection' => 'Close', + 'Content-Type' => 'text/xml; charset=utf-8', + 'Content-Length' => length($body), + } ); + $ret .= "\r\n"; + $ret .= $body; + +#Log 1, $ret; + return $ret; + } + + } elsif( $body->{getLastUpdate} ) { + $handled = 1; + + my ($seconds) = gettimeofday(); + my $body; + $body .= ''; + $body .= ' '; + $body .= ' '; + $body .= ' '; + $body .= " $seconds"; + $body .= ' '; + $body .= ' 120'; + $body .= ' '; + $body .= ' '; + $body .= ' '; + $body .= ''; + + my $ret = "HTTP/1.1 200 OK\r\n"; + $ret .= plex_hash2header( { 'Connection' => 'Close', + 'Content-Type' => 'text/xml; charset=utf-8', + 'Content-Length' => length($body), + } ); + $ret .= "\r\n"; + $ret .= $body; + +#Log 1, $ret; + return $ret; + } + + Log3 $name, 2, "$name: unhandled soap request:". Dumper $body if( !$handled ); + + return undef; + } + } + } + + Log3 $name, 2, "$name: unhandled message: $msg" if( !$handled ); + + return undef; +} +sub plex_Parse($$;$$$) { my ($hash,$msg,$peerhost,$peerport,$sockport) = @_; @@ -2678,6 +3239,9 @@ $hash->{sonos}{status} = $cmd; } } + } elsif( $msg =~ '^POST /SMAPI HTTP/1.\d' ) { + return plex_handleSMAPI($hash, $msg); + } if( !$handled ) { @@ -2728,6 +3292,7 @@ plex_parseHttpAnswer($$$) $param->{url} =~ s/commandID=\d*/commandID=$hash->{commandID}/; } Log3 $name, 5, " ($param->{url})"; + RemoveInternalTimer($hash, "HttpUtils_NonblockingGet"); InternalTimer(gettimeofday()+5, "HttpUtils_NonblockingGet", $param, 0); return; @@ -2748,6 +3313,16 @@ plex_parseHttpAnswer($$$) $data = encode('UTF-8', $data ); if( $data =~ m/^(.*)/ ) { + if( $param->{key} eq 'tokenOfPin' ) { + delete $hash->{PIN}; + delete $hash->{PIN_ID}; + delete $hash->{PIN_EXPIRES}; + + Log3 $name, 2, "$name: PIN expired"; + + return undef; + } + Log3 $name, 2, "$name: failed: $1"; return undef; @@ -2773,15 +3348,69 @@ plex_parseHttpAnswer($$$) if( $param->{key} eq 'token' ) { $handled = 1; + $hash->{token} = $xml->{'authenticationToken'}; + readingsSingleUpdate($hash, '.token', $hash->{token}, 0 ); + CommandSave(undef,undef) if( AttrVal( "autocreate", "autosave", 1 ) ); + + Log3 $name, 3, "$name: got token from user/password"; plex_sendApiCmd($hash, "https://plex.tv/pms/servers.xml", "myPlex:servers" ); plex_sendApiCmd($hash, "https://plex.tv/devices.xml", "myPlex:devices" ); #https://plex.tv/pms/resources.xml?includeHttps=1 + } elsif( $param->{key} eq 'getPinForToken' ) { + $handled = 1; + + delete $hash->{PIN}; + delete $hash->{PIN_ID}; + delete $hash->{PIN_EXPIRES}; + + $hash->{PIN} = $xml->{code}[0] if( $xml->{code} ); + $hash->{PIN_ID} = $xml->{id}[0]{content} if( $xml->{id} ); + $hash->{PIN_EXPIRES} = $xml->{'expires-at'}[0]{content} if( $xml->{'expires-at'} ); + + Log3 $name, 2, "$name: PIN: $hash->{PIN}"; + + #plex_sendApiCmd($hash, "https://plex.tv/pms/servers.xml", "myPlex:servers" ); + #plex_sendApiCmd($hash, "https://plex.tv/devices.xml", "myPlex:devices" ); + + #https://plex.tv/pms/resources.xml?includeHttps=1 + + if( $param->{cl} && $param->{cl}{canAsyncOutput} ) { + asyncOutput( $param->{cl}, "PIN: $hash->{PIN}\n" ); + + plex_getTokenOfPin($hash); + } + + } elsif( $param->{key} eq 'tokenOfPin' ) { + $handled = 1; + + RemoveInternalTimer($hash, "plex_getTokenOfPin"); + + if( $xml->{auth_token}[0] && !ref($xml->{auth_token}[0]) ) { + delete $hash->{PIN}; + delete $hash->{PIN_ID}; + delete $hash->{PIN_EXPIRES}; + + $hash->{token} = $xml->{auth_token}[0]; + readingsSingleUpdate($hash, '.token', $hash->{token}, 0 ); + CommandSave(undef,undef) if( AttrVal( "autocreate", "autosave", 1 ) ); + + Log3 $name, 3, "$name: got token from pin"; + + plex_sendApiCmd($hash, "https://plex.tv/pms/servers.xml", "myPlex:servers" ); + plex_sendApiCmd($hash, "https://plex.tv/devices.xml", "myPlex:devices" ); + + } else { + + InternalTimer(gettimeofday()+4, "plex_getTokenOfPin", $hash, 0); + } + } elsif( $param->{key} eq 'clients' ) { $handled = 1; + foreach my $entry (@{$xml->{Server}}) { #next if( $entry->{address} eq $hash->{fhemIP} # && $hash->{helper}{timelineListener} && $hash->{helper}{timelineListener}->{PORT} == $entry->{port} ); @@ -3066,6 +3695,13 @@ plex_parseHttpAnswer($$$) } + } elsif( $param->{key} eq 'publishToSonos' ) { + $handled = 1; + + if( $param->{cl} && $param->{cl}{canAsyncOutput} ) { + asyncOutput( $param->{cl}, "SMAPI registration for $param->{player}: $xml->{body}[0]\n" ); + } + } elsif( $param->{key} eq '#raw' ) { $handled = 1; @@ -3232,8 +3868,10 @@ Log 1, "!!!!!!!!!!"; $add_header .= "Content-Length: 0\r\n"; } - syswrite($hash->{CD}, $add_header) if( $add_header ); - Log3 $pname, 4, "$name: add header: $add_header"; + if( $add_header ) { + Log3 $pname, 5, "$name: add header: $add_header"; + syswrite($hash->{CD}, $add_header); + } if( $ret ) { syswrite($hash->{CD}, $ret); @@ -3269,6 +3907,75 @@ Log 1, "!!!!!!!!!!"; return undef; } +sub +plex_publishToSonos($$;$) +{ + my ($hash,$service,$player) = @_; + $hash = $defs{$hash} if( ref($hash) ne 'HASH' ); + return undef if( !$hash ); + my $name = $hash->{NAME}; + + my $i = 0; + foreach my $d (devspec2array("TYPE=SONOSPLAYER")) { + next if( $player && $d !~ /$player/ ); + my $location = ReadingsVal($d,'location',undef); +Log 1, $location; + + my $ip = ($location =~ m/https?:..([\d.]*)/)[0]; +Log 1, $ip; + next if( !$ip ); + + my $url = "http://$ip:1400/customsd"; + + Log3 $name, 4, "$name: requesting $url"; + + my $fhem_base_url = "http://$hash->{fhemIP}:$hash->{helper}{timelineListener}{PORT}"; +Log 1, $fhem_base_url; + + my $data = plex_hash2form( { 'sid' => '246', + 'name' => $service, + 'uri' => "$fhem_base_url/SMAPI", + 'secureUri' => "$fhem_base_url/SMAPI", + 'pollInterval' => '1200', + 'authType' => 'Anonymous', + 'containerType' => 'MService', + #'presentationMapVersion' => '1', + #'presentationMapUri' => "$fhem_base_url/sonos/presentationMap.xml", + #'stringsVersion' => '5', + #'stringsUri' => "$fhem_base_url/sonos/strings.xml", + } ); + $data .= "&caps=search"; + $data .= "&caps=ucPlaylists"; + $data .= "&caps=extendedMD"; + + my $param = { + url => $url, + method => 'POST', + timeout => 10, + noshutdown => 0, + hash => $hash, + key => 'publishToSonos', + player => $d, + data => $data, + }; + + $param->{cl} = $hash->{CL} if( ref($hash->{CL}) eq 'HASH' ); + + $param->{callback} = \&plex_parseHttpAnswer; + my($err,$data) = HttpUtils_NonblockingGet( $param ); + + Log3 $name, 2, "$name: http request ($url) failed: $err" if( $err ); + + ++$i; + } + + return 'no sonos players found' if( !$i ); + + return "send SMAPI registration to $i players"; + + return undef; +} + 1; =pod @@ -3335,13 +4042,13 @@ Log 1, "!!!!!!!!!!";
  • [<server>] ls [<path>]
    browse the media library. eg:

    get <plex> ls -
      Plex Library 
    +      
      Plex Library
       key                                 type       title
       1                                   artist       Musik
       2                      ...

    get <plex> ls /1 -
      Musik 
    +      
      Musik
       key                                 type       title
       all                                            All Artists
       albums                                         By Album
    @@ -3356,7 +4063,7 @@ Log 1, "!!!!!!!!!!";
       search?type=10                                 Search Tracks...

    get <plex> ls /1/albums -
      Musik ; By Album 
    +      
      Musik ; By Album
       key                                  type       title
       /library/metadata/133999/children   album       ...
       /library/metadata/134207/children   album       ...
    @@ -3396,11 +4103,16 @@ Log 1, "!!!!!!!!!!";
     
         
  • servers
    list the known servers
  • + +
  • pin
    + get a pin for authentication at https://plex.tv/pin
  • +
    Attr
      +
    • httpPort
    • ignoredClients
    • ignoredServers
    • user