# $Id$ #http://10.0.1.21:32400/music/:/transcode/generic.mp3?offset=0&format=mp3&audioCodec=libmp3lame&audioBitrate=320&audioSamples=44100&url=http%3A%2F%2F127.0.0.1%3A32400%2Flibrary%2Fparts%2F71116%2Ffile.mp3 package main; use strict; use warnings; use Sys::Hostname; use IO::Socket::INET; #use Net::Address::IP::Local; #use MIME::Base64; use JSON; use Encode qw(encode); use XML::Simple qw(:strict); use Digest::MD5 qw(md5_hex); #use Socket; use Time::HiRes qw(usleep nanosleep); use HttpUtils; use Time::Local; use Data::Dumper; my $plex_hasMulticast = 1; sub plex_Initialize($) { my ($hash) = @_; eval "use IO::Socket::Multicast;"; $plex_hasMulticast = 0 if($@); $hash->{ReadFn} = "plex_Read"; $hash->{DefFn} = "plex_Define"; $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" . " fhemIP httpPort ignoredClients ignoredServers" . " removeUnusedReadings:1,0 responder:1,0" . " user password " . $readingFnAttributes; } ##################################### sub plex_getLocalIP() { my $socket = IO::Socket::INET->new( Proto => 'udp', PeerAddr => '8.8.8.8:53', # google dns #PeerAddr => '198.41.0.4:53', # a.root-servers.net ); return '' if( !$socket ); my $ip = $socket->sockhost; close( $socket ); return $ip if( $ip ); #$ip = inet_ntoa( scalar gethostbyname( hostname() || 'localhost' ) ); #return $ip if( $ip ); return ''; } sub plex_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); return "Usage: define plex [server]" if(@a < 2); my $name = $a[0]; my ($ip,$port); ($ip,$port) = split( ':', $a[2] ) if( $a[2] ); my $server = $ip; my $client = $ip; $server = '' if( $server && $server !~ m/^\d+\.\d+\.\d+\.\d+$/ ); $hash->{NAME} = $name; if( $server ) { $hash->{server} = $server; $hash->{port} = $port?$port:32400; $modules{plex}{defptr}{$server} = $hash; } elsif( $client ) { if( $port ) { $hash->{client} = $client; $hash->{port} = $port; $modules{plex}{defptr}{$client} = $hash; } else { $hash->{machineIdentifier} = $client; $modules{plex}{defptr}{$client} = $hash; } } else { my $defptr = $modules{plex}{defptr}{MASTER}; return "plex master already defined as '$defptr->{NAME}'" if( defined($defptr) && $defptr->{NAME} ne $name); $modules{plex}{defptr}{MASTER} = $hash; return "give ip or install IO::Socket::Multicast to use server and client autodiscovery" if(!$plex_hasMulticast && !$server); $hash->{"HAS_IO::Socket::Multicast"} = $plex_hasMulticast; } $hash->{id} = md5_hex(getUniqueId()); $hash->{fhemHostname} = hostname(); $hash->{fhemIP} = plex_getLocalIP(); $hash->{NOTIFYDEV} = "global"; if( $init_done ) { plex_getToken($hash); plex_startDiscovery($hash); plex_startTimelineListener($hash); plex_sendApiCmd( $hash, "http://$hash->{server}:$hash->{port}/servers", "servers" ) if( $hash->{server} ); } else { readingsSingleUpdate($hash, 'state', 'initialized', 1 ); } return undef; } 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); plex_sendApiCmd( $hash, "http://$hash->{server}:$hash->{port}/servers", "servers" ) if( $hash->{server} ); return undef; } sub plex_sendDiscover($) { my ($hash) = @_; my $name = $hash->{NAME}; my $pname = $hash->{PNAME} || $name; if( $hash->{multicast} ) { Log3 $pname, 5, "$name: sending multicast discovery message to $hash->{PORT}"; $hash->{CD}->mcast_send('M-SEARCH * HTTP/1.1', '239.0.0.250:'.$hash->{PORT}); } elsif( $hash->{broadcast} ) { Log3 $pname, 5, "$name: sending broadcast discovery message to $hash->{PORT}"; my $sin = sockaddr_in($hash->{PORT}, inet_aton('255.255.255.255')); $hash->{CD}->send('M-SEARCH * HTTP/1.1', 0, $sin ); } else { Log3 $pname, 2, "$name: can't send unknown discovery message type to $hash->{PORT}"; } RemoveInternalTimer($hash, "plex_sendDiscover"); if( $hash->{interval} ) { InternalTimer(gettimeofday()+$hash->{interval}, "plex_sendDiscover", $hash, 0); } } sub plex_closeSocket($) { my ($hash) = @_; my $name = $hash->{NAME}; if( !$hash->{CD} ) { my $pname = $hash->{PNAME} || $name; Log3 $pname, 2, "$name: trying to close a non socket hash"; return undef; } RemoveInternalTimer($hash); close($hash->{CD}); delete($hash->{CD}); delete($selectlist{$name}); delete($hash->{FD}); } sub plex_newChash($$$) { my ($hash,$socket,$chash) = @_; $chash->{TYPE} = $hash->{TYPE}; $chash->{NR} = $devcount++; $chash->{phash} = $hash; $chash->{PNAME} = $hash->{NAME}; $chash->{CD} = $socket; $chash->{FD} = $socket->fileno(); $chash->{PORT} = $socket->sockport if( $socket->sockport ); $chash->{TEMPORARY} = 1; $attr{$chash->{NAME}}{room} = 'hidden'; $defs{$chash->{NAME}} = $chash; $selectlist{$chash->{NAME}} = $chash; } sub plex_startDiscovery($) { my ($hash) = @_; my $name = $hash->{NAME}; return undef if( $hash->{server} ); return undef if( $hash->{client} ); return undef if( $hash->{machineIdentifier} ); return undef if( !$plex_hasMulticast ); plex_stopDiscovery($hash); return undef if( AttrVal($name, "disable", 0 ) == 1 ); # udp multicast for servers if( my $socket = IO::Socket::Multicast->new(Proto => 'udp', Timeout => 5, ReuseAddr=>1, ReusePort=>defined(&SO_REUSEPORT)?1:0) ) { my $chash = plex_newChash( $hash, $socket, {NAME=>"$name:serverDiscoveryMcast", STATE=>'discovering', multicast => 1} ); $hash->{helper}{discoverServersMcast} = $chash; $chash->{PORT} = 32414; $chash->{interval} = 10; #plex_sendDiscover($chash); InternalTimer(gettimeofday()+$chash->{interval}/2, "plex_sendDiscover", $chash, 0); Log3 $name, 3, "$name: multicast server discovery started"; } else { Log3 $name, 3, "$name: failed to start multicast server discovery: $@"; InternalTimer(gettimeofday()+10, "plex_startDiscovery", $hash, 0); } # udp broadcast for servers if( my $socket = new IO::Socket::INET ( Proto => 'udp', Broadcast => 1, ) ) { my $chash = plex_newChash( $hash, $socket, {NAME=>"$name:serverDiscoveryBcast", STATE=>'discovering', broadcast => 1} ); $hash->{helper}{discoverServersBcast} = $chash; $chash->{PORT} = 32414; $chash->{interval} = 10; plex_sendDiscover($chash); Log3 $name, 3, "$name: broadcast server discovery started"; } else { Log3 $name, 3, "$name: failed to start broadcast server discovery: $@"; InternalTimer(gettimeofday()+10, "plex_startDiscovery", $hash, 0); } # udp multicast for clients if( my $socket = IO::Socket::Multicast->new(Proto=>'udp', ReuseAddr=>1, ReusePort=>defined(&SO_REUSEPORT)?1:0) ) { $socket->mcast_add('239.0.0.250'); my $chash = plex_newChash( $hash, $socket, {NAME=>"$name:clientDiscoveryMcast", STATE=>'discovering', multicast => 1} ); $hash->{helper}{discoverClientsMcast} = $chash; $chash->{PORT} = 32412; $chash->{interval} = 10; #plex_sendDiscover($chash); InternalTimer(gettimeofday()+$chash->{interval}/2, "plex_sendDiscover", $chash, 0); Log3 $name, 3, "$name: multicast client discovery started"; } else { Log3 $name, 3, "$name: failed to start multicast client discovery: $@"; InternalTimer(gettimeofday()+10, "plex_startDiscovery", $hash, 0); } # udp broadcast for clients if( my $socket = new IO::Socket::INET ( Proto => 'udp', Broadcast => 1, ) ) { my $chash = plex_newChash( $hash, $socket, {NAME=>"$name:clientDiscoveryBcast", STATE=>'discovering', broadcast => 1} ); $hash->{helper}{discoverClientsBcast} = $chash; $chash->{PORT} = 32412; $chash->{interval} = 10; plex_sendDiscover($chash); Log3 $name, 3, "$name: broadcast client discovery started"; } else { Log3 $name, 3, "$name: failed to start broadcast client discovery: $@"; InternalTimer(gettimeofday()+10, "plex_startDiscovery", $hash, 0); } # listen for udp mulicast HELLO and BYE messages from PHT if( my $socket = IO::Socket::Multicast->new(Proto=>'udp', LocalPort=>32413, ReuseAddr=>1, ReusePort=>defined(&SO_REUSEPORT)?1:0) ) { $socket->mcast_add('239.0.0.250'); my $chash = plex_newChash( $hash, $socket, {NAME=>"$name:clientDiscoveryPHT", STATE=>'listening', multicast => 1} ); $hash->{helper}{discoverClientsListen} = $chash; Log3 $name, 3, "$name: pht client discovery started"; } else { Log3 $name, 3, "$name: failed to pht start client listener"; InternalTimer(gettimeofday()+10, "plex_startDiscovery", $hash, 0); } # listen for udp multicast server UPDATE messages (playerAdd, playerDel) # if( my $socket = IO::Socket::Multicast->new(Proto=>'udp', LocalPort=>32415, ReuseAddr=>1, ReusePort=>defined(&SO_REUSEPORT)?1:0) ) { # $socket->mcast_add('239.0.0.250'); # # my $chash = plex_newChash( $hash, $socket, # {NAME=>"$name:clientDiscovery4", STATE=>'discovering', multicast => 1} ); # # $hash->{helper}{discoverClients4} = $chash; # # Log3 $name, 3, "$name: client discovery4 started"; # # } else { # Log3 $name, 3, "$name: failed to start client discovery4: $@"; # # InternalTimer(gettimeofday()+10, "plex_startDiscovery", $hash, 0); # } if( AttrVal($name, 'responder', undef) ) { # respond to multicast client discovery messages if( my $socket = IO::Socket::Multicast->new(Proto=>'udp', LocalPort=>32412, ReuseAddr=>1, ReusePort=>defined(&SO_REUSEPORT)?1:0) ) { $socket->mcast_add('239.0.0.250'); my $chash = plex_newChash( $hash, $socket, {NAME=>"$name:clientDiscoveryResponderMcast", STATE=>'listening', multicast => 1} ); $hash->{helper}{clientDiscoveryResponderMcast} = $chash; Log3 $name, 3, "$name: multicast client discovery responder started"; } else { Log3 $name, 3, "$name: failed to start multicast client discovery responder: $@"; InternalTimer(gettimeofday()+10, "plex_startDiscovery", $hash, 0); } # respond to broadcast client discovery messages #if( my $socket = new IO::Socket::INET ( Proto => 'udp', Broadcast => 1, LocalAddr => '0.0.0.0', LocalPort => 32412, ReuseAddr=>1) ) { # my $chash = plex_newChash( $hash, $socket, # {NAME=>"$name:clientDiscoveryResponderBcast", STATE=>'listening', broadcast => 1} ); # $hash->{helper}{clientDiscoveryResponderBcast} = $chash; # Log3 $name, 3, "$name: broadcast client discovery responder started"; #} else { # Log3 $name, 3, "$name: failed to start broadcast client discovery responder: $@"; # InternalTimer(gettimeofday()+10, "plex_startDiscovery", $hash, 0); #} } readingsSingleUpdate($hash, 'state', 'running', 1 ); } sub plex_stopDiscovery($) { my ($hash) = @_; my $name = $hash->{NAME}; RemoveInternalTimer($hash, "plex_startDiscovery"); if( my $chash = $hash->{helper}{discoverServersMcast} ) { my $cname = $chash->{NAME}; plex_closeSocket($chash); delete($defs{$cname}); delete $hash->{helper}{discoverServersMcast}; Log3 $name, 3, "$name: multicast server discovery stoped"; } if( my $chash = $hash->{helper}{discoverServersBcast} ) { my $cname = $chash->{NAME}; plex_closeSocket($chash); delete($defs{$cname}); delete $hash->{helper}{discoverServersBcast}; Log3 $name, 3, "$name: broadcast server discovery stoped"; } if( my $chash = $hash->{helper}{discoverClientsMcast} ) { my $cname = $chash->{NAME}; plex_closeSocket($chash); delete($defs{$cname}); delete $hash->{helper}{discoverClientsMcast}; Log3 $name, 3, "$name: multicast client discovery stoped"; } if( my $chash = $hash->{helper}{discoverClientsBcast} ) { my $cname = $chash->{NAME}; plex_closeSocket($chash); delete($defs{$cname}); delete $hash->{helper}{discoverClientsBcast}; Log3 $name, 3, "$name: broadcast client discovery stoped"; } if( my $chash = $hash->{helper}{discoverClientsListen} ) { my $cname = $chash->{NAME}; plex_closeSocket($chash); delete($defs{$cname}); delete $hash->{helper}{discoverClientsListen}; Log3 $name, 3, "$name: pht client listener stoped"; } if( my $chash = $hash->{helper}{discoverClients4} ) { my $cname = $chash->{NAME}; plex_closeSocket($chash); delete($defs{$cname}); delete $hash->{helper}{discoverClients4}; Log3 $name, 3, "$name: client discovery4 stoped"; } if( my $chash = $hash->{helper}{clientDiscoveryResponderMcast} ) { my $cname = $chash->{NAME}; plex_closeSocket($chash); delete($defs{$cname}); delete $hash->{helper}{clientDiscoveryResponderMcast}; Log3 $name, 3, "$name: multicast client discovery responder stoped"; } } sub plex_sendSubscription($$) { my ($hash,$ip) = @_; return undef if( !$hash ); my $name = $hash->{NAME}; my $phash = $hash->{phash}; return undef if( !$phash ); my $entry = $phash->{clients}{$ip}; return undef if( !$entry ); my $pname = $hash->{PNAME}; if( !$hash->{subscriptionsTo}{$ip} ) { $hash->{subscriptionsTo}{$ip} = $ip; Log3 $pname, 4, "$name: adding timeline subscription for $ip"; } else { Log3 $pname, 5, "$name: sending subscribe message to $ip:$entry->{port}"; } plex_sendApiCmd( $phash, "http://$ip:$entry->{port}/player/timeline/subscribe?protocol=http&port=$hash->{PORT}", "subscribe" ); } sub plex_removeSubscription($$) { my ($hash,$ip) = @_; return undef if( !$hash ); my $name = $hash->{NAME}; return undef if( !$hash->{subscriptionsTo}{$ip} ); my $phash = $hash->{phash}; return undef if( !$phash ); my $entry = $phash->{clients}{$ip}; return undef if( !$entry ); my $pname = $hash->{PNAME}; Log3 $pname, 4, "$name: removing timeline subscription for $ip"; plex_sendApiCmd( $phash, "http://$ip:$entry->{port}/player/timeline/unsubscribe?", "unsubscribe" ) if( $entry->{online} ); delete $hash->{subscriptionsTo}{$ip}; if( !%{$hash->{subscriptionsTo}} ) { $phash->{commandID} = 0; } if( my $chash = $hash->{helper}{timelineListener} ) { foreach my $key ( keys %{$chash->{connections}} ) { my $hash = $chash->{connections}{$key}; my $name = $hash->{NAME}; next if( !$hash->{machineIdentifier} ); next if( $hash->{machineIdentifier} ne $entry->{machineIdentifier} ); plex_closeSocket($hash); delete($defs{$name}); delete($chash->{connections}{$name}); } } } sub plex_refreshSubscriptions($) { my ($hash) = @_; my $name = $hash->{NAME}; my $pname = $hash->{PNAME}; Log3 $pname, 4, "$name: refreshing timeline subscriptions" if( %{$hash->{subscriptionsTo}} ); foreach my $ip ( keys %{$hash->{subscriptionsTo}} ) { plex_sendSubscription($hash, $ip); } RemoveInternalTimer($hash,"plex_refreshSubscriptions"); if( $hash->{interval} ) { InternalTimer(gettimeofday()+$hash->{interval}, "plex_refreshSubscriptions", $hash, 0); } } my $lastCommandID; sub plex_sendTimelines($$) { my ($hash,$commandID) = @_; if( ref($hash) ne 'HASH' ) { my ($name) = split( ':', $hash, 2 ); $hash = $defs{$name}; } my $name = $hash->{NAME}; $commandID = $lastCommandID if( !$commandID ); $lastCommandID = $commandID; return undef if( !$hash->{subscriptionsFrom} ); foreach my $key ( keys %{$hash->{subscriptionsFrom}} ) { my $addr = $hash->{subscriptionsFrom}{$key}; my $chash; if( $hash->{helper}{subscriptionsFrom}{$key} ) { $chash = $hash->{helper}{subscriptionsFrom}{$key}; } elsif( my $socket = IO::Socket::INET->new(PeerAddr=>$addr, Timeout=>2, Blocking=>1, ReuseAddr=>1) ) { $chash = plex_newChash( $hash, $socket, {NAME=>"$name:timelineSubscription:$addr", STATE=>'opened', timeline=>1} ); Log3 $name, 3, "$name: timeline subscription opened"; $hash->{helper}{subscriptionsFrom}{$key} = $chash; $chash->{machineIdentifier} = $key; $chash->{commandID} = $commandID; } plex_sendTimeline($chash); } $hash->{interval} = 60; $hash->{interval} = 2 if( $hash->{sonos}{status} && $hash->{sonos}{status} eq 'playing' ); RemoveInternalTimer("$name:sendTimelines"); if( $hash->{interval} ) { InternalTimer(gettimeofday()+$hash->{interval}, 'plex_sendTimelines', "$name:sendTimelines", 0); } } sub plex_sendTimeline($) { my ($hash) = @_; my $name = $hash->{NAME}; my $phash = $hash->{phash}; my $pname = $hash->{PNAME}; return undef if( !$hash->{CD} ); Log3 $pname, 4, "$name: refreshing timeline status"; my $xml = { MediaContainer => { size => 1, machineIdentifier => $phash->{id}, Timeline => { state => $phash->{sonos}{status}, type => 'music', volume => 100, }, }, }; $xml->{MediaContainer}{commandID} = $hash->{commandID} if( defined($hash->{commandID}) ); if( !$phash->{sonos} || !$phash->{sonos}{playqueue}{size} || $phash->{sonos}{playqueue}{size} < 2 ) { $xml->{MediaContainer}{Timeline}{controllable} = 'volume,stop,playPause'; } else { $xml->{MediaContainer}{Timeline}{controllable} = 'volume,stop,playPause,skipNext,skipPrevious'; } if( !$phash->{sonos} || $phash->{sonos}{status} eq 'stopped' ) { $xml->{MediaContainer}{Timeline}{location} = 'navigation'; } else { $xml->{MediaContainer}{Timeline}{location} = 'fullScreenMusic'; $xml->{MediaContainer}{Timeline}{mediaIndex} = $phash->{sonos}{currentTrack}+1; $xml->{MediaContainer}{Timeline}{playQueueID} = $phash->{sonos}{playqueue}{playQueueID} if( $phash->{sonos}{playqueue}{playQueueID} ); $xml->{MediaContainer}{Timeline}{containerKey} = $phash->{sonos}{containerKey} if( $phash->{sonos}{containerKey} ); $xml->{MediaContainer}{Timeline}{machineIdentifier} = $phash->{sonos}{machineIdentifier}; my $tracks = $phash->{sonos}{playqueue}{Track}; my $track = $tracks->[$phash->{sonos}{currentTrack}]; $xml->{MediaContainer}{Timeline}{duration} = $track->{duration}; $xml->{MediaContainer}{Timeline}{seekRange} = "0-$track->{duration}"; $xml->{MediaContainer}{Timeline}{key} = $track->{key}; $xml->{MediaContainer}{Timeline}{ratingKey} = $track->{ratingKey}; $xml->{MediaContainer}{Timeline}{playQueueItemID} = $track->{playQueueItemID}; if( $phash->{sonos}{status} eq 'playing' ) { $phash->{sonos}{currentTime} += time() - $phash->{sonos}{updateTime}; if( $phash->{sonos}{currentTime} >= $track->{duration}/1000 ) { if( !$phash->{sonos}{playqueue}{size} || $phash->{sonos}{playqueue}{size} < 2 ) { fhem( "set $phash->{id} stop" ); } else { fhem( "set $phash->{id} skipNext" ); } return undef; } } $phash->{sonos}{updateTime} = time(); $xml->{MediaContainer}{Timeline}{time} = $phash->{sonos}{currentTime}*1000; } my $body = ''; $body .= "\n"; $body .= XMLout( $xml, KeyAttr => { }, RootName => undef ); $body =~ s/^ //gm; #Log 1, $body; my $ret = "POST /:/timeline HTTP/1.1\r\n"; $ret .= plex_hash2header( { 'Host' => $hash->{CD}->peerhost .':'. $hash->{CD}->peerport, #'Host' => '10.0.1.45:32500', #'Host' => '10.0.1.17:32400', 'Accept' => '*/*', 'X-Plex-Client-Capabilities' => 'audioDecoders=mp3', 'X-Plex-Client-Identifier' => $phash->{id}, 'X-Plex-Device-Name' => $phash->{fhemHostname}, 'X-Plex-Platform' => $^O, 'X-Plex-Version' => '0.0.0', 'X-Plex-Provides' => 'player', 'Content-Length' => length($body), #'Content-Range' => 'bytes 0-/-1', #'Connection' => 'Close', 'Connection' => 'Keep-Alive', #'Content-Type' => 'text/xml;charset=utf-8', 'Content-Type' => 'application/x-www-form-urlencoded', #'X-Plex-Http-Pipeline' => 'infinite', } ); $ret .= "\r\n"; $ret .= $body; #Log 1, $ret; syswrite($hash->{CD}, $ret ); } sub plex_startTimelineListener($) { my ($hash) = @_; my $name = $hash->{NAME}; return undef if( $hash->{server} && $modules{plex}{defptr}{MASTER} ); return undef if( $hash->{client} && $modules{plex}{defptr}{MASTER} ); return undef if( $hash->{machineIdentifier} ); plex_stopTimelineListener($hash); 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) ) { my $chash = plex_newChash( $hash, $socket, {NAME=>"$name:timelineListener", STATE=>'accepting'} ); $chash->{connections} = {}; $chash->{subscriptionsTo} = {}; $hash->{helper}{timelineListener} = $chash; Log3 $name, 3, "$name: timeline listener started"; $chash->{interval} = 30; plex_refreshSubscriptions($chash); } else { Log3 $name, 3, "$name: failed to start timeline listener on port $port $@"; InternalTimer(gettimeofday()+10, "plex_startTimelineListener", $hash, 0); } } sub plex_stopTimelineListener($) { my ($hash) = @_; my $name = $hash->{NAME}; RemoveInternalTimer($hash, "plex_startTimelineListener"); if( my $chash = $hash->{helper}{timelineListener} ) { my $cname = $chash->{NAME}; foreach my $key ( keys %{$chash->{connections}} ) { my $hash = $chash->{connections}{$key}; my $name = $hash->{NAME}; plex_closeSocket($hash); delete($defs{$name}); delete($chash->{connections}{$name}); } plex_closeSocket($chash); delete($defs{$cname}); delete $hash->{helper}{timelineListener}; Log3 $name, 3, "$name: timeline listener stoped"; } } sub plex_Undefine($$) { my ($hash, $arg) = @_; plex_stopTimelineListener($hash); plex_stopWebsockets($hash); plex_stopDiscovery($hash); delete $modules{plex}{defptr}{MASTER} if( $modules{plex}{defptr}{MASTER} == $hash ) ; delete $modules{plex}{defptr}{$hash->{server}} if( $hash->{server} ); delete $modules{plex}{defptr}{$hash->{client}} if( $hash->{client} ); delete $modules{plex}{defptr}{$hash->{machineIdentifier}} if( $hash->{machineIdentifier} ); return undef; } sub plex_Set($$@) { my ($hash, $name, $cmd, @params) = @_; $hash->{".triggerUsed"} = 1; my $list = ''; if( $hash->{'myPlex-servers'} ) { if( $cmd eq 'autocreate' ) { return "usage: autocreate " if( !$params[0] ); if( $hash->{'myPlex-servers'}{Server} ) { foreach my $entry (@{$hash->{'myPlex-servers'}{Server}}) { if( $entry->{localAddresses} eq $params[0] || $entry->{machineIdentifier} eq $params[0] ) { #Log 1, Dumper $entry; my $define = "$entry->{machineIdentifier} plex $entry->{address}"; if( my $cmdret = CommandDefine(undef,$define) ) { return $cmdret; } my $chash = $defs{$entry->{machineIdentifier}}; $chash->{token} = $entry->{accessToken}; fhem( "setreading $entry->{machineIdentifier} .token $entry->{accessToken}" ); return undef; } } } return "unknown server: $params[0]"; } $list .= 'autocreate '; } if( my $entry = plex_serverOf($hash, $cmd, !$hash->{machineIdentifier}) ) { my @params = @params; $cmd = shift @params if( $cmd eq $entry->{address} ); $cmd = shift @params if( $cmd eq $entry->{machineIdentifier} ); my $ip = $entry->{address}; if( $cmd eq 'refreshToken' ) { delete $hash->{token}; plex_getToken($hash); return undef; } return "server $ip not online" if( $cmd ne '?' && !$entry->{online} ); if( $cmd eq 'playlistCreate' ) { return "usage: playlistCreate " if( !$params[0] ); return undef; } elsif( $cmd eq 'playlistAdd' ) { my $server = plex_serverOf($hash, $params[0], 1); return "unknown server" if( !$server ); shift @params if( $params[0] eq $server->{address} ); my $playlist = shift(@params); return "usage: [] playlistAdd " if( !$params[0] ); foreach my $key ( @params ) { plex_addToPlaylist($hash, $server, $playlist, $key); } return undef; } elsif( $cmd eq 'playlistRemove' ) { #my $server = plex_serverOf($hash, $params[0], 1); #return "unknown server" if( !$server ); #shift @params if( $params[0] eq $server->{address} ); #my $playlist = shift(@params); #return "usage: [] playlistRemove " if( !$params[0] ); #foreach my $key ( @params ) { # plex_removeFromPlaylist($hash, $server, $playlist, $key); #} } elsif( $cmd eq 'unwatched' || $cmd eq 'watched' ) { return "usage: unwatched " if( !@params ); $cmd = $cmd eq 'watched' ? 'scrobble' : 'unscrobble'; foreach my $key ( @params ) { $key =~ s'^/library/metadata/''; plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/:/$cmd?key=$key&identifier=com.plexapp.plugins.library", $cmd ); } return undef; } elsif( $cmd eq 'smapiRegister' ) { return "first use the httpPort attribute to configure a fixed http port" if( !AttrVal($name, 'httpPort', 0) ); return plex_publishToSonos($name, 'PLEX', $params[0]); } $list .= 'playlistCreate playlistAdd playlistRemove '; $list .= 'smapiRegister ' if( $hash->{helper}{timelineListener} ); $list .= 'unwatched watched '; } if( my $entry = plex_clientOf($hash, $cmd) ) { my @params = @params; $cmd = shift @params if( $cmd eq $entry->{address} ); my $ip = $entry->{address}; return "client $ip not online" if( $cmd ne '?' && !$entry->{online} ); if( ($cmd eq 'playMedia' || $cmd eq 'resume' ) && $params[0] ) { my $server = plex_serverOf($hash, $params[0], 1); return "unknown server" if( !$server ); shift @params if( $params[0] eq $server->{address} ); my $offset = ''; if( $cmd eq 'resume' ) { my $xml = plex_sendApiCmd( $hash, "http://$server->{address}:$server->{port}$params[0]", '#raw', 1 ); if( $xml && $xml->{Video} ) { $offset = "&offset=$xml->{Video}[0]{viewOffset}" if( $xml->{Video}[0]{viewOffset} ); } } plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/player/playback/playMedia?key=$params[0]&machineIdentifier=$server->{machineIdentifier}&address=$server->{address}&port=$server->{port}$offset", "playback" ); return undef; } elsif( $cmd eq 'mirror' ) { return "mirror not supported" if( $hash->{protocolCapabilities} && $hash->{protocolCapabilities} !~ m/\bmirror\b/ ); return "usage: mirror " if( !$params[0] ); my $server = plex_serverOf($hash, $params[0], 1); return "unknown server" if( !$server ); shift @params if( $params[0] eq $server->{address} ); plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/player/mirror/details?key=$params[0]&machineIdentifier=$server->{machineIdentifier}&address=$server->{address}&port=$server->{port}", "mirror" ); return undef; } elsif( lc($cmd) eq 'play' && $params[0] ) { return "usage: play " if( !$params[0] ); my $server = plex_serverOf($hash, $params[0], 1); return "unknown server" if( !$server ); shift @params if( $params[0] eq $server->{address} ); return plex_play($hash, $entry, $server, $params[0] ); return undef; } elsif( $cmd eq 'pause' || $cmd eq 'play' || $cmd eq 'resume' || $cmd eq 'stop' || $cmd eq 'skipNext' || $cmd eq 'skipPrevious' || $cmd eq 'stepBack' || $cmd eq 'stepForward' ) { return "$cmd not supported" if( $cmd ne 'pause' && $cmd ne 'play' && $cmd ne 'resume' && $hash->{controllable} && $hash->{controllable} !~ m/\b$cmd\b/ ); if( ($cmd eq 'playMedia' || $cmd eq 'resume') && $hash->{STATE} eq 'stopped' ) { my $key = ReadingsVal($name,'key', undef); return 'no current media key' if( !$key ); my $server = ReadingsVal($name,'server', undef); return 'no current server' if( !$server ); my $entry = plex_serverOf($hash, $server, 1); return "unknown server: $server" if( !$entry ); CommandSet( undef, "$hash->{NAME} $cmd $entry->{address} $key" ); return undef; } if( $cmd eq 'pause' ) { return undef if( $hash->{STATE} !~ m/playing/ ); } elsif( $cmd eq 'play' ) { return undef if( $hash->{STATE} =~ m/playing/ ); } elsif( $cmd eq 'resume' ) { return undef if( $hash->{STATE} =~ m/playing/ ); $cmd = 'play'; } my $type = $params[0]; $type = $hash->{currentMediaType} if( !$type ); $type = "type=$type" if( $type ); $type = "" if( !$type ); plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/player/playback/$cmd?$type", "playback" ); return undef; } elsif( $cmd eq 'seekTo' ) { return "$cmd not supported" if( $hash->{controllable} && $hash->{controllable} !~ m/\b$cmd\b/ ); return "usage: $cmd " if( !defined($params[0]) ); $params[0] =~ s/[^\d]//g; my $type = $params[1]; $type = $hash->{currentMediaType} if( !$type ); $type = "type=$type" if( $type ); $type = "" if( !$type ); plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/player/playback/seekTo?$type&offset=$params[0]", "parameters" ); return undef; } elsif( $cmd eq 'volume' || $cmd eq 'shuffle' || $cmd eq 'repeat' ) { return "$cmd not supported" if( $hash->{controllable} && $hash->{controllable} !~ m/\b$cmd\b/ ); return "usage: $cmd " if( !defined($params[0]) ); $params[0] =~ s/[^\d]//g; return "usage: $cmd [0/1]" if( $cmd eq 'shuffle' && ($params[0] < 0 || $params[0] > 1) ); return "usage: $cmd [0/1/2]" if( $cmd eq 'repeat' && ($params[0] < 0 || $params[0] > 2) ); return "usage: $cmd [0-100]" if( $cmd eq 'volume' && ($params[0] < 0 || $params[0] > 100) ); my $type = $params[1]; $type = $hash->{currentMediaType} if( !$type ); $type = "type=$type" if( $type ); $type = "" if( !$type ); plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/player/playback/setParameters?$type&$cmd=$params[0]", "parameters" ); return undef; } elsif( $cmd eq 'home' || $cmd eq 'music' ) { plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/player/navigation/$cmd?", "navigation" ); return undef; } elsif( $cmd eq 'unwatched' || $cmd eq 'watched' ) { my $key = ReadingsVal($name,'key', undef); return 'no current media key' if( !$key ); my $server = ReadingsVal($name,'server', undef); return 'no current server' if( !$server ); plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/player/playback/stop?type=video", "playback" ) if( $cmd eq 'watched' ); my $entry = plex_serverOf($hash, $server, 1); return "unknown server: $server" if( !$entry ); $cmd = $cmd eq 'watched' ? 'scrobble' : 'unscrobble'; $key =~ s'^/library/metadata/''; plex_sendApiCmd( $hash, "http://$entry->{address}:$entry->{port}/:/$cmd?key=$key&identifier=com.plexapp.plugins.library", $cmd ); return undef; } $list .= 'playMedia ' if( !$hash->{controllable} || $hash->{controllable} =~ m/\bplayPause\b/ ); $list .= 'play ' if( $hash->{protocolCapabilities} && $hash->{protocolCapabilities} =~ m/\bplayqueues\b/ ); $list .= 'resume:noArg ' if( !$hash->{controllable} || $hash->{controllable} =~ m/\bplayPause\b/ ); $list .= 'pause:noArg ' if( $hash->{controllable} && $hash->{controllable} =~ m/\bplayPause\b/ );; $list .= 'stop:noArg ' if( $hash->{controllable} && $hash->{controllable} =~ m/\bstop\b/ );; $list .= 'skipNext:noArg ' if( $hash->{controllable} && $hash->{controllable} =~ m/\bskipNext\b/ );; $list .= 'skipPrevious:noArg ' if( $hash->{controllable} && $hash->{controllable} =~ m/\bskipPrevious\b/ );; $list .= 'stepBack:noArg ' if( $hash->{controllable} && $hash->{controllable} =~ m/\bstepBack\b/ );; $list .= 'stepForward:noArg ' if( $hash->{controllable} && $hash->{controllable} =~ m/\bstepForward\b/ );; $list .= 'seekTo ' if( $hash->{controllable} && $hash->{controllable} =~ m/\bseekTo\b/ );; $list .= 'mirror ' if( !$hash->{controllable} || $hash->{controllable} =~ m/\bmirror\b/ ); $list .= 'volume:slider,0,1,100 ' if( $hash->{controllable} && $hash->{controllable} =~ m/\bvolume\b/ ); $list .= 'repeat ' if( $hash->{controllable} && $hash->{controllable} =~ m/\brepeat\b/ ); $list .= 'shuffle ' if( $hash->{controllable} && $hash->{controllable} =~ m/\bshuffle\b/ ); $list .= 'home:noArg music:noArg '; $list .= 'unwatched:noArg watched:noArg '; } if( $modules{plex}{defptr}{MASTER} && $hash == $modules{plex}{defptr}{MASTER} ) { if( $cmd eq 'restartDiscovery' ) { plex_startDiscovery($hash); return undef; } elsif( $cmd eq 'subscribe' ) { return 'usage: subscribe ' if( !$params[0] ); my $client = plex_clientOf( $hash, $params[0] ); return "no client found for $params[0]" if( !$client ); plex_sendSubscription($hash->{helper}{timelineListener}, $client->{address}); return undef; } elsif( $cmd eq 'unsubscribe' ) { return 'usage: unsubscribe ' if( !$params[0] ); my $client = plex_clientOf( $hash, $params[0] ); return "no client found for $params[0]" if( !$client ); plex_removeSubscription($hash->{helper}{timelineListener}, $client->{address}); return undef; } elsif( $cmd eq 'offline' ) { return 'usage: offline ' if( !$params[0] ); my $client = plex_clientOf( $hash, $params[0] ); return "no client found for $params[0]" if( !$client ); $client->{online} = 1; plex_disappeared($hash, 'client', $client->{address}); return undef; } elsif( $cmd eq 'online' ) { return 'usage: online ' if( !$params[0] ); my $client = plex_clientOf( $hash, $params[0] ); return "no client found for $params[0]" if( !$client ); $client->{online} = 0; plex_discovered($hash, 'client', $client->{address}, $client); return undef; } elsif( $cmd eq 'showAccount' ) { my $user = AttrVal($name, 'user', undef); my $password = AttrVal($name, 'password', undef); return 'no user set' if( !$user ); return 'no password set' if( !$password ); $user = plex_decrypt( $user ); $password = plex_decrypt( $password ); return "$user: $password"; } elsif( $cmd eq 'refreshToken' ) { delete $hash->{token}; plex_getToken($hash); return undef; } $list .= 'restartDiscovery:noArg subscribe unsubscribe showAccount:noArg '; } $list =~ s/ $//; return "Unknown argument $cmd, choose one of $list"; } sub plex_deviceList($$) { my ($hash, $type) = @_; my $ret = ''; my $entries = $hash->{$type}; $ret .= "$type from discovery:\n"; $ret .= sprintf( "%16s %19s %4s %-23s %s\n", 'ip', 'updatedAt', 'onl.', 'name', 'machineIdentifier' ); foreach my $ip ( keys %{$entries} ) { my $entry = $entries->{$ip}; $ret .= sprintf( "%16s %19s %4s %-23s %s\n", $entry->{address}, $entry->{updatedAt}?strftime("%Y-%m-%d %H:%M:%S", localtime($entry->{updatedAt}) ):'',, $entry->{online}?'yes':'no', $entry->{name}, $entry->{machineIdentifier} ); } if( $type eq 'servers' && $hash->{'myPlex-servers'} ) { $ret .= "\n"; $ret .= "$type from myPlex:\n"; if( $hash->{'myPlex-servers'}{Server} ) { $ret .= sprintf( "%16s %19s %-23s %1s %s\n", 'ip', 'updatedAt', 'name', 'o', 'machineIdentifier' ); foreach my $entry (@{$hash->{'myPlex-servers'}{Server}}) { #next if( !$entry->{owned} ); $entry->{owned} = 0 if( !defined($entry->{owned}) ); $entry->{localAddresses} = '' if( !$entry->{localAddresses} ); $entry->{address} = '' if( !$entry->{address} ); $ret .= sprintf( "%16s %19s %-23s %1s %s\n", $entry->{address}, strftime("%Y-%m-%d %H:%M:%S", localtime($entry->{updatedAt}) ), $entry->{name}, $entry->{owned}, $entry->{machineIdentifier} ); } } } if( $type eq 'clients' && $hash->{'myPlex-devices'} ) { $ret .= "\n"; $ret .= "$type from myPlex:\n"; if( $hash->{'myPlex-devices'}{Device} ) { $ret .= sprintf( "%16s %19s %-25s %-20s %-40s %s\n", 'ip', 'lastSeenAt', 'name', 'product', 'clientIdentifier', 'provides' ); foreach my $entry (@{$hash->{'myPlex-devices'}{Device}}) { next if( !$entry->{provides} ); #next if( !$entry->{localAddresses} ); $ret .= sprintf( "%16s %19s %-25s %-20s %-40s %s\n", $entry->{localAddresses}?$entry->{localAddresses}:'', $entry->{lastSeenAt}?strftime("%Y-%m-%d %H:%M:%S", localtime($entry->{lastSeenAt}) ):'', $entry->{name}, $entry->{product}, $entry->{clientIdentifier}, $entry->{provides} ); } } } return $ret; } sub plex_makeLink($$$$;$) { my ($hash, $cmd, $parentSection, $key, $txt) = @_; return $txt if( !$key ); $txt = $key if( !$txt ); if( defined($parentSection) && $parentSection eq '' && $key !~ '^/' ) { $cmd = "get $hash->{NAME} $cmd /library/sections/$key"; } elsif( defined($parentSection) && $key !~ '^/' ) { $cmd = "get $hash->{NAME} $cmd $parentSection/$key"; } elsif( $key !~ '^/' ) { $cmd = "get $hash->{NAME} $cmd /library/metadata/$key"; } else { $cmd = "get $hash->{NAME} $cmd $key"; } return $txt if( !$FW_ME ); return "$txt"; } sub plex_makeImage($$$$) { my ($hash, $server, $url, $size) = @_; return '' if( !$url ); my $token = $server->{accessToken}; $token = $hash->{token} if( !$token ); my $ret .= "{address}:$server->{port}/photo/:/transcode?X-Plex-Token=$token&url=". urlEncode("127.0.0.1:32400$url?X-Plex-Token=$token") ."&width=$size&height=$size\">\n"; return $ret; } sub plex_mediaList2($$$$;$) { my ($hash, $type, $xml, $items, $cmd) = @_; if( $items ) { if( 0 && !$xml->{sortAsc} ) { my @items; if( $xml->{Track} ) { @items = sort { $a->{index} <=> $b->{index} } @{$items}; } else { @items = sort { $a->{title} cmp $b->{title} } @{$items}; } $items = \@items; } } my $ret; if( $type eq 'Directory' ) { #Log 1, Dumper $items; $ret .= "\n" if( $ret ); $ret .= "$type\n"; $ret .= sprintf( "%-35s %-10s %s\n", 'key', 'type', 'title' ); foreach my $item (@{$items}) { $item->{type} = '' if( !$item->{type} ); $item->{title} = encode('UTF-8', $item->{title}); $ret .= plex_makeLink($hash, 'ls', $xml->{parentSection}, $item->{key}, sprintf( "%-35s %-10s %s", $item->{key}, $item->{type}, $item->{title} ) ); $ret .= " ($item->{year})" if( $item->{year} ); $ret .= "\n"; } } if( $type eq 'Playlist' ) { $ret .= "\n" if( $ret ); $ret .= "$type\n"; $ret .= sprintf( "%-35s %-10s %s\n", 'key', 'type', 'title' ); foreach my $item (@{$items}) { $item->{type} = '' if( !$item->{type} ); $item->{title} = encode('UTF-8', $item->{title}); $ret .= plex_makeLink($hash, 'ls', $xml->{parentSection}, $item->{key}, sprintf( "%-35s %-10s %s\n", $item->{key}, $item->{type}, $item->{title} ) ); #$ret .= plex_makeImage($hash, $server, $xml->{composite}, 100); } } if( $type eq 'Video' ) { $ret .= "\n" if( $ret ); $ret .= "$type\n"; $ret .= sprintf( "%-35s %-10s nr %s\n", 'key', 'type', 'title' ); foreach my $item (@{$items}) { $item->{title} = encode('UTF-8', $item->{title}); if( defined($item->{index}) ) { $ret .= plex_makeLink($hash, 'detail', $xml->{parentSection}, $item->{key}, sprintf( "%-35s %-10s %3i %s", $item->{key}, $item->{type}, $item->{index}, $item->{title} ) ); $ret .= plex_makeLink($hash,'detail', undef, $item->{grandparentKey}, " ($item->{grandparentTitle}" ) if( $item->{grandparentTitle} ); #$ret .= " ($item->{year})" if( $item->{year} ); $ret .= sprintf(": S%02iE%02i",$item->{parentIndex}, $item->{index} ) if( $item->{parentIndex} ); $ret .= ")" if( $item->{grandparentTitle} ); } else { $ret .= plex_makeLink($hash,'detail', $xml->{parentSection}, $item->{key}, sprintf( "%-35s %-10s %s", $item->{key}, $item->{type}, $item->{title} ) ); } if( $cmd && $cmd eq 'files' && $item->{Media} && $item->{Media}[0]{Part} ) { $ret .= " ($item->{Media}[0]{Part}[0]{file})"; } $ret .= "\n"; } } if( $type eq 'Track' ) { $ret .= "\n" if( $ret ); $ret .= "$type\n"; $ret .= sprintf( "%-35s %-10s nr %s\n", 'key', 'type', 'title' ); foreach my $item (@{$items}) { $item->{title} = encode('UTF-8', $item->{title}); $ret .= sprintf( "%-35s %-10s %3i %s\n", $item->{key}, $item->{type}, $item->{index}, $item->{title} ); } } return $ret; } sub plex_mediaList($$$;$) { my ($hash, $server, $xml, $cmd) = @_; #Log 1, Dumper $xml; return $xml if( ref($xml) ne 'HASH' ); $xml->{librarySectionTitle} = encode('UTF-8', $xml->{librarySectionTitle}) if( $xml->{librarySectionTitle} ); $xml->{title} = encode('UTF-8', $xml->{title}) if( $xml->{title} ); $xml->{title1} = encode('UTF-8', $xml->{title1}) if( $xml->{title1} ); $xml->{title2} = encode('UTF-8', $xml->{title2}) if( $xml->{title2} ); $xml->{title3} = encode('UTF-8', $xml->{title3}) if( $xml->{title3} ); my $ret = ''; $ret .= plex_makeImage($hash, $server, $xml->{thumb}, 100); $ret .= plex_makeImage($hash, $server, $xml->{composite}, 100); $ret .= "$xml->{librarySectionTitle}: " if( $xml->{librarySectionTitle} ); $ret .= plex_makeLink($hash, 'detail', undef, $xml->{ratingKey}, "$xml->{title} ") if( $xml->{title} ); $ret .= plex_makeLink($hash, 'detail', undef, $xml->{grandparentRatingKey}, "$xml->{title1} ") if( $xml->{title1} ); $ret .= plex_makeLink($hash, 'detail', undef, $xml->{key}, "; $xml->{title2} ") if( $xml->{title2} ); $ret .= "; $xml->{title3} " if( $xml->{title3} ); $ret .= "\n"; $ret .= plex_mediaList2( $hash, 'Directory', $xml, $xml->{Directory} ) if( $xml->{Directory} ); $ret .= plex_mediaList2( $hash, 'Playlist', $xml, $xml->{Playlist} ) if( $xml->{Playlist} ); $ret .= plex_mediaList2( $hash, 'Video', $xml, $xml->{Video}, $cmd ) if( $xml->{Video} ); $ret .= plex_mediaList2( $hash, 'Track', $xml, $xml->{Track} ) if( $xml->{Track} ); if( !$xml->{Directory} && !$xml->{Playlist} && !$xml->{Video} && !$xml->{Track} ) { return $xml->{head}[0]{title}[0] if( ref $xml->{head} eq 'ARRAY' && ref $xml->{head}[0]{title} eq 'ARRAY' ); return "unknown media type"; } return $ret; } sub plex_mediaDetail2($$$$) { my ($hash, $server, $xml, $items) = @_; #Log 1, Dumper $xml; if( $items ) { if( 0 && !$xml->{sortAsc} ) { my @items = sort { $a->{index} <=> $b->{index} } @{$items}; #my @items = sort { $a->{title} cmp $b->{title} } @{$items}; $items = \@items; } } $xml->{viewGroup} = encode('UTF-8', $xml->{viewGroup}) if( $xml->{viewGroup} ); my $ret = ''; foreach my $item (@{$items}) { $item->{grandparentTitle} = encode('UTF-8', $item->{grandparentTitle}) if( $item->{grandparentTitle} ); $item->{parentTitle} = encode('UTF-8', $item->{parentTitle}) if( $item->{parentTitle} ); $item->{title} = encode('UTF-8', $item->{title}) if( $item->{title} ); $item->{summary} = encode('UTF-8', $item->{summary}) if( $item->{summary} ); $ret .= "\n" if( $ret && (!$xml->{viewGroup} || ($xml->{viewGroup} ne 'track' && $xml->{viewGroup} ne 'secondary') ) ); if( $item->{type} eq 'playlist' ) { $ret .= sprintf( "%s ", $item->{title} ) if( $item->{title} ); $ret .= "\n"; $ret .= plex_makeImage($hash, $server, $item->{composite}, 250); $ret .= "\n"; $ret .= sprintf( "%s ", $item->{playlistType} ) if( $item->{playlistType} ); $ret .= sprintf( "%s ", plex_timestamp2date($item->{addedAt}) ) if( $item->{addedAt} ); $ret .= sprintf( "items: %i ", $item->{leafCount} ) if( $item->{leafCount} && $item->{leafCount} > 1 ); $ret .= sprintf( "viewCount: %i ", $item->{viewCount} ) if( $item->{viewCount} ); $ret .= "\n"; } elsif( $item->{type} eq 'album' || $item->{type} eq 'artist' || $item->{type} eq 'show' || $item->{type} eq 'season' ) { $ret .= plex_makeLink($hash, 'detail', undef, $item->{grandparentRatingKey}, "$item->{grandparentTitle}: ") if( $item->{grandparentTitle} ); $ret .= plex_makeLink($hash, 'detail', undef, $item->{parentRatingKey}, "$item->{parentTitle}: ") if( $item->{parentTitle} ); $ret .= sprintf( "%s ", $item->{title} ) if( $item->{title} ); $ret .= sprintf("(S%02iE%02i)",$item->{parentIndex}, $item->{index} ) if( $item->{parentIndex} && $item->{type} ne 'season' ); #$ret .= sprintf("(S%02i)", $item->{index} ) if( $item->{index} && $item->{type} eq 'season' ); $ret .= "\n"; $ret .= plex_makeImage($hash, $server, $item->{thumb}, 250); $ret .= "\n"; if( $item->{Genre} ) { foreach my $genre ( @{$item->{Genre}}) { $ret .= sprintf( "%s ", $genre->{tag} ) if( $genre->{tag} ); } $ret .= ' '; } $ret .= sprintf( "%s ", $item->{contentRating} ) if( $item->{contentRating} ); $ret .= sprintf( "%s ", $item->{rating} ) if( $item->{rating} ); $ret .= sprintf( "%i ", $item->{year} ) if( $item->{year} ); $ret .= sprintf( "%s ", plex_timestamp2date($item->{addedAt}) ) if( $item->{addedAt} ); $ret .= sprintf( "items: %i ", $item->{leafCount} ) if( $item->{leafCount} && $item->{leafCount} > 1 ); $ret .= sprintf( "viewCount: %i ", $item->{viewCount} ) if( $item->{viewCount} ); $ret .= "\n"; } elsif( $item->{type} eq 'track' ) { $ret .= sprintf("(Disk %02i Track %02i) ",$item->{parentIndex}, $item->{index} ) if( $item->{parentIndex} ); $ret .= sprintf("%2i ",$item->{index}, $item->{index} ) if( !$item->{parentIndex} ); $ret .= plex_sec2hms($item->{duration}/1000); $ret .= " "; $ret .= sprintf( "%s: ", $item->{grandparentTitle} ) if( !$xml->{title1} && $item->{grandparentTitle} ); $ret .= sprintf( "%s: ", $item->{parentTitle} ) if( !$xml->{title2} && $item->{parentTitle} ); $ret .= sprintf( "%s ", $item->{title} ) if( $item->{title} ); #$ret .= "\n"; $ret .= "\n"; $ret .= plex_makeImage($hash, $server, $item->{thumb}, 250); #$ret .= "\n"; $ret .= sprintf( "%s ", $item->{contentRating} ) if( $item->{contentRating} ); $ret .= sprintf( "%i ", $item->{year} ) if( $item->{year} ); #$ret .= sprintf( "%s ", plex_timestamp2date($item->{addedAt}) ) if( $item->{addedAt} ); #$ret .= sprintf( "viewCount: %i ", $item->{viewCount} ) if( $item->{viewCount} ); #$ret .= "\n"; } elsif( $item->{type} eq 'episode' || $item->{type} eq 'movie' ) { $ret .= plex_makeLink($hash, 'detail', undef, $item->{grandparentRatingKey}, "$item->{grandparentTitle}: ") if( $item->{grandparentTitle} ); $ret .= plex_makeLink($hash, 'detail', undef, $item->{parentKey}, "; $item->{parentTitle} ") if( $item->{parentTitle} ); $ret .= sprintf( "%s ", $item->{title} ) if( $item->{title} ); $ret .= sprintf("(S%02iE%02i)",$item->{parentIndex}, $item->{index} ) if( defined($item->{parentIndex}) ); $ret .= sprintf("(Episode %02i)",$item->{index}, $item->{index} ) if( !defined($item->{parentIndex}) && $item->{index} ); $ret .= " "; $ret .= plex_sec2hms($item->{duration}/1000); $ret .= "\n"; $ret .= plex_makeImage($hash, $server, $item->{thumb}, 250); $ret .= "\n"; $ret .= sprintf( "%s ", $item->{contentRating} ) if( $item->{contentRating} ); $ret .= sprintf( "%s ", $item->{rating} ) if( $item->{rating} ); $ret .= sprintf( "%i ", $item->{year} ) if( $item->{year} ); $ret .= sprintf( "%s ", plex_timestamp2date($item->{addedAt}) ) if( $item->{addedAt} ); $ret .= sprintf( "viewCount: %i ", $item->{viewCount} ) if( $item->{viewCount} ); $ret .= "\n"; } elsif( $item->{type} ) { $ret .= "unknown item type: $item->{type}\n"; } else { $ret .= sprintf( "%-35s %-10s %s\n", $item->{key}, $item->{title} ); } if( !$xml->{viewGroup} || ($xml->{viewGroup} ne 'track' && $xml->{viewGroup} ne 'secondary') ) { if( my $mhash = $modules{plex}{defptr}{MASTER} ) { if( my $clients = $mhash->{clients} ) { $ret .= "\nplay: "; foreach my $ip ( keys %{$clients} ) { my $client = $clients->{$ip}; next if( !$client->{online} ); my $cmd = 'play'; my $key = $item->{key}; $key =~ s/.children$//; $cmd = "set $hash->{NAME} $client->{address} $cmd $key"; $ret .= "$ip "; } $ret .= "\n\n"; } } } $ret .= $item->{summary} ."\n" if( $item->{summary} ); } return $ret; } sub plex_mediaDetail($$$) { my ($hash, $server, $xml) = @_; return $xml if( ref($xml) ne 'HASH' ); $xml->{title} = encode('UTF-8', $xml->{title}) if( $xml->{title} ); $xml->{title1} = encode('UTF-8', $xml->{title1}) if( $xml->{title1} ); $xml->{title2} = encode('UTF-8', $xml->{title2}) if( $xml->{title2} ); $xml->{summary} = encode('UTF-8', $xml->{summary}) if( $xml->{summary} ); #Log 1, Dumper $xml; my $ret = ''; $ret .= plex_makeImage($hash, $server, $xml->{thumb}, 250); $ret .= plex_makeLink($hash, 'detail', undef, $xml->{ratingKey}, "$xml->{title} ") if( $xml->{title} ); $ret .= sprintf( "%s: ", $xml->{title1} ) if( $xml->{title1} ); $ret .= sprintf( "%s: ", $xml->{title2} ) if( $xml->{title2} ); $ret .= sprintf( "(%s)\n", $xml->{parentYear} ) if( $xml->{parentYear} ); $ret .= $xml->{summary} ."\n" if( $xml->{summary} ); $ret .= plex_mediaDetail2( $hash, $server, $xml, $xml->{Directory} ) if( $xml->{Directory} ); $ret .= plex_mediaDetail2( $hash, $server, $xml, $xml->{Playlist} ) if( $xml->{Playlist} ); $ret .= plex_mediaDetail2( $hash, $server, $xml, $xml->{Video} ) if( $xml->{Video} ); $ret .= plex_mediaDetail2( $hash, $server, $xml, $xml->{Track} ) if( $xml->{Track} ); if( !$xml->{Directory} && !$xml->{Playlist} && !$xml->{Video} && !$xml->{Track} ) { Log 1, Dumper $xml; return "unknown media type"; } return $ret; } sub plex_Get($$@) { my ($hash, $name, $cmd, @params) = @_; my $list = ''; if( my $hash = $modules{plex}{defptr}{MASTER} ) { if( $cmd eq 'servers' || $cmd eq 'clients' ) { if( my $entry = plex_serverOf($hash, $cmd, !$hash->{machineIdentifier}) ) { plex_sendApiCmd( $hash, "http://$entry->{address}:$entry->{port}/clients", "clients" ); } return plex_deviceList($hash, $cmd ); } elsif( $cmd eq 'pin' ) { return plex_getPinForToken($hash); } $list .= 'clients:noArg servers:noArg pin:noArg '; } if( my $entry = plex_serverOf($hash, $cmd, !$hash->{machineIdentifier}) ) { my @params = @params; $cmd = shift @params if( $cmd eq $entry->{address} ); $cmd = shift @params if( $cmd eq $entry->{machineIdentifier} ); if( $cmd eq 'servers' ) { return plex_deviceList($hash, 'servers' ); } elsif( $cmd eq 'clients' ) { return plex_deviceList($hash, 'clients' ); } elsif( $cmd eq 'pin' ) { return plex_getPinForToken($hash); } my $ip = $entry->{address}; return "server $ip not online" if( $cmd ne '?' && !$entry->{online} ); my $param = shift( @params ); if( !$param ) { $param = ''; } if( $cmd eq 'sections' || $cmd eq 'ls' || $cmd eq 'files' ) { $param = "/$param" if( $param && $param !~ '^/' ); my $ret; if( $param =~ m'/playlists' ) { $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}$param", 'sections', $hash->{CL} || 1, $entry->{accessToken} ); } elsif( $param =~ m'^/library' ) { $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}$param", "sections:$param $cmd", $hash->{CL} || 1, $entry->{accessToken} ); } else { $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/library/sections$param", "sections:$param $cmd", $hash->{CL} || 1, $entry->{accessToken} ); } return $ret; } elsif( $cmd eq 'search' ) { return "usage: search " if( !$param ); $param .= ' '. join( ' ', @params ) if( @params ); $param = urlEncode( $param ); my $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/search?query=$param", 'search', $hash->{CL} || 1 ); return $ret; } elsif( $cmd eq 'playlists' ) { $param = "/$param" if( $param && $param !~ '^/' ); $param = '' if( !$param ); $param =~ s'^/playlists''; my $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/playlists$param", "playlists", $hash->{CL} || 1 ); return $ret; } elsif( $cmd eq 'sessions' ) { my $xml = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/status/sessions", 'sessions', 1 ); return undef if( !$xml ); return Dumper $xml; } elsif( $cmd eq 'identity' ) { my $xml = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/identity", 'identity', 1 ); return undef if( !$xml ); return Dumper $xml; } elsif( $cmd eq 'detail' ) { return "usage: detail " if( !$param ); my $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}$param", 'detail', $hash->{CL} || 1 ); return $ret; } elsif( lc($cmd) eq 'ondeck' ) { my $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/library/onDeck", 'onDeck', $hash->{CL} || 1 ); return $ret; } elsif( lc($cmd) eq 'recentlyadded' ) { my $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/library/recentlyAdded", 'recentlyAdded', $hash->{CL} || 1 ); return $ret; } elsif( $cmd eq 'm3u' || $cmd eq 'pls' ) { return "usage: $cmd " if( !$param ); $param = "/library/metadata/$param" if( $param !~ '^/' ); my $ret; if( $param =~ m'/playlists' ) { $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}$param", "#$cmd:$entry->{machineIdentifier}", $hash->{CL} || 1 ); } else { $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}$param", "#$cmd:$entry->{machineIdentifier}", $hash->{CL} || 1 ); } return $ret; } $list .= 'identity:noArg ls files search sessions:noArg detail onDeck:noArg recentlyAdded:noArg playlists:noArg '; $list .= 'servers:noArg pin:noArg ' if( $list !~ m/\bservers\b/ ); } if( my $entry = plex_clientOf($hash, $cmd) ) { my @params = @params; $cmd = shift @params if( $cmd eq $entry->{address} ); $cmd = shift @params if( $cmd eq $entry->{machineIdentifier} ); my $key = ReadingsVal($name,'key', undef); my $server = ReadingsVal($name,'server', undef); if( $cmd eq 'detail' ) { return 'no current media key' if( !$key ); return 'no current server' if( !$server ); my $entry = plex_serverOf($hash, $server, 1); return "unknown server: $server" if( !$entry ); my $ret = plex_sendApiCmd( $hash, "http://$entry->{address}:$entry->{port}$key", 'detail', $hash->{CL} || 1 ); return $ret; } my $ip = $entry->{address}; return "client $ip not online" if( $cmd ne '?' && !$entry->{online} ); if( $cmd eq 'resources' ) { my $xml = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/resources", 'resources', 1 ); return undef if( !$xml ); return Dumper $xml; } elsif( $cmd eq 'timeline' ) { my $xml = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/player/timeline/poll?&wait=0", 'timeline', 1 ); return undef if( !$xml ); return Dumper $xml; } $list .= 'detail:noArg '; $list .= 'resources:noArg timeline:noArg '; } $list =~ s/ $//; return "Unknown argument $cmd, choose one of $list"; } sub plex_encrypt($) { my ($decoded) = @_; my $key = getUniqueId(); my $encoded; return $decoded if( $decoded =~ /^crypt:(.*)/ ); for my $char (split //, $decoded) { my $encode = chop($key); $encoded .= sprintf("%.2x",ord($char)^ord($encode)); $key = $encode.$key; } return 'crypt:'. $encoded; } sub plex_decrypt($) { my ($encoded) = @_; my $key = getUniqueId(); my $decoded; $encoded = $1 if( $encoded =~ /^crypt:(.*)/ ); for my $char (map { pack('C', hex($_)) } ($encoded =~ /(..)/g)) { my $decode = chop($key); $decoded .= chr(ord($char)^ord($decode)); $key = $decode.$key; } return $decoded; } sub plex_Attr($$$) { my ($cmd, $name, $attrName, $attrVal) = @_; my $orig = $attrVal; $attrVal = int($attrVal) if($attrName eq "interval"); $attrVal = 60 if($attrName eq "interval" && $attrVal < 60 && $attrVal != 0); my $hash = $defs{$name}; if( $attrName eq 'disable' ) { if( $cmd eq "set" && $attrVal ) { plex_stopTimelineListener($hash); plex_stopWebsockets($hash); plex_stopDiscovery($hash); foreach my $ip ( keys %{$hash->{clients}} ) { $hash->{clients}{$ip}{online} = 0; } readingsSingleUpdate($hash, 'state', 'disabled', 1 ); } else { readingsSingleUpdate($hash, 'state', 'running', 1 ); $attr{$name}{$attrName} = 0; plex_startDiscovery($hash); plex_startTimelineListener($hash); } } elsif( $attrName eq 'httpPort' ) { plex_stopTimelineListener($hash); plex_startTimelineListener($hash); } elsif( $attrName eq 'responder' ) { if( $cmd eq "set" && $attrVal ) { $attr{$name}{$attrName} = 1; plex_startDiscovery($hash); } else { $attr{$name}{$attrName} = 0; plex_startDiscovery($hash); } } elsif( $attrName eq 'user' ) { if( $cmd eq "set" && $attrVal ) { $attrVal = plex_encrypt($attrVal); if( $attr{$name}{'user'} && $attr{$name}{'password'} ) { delete $hash->{token}; plex_getToken($hash); } } } elsif( $attrName eq 'password' ) { if( $cmd eq "set" && $attrVal ) { $attrVal = plex_encrypt($attrVal); if( $attr{$name}{'user'} && $attr{$name}{'password'} ) { delete $hash->{token}; plex_getToken($hash); } } } elsif( $attrName eq 'fhemIP' ) { if( $cmd eq "set" && $attrVal ) { $hash->{fhemIP} = $attrVal; } else { $hash->{fhemIP} = plex_getLocalIP(); } } if( $cmd eq "set" ) { if( $attrVal && $orig ne $attrVal ) { $attr{$name}{$attrName} = $attrVal; return $attrName ." set to ". $attrVal if( $init_done ); } } return; } sub plex_getToken($) { my ($hash) = @_; my $name = $hash->{NAME}; return $hash->{token} if( $hash->{token} ); my $user = AttrVal($name, 'user', undef); my $password = AttrVal($name, 'password', undef); return '' if( !$user ); return '' if( !$password ); $user = plex_decrypt( $user ); $password = plex_decrypt( $password ); my $url = 'https://plex.tv/users/sign_in.xml'; Log3 $name, 4, "$name: requesting $url"; my $param = { url => $url, method => 'POST', timeout => 5, noshutdown => 0, hash => $hash, key => 'token', 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', }, data => { 'user[login]' => $user, 'user[password]' => $password }, }; $param->{callback} = \&plex_parseHttpAnswer; HttpUtils_NonblockingGet( $param ); 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; HttpUtils_NonblockingGet( $param ); 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; HttpUtils_NonblockingGet( $param ); return undef; } sub plex_sendApiCmd($$$;$$) { my ($hash,$url,$key,$blocking,$token) = @_; $token = $hash->{token} if( !$token && $hash->{token} ); my $name = $hash->{NAME}; if( $url =~ m/.player./ ) { my $mhash = $modules{plex}{defptr}{MASTER}; $mhash = $hash if( !$mhash ); ++$mhash->{commandID}; $url .= "&commandID=$mhash->{commandID}"; } Log3 $name, 4, "$name: requesting $url"; my $address; my $port; if( $url =~ m'//([^:]*):(\d*)' ) { $address = $1; $port = $2; } #X-Plex-Platform (Platform name, eg iOS, MacOSX, Android, LG, etc) #X-Plex-Platform-Version (Operating system version, eg 4.3.1, 10.6.7, 3.2) #X-Plex-Provides (one or more of [player, controller, server]) #X-Plex-Product (Plex application name, eg Laika, Plex Media Server, Media Link) #X-Plex-Version (Plex application version number) #X-Plex-Device (Device name and model number, eg iPhone3,2, Motorola XOOMâ„¢, LG5200TV) #X-Plex-Client-Identifier (UUID, serial number, or other number unique per device) my $param = { url => $url, timeout => 5, noshutdown => 1, httpversion => '1.1', hash => $hash, key => $key, address => $address, port => $port, 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->{header}{'X-Plex-Token'} = $token if( $token ); if( my $entry = plex_entryOfIP($hash, 'client', $address) ) { $param->{header}{'X-Plex-Target-Client-Identifier'} = $entry->{machineIdentifier} if( $entry->{machineIdentifier} ); } $param->{cl} = $blocking if( ref($blocking) eq 'HASH' ); if( $blocking && (!ref($blocking) || !$blocking->{canAsyncOutput}) ) { my($err,$data) = HttpUtils_BlockingGet( $param ); return $err if( $err ); $param->{blocking} = 1; return( plex_parseHttpAnswer( $param, $err, $data ) ); } $param->{callback} = \&plex_parseHttpAnswer; HttpUtils_NonblockingGet( $param ); return undef; } sub plex_play($$$$) { my ($hash, $client, $server,$key) = @_; my $name = $hash->{NAME}; my $url; if ($key =~ m/\bplaylists\b/) { #play playlist $key =~ s/[^0-9]//g; $url = "http://$server->{address}:$server->{port}/playQueues?type=&playlistID=$key"; $url .= "&shuffle=0&repeat=0&includeChapters=1&includeRelated=1"; } else { # play album or single track $key = "/library/metadata/$key" if( $key !~ '^/' ); my $xml = plex_sendApiCmd( $hash, "http://$server->{address}:$server->{port}$key", '#raw', 1, $server->{accessToken} ); #Log 1, Dumper $xml; if( !$xml || !$xml->{librarySectionUUID} ) { return $xml->{head}[0]{title}[0] if( ref $xml->{head} eq 'ARRAY' && ref $xml->{head}[0]{title} eq 'ARRAY' ); return "item not found"; } $url = "http://$server->{address}:$server->{port}/playQueues?type=&uri=". urlEncode( "library://$xml->{librarySectionUUID}/item/$key" ); $url .= "&shuffle=0&repeat=0&includeChapters=1&includeRelated=1"; } Log3 $name, 4, "$name: requesting $url"; my $address; my $port; if( $url =~ m'//([^:]*):(\d*)' ) { $address = $1; $port = $2; } #X-Plex-Platform (Platform name, eg iOS, MacOSX, Android, LG, etc) #X-Plex-Platform-Version (Operating system version, eg 4.3.1, 10.6.7, 3.2) #X-Plex-Provides (one or more of [player, controller, server]) #X-Plex-Product (Plex application name, eg Laika, Plex Media Server, Media Link) #X-Plex-Version (Plex application version number) #X-Plex-Device (Device name and model number, eg iPhone3,2, Motorola XOOMâ„¢, LG5200TV) #X-Plex-Client-Identifier (UUID, serial number, or other number unique per device) my $param = { url => $url, method => 'POST', timeout => 5, noshutdown => 1, httpversion => '1.1', hash => $hash, key => 'playAlbum', album => $key, client => $client, server => $server, address => $address, port => $port, 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->{header}{'X-Plex-Token'} = $hash->{token} if( $hash->{token} ); $param->{header}{'X-Plex-Token'} = $server->{accessToken} if( $server->{accessToken} ); if( my $entry = plex_entryOfIP($hash, 'client', $address) ) { $param->{header}{'X-Plex-Target-Client-Identifier'} = $entry->{machineIdentifier} if( $entry->{machineIdentifier} ); } $param->{callback} = \&plex_parseHttpAnswer; HttpUtils_NonblockingGet( $param ); return undef; } sub plex_addToPlaylist($$$$) { my ($hash, $server,$playlist,$key) = @_; my $name = $hash->{NAME}; $playlist = "/playlists/$playlist" if( $playlist !~ '^/' ); $playlist .= "/items" if( $playlist !~ '/items$' ); $key = "/library/metadata/$key" if( $key !~ '^/' ); my $xml = plex_sendApiCmd( $hash, "http://$server->{address}:$server->{port}$key", '#raw', 1 ); #Log 1, Dumper $xml; return "item not found" if( !$xml || !$xml->{librarySectionUUID} ); my $url = "http://$server->{address}:$server->{port}$playlist?uri=". urlEncode( "library://$xml->{librarySectionUUID}/directory$key" ); Log3 $name, 4, "$name: requesting $url"; my $address; my $port; if( $url =~ m'//([^:]*):(\d*)' ) { $address = $1; $port = $2; } #X-Plex-Platform (Platform name, eg iOS, MacOSX, Android, LG, etc) #X-Plex-Platform-Version (Operating system version, eg 4.3.1, 10.6.7, 3.2) #X-Plex-Provides (one or more of [player, controller, server]) #X-Plex-Product (Plex application name, eg Laika, Plex Media Server, Media Link) #X-Plex-Version (Plex application version number) #X-Plex-Device (Device name and model number, eg iPhone3,2, Motorola XOOMâ„¢, LG5200TV) #X-Plex-Client-Identifier (UUID, serial number, or other number unique per device) my $param = { url => $url, method => 'PUT', timeout => 5, noshutdown => 1, httpversion => '1.1', hash => $hash, key => 'addToPlaylist', server => $server, address => $address, port => $port, 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->{header}{'X-Plex-Token'} = $hash->{token} if( $hash->{token} ); $param->{header}{'X-Plex-Token'} = $server->{accessToken} if( $server->{accessToken} ); if( my $entry = plex_entryOfIP($hash, 'client', $address) ) { $param->{header}{'X-Plex-Target-Client-Identifier'} = $entry->{machineIdentifier} if( $entry->{machineIdentifier} ); } $param->{callback} = \&plex_parseHttpAnswer; HttpUtils_NonblockingGet( $param ); return undef; } sub plex_entryOfID($$$); sub plex_entryOfIP($$$); sub plex_entryOfID($$$) { my ($hash,$type,$id) = @_; return undef if( !$id ); $hash->{$type.'s'} = {} if( !$hash->{$type.'s'} ); my $entries = $hash->{$type.'s'}; foreach my $ip ( keys %{$entries} ) { return $entries->{$ip} if( $entries->{$ip}{machineIdentifier} && $entries->{$ip}{machineIdentifier} eq $id ); return $entries->{$ip} if( $entries->{$ip}{resourceIdentifier} && $entries->{$ip}{resourceIdentifier} eq $id ); } if( $type eq 'server' ) { if( $hash->{'myPlex-servers'}{Server} ) { foreach my $entry (@{$hash->{'myPlex-servers'}{Server}}) { if( $id eq $entry->{machineIdentifier} ) { $entry->{online} = 1; return $entry; } } } } if( my $mhash = $modules{plex}{defptr}{MASTER} ) { return plex_entryOfID($mhash,$type,$id) if( $mhash != $hash ); } return undef; } sub plex_entryOfIP($$$) { my ($hash,$type,$ip) = @_; return undef if( !$ip ); $hash->{$type.'s'} = {} if( !$hash->{$type.'s'} ); my $entries = $hash->{$type.'s'}; foreach my $key ( keys %{$entries} ) { return $entries->{$key} if( $entries->{$key}{address} eq $ip ); } if( $type eq 'server' ) { if( $hash->{'myPlex-servers'}{Server} ) { foreach my $entry (@{$hash->{'myPlex-servers'}{Server}}) { if( $ip eq $entry->{address} ) { $entry->{online} = 1; return $entry; } } } } if( my $mhash = $modules{plex}{defptr}{MASTER} ) { return plex_entryOfIP($mhash,$type,$ip) if( $mhash != $hash ); } return undef; } sub plex_serverOf($$;$) { my ($hash,$server,$only) = @_; my $entry; $entry = plex_entryOfID($hash, 'server', $hash->{currentServer} ) if( $hash->{currentServer} ); $entry = plex_entryOfIP($hash, 'server', $server) if( $server && $server =~ m/^\d+\.\d+\.\d+\.\d+$/ ); $entry = plex_entryOfID($hash, 'server', $server) if( $server && !$entry ); $entry = plex_entryOfIP($hash, 'server', $hash->{server} ) if( !$entry ); $entry = plex_entryOfID($hash, 'server', $hash->{machineIdentifier} ) if( !$entry ); $entry = plex_entryOfID($hash, 'server', $hash->{resourceIdentifier} ) if( !$entry ); if( !$entry && $only ) { if( my $mhash = $modules{plex}{defptr}{MASTER} ) { #Log 1, Dumper $mhash; my @keys = keys(%{$modules{plex}{defptr}{MASTER}{servers}}); if( @keys == 1 ) { $entry = $modules{plex}{defptr}{MASTER}{servers}{$keys[0]}; } } elsif( $hash->{server} && $hash->{servers} ) { my @keys = keys(%{$hash->{servers}}); if( @keys == 1 ) { $entry = $hash->{servers}{$keys[0]}; } } } return $entry; } sub plex_clientOf($$) { my ($hash,$client) = @_; if( my $chash = $defs{$client} ) { $client = $chash->{machineIdentifier} if( $chash->{machineIdentifier} ); } my $entry; $entry = plex_entryOfIP($hash, 'client', $client) if( $client =~ m/^\d+\.\d+\.\d+\.\d+$/ ); $entry = plex_entryOfID($hash, 'client', $client) if( !$entry ); $entry = plex_entryOfIP($hash, 'client', $hash->{client} ) if( !$entry ); $entry = plex_entryOfID($hash, 'client', $hash->{machineIdentifier} ) if( !$entry ); $entry = plex_entryOfID($hash, 'client', $hash->{resourceIdentifier} ) if( !$entry ); return $entry; } sub plex_msg2hash($;$) { my ($string,$keep) = @_; my %hash = (); if( $string !~ m/\r/ ) { $string =~ s/\n/\r\n/g; } foreach my $line (split("\r\n", $string)) { my ($key,$value) = split( ": ", $line ); next if( !$value ); if( !$keep ) { $key =~ s/-//g; $key = lcfirst( $key ); } $value =~ s/^ //; $hash{$key} = $value; } return \%hash; } sub plex_hash2header($) { my ($hash) = @_; return $hash if( ref($hash) ne 'HASH' ); my $header; foreach my $key (keys %{$hash}) { #$header .= "\r\n" if( $header ); $header .= "$key: $hash->{$key}\r\n"; } 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) = @_; my $name = $hash->{NAME}; if( !$type ) { $type = 'server' if( $hash->{servers}{$ip} || ($hash->{server} && $hash->{server} eq $ip) ); $type = 'client' if( $hash->{clients}{$ip} || ($hash->{client} && $hash->{client} eq $ip) ); return undef if( !$type ); } $hash->{$type.'s'} = {} if( !$hash->{$type.'s'} ); my $entries = $hash->{$type.'s'}; my $new; $new = 1 if( !$entries->{$ip} || !$entries->{$ip}{online} || !$entries->{$ip}{port} || !$entry->{port} || $entries->{$ip}{port} ne $entry->{port} ); if( $new ) { $entry->{machineIdentifier} = $entry->{resourceIdentifier} if( $entry->{resourceIdentifier} && !$entry->{machineIdentifier} ); my $type = ucfirst( $type ); if( my $ignored = AttrVal($name, "ignored${type}s", '' ) ) { if( $ignored =~ m/\b$ip\b/ ) { Log3 $name, 5, "$name: ignoring $type $ip"; return undef; } elsif( $entry->{machineIdentifier} && $ignored =~ m/\b$entry->{machineIdentifier}\b/ ) { Log3 $name, 5, "$name: ignoring $type $entry->{machineIdentifier}"; return undef; } } $entries->{$ip} = $entry; $entries->{$ip}{online} = 1; } else { @{$entries->{$ip}}{ keys %{$entry} } = values %{$entry}; } $entry = $entries->{$ip}; $entry->{address} = $ip; $entry->{updatedAt} = gettimeofday(); if( $type eq 'client' && $entry->{machineIdentifier} ) { if( my $chash = $modules{plex}{defptr}{$entry->{machineIdentifier}} ) { readingsBeginUpdate($chash); readingsBulkUpdate($chash, 'presence', 'present' ) if( ReadingsVal($chash->{NAME}, 'presence', '') ne 'present' ); readingsBulkUpdate($chash, 'state', 'appeared' ) if( ReadingsVal($chash->{NAME}, 'state', '') eq 'disappeared' ); readingsEndUpdate($chash, 1); #$chash->{name} = $entry->{name}; $chash->{product} = $entry->{product}; $chash->{version} = $entry->{version}; $chash->{platform} = $entry->{platform}; $chash->{deviceClass} = $entry->{deviceClass}; $chash->{platformVersion} = $entry->{platformVersion}; $chash->{protocolCapabilities} = $entry->{protocolCapabilities}; } } if( $type eq 'server' ) { Log3 $name, 3, "$name: $type discovered: $ip" if( $new ); if( $new && $entry->{port} ) { plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/clients", "clients" ); } plex_requestNotifications( $hash, $entry ); } elsif( $type eq 'client' ) { Log3 $name, 3, "$name: $type discovered: $ip" if( $new ); if( $new && $entry->{port} ) { plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/resources", "resources" ); } } else { Log3 $name, 2, "$name: discovered unknown type: $type"; } } sub plex_disappeared($$$) { my ($hash, $type, $ip) = @_; my $name = $hash->{NAME}; if( !$type ) { $type = 'server' if( $hash->{servers}{$ip} || ($hash->{server} && $hash->{server} eq $ip) ); $type = 'client' if( $hash->{clients}{$ip} || ($hash->{client} && $hash->{client} eq $ip) ); return undef if( !$type ); } $hash->{$type.'s'} = {} if( !$hash->{$type.'s'} ); my $entries = $hash->{$type.'s'}; my $new; $new = 1 if( !$entries->{$ip} || $entries->{$ip}{online} ); $entries->{$ip} = {} if( !$entries->{$ip} ); $entries->{$ip}{online} = 0; my $machineIdentifier = $entries->{$ip}{machineIdentifier}; if( $type eq 'client' && $new && $machineIdentifier ) { delete $hash->{subscriptionsFrom}{$machineIdentifier}; if( my $chash = $hash->{helper}{subscriptionsFrom}{$machineIdentifier} ) { plex_closeSocket( $chash ); delete($defs{$chash->{NAME}}); delete $hash->{helper}{subscriptionsFrom}{$machineIdentifier}; } if( my $chash = $modules{plex}{defptr}{$machineIdentifier} ) { delete $chash->{controllable}; delete $chash->{currentMediaType}; readingsBeginUpdate($chash); readingsBulkUpdate($chash, 'presence', 'absent' ); readingsBulkUpdate($chash, 'state', 'disappeared' ); readingsEndUpdate($chash, 1); CommandDeleteReading( undef, "$chash->{NAME} currentTitle|currentAlbum|currentArtist|episode|series|key|cover|duration|type|track|playQueueID|playQueueItemID|server|section|shuffle|repeat" ) if( AttrVal($chash->{NAME}, 'removeUnusedReadings', 0 ) ); } } if( $type eq 'server' ) { Log3 $name, 3, "$name: $type disappeared: $ip" if( $new ); } elsif( $type eq 'client' ) { Log3 $name, 3, "$name: $type disappeared: $ip" if( $new ); plex_removeSubscription($hash->{helper}{timelineListener}, $ip); } else { Log3 $name, 2, "$name: unknown type $type disappeared"; } } sub plex_requestNotifications($$) { my ($hash,$server) = @_; my $name = $hash->{NAME}; return if( $hash->{helper}{websockets}{$server->{machineIdentifier}} ); if( my $socket = IO::Socket::INET->new(PeerAddr=>"$server->{address}:$server->{port}", Timeout=>2, Blocking=>1, ReuseAddr=>1) ) { my $chash = plex_newChash( $hash, $socket, {NAME=>"$name:websocket:$server->{machineIdentifier}", STATE=>'listening', websocket=>0} ); $chash->{address} = $server->{address}; $chash->{machineIdentifier} = $server->{machineIdentifier}; Log3 $name, 3, "$name: notification websocket opened to $server->{address}"; $hash->{helper}{websockets}{$server->{machineIdentifier}} = $chash; my $ret = "GET /:/websockets/notifications HTTP/1.1\r\n"; $ret .= plex_hash2header( { 'Host' => "$server->{address}:$server->{port}", 'X-Plex-Token' => $server->{accessToken}?$server->{accessToken}:$hash->{token}, 'Upgrade' => 'websocket', 'Connection' => 'Upgrade', 'Pragma' => 'no-cache', 'Cache-Control' => 'no-cache', 'Sec-WebSocket-Key' => 'RkhFTQ==', 'Sec-WebSocket-Version' => '13', } ); $ret .= "\r\n"; #Log 1, $ret; syswrite($chash->{CD}, $ret ); } else { Log3 $name, 2, "$name: failed to open notification websocket to $server->{address}"; } } sub plex_closeNotifications($) { my ($hash,$server) = @_; my $name = $hash->{NAME}; } sub plex_stopWebsockets($) { my ($hash,$server) = @_; my $name = $hash->{NAME}; return if( !$hash->{helper}{websockets} ); foreach my $key ( keys %{$hash->{helper}{websockets}} ) { my $chash = $hash->{helper}{websockets}{$key}; my $cname = $chash->{NAME}; plex_closeSocket($chash); delete($hash->{servers}{$chash->{address}}{sessions}); delete($hash->{helper}{websockets}{$key}); delete($defs{$cname}); } Log3 $name, 3, "$name: websockets stoped"; } sub plex_readingsBulkUpdateIfChanged($$$) { my ($hash,$reading,$value) = @_; readingsBulkUpdate($hash, $reading, $value ) if( defined($value) && $value ne ReadingsVal($hash->{NAME}, $reading, '') ); } sub plex_parseTimeline($$$) { my ($hash,$id,$xml) = @_; my $name = $hash->{NAME}; if( !$id ) { Log3 $name, 2, "$name: can't parse timeline for unknown device"; return undef if( !$id ); } my $chash = $modules{plex}{defptr}{$id}; if( !$chash ) { my $cname = $id; $cname =~ s/-//g; my $define = "$cname plex $id"; if( my $cmdret = CommandDefine(undef,$define) ) { Log3 $name, 1, "$name: Autocreate: An error occurred while creating device for id '$id': $cmdret"; return undef; } CommandAttr(undef, "$cname room plex"); if( my $entry = plex_entryOfID($hash, 'client', $id ) ) { CommandAttr(undef, "$cname alias ".$entry->{name}); #CommandAttr(undef, "$cname alias ".$entry->{product}); } $chash = $modules{plex}{defptr}{$id}; } readingsBeginUpdate($chash); plex_readingsBulkUpdateIfChanged($chash, 'location', $xml->{location} ); my $state; my $entries; delete $chash->{time}; delete $chash->{seekRange}; delete $chash->{controllable}; foreach my $entry (@{$xml->{Timeline}}) { next if( !$entry->{state} ); my $key = $entry->{key}; if( $key && $key ne ReadingsVal($chash->{NAME}, 'key', '') ) { $chash->{currentServer} = $entry->{machineIdentifier}; readingsBulkUpdate($chash, 'key', $key ); readingsBulkUpdate($chash, 'server', $entry->{machineIdentifier} ); my $server = plex_entryOfID($hash, 'server', $entry->{machineIdentifier} ); $server = $entry if( !$server ); plex_sendApiCmd( $hash, "http://$server->{address}:$server->{port}$key", "#update:$chash->{NAME}" ); } plex_readingsBulkUpdateIfChanged($chash, 'volume', $entry->{volume} ) if( $entry->{controllable} && $entry->{controllable} =~ m/\bvolume\b/ ); $chash->{controllable} = $entry->{controllable} if( $entry->{controllable} ); if( $entry->{type} ) { $entries->{ $entry->{type} } = $entry; } my $time = $entry->{time}; if( defined($time) ) { # if( !$chash->{helper}{time} || abs($time - $chash->{helper}{time}) > 2000 ) { # plex_readingsBulkUpdateIfChanged($chash, 'time', plex_sec2hms($time/1000) ); # # $chash->{helper}{time} = $time; # } $chash->{time} = $time; } $chash->{seekRange} = $entry->{seekRange} if( $entry->{seekRange} && $entry->{seekRange} ne "0-0" ); $state .= ' ' if( $state ); $state .= "$entry->{type}:$entry->{state}"; #$state = undef if( $state && $entry->{continuing} ); } $state = 'stopped' if( !$state ); $state = $1 if( $state =~ /^[\w]*:(stopped)$/ ); if( $state =~ '\w*:(\w*) \w*:(\w*) .*:(\w*)' ) { $state = $1 if( $1 eq $2 && $2 eq $3 ); } if( $state =~ '(\w*):(playing|paused)' ) { $chash->{currentMediaType} = $1; if( defined($entries->{$1}) ) { $chash->{controllable} = $entries->{$1}->{controllable} if ( defined($entries->{$1}->{controllable}) ); plex_readingsBulkUpdateIfChanged($chash, 'repeat', $entries->{$1}->{repeat} ); plex_readingsBulkUpdateIfChanged($chash, 'shuffle', $entries->{$1}->{shuffle} ); plex_readingsBulkUpdateIfChanged($chash, 'playQueueID', $entries->{$1}->{playQueueID} ); plex_readingsBulkUpdateIfChanged($chash, 'playQueueItemID', $entries->{$1}->{playQueueItemID} ); } } else { delete $chash->{currentMediaType}; #FIXME: move after stop event CommandDeleteReading( undef, "$chash->{NAME} currentTitle|currentAlbum|currentArtist|episode|series|key|cover|duration|type|track|playQueueID|playQueueItemID|server|section|shuffle|repeat" ) if( AttrVal($chash->{NAME}, 'removeUnusedReadings', 0 ) ); } plex_readingsBulkUpdateIfChanged($chash, 'state', $state ); 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( $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)); return '' if( $last && ord($last) > ord($current ) ); 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) = @_; my $name = $hash->{NAME}; Log3 $name, 5, "$name: from: $peerhost" if( $peerhost ); Log3 $name, 5, "$name: $msg"; my $handled = 0; if( $peerhost ) { #from broadcast if( $msg =~ '^HTTP/1.\d 200 OK' ) { my $params = plex_msg2hash($msg); if( $params->{'contentType'} eq 'plex/media-server' ) { $handled = 1; plex_discovered($hash, 'server', $peerhost, $params ); } elsif( $params->{'contentType'} eq 'plex/media-player' ) { return undef if( $peerhost eq $hash->{fhemIP} && $hash->{clients}{$peerhost}{online} ); $handled = 1; plex_discovered($hash, 'client', $peerhost, $params ); } } elsif( $msg =~ '^([\w\-]+) \* HTTP/1.\d' ) { my $type = $1; my $params = plex_msg2hash($msg); if( $type eq 'HELLO' ) { $handled = 1; plex_discovered($hash, 'client', $peerhost, $params ); } elsif( $type eq 'BYE' ) { plex_disappeared($hash, 'client', $peerhost ); $handled = 1; } elsif( $type eq 'UPDATE' ) { if( $params->{parameters} =~ m/playerAdd=(.*)/ ) { $handled = 1; my $ip = $peerhost; if( $hash->{servers}{$ip}{port} ) { plex_sendApiCmd( $hash, "http://$ip:$hash->{servers}{$ip}{port}/clients", "clients" ); } } elsif( $params->{parameters} =~ m/playerDel=(.*)/ ) { my $ip = $1; $handled = 1; if( !$hash->{clients}{$ip} || $hash->{clients}{$ip}{product} ne 'Plex Home Theater' ) { plex_disappeared($hash, 'client', $ip ); } } } elsif( $type eq 'M-SEARCH' ) { $handled = 1; if( $peerhost eq $hash->{fhemIP} && $hash->{clients}{$peerhost}{online} ) { if( $hash->{helper}{discoverClientsMcast} && $hash->{helper}{discoverClientsMcast}->{CD}->sockport() == $peerport ) { #Log3 $name, 5, "$name: ignoring multicast M-Search from self ($peerhost:$peerport)"; return undef; } if( $hash->{helper}{discoverClientsBcast} && $hash->{helper}{discoverClientsBcast}->{CD}->sockport() == $peerport ) { #Log3 $name, 5, "$name: ignoring broadcast M-Search from self ($peerhost:$peerport)"; return undef; } } #Log3 $name, 5, "$name: received from: $peerhost:$peerport to $sockport: $msg"; my $msg = "HTTP/1.0 200 OK\r\n"; $msg .= plex_hash2header( { 'Content-Type' => 'plex/media-player', 'Resource-Identifier' => $hash->{id}, 'Name' => $hash->{fhemHostname}, #'Host' => $hash->{fhemIP}, 'Port' => $hash->{helper}{timelineListener}{PORT}, #'Updated-At' => 1447614540, 'Product' => 'FHEM SONOS Proxy', 'Version' => '0.0.0', #'Protocol' => 'plex', 'Protocol-Version' => 1, 'Protocol-Capabilities' => 'playback,timeline', } ); $msg .= "\r\n"; my $sin = sockaddr_in($peerport, inet_aton($peerhost)); $hash->{helper}{clientDiscoveryResponderMcast}->{CD}->send($msg, 0, $sin ); } } } elsif( $msg =~ '^GET\s*([^\s]*)\s*HTTP/1.\d' ) { my $request = $1; if( $msg =~ m/^(.*?)\r?\n\r?\n(.*)$/s ) { my $header = $1; my $body = $2; my $params; if( $request =~ m/^([^?]*)(\?(.*))?/ ) { #$request = $1; if( $3 ) { foreach my $param (split("&", $3)) { my ($key,$value) = split("=",$param); $params->{$key} = $value; } } } $header = plex_msg2hash($header, 1); my $ret; if( $request =~ m'^/resources' ) { $handled = 1; Log3 $name, 4, "$name: answering $request"; my $xml = { MediaContainer => [ {Player => { title => $hash->{fhemHostname}, protocol => 'plex', protocolVersion =>'1', protocolCapabilities => 'playback,timeline,skipNext,skipPrevious', machineIdentifier => $hash->{id}, product => 'FHEM SONOS Proxy', platform => $^O, platformVersion => '0.0.0', deviceClass => 'pc', deviceProtocol => 'sonos' } }] }; my $body = ''; $body .= "\n"; $body .= XMLout( $xml, KeyAttr => { }, RootName => undef ); $ret = "HTTP/1.1 200 OK\r\n"; $ret .= plex_hash2header( { 'Connection' => 'Close', 'X-Plex-Client-Identifier' => $hash->{id}, 'Content-Type' => 'text/xml;charset=utf-8', 'Content-Length' => length($body), } ); $ret .= "\r\n"; $ret .= $body; } my $entry = plex_entryOfID($hash, 'client', $header->{'X-Plex-Client-Identifier'} ); if( $entry ) { my $addr = "$entry->{address}:$entry->{port}"; if( $request =~ m'^/player/timeline/subscribe' ) { $handled = 1; Log3 $name, 4, "$name: answering $request"; $hash->{subscriptionsFrom}{$header->{'X-Plex-Client-Identifier'}} = $addr; plex_sendTimelines($hash, $params->{commandID}); $ret = "HTTP/1.1 200 OK\r\n"; $ret .= plex_hash2header( { 'Connection' => 'Close', 'X-Plex-Client-Identifier' => $hash->{id}, 'Content-Type' => 'text/xml;charset=utf-8', 'Content-Length' => 0, } ); $ret .= "\r\n"; } elsif( $request =~ m'^/player/timeline/unsubscribe' ) { $handled = 1; Log3 $name, 4, "$name: answering $request"; delete $hash->{subscriptionsFrom}{$header->{'X-Plex-Client-Identifier'}}; if( my $chash = $hash->{helper}{subscriptionsFrom}{$header->{'X-Plex-Client-Identifier'}} ) { plex_closeSocket( $chash ); delete($defs{$chash->{NAME}}); delete $hash->{helper}{subscriptionsFrom}{$header->{'X-Plex-Client-Identifier'}}; } $ret = "HTTP/1.1 200 OK\r\n"; $ret .= plex_hash2header( { 'Connection' => 'Close', 'X-Plex-Client-Identifier' => $hash->{id}, 'Content-Type' => 'text/xml;charset=utf-8', 'Content-Length' => 0, } ); $ret .= "\r\n"; } elsif( $request =~ m'^/player/mirror/details' ) { $handled = 1; Log3 $name, 4, "$name: answering $request"; if( my $chash = $hash->{helper}{subscriptionsFrom}{$header->{'X-Plex-Client-Identifier'}} ) { $chash->{commandID} = $params->{commandID}; } $ret = "HTTP/1.1 200 OK\r\n"; $ret .= plex_hash2header( { 'Connection' => 'Close', 'X-Plex-Client-Identifier' => $hash->{id}, 'Content-Type' => 'text/xml;charset=utf-8', 'Content-Length' => 0, } ); $ret .= "\r\n"; } elsif( $request =~ m'^/player/playback/playMedia' ) { delete $hash->{sonos}{playqueue}; delete $hash->{sonos}{containerKey} ; delete $hash->{sonos}{machineIdentifier}; my $entry = plex_entryOfID($hash, 'server', $params->{machineIdentifier} ); if( $params->{containerKey} ) { my ($containerKey) = split( '\?', $params->{containerKey}, 2 ); return "HTTP/1.1 400 Bad Request\r\n\r\n" if( !$containerKey); my $xml = plex_sendApiCmd( $hash, "http://$entry->{address}:$entry->{port}$containerKey", '#raw', 1 ); return undef if( !$xml || ref($xml) ne 'HASH' ); $hash->{sonos}{playqueue} = $xml; $hash->{sonos}{containerKey} = $containerKey; } elsif( my $key = $params->{key} ) { my $xml = plex_sendApiCmd( $hash, "http://$entry->{address}:$entry->{port}$key", '#raw', 1 ); return undef if( !$xml || ref($xml) ne 'HASH' || !$xml->{Track} ); $hash->{sonos}{playqueue} = (); $hash->{sonos}{playqueue}{size} = 1; $hash->{sonos}{playqueue}{Track} = $xml->{Track}; } $hash->{sonos}{machineIdentifier} = $params->{machineIdentifier}; $hash->{sonos}{currentTrack} = 0; $hash->{sonos}{updateTime} = time(); $hash->{sonos}{currentTime} = 0; $hash->{sonos}{status} = 'playing'; $handled = 1; Log3 $name, 4, "$name: answering $request"; my $tracks = $hash->{sonos}{playqueue}{Track}; my $track = $tracks->[$hash->{sonos}{currentTrack}]; my $server = plex_entryOfID($hash, 'server', $hash->{sonos}{machineIdentifier}); fhem( "set sonos_Esszimmer playURI http://$server->{address}:$server->{port}$track->{Media}[0]{Part}[0]{key}" ); plex_sendTimelines($hash, $params->{commandID}); $ret = "HTTP/1.1 200 OK\r\n"; $ret .= plex_hash2header( { 'Connection' => 'Close', 'X-Plex-Client-Identifier' => $hash->{id}, 'Content-Type' => 'text/xml;charset=utf-8', 'Content-Length' => 0, } ); $ret .= "\r\n"; } elsif( $request =~ m'^/player/playback/setParameters' ) { $handled = 1; Log3 $name, 4, "$name: answering $request"; plex_sendTimelines($hash, $params->{commandID}); $ret = "HTTP/1.1 200 OK\r\n"; $ret .= plex_hash2header( { 'Connection' => 'Close', 'X-Plex-Client-Identifier' => $hash->{id}, 'Content-Type' => 'text/xml;charset=utf-8', 'Content-Length' => 0, } ); $ret .= "\r\n"; } elsif( $request =~ m'^/player/playback/(\w*)' ) { my $cmd = $1; $handled = 1; Log3 $name, 4, "$name: answering $request"; return "HTTP/1.1 400 Bad Request\r\n\r\n" if( !$hash->{sonos}{playqueue} ); if( $cmd eq 'play' ) { $cmd = 'playing'; fhem( "set sonos_Esszimmer play" ); } elsif( $cmd eq 'pause' ) { $cmd = 'paused'; fhem( "set sonos_Esszimmer pause" ); } elsif( $cmd eq 'stop' ) { $cmd = 'stopped' if( $cmd eq 'stop' ); fhem( "set sonos_Esszimmer stop" ); } elsif( $cmd eq 'skipNext' ) { $cmd = 'playing'; $hash->{sonos}{currentTrack}++; $hash->{sonos}{currentTrack} = 0 if( $hash->{sonos}{currentTrack} > $hash->{sonos}{playqueue}{size}-1 ); $hash->{sonos}{updateTime} = time(); $hash->{sonos}{currentTime} = 0; my $server = plex_entryOfID($hash, 'server', $hash->{sonos}{machineIdentifier}); my $tracks = $hash->{sonos}{playqueue}{Track}; my $track = $tracks->[$hash->{sonos}{currentTrack}]; fhem( "set sonos_Esszimmer playURI http://$server->{address}:$server->{port}$track->{Media}[0]{Part}[0]{key}" ); } elsif( $cmd eq 'skipPrevious' ) { $cmd = 'playing'; if( $hash->{sonos}{currentTime} < 10 ) { $hash->{sonos}{currentTrack}--; $hash->{sonos}{currentTrack} = $hash->{sonos}{playqueue}{size} - 1 if( $hash->{sonos}{currentTrack} < 0 ); my $server = plex_entryOfID($hash, 'server', $hash->{sonos}{machineIdentifier}); my $tracks = $hash->{sonos}{playqueue}{Track}; my $track = $tracks->[$hash->{sonos}{currentTrack}]; fhem( "set sonos_Esszimmer playURI http://$server->{address}:$server->{port}$track->{Media}[0]{Part}[0]{key}" ); } $hash->{sonos}{updateTime} = time(); $hash->{sonos}{currentTime} = 0; } elsif( $cmd eq 'seekTo' ) { $cmd = $hash->{sonos}{status}; $hash->{sonos}{updateTime} = time(); $hash->{sonos}{currentTime} = int($params->{offset} / 1000); fhem( "set sonos_Esszimmer currentTrackPosition ". plex_sec2hms(int($params->{offset} / 1000) ) ); } $hash->{sonos}{updateTime} = time() if( $cmd eq 'playing' && $hash->{sonos}{status} ne 'playing' ); $hash->{sonos}{status} = $cmd; plex_sendTimelines($hash, $params->{commandID}); $ret = "HTTP/1.1 200 OK\r\n"; $ret .= plex_hash2header( { 'Connection' => 'Close', 'X-Plex-Client-Identifier' => $hash->{id}, 'Content-Type' => 'text/xml;charset=utf-8', 'Content-Length' => 0, } ); $ret .= "\r\n"; } } if( !$handled ) { $peerhost = $peerhost ? " from $peerhost" : ''; Log3 $name, 2, "$name: unhandled request: $msg"; } return $ret; } } elsif( $msg =~ '^POST /:/timeline\?? HTTP/1.\d' ) { #Log 1, $msg; if( $msg =~ m/^(.*?)\r?\n\r?\n(.*)$/s ) { my $header = $1; my $body = $2; if( !$body ) { $handled = 1; Log3 $name, 5, "$name: empty timeline received"; } elsif( $body !~ m/^<.*>$/ms ) { $handled = 1; Log3 $name, 2, "$name: unknown timeline content: $body"; } else { $handled = 1; my $header = plex_msg2hash($header, 1); my $id = $header->{'X-Plex-Client-Identifier'}; if( !$id ) { my $entry = plex_entryOfIP($hash, 'client', $peerhost); $id = $entry->{machineIdentifier}; } #Log 1, ">>$body<<"; my $xml = eval { XMLin( $body, KeyAttr => {}, ForceArray => 1 ); }; Log3 $name, 2, "$name: xml error: $@" if( $@ ); return undef if( !$xml ); plex_parseTimeline($hash, $id, $xml); } } } elsif( $msg =~ '^POST /SMAPI HTTP/1.\d' ) { return plex_handleSMAPI($hash, $msg); } if( !$handled ) { $peerhost = $peerhost ? " from $peerhost" : ''; Log3 $name, 2, "$name: unhandled message$peerhost: $msg"; } return undef; } sub plex_sec2hms($) { my ($sec) = @_; my $s = $sec % 60; $sec = int( $sec / 60 ); my $m = $sec % 60; $sec = int( $sec / 60 ); my $h = $sec % 24; return sprintf("%02d:%02d:%02d", $h, $m, $s); } sub plex_timestamp2date($) { my @t = localtime(shift); return sprintf("%04d-%02d-%02d", $t[5]+1900, $t[4]+1, $t[3]); } sub plex_parseHttpAnswer($$$) { my ($param, $err, $data) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; if( $err ) { if( $param->{key} eq 'publishToSonos' ) { if( $param->{cl} && $param->{cl}{canAsyncOutput} ) { asyncOutput( $param->{cl}, "SMAPI registration for $param->{player}: failed\n" ); } } elsif( $err =~ m/Connection refused$/ || $err =~ m/timed out$/ || $err =~ m/empty answer received$/ ) { if( !$param->{retry} || $param->{retry} < 1 ) { ++$param->{retry}; delete $param->{conn}; Log3 $name, 4, "$name: http request ($param->{url}) failed: $err; retrying"; if( $param->{url} =~ m/.player./ ) { ++$hash->{commandID}; $param->{url} =~ s/commandID=\d*/commandID=$hash->{commandID}/; } Log3 $name, 5, " ($param->{url})"; RemoveInternalTimer($hash); InternalTimer(gettimeofday()+5, "HttpUtils_NonblockingGet", $param, 0); return; } } Log3 $name, 2, "$name: http request ($param->{url}) failed: $err"; plex_disappeared($hash, undef, $param->{address} ) if( $param->{retry} ); return undef; return $err; } Log3 $name, 5, "$name: received $data"; return undef if( !$data ); $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; } elsif( $data =~ m/200 OK/ ) { Log3 $name, 5, "$name: http request ($param->{url}) received code : $data"; return undef; } elsif( $data !~ m/^<.*>$/ms ) { Log3 $name, 2, "$name: http request ($param->{url}) unknown content: $data"; return undef; } #Log 1, $param->{url}; #Log 1, Dumper $xml; my $handled = 0; #Log 1, $data; my $xml = eval { XMLin( $data, KeyAttr => {}, ForceArray => 1 ); }; Log3 $name, 2, "$name: xml error: $@" if( $@ ); return undef if( !$xml ); 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} ); plex_discovered($hash, 'client', $entry->{address}, $entry); } } elsif( $param->{key} eq 'servers' ) { $handled = 1; foreach my $entry (@{$xml->{Server}}) { my $ip = $entry->{address}; $ip = $param->{address} if( !$ip ); $entry->{port} = $param->{port} if( !$entry->{port} ); plex_discovered($hash, 'server', $ip, $entry); } } elsif( $param->{key} eq 'resources' ) { $handled = 1; foreach my $entry (@{$xml->{Server}}) { my $ip = $entry->{address}; $ip = $param->{address} if( !$ip ); $entry->{port} = $param->{port} if( !$entry->{port} ); plex_discovered($hash, 'server', $ip, $entry); } foreach my $entry (@{$xml->{Player}}) { my $ip = $entry->{address}; $ip = $param->{address} if( !$ip ); $entry->{port} = $param->{port} if( !$entry->{port} ); plex_discovered($hash, 'client', $ip, $entry); plex_sendSubscription($hash->{helper}{timelineListener}, $ip) if( $entry->{protocolCapabilities} && $entry->{protocolCapabilities} =~ m/timeline/); } } elsif( $param->{key} eq 'detail' ) { $handled = 1; my $server = plex_entryOfIP($hash, 'server', $param->{address}); my $ret = plex_mediaDetail( $hash, $server, $xml ); #Log 1, Dumper $xml; if( $param->{cl} && $param->{cl}->{TYPE} eq 'FHEMWEB' ) { $ret =~ s/&/&/g; $ret =~ s/'/'/g; $ret =~ s/\n/
/g; $ret = "
$ret
" if( $ret =~ m/ / ); $ret = "$ret"; } else { $ret =~ s/]*>//g; $ret =~ s/<\/a>//g; $ret =~ s/]*>\n//g; $ret .= "\n"; } if( $param->{cl} && $param->{cl}{canAsyncOutput} ) { #Log 1, $ret; asyncOutput( $param->{cl}, $ret ); } elsif( $param->{blocking} ) { return $ret; } return undef; } elsif( $param->{key} eq 'onDeck' || $param->{key} eq 'playlists' || $param->{key} eq 'recentlyAdded' || $param->{key} eq 'search' || $param->{key} =~ m'sections(:(\S*))?( (.*))?' ) { $handled = 1; my $cmd = $4; $xml->{parentSection} = $2; my $server = plex_entryOfIP($hash, 'server', $param->{address}); my $ret = plex_mediaList( $hash, $server, $xml, $cmd ); if( $param->{cl} && $param->{cl}->{TYPE} eq 'FHEMWEB' ) { $ret =~ s/&/&/g; $ret =~ s/'/'/g; $ret =~ s/\n/
/g; $ret = "
$ret
" if( $ret =~ m/ / ); $ret = "$ret"; } else { $ret =~ s/]*>//g; $ret =~ s/<\/a>//g; $ret =~ s/]*>//g; $ret .= "\n"; } if( $param->{cl} ) { #Log 1, $ret; asyncOutput( $param->{cl}, $ret ."\n" ); } elsif( $param->{blocking} ) { return $ret; } return undef; } elsif( $param->{key} eq 'playAlbum' ) { $handled = 1; my $client = $param->{client}; my $server = $param->{server}; my $queue = $xml->{playQueueID}; my $key = $param->{album}; my $url = "http://$client->{address}:$client->{port}/player/playback/playMedia?key=$key&offset=0"; $url .= "&machineIdentifier=$server->{machineIdentifier}&protocol=http&address=$server->{address}&port=$server->{port}"; $url .= "&containerKey=/playQueues/$queue?own=1&window=200"; plex_sendApiCmd( $hash, $url, "playback" ); } elsif( $param->{key} eq 'timeline' ) { $handled = 1; my $id = $xml->{machineIdentifier}; if( !$id ) { my $entry = plex_entryOfIP($hash, 'client', $param->{address}); $id = $entry->{machineIdentifier}; } plex_parseTimeline($hash, $id, $xml); } elsif( $param->{key} eq 'subscribe' ) { $handled = 1; my $id = $xml->{machineIdentifier}; if( !$id ) { my $entry = plex_entryOfIP($hash, 'client', $param->{address}); $id = $entry->{machineIdentifier}; } #plex_parseTimeline($hash, $id, $xml); } elsif( $param->{key} =~ m/#update:(.*)/ ) { $handled = 1; my $chash = $defs{$1}; return undef if( !$chash ); #Log 1, Dumper $xml; #Log 1, Dumper $param; if( $xml->{librarySectionTitle} ne ReadingsVal($chash->{NAME}, 'section', '' ) ) { CommandDeleteReading( undef, "$chash->{NAME} currentAlbum|currentArtist|episode|series|track" ); } readingsBeginUpdate($chash); plex_readingsBulkUpdateIfChanged($chash, 'section', $xml->{librarySectionTitle} ); if( $xml->{Video} ) { foreach my $entry (@{$xml->{Video}}) { plex_readingsBulkUpdateIfChanged($chash, 'type', $entry->{type} ); plex_readingsBulkUpdateIfChanged($chash, 'series', $entry->{grandparentTitle} ); plex_readingsBulkUpdateIfChanged($chash, 'currentTitle', $entry->{title} ); if( $entry->{parentThumb} ) { plex_readingsBulkUpdateIfChanged($chash, 'cover', "http://$param->{address}:$param->{port}$entry->{parentThumb}" ); } elsif( $entry->{grandparentThumb} ) { plex_readingsBulkUpdateIfChanged($chash, 'cover', "http://$param->{address}:$param->{port}$entry->{grandparentThumb}" ); } else { plex_readingsBulkUpdateIfChanged($chash, 'cover', "http://$param->{address}:$param->{port}$entry->{thumb}" ); } plex_readingsBulkUpdateIfChanged($chash, 'episode', sprintf("S%02iE%02i",$entry->{parentIndex}, $entry->{index} ) ) if( $entry->{parentIndex} ); if( !$chash->{duration} || $chash->{duration} != $entry->{duration} ) { $chash->{duration} = $entry->{duration}; plex_readingsBulkUpdateIfChanged($chash, 'duration', plex_sec2hms($entry->{duration}/1000) ); } } } elsif( $xml->{Track} ) { foreach my $entry (@{$xml->{Track}}) { plex_readingsBulkUpdateIfChanged($chash, 'type', $entry->{type} ); plex_readingsBulkUpdateIfChanged($chash, 'currentArtist', $entry->{grandparentTitle} ); plex_readingsBulkUpdateIfChanged($chash, 'currentAlbum', $entry->{parentTitle} ); plex_readingsBulkUpdateIfChanged($chash, 'currentTitle', $entry->{title} ); plex_readingsBulkUpdateIfChanged($chash, 'track', $entry->{index} ); if( $entry->{parentThumb} ) { plex_readingsBulkUpdateIfChanged($chash, 'cover', "http://$param->{address}:$param->{port}$entry->{parentThumb}" ); } elsif( $entry->{grandparentThumb} ) { plex_readingsBulkUpdateIfChanged($chash, 'cover', "http://$param->{address}:$param->{port}$entry->{grandparentThumb}" ); } else { plex_readingsBulkUpdateIfChanged($chash, 'cover', "http://$param->{address}:$param->{port}$entry->{thumb}" ); } if( !$chash->{duration} || $chash->{duration} != $entry->{duration} ) { $chash->{duration} = $entry->{duration}; plex_readingsBulkUpdateIfChanged($chash, 'duration', plex_sec2hms($entry->{duration}/1000) ); } } } readingsEndUpdate($chash, 1); } elsif( $param->{key} =~ m/myPlex:servers/ ) { $handled = 1; $hash->{'myPlex-servers'} = $xml; foreach my $server (@{$xml->{Server}}) { if( $hash->{server} && $server->{address} eq $hash->{server} ) { my $entry = $server; my $ip = $entry->{address}; $ip = $param->{address} if( !$ip ); $entry->{port} = $param->{port} if( !$entry->{port} ); if( my $entry = plex_serverOf($hash, $entry->{machineIdentifier}, !$hash->{machineIdentifier}) ) { $entry->{address} = $server->{address}; $entry->{port} = $server->{port}; } #plex_discovered($hash, 'server', $ip, $entry); } elsif( my $entry = plex_entryOfID($hash, 'server', $server->{machineIdentifier} ) ) { } if( my $chash = $modules{plex}{defptr}{$server->{machineIdentifier}} ) { } } } elsif( $param->{key} =~ m/myPlex:devices/ ) { $handled = 1; $hash->{'myPlex-devices'} = $xml; foreach my $device (@{$xml->{Device}}) { if( my $entry = plex_entryOfID($hash, 'server', $device->{clientIdentifier} ) ) { } if( my $entry = plex_entryOfID($hash, 'client', $device->{clientIdentifier} ) ) { } if( my $chash = $modules{plex}{defptr}{$device->{clientIdentifier}} ) { } } } elsif( $param->{key} eq 'sessions' ) { $handled = 1; if( my $server = plex_serverOf($hash, $param->{host}) ) { delete $server->{sessions}; foreach my $type ( keys %{$xml} ) { next if( ref($xml->{$type}) ne 'ARRAY' ); foreach my $item (@{$xml->{$type}}) { $server->{sessions}{$item->{sessionKey}} = $item; } } } } elsif( $param->{key} =~ m/#m3u:(.*)/ ) { my $entry = plex_entryOfID($hash, 'server', $1); $handled = 1; my $items; $items = $xml->{Directory} if( $xml->{Directory} ); $items =$xml->{Playlist} if( $xml->{Playlist} ); $items = $xml->{Video} if( $xml->{Video} ); $items = $xml->{Track} if( $xml->{Track} ); my $artist = ''; $artist = $xml->{grandparentTitle} if( $xml->{grandparentTitle} ); my $album = ''; $album = $xml->{parentTitle} if( $xml->{parentTitle} ); my $ret = "#EXTM3U\n"; if( $entry && $items ) { foreach my $item (@{$items}) { $ret .= '#EXTINF:'. int($item->{duration}/1000) .",$artist - $album - $item->{title}\n"; if( $item->{Media} && $item->{Media}[0]{Part} ) { $ret .= "http://$entry->{address}:$entry->{port}$item->{Media}[0]{Part}[0]{key}\n"; } } } if( $param->{cl} && $param->{cl}{canAsyncOutput} ) { #Log 1, $ret; asyncOutput( $param->{cl}, $ret ); } elsif( $param->{blocking} ) { return $ret; } } elsif( $param->{key} =~ m/#pls:(.*)/ ) { my $entry = plex_entryOfID($hash, 'server', $1); $handled = 1; my $items; $items = $xml->{Directory} if( $xml->{Directory} ); $items =$xml->{Playlist} if( $xml->{Playlist} ); $items = $xml->{Video} if( $xml->{Video} ); $items = $xml->{Track} if( $xml->{Track} ); my $artist = ''; $artist = $xml->{grandparentTitle} if( $xml->{grandparentTitle} ); my $album = ''; $album = $xml->{parentTitle} if( $xml->{parentTitle} ); my $ret = "[playlist]\n"; if( $entry && $items ) { my $i = 0; foreach my $item (@{$items}) { ++$i; if( $item->{Media} && $item->{Media}[0]{Part} ) { $ret .= "File$i=http://$entry->{address}:$entry->{port}$item->{Media}[0]{Part}[0]{key}\n"; } $ret .= "Title$i=$artist - $album - $item->{title}\n"; $ret .= "Length$i=". int($item->{duration}/1000) ."\n"; } $ret .= "NumberOfEntries=". $i ."\n"; $ret .= "Version=2\n"; } if( $param->{cl} && $param->{cl}{canAsyncOutput} ) { #Log 1, $ret; asyncOutput( $param->{cl}, $ret ); } elsif( $param->{blocking} ) { return $ret; } } 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; return $xml if( $param->{blocking} ); } elsif( $xml->{code} && $xml->{status} ) { $handled = 1; if( $xml->{code} == 200 ) { Log3 $name, 5, "$name: http request ($param->{url}) received code $xml->{code}: $xml->{status}"; } else { Log3 $name, 2, "$name: http request ($param->{url}) received code $xml->{code}: $xml->{status}"; } } if( !$handled ) { Log3 $name, 2, "$name: unhandled message '$param->{key}': ". Dumper $xml; } return $xml if( $param->{blocking} ); } sub plex_Read($) { my ($hash) = @_; my $name = $hash->{NAME}; my $len; my $buf; if( $hash->{multicast} || $hash->{broadcast} ) { my $phash = $hash->{phash}; $len = $hash->{CD}->recv($buf, 1024); if( !defined($len) || !$len ) { Log 1, "!!!!!!!!!!"; return; } my $peerhost = $hash->{CD}->peerhost; my $peerport = $hash->{CD}->peerport; my $sockport = $hash->{CD}->sockport; plex_Parse($phash, $buf, $peerhost, $peerport, $sockport); } elsif( $hash->{timeline} ) { $len = sysread($hash->{CD}, $buf, 10240); #Log 1, "1:$len: $buf"; my $peerhost = $hash->{CD}->peerhost; my $peerport = $hash->{CD}->peerport; if( !defined($len) || !$len ) { plex_closeSocket( $hash ); delete($defs{$name}); if( my $entry = plex_clientOf($hash->{phash}, $peerhost) ) { delete($hash->{phash}{helper}{subscriptionsFrom}{$entry->{machineIdentifier}}); } return undef; } #Log 1, "timeline ($peerhost:$peerport): $buf"; return undef; } elsif( defined($hash->{websocket}) ) { my $pname = $hash->{PNAME} || $name; $len = sysread($hash->{CD}, $buf, 10240); #Log 1, "2:$len: $buf"; my $peerhost = $hash->{CD}->peerhost; my $peerport = $hash->{CD}->peerport; my $close = 0; if( !defined($len) || !$len ) { $close = 1; } elsif( $hash->{websocket} ) { $hash->{buf} .= $buf; do { my $fin = (ord(substr($hash->{buf},0,1)) & 0x80)?1:0; my $op = (ord(substr($hash->{buf},0,1)) & 0x0F); my $mask = (ord(substr($hash->{buf},1,1)) & 0x80)?1:0; my $len = (ord(substr($hash->{buf},1,1)) & 0x7F); my $i = 2; if( $len == 126 ) { $len = unpack( 'n', substr($hash->{buf},$i,2) ); $i += 2; } elsif( $len == 127 ) { $len = unpack( 'N', substr($hash->{buf},$i+4,8) ); $i += 8; } if( $mask ) { $i += 4; } #Log 1, "$fin $op $mask $len"; #FIXME: hande !$fin return if( $len > length($hash->{buf})-$i ); my $data = substr($hash->{buf}, $i, $len); $hash->{buf} = substr($hash->{buf},$i+$len); #Log 1, ">>>$data<<<"; if( $data eq '?' ) { #ignore keepalive } elsif( $op == 0x01 ) { my $obj = eval { decode_json($data) }; if( $obj ) { Log3 $pname, 5, "$pname: websocket data: ". Dumper $obj; my $phash = $hash->{phash}; my $handled = 0; if( $obj->{NotificationContainer} ) { $obj = $obj->{NotificationContainer}; if( $obj->{type} eq 'update.statechange' ) { $handled = 1; Log3 $pname, 4, "$pname: update available $obj->{AutoUpdateNotification}[0]{fixed}"; } } elsif( $obj->{_elementType} && $obj->{_elementType} eq 'NotificationContainer' ) { if( $obj->{type} eq 'playing' ) { $handled = 1; my $cname; my $session_info_requested; if( my $session = $obj->{_children}[0]{sessionKey} ) { if( my $server = plex_serverOf($phash, $peerhost) ) { if( my $session = $server->{sessions}{$session} ) { if( my $chash = $modules{plex}{defptr}{$session->{Player}[0]{machineIdentifier}} ) { $cname = $chash->{NAME}; #Log 1, Dumper $obj; readingsBeginUpdate($chash); my $key = $obj->{_children}[0]{key}; if( $key && $key ne ReadingsVal($chash->{NAME}, 'key', '') ) { $chash->{currentServer} = $server->{machineIdentifier}; readingsBulkUpdate($chash, 'key', $key ); readingsBulkUpdate($chash, 'server', $server->{machineIdentifier} ); plex_sendApiCmd( $phash, "http://$server->{address}:$server->{port}$key", "#update:$chash->{NAME}" ); } my $time = $obj->{_children}[0]{viewOffset}; if( defined($time) ) { # if( !$chash->{helper}{time} || abs($time - $chash->{helper}{time}) > 2000 ) { # plex_readingsBulkUpdateIfChanged($chash, 'time', plex_sec2hms($time/1000) ); # # $chash->{helper}{time} = $time; # } $chash->{time} = $time; } plex_readingsBulkUpdateIfChanged($chash, 'state', $obj->{_children}[0]{state} ); readingsEndUpdate($chash, 1); } else { Log3 $pname, 3, "$pname: unknown player: $session->{Player}[0]{machineIdentifier}"; } } else { Log3 $pname, 3, "$pname: new session $obj->{_children}[0]{sessionKey}"; $session_info_requested = 1; plex_sendApiCmd( $phash, "http://$server->{address}:$server->{port}/status/sessions", 'sessions' ); } } } else { Log3 $pname, 3, "$pname: no session in notifcation "; } if( !$session_info_requested ) { if( $obj->{_children}[0]{state} eq 'playing' || $obj->{_children}[0]{state} eq 'stopped' ) { if( !$cname || $obj->{_children}[0]{key} ne ReadingsVal($cname, 'key', '' ) ) { if( my $server = plex_serverOf($phash, $peerhost) ) { plex_sendApiCmd( $phash, "http://$server->{address}:$server->{port}/status/sessions", 'sessions' ); } } } } } elsif( $obj->{type} eq 'status' ) { $handled = 1; #Log 1, Dumper $obj; DoTrigger( $pname, "$obj->{_children}[0]{notificationName}: $obj->{_children}[0]{title}" ); } } if( $obj->{type} ) { Log3 $pname, 4, "$pname: unhandled websocket text type: $obj->{type}: $data" if( !$handled ); } else { Log3 $pname, 4, "$pname: unhandled websocket data: $data" if( !$handled ); } } else { Log3 $pname, 2, "$pname: unhandled websocket text $data"; } } else { Log3 $pname, 2, "$pname: unhandled websocket data: $data"; } } while( $hash->{buf} && !$close ); } elsif( $buf =~ m'^HTTP/1.1 101 Switching Protocols'i ) { $hash->{websocket} = 1; my $buf = plex_msg2hash($buf, 1); Log3 $pname, 3, "$pname: notification websocket: Switching Protocols ok"; } else { $close = 1; Log3 $pname, 2, "$pname: notification websocket: Switching Protocols failed"; } if( $close ) { my $phash = $hash->{phash}; plex_closeSocket( $hash ); delete($phash->{helper}{websockets}{$hash->{machineIdentifier}}); delete($phash->{servers}{$hash->{address}}{sessions}); delete($defs{$name}); } return undef; } elsif ( $hash->{phash} ) { my $phash = $hash->{phash}; my $pname = $hash->{PNAME}; if( $phash->{helper}{timelineListener} == $hash ) { my @clientinfo = $hash->{CD}->accept(); if( !@clientinfo ) { Log3 $name, 1, "Accept failed ($name: $!)" if($! != EAGAIN); return undef; } $hash->{CONNECTS}++; my ($port, $iaddr) = sockaddr_in($clientinfo[1]); my $caddr = inet_ntoa($iaddr); my $chash = plex_newChash( $phash, $clientinfo[0], {NAME=>"$name:$port", STATE=>'listening'} ); $chash->{buf} = ''; $hash->{connections}{$chash->{NAME}} = $chash; Log3 $name, 5, "$name: timeline sender $caddr connected to $port"; return; } $len = sysread($hash->{CD}, $buf, 10240); #Log 1, "2:$len: $buf"; do { my $close = 1; if( $len ) { $hash->{buf} .= $buf; return if $hash->{buf} !~ m/^(.*?)\r?\n\r?\n(.*)?$/s; my $header = $1; my $body = $2; my $content_length; my $length = length($body); if( $header =~ m/Content-Length:\s*(\d+)/si ) { $content_length = $1; return if( $length < $content_length ); if( $header !~ m/Connection: Close/si ) { $close = 0; Log3 $pname, 5, "$name: keepalive"; #syswrite($hash->{CD}, "HTTP/1.1 200 OK\r\nConnection: Keep-Alive\r\nContent-Length: 0\r\n\r\n" ); if( $length > $content_length ) { $buf = substr( $body, $content_length ); $hash->{buf} = "$header\r\n\r\n". substr( $body, 0, $content_length ); } else { $buf =''; } if( !$hash->{machineIdentifier} && $header =~ m/X-Plex-Client-Identifier:\s*(.*)/i ) { $hash->{machineIdentifier} = $1; } } else { Log3 $pname, 5, "$name: close"; #syswrite($hash->{CD}, "HTTP/1.1 200 OK\r\nConnection: Close\r\n\r\n" ); } } elsif( $length == 0 && $header =~ m/^GET/ ) { $buf = ''; } else { return; } } Log3 $pname, 4, "$name: disconnected" if( !$len ); my $ret; $ret = plex_Parse($phash, $hash->{buf}) if( $hash->{buf} ); if( $len ) { my $add_header; if( !$ret || $ret !~ m/^HTTP/si ) { $add_header .= "HTTP/1.1 200 OK\r\n"; } if( !$ret || $ret !~ m/Connection:/si ) { if( $close ) { $add_header .= "Connection: Close\r\n"; } else { $add_header .= "Connection: Keep-Alive\r\n"; } } if( !$ret ) { $add_header .= "Content-Length: 0\r\n"; } if( $add_header ) { Log3 $pname, 5, "$name: add header: $add_header"; syswrite($hash->{CD}, $add_header); } if( $ret ) { syswrite($hash->{CD}, $ret); if( $ret !~ m/Connection: Close/si ) { $close = 0; Log3 $pname, 5, "$name: keepalive"; } } else { syswrite($hash->{CD}, "\r\n" ); } } $hash->{buf} = $buf; $buf = ''; if( $close || !$len ) { plex_closeSocket( $hash ); delete($defs{$name}); delete($hash->{phash}{helper}{timelineListener}{connections}{$hash->{NAME}}); return; } } while( $hash->{buf} ); } return undef; } sub plex_publishToSonos(;$$$) { my ($hash,$service,$player) = @_; $hash = $modules{plex}{defptr}{MASTER} if( !$hash && defined($modules{plex}{defptr}{MASTER}) ); $hash = $defs{$hash} if( ref($hash) ne 'HASH' ); return 'no plex device found' if( !$hash ); my $name = $hash->{NAME}; return 'no timeline listener started' if( !$hash->{helper}{timelineListener} ); $service = 'PLEX' if( !$service ); my $i = 0; foreach my $d (devspec2array("TYPE=SONOSPLAYER")) { next if( $player && $d !~ /$player/ ); my $location = ReadingsVal($d,'location',undef); my $ip = ($location =~ m/https?:..([\d.]*)/)[0]; 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}"; 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; HttpUtils_NonblockingGet( $param ); ++$i; } if( !$i && $player ) { my $url = "http://$player:1400/customsd"; Log3 $name, 4, "$name: requesting $url"; my $fhem_base_url = "http://$hash->{fhemIP}:$hash->{helper}{timelineListener}{PORT}"; 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 => $player, data => $data, }; $param->{cl} = $hash->{CL} if( ref($hash->{CL}) eq 'HASH' ); $param->{callback} = \&plex_parseHttpAnswer; HttpUtils_NonblockingGet( $param ); ++$i; } return 'no sonos players found' if( !$i ); return "send SMAPI registration to $i players"; return undef; } 1; =pod =item summary control and receive events from PLEX players =item summary_DE Steuern und überwachen von PLEX Playern =begin html

plex

    This module allows you to control and receive events from plex.



    Notes:
    • IO::Socket::Multicast is needed to use server and client autodiscovery.
    • As far as possible alle get and set commands are non-blocking. Any output is displayed asynchronous and is using fhemweb popup windows.


    Define
      define <name> plex [<server>]

    Set
    • play [<server> [<item>]]
    • resume [<server>] <item>]
    • pause [<type>]
    • stop [<type>]
    • skipNext [<type>]
    • skipPrevious [<type>]
    • stepBack [<type>]
    • stepForward [<type>]
    • seekTo <value> [<type>]
    • volume <value> [<type>]
    • shuffle 0|1 [<type>]
    • repeat 0|1|2 [<type>]
    • mirror [<server>] <item>
      show preplay screen for <item>
    • home
    • music
    • showAccount
      display obfuscated user and password in cleartext
    • playlistCreate [<server>] <name>
    • playlistAdd [<server>] <key> <keys>
    • playlistRemove [<server>] <key> <keys>
    • unwatched [[<server>] <items>]
    • watched [[<server>] <items>]
    • autocreate <machineIdentifier>
      create device for remote/shared server

    Get
    • [<server>] ls [<path>]
      browse the media library. eg:

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

      get <plex> ls /1
        Musik
        key                                 type       title
        all                                            All Artists
        albums                                         By Album
        collection                                     By Collection
        decade                                         By Decade
        folder                                         By Folder
        genre                                          By Genre
        year                                           By Year
        recentlyAdded                                  Recently Added
        search?type=9                                  Search Albums...
        search?type=8                                  Search Artists...
        search?type=10                                 Search Tracks...

      get <plex> ls /1/albums
        Musik ; By Album
        key                                  type       title
        /library/metadata/133999/children   album       ...
        /library/metadata/134207/children   album       ...
        /library/metadata/168437/children   album       ...
        /library/metadata/82906/children    album       ...
        ...

      get <plex> ls /library/metadata/133999/children
        ...


      if used from fhemweb album art can be displayed and keys and other items are klickable.

    • [<server>] search <keywords>
      search the media library for items that match <keywords>
    • [<server>] onDeck
      list the global onDeck items
    • [<server>] recentlyAdded
      list the global recentlyAdded items
    • [<server>] detail <key>
      show detail information for media item <key>
    • [<server>] playlists
      list playlists
    • [<server>] m3u [album]
      creates an album playlist in m3u format. can be used with other players like sonos.
    • [<server>] pls [album]
      creates an album playlist in pls format. can be used with other players like sonos.
    • clients
      list the known clients
    • servers
      list the known servers
    • pin
      get a pin for authentication at https://plex.tv/pin

    Attr
    • httpPort
    • ignoredClients
    • ignoredServers
    • removeUnusedReadings
    • user
    • password
      user and password of a myPlex account. required if plex home is used. both are stored obfuscated

=end html =cut