| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580 |
- # $Id: 37_plex.pm 13362 2017-02-08 18:47:04Z justme1968 $
- #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 '<unknown>' if( !$socket );
- my $ip = $socket->sockhost;
- close( $socket );
- return $ip if( $ip );
- #$ip = inet_ntoa( scalar gethostbyname( hostname() || 'localhost' ) );
- #return $ip if( $ip );
- return '<unknown>';
- }
- sub
- plex_Define($$)
- {
- my ($hash, $def) = @_;
- my @a = split("[ \t][ \t]*", $def);
- return "Usage: define <name> 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 = '<?xml version="1.0" encoding="utf-8" ?>';
- $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 <server>" 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 <name>" 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: [<server>] playlistAdd <key> <keys>" 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: [<server>] playlistRemove <key> <keys>" if( !$params[0] );
- #foreach my $key ( @params ) {
- # plex_removeFromPlaylist($hash, $server, $playlist, $key);
- #}
- } elsif( $cmd eq 'unwatched' || $cmd eq 'watched' ) {
- return "usage: unwatched <keys>" 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 <key>" 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 <key>" 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 <value>" 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 <value>" 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 == '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 <id|ip>' 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 <id|ip>' 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 <id|ip>' 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 <id|ip>' 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 "<a style=\"cursor:pointer\" onClick=\"FW_cmd(\\\'$FW_ME$FW_subdir?XHR=1&cmd=$cmd\\\')\">$txt</a>";
- }
- sub
- plex_makeImage($$$$)
- {
- my ($hash, $server, $url, $size) = @_;
- return '' if( !$url );
- my $token = $server->{accessToken};
- $token = $hash->{token} if( !$token );
- my $ret .= "<img src=\"http://$server->{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) = @_;
- 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' ) {
- $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} );
- $ret .= "\n";
- } else {
- $ret .= plex_makeLink($hash,'detail', $xml->{parentSection}, $item->{key}, sprintf( "%-35s %-10s %s\n", $item->{key}, $item->{type}, $item->{title} ) );
- }
- }
- }
- 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) = @_;
- #Log 1, Dumper $xml;
- return $xml if( ref($xml) ne 'HASH' );
- my $token = $server->{accessToken};
- $token = $hash->{token} if( !$token );
- $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} ) 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) = @_;
- my $token = $server->{accessToken};
- $token = $hash->{token} if( !$token );
- #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 .= "<a style=\"cursor:pointer\" onClick=\"FW_cmd(\\\'$FW_ME$FW_subdir?XHR=1&cmd=$cmd\\\')\">$ip</a> ";
- }
- $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' );
- my $token = $server->{accessToken};
- $token = $hash->{token} if( !$token );
- $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' ) {
- $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", $hash->{CL} || 1, $entry->{accessToken} );
- } else {
- $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/library/sections$param", "sections:$param", $hash->{CL} || 1, $entry->{accessToken} );
- }
- return $ret;
- } elsif( $cmd eq 'search' ) {
- return "usage: search <keywords>" 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 <key>" 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 <key>" 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 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} );
- 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} ) {
- 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' => $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->{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 .= '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">';
- $body .= ' <s: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 .= '<mediaCollection>';
- $body .= " <title>$title</title>";
- $body .= " <id>$item->{key}</id>" if( $item->{key} =~ '^/' );
- $body .= " <id>$key/$item->{key}</id>" if( $item->{key} !~ '^/' );
- $body .= " <albumArtURI>http://$server->{address}:$server->{port}$item->{thumb}</albumArtURI>" if( $item->{thumb} );
- $body .= ' <canScroll>true</canScroll>';
- if( $item->{type} eq 'album' ) {
- $body .= '<canPlay>true</canPlay>';
- $body .= '<itemType>album</itemType>';
- } elsif( $item->{type} eq 'artist' ) {
- $body .= '<canPlay>true</canPlay>';
- $body .= '<itemType>artist</itemType>';
- } elsif( $item->{type} eq 'genre' ) {
- $body .= '<canPlay>true</canPlay>';
- $body .= '<itemType>genre</itemType>';
- } else {
- $body .= '<itemType>collection</itemType>';
- }
- $body .= '</mediaCollection>';
- 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 .= '<mediaMetadata>';
- $body .= " <title>$item->{title}</title>";
- $body .= " <id>$item->{key}</id>" if( $item->{key} =~ '^/' );
- $body .= " <id>$key/$item->{key}</id>" if( $item->{key} !~ '^/' );
- $body .= ' <mimeType>audio/mp3</mimeType>';
- $body .= ' <itemType>track</itemType>';
- $body .= ' <trackMetadata>';
- $body .= " <album>$item->{parentTitle}</album>";
- $body .= " <albumId>$item->{parentKey}</albumId>";
- $body .= " <artist>$item->{grandparentTitle}</artist>";
- $body .= " <artistId>$item->{grandparentKey}</artistId>";
- $body .= " <trackNumber>$item->{index}</trackNumber>";
- $body .= " <duration>". int($item->{duration}/1000) ."</duration>";
- $body .= " <albumArtURI>http://$server->{address}:$server->{port}$item->{parentThumb}</albumArtURI>" if( $item->{parentThumb} );
- $body .= ' </trackMetadata>';
- $body .= '</mediaMetadata>';
- last if( ++$i >= $index + $count );
- }
- }
- $body .= " <total>$total</total>";
- $body .= " <index>$index</index>";
- $body .= " <count>". ($i-$index) ."</count>";
- $body .= ' </'.$type.'Result>';
- $body .= ' </'.$type.'Response>';
- $body .= ' </s:Body>';
- $body .= '</s:Envelope>';
- #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 .= '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">';
- $body .= ' <s:Body>';
- $body .= ' <getScrollIndicesResponse xmlns="http://www.sonos.com/Services/1.1">';
- $body .= ' <getScrollIndicesResult>';
- $body .= plex_getScrollindicesForSMAPI($hash,$xml);
- $body .= ' </getScrollIndicesResult>';
- $body .= ' </getScrollIndicesResponse>';
- $body .= ' </s:Body>';
- $body .= '</s:Envelope>';
- 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 .= '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">';
- $body .= ' <s:Body>';
- $body .= ' <getMediaMetadataResponse xmlns="http://www.sonos.com/Services/1.1">';
- $body .= ' <getMediaMetadataResult>';
- if( $xml->{Track} ) {
- foreach my $item (@{$xml->{Track}}) {
- $item->{title} =~ s/&/&/g;
- $item->{parentTitle} =~ s/&/&/g;
- $item->{grandparentTitle} =~ s/&/&/g;
- $body .= "<title>$item->{title}</title>";
- $body .= "<id>$item->{key}</id>" if( $item->{key} =~ '^/' );
- $body .= "<id>$key/$item->{key}</id>" if( $item->{key} !~ '^/' );
- $body .= '<mimeType>audio/mp3</mimeType>';
- $body .= '<itemType>track</itemType>';
- $body .= '<trackMetadata>';
- $body .= " <album>$item->{parentTitle}</album>";
- $body .= " <albumId>$item->{parentKey}</albumId>";
- $body .= " <artist>$item->{grandparentTitle}</artist>";
- $body .= " <artistId>$item->{grandparentKey}</artistId>";
- $body .= " <trackNumber>$item->{index}</trackNumber>";
- $body .= " <duration>". int($item->{duration}/1000) ."</duration>";
- $body .= " <albumArtURI>http://$server->{address}:$server->{port}$item->{parentThumb}</albumArtURI>" if( $item->{parentThumb} );
- $body .= '</trackMetadata>';
- }
- }
- $body .= ' </getMediaMetadataResult>';
- $body .= ' </getMediaMetadataResponse>';
- $body .= ' </s:Body>';
- $body .= '</s:Envelope>';
- 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 .= '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">';
- $body .= ' <s:Body>';
- $body .= ' <getMediaURIResponse xmlns="http://www.sonos.com/Services/1.1">';
- $body .= ' <getMediaURIResult>';
- 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 .= ' </getMediaURIResult>';
- if( $hash->{token} ) {
- $body .= '<httpHeaders>';
- $body .= ' <httpHeader>';
- $body .= ' <header>X-Plex-Token</header>';
- $body .= " <value>$hash->{token}</value>";
- $body .= ' </httpHeader>';
- $body .= '</httpHeaders>';
- }
- $body .= ' </getMediaMetadataResponse>';
- $body .= ' </s:Body>';
- $body .= '</s:Envelope>';
- 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 .= '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">';
- $body .= ' <s:Body>';
- $body .= ' <getLastUpdateResponse xmlns="http://www.sonos.com/Services/1.1">';
- $body .= ' <getLastUpdateResult>';
- $body .= " <catalog>$seconds</catalog>";
- $body .= ' <favorites></favorites>';
- $body .= ' <pollInterval>120</pollInterval>';
- $body .= ' </getLastUpdateResult>';
- $body .= ' </getLastUpdateResponse>';
- $body .= ' </s:Body>';
- $body .= '</s:Envelope>';
- 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 = '<?xml version="1.0" encoding="utf-8" ?>';
- $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/^<!DOCTYPE html>(.*)/ ) {
- 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/<br>/g;
- $ret = "<pre>$ret</pre>" if( $ret =~ m/ / );
- $ret = "<html>$ret</html>";
- } else {
- $ret =~ s/<a[^>]*>//g;
- $ret =~ s/<\/a>//g;
- $ret =~ s/<img[^>]*>\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(:(.*))?' ) {
- $handled = 1;
- $xml->{parentSection} = $2;
- my $server = plex_entryOfIP($hash, 'server', $param->{address});
- my $ret = plex_mediaList( $hash, $server, $xml );
- if( $param->{cl} && $param->{cl}->{TYPE} eq 'FHEMWEB' ) {
- $ret =~ s/&/&/g;
- $ret =~ s/'/'/g;
- $ret =~ s/\n/<br>/g;
- $ret = "<pre>$ret</pre>" if( $ret =~ m/ / );
- $ret = "<html>$ret</html>";
- } else {
- $ret =~ s/<a[^>]*>//g;
- $ret =~ s/<\/a>//g;
- $ret =~ s/<img[^>]*>//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( 'q', substr($hash->{buf},$i,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);
- if( $op == 0x01 ) {
- my $obj = eval { decode_json($data) };
- if( $obj ) {
- my $phash = $hash->{phash};
- my $handled = 0;
- if( $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}" );
- }
- }
- Log3 $pname, 4, "$pname: unhandled websocket text type: $obj->{type}: $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
- <a name="plex"></a>
- <h3>plex</h3>
- <ul>
- This module allows you to control and receive events from plex.<br><br>
- <br><br>
- Notes:
- <ul>
- <li>IO::Socket::Multicast is needed to use server and client autodiscovery.</li>
- <li>As far as possible alle get and set commands are non-blocking.
- Any output is displayed asynchronous and is using fhemweb popup windows.</li>
- </ul>
- <br><br>
- <a name="plex_Define"></a>
- <b>Define</b>
- <ul>
- <code>define <name> plex [<server>]</code>
- <br><br>
- </ul>
- <a name="plex_Set"></a>
- <b>Set</b>
- <ul>
- <li>play [<server> [<item>]]<br>
- </li>
- <li>resume [<server>] <item>]<br>
- </li>
- <li>pause [<type>]</li>
- <li>stop [<type>]</li>
- <li>skipNext [<type>]</li>
- <li>skipPrevious [<type>]</li>
- <li>stepBack [<type>]</li>
- <li>stepForward [<type>]</li>
- <li>seekTo <value> [<type>]</li>
- <li>volume <value> [<type>]</li>
- <li>shuffle 0|1 [<type>]</li>
- <li>repeat 0|1|2 [<type>]</li>
- <li>mirror [<server>] <item><br>
- show preplay screen for <item></li>
- <li>home</li>
- <li>music</li>
- <li>showAccount<br>
- display obfuscated user and password in cleartext</li>
- <li>playlistCreate [<server>] <name></li>
- <li>playlistAdd [<server>] <key> <keys></li>
- <li>playlistRemove [<server>] <key> <keys></li>
- <li>unwatched [[<server>] <items>]</li>
- <li>watched [[<server>] <items>]</li>
- <li>autocreate <machineIdentifier><br>
- create device for remote/shared server</li>
- </ul><br>
- <a name="plex_Get"></a>
- <b>Get</b>
- <ul>
- <li>[<server>] ls [<path>]<br>
- browse the media library. eg:<br><br>
- <b><code>get <plex> ls</code></b>
- <pre> Plex Library
- key type title
- 1 artist Musik
- 2 ...</pre><br>
- <b><code>get <plex> ls /1</code></b>
- <pre> 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...</pre><br>
- <b><code>get <plex> ls /1/albums</code></b>
- <pre> 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 ...
- ...</pre><br>
- <b><code>get <plex> ls /library/metadata/133999/children</code></b>
- <pre> ...</pre><br>
- <br>if used from fhemweb album art can be displayed and keys and other items are klickable.<br><br>
- </li>
- <li>[<server>] search <keywords><br>
- search the media library for items that match <keywords></li>
- <li>[<server>] onDeck<br>
- list the global onDeck items</li>
- <li>[<server>] recentlyAdded<br>
- list the global recentlyAdded items</li>
- <li>[<server>] detail <key><br>
- show detail information for media item <key></li>
- <li>[<server>] playlists<br>
- list playlists</li>
- <li>[<server>] m3u [album]<br>
- creates an album playlist in m3u format. can be used with other players like sonos.</li>
- <li>[<server>] pls [album]<br>
- creates an album playlist in pls format. can be used with other players like sonos.</li>
- <li>clients<br>
- list the known clients</li>
- <li>servers<br>
- list the known servers</li>
- <li>pin<br>
- get a pin for authentication at <a href="https://plex.tv/pin">https://plex.tv/pin</a></li>
- </ul><br>
- <a name="plex_Attr"></a>
- <b>Attr</b>
- <ul>
- <li>httpPort</li>
- <li>ignoredClients</li>
- <li>ignoredServers</li>
- <li>removeUnusedReadings</li>
- <li>user</li>
- <li>password<br>
- user and password of a myPlex account. required if plex home is used. both are stored obfuscated</li>
- </ul>
- </ul><br>
- =end html
- =cut
|