37_plex.pm 149 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596
  1. # $Id: 37_plex.pm 14601 2017-06-30 07:33:29Z justme1968 $
  2. #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
  3. package main;
  4. use strict;
  5. use warnings;
  6. use Sys::Hostname;
  7. use IO::Socket::INET;
  8. #use Net::Address::IP::Local;
  9. #use MIME::Base64;
  10. use JSON;
  11. use Encode qw(encode);
  12. use XML::Simple qw(:strict);
  13. use Digest::MD5 qw(md5_hex);
  14. #use Socket;
  15. use Time::HiRes qw(usleep nanosleep);
  16. use HttpUtils;
  17. use Time::Local;
  18. use Data::Dumper;
  19. my $plex_hasMulticast = 1;
  20. sub
  21. plex_Initialize($)
  22. {
  23. my ($hash) = @_;
  24. eval "use IO::Socket::Multicast;";
  25. $plex_hasMulticast = 0 if($@);
  26. $hash->{ReadFn} = "plex_Read";
  27. $hash->{DefFn} = "plex_Define";
  28. $hash->{NotifyFn} = "plex_Notify";
  29. $hash->{UndefFn} = "plex_Undefine";
  30. $hash->{SetFn} = "plex_Set";
  31. $hash->{GetFn} = "plex_Get";
  32. $hash->{AttrFn} = "plex_Attr";
  33. $hash->{AttrList} = "disable:1,0"
  34. . " fhemIP httpPort ignoredClients ignoredServers"
  35. . " removeUnusedReadings:1,0 responder:1,0"
  36. . " user password "
  37. . $readingFnAttributes;
  38. }
  39. #####################################
  40. sub
  41. plex_getLocalIP()
  42. {
  43. my $socket = IO::Socket::INET->new(
  44. Proto => 'udp',
  45. PeerAddr => '8.8.8.8:53', # google dns
  46. #PeerAddr => '198.41.0.4:53', # a.root-servers.net
  47. );
  48. return '<unknown>' if( !$socket );
  49. my $ip = $socket->sockhost;
  50. close( $socket );
  51. return $ip if( $ip );
  52. #$ip = inet_ntoa( scalar gethostbyname( hostname() || 'localhost' ) );
  53. #return $ip if( $ip );
  54. return '<unknown>';
  55. }
  56. sub
  57. plex_Define($$)
  58. {
  59. my ($hash, $def) = @_;
  60. my @a = split("[ \t][ \t]*", $def);
  61. return "Usage: define <name> plex [server]" if(@a < 2);
  62. my $name = $a[0];
  63. my ($ip,$port);
  64. ($ip,$port) = split( ':', $a[2] ) if( $a[2] );
  65. my $server = $ip;
  66. my $client = $ip;
  67. $server = '' if( $server && $server !~ m/^\d+\.\d+\.\d+\.\d+$/ );
  68. $hash->{NAME} = $name;
  69. if( $server ) {
  70. $hash->{server} = $server;
  71. $hash->{port} = $port?$port:32400;
  72. $modules{plex}{defptr}{$server} = $hash;
  73. } elsif( $client ) {
  74. if( $port ) {
  75. $hash->{client} = $client;
  76. $hash->{port} = $port;
  77. $modules{plex}{defptr}{$client} = $hash;
  78. } else {
  79. $hash->{machineIdentifier} = $client;
  80. $modules{plex}{defptr}{$client} = $hash;
  81. }
  82. } else {
  83. my $defptr = $modules{plex}{defptr}{MASTER};
  84. return "plex master already defined as '$defptr->{NAME}'" if( defined($defptr) && $defptr->{NAME} ne $name);
  85. $modules{plex}{defptr}{MASTER} = $hash;
  86. return "give ip or install IO::Socket::Multicast to use server and client autodiscovery" if(!$plex_hasMulticast && !$server);
  87. $hash->{"HAS_IO::Socket::Multicast"} = $plex_hasMulticast;
  88. }
  89. $hash->{id} = md5_hex(getUniqueId());
  90. $hash->{fhemHostname} = hostname();
  91. $hash->{fhemIP} = plex_getLocalIP();
  92. $hash->{NOTIFYDEV} = "global";
  93. if( $init_done ) {
  94. plex_getToken($hash);
  95. plex_startDiscovery($hash);
  96. plex_startTimelineListener($hash);
  97. plex_sendApiCmd( $hash, "http://$hash->{server}:$hash->{port}/servers", "servers" ) if( $hash->{server} );
  98. } else {
  99. readingsSingleUpdate($hash, 'state', 'initialized', 1 );
  100. }
  101. return undef;
  102. }
  103. sub
  104. plex_Notify($$)
  105. {
  106. my ($hash,$dev) = @_;
  107. my $name = $hash->{NAME};
  108. return if($dev->{NAME} ne "global");
  109. return if(!grep(m/^INITIALIZED|REREADCFG$/, @{$dev->{CHANGED}}));
  110. if( my $token = ReadingsVal($name, '.token', undef) ) {
  111. Log3 $name, 3, "$name: restoring token from reading";
  112. $hash->{token} = $token;
  113. plex_sendApiCmd($hash, "https://plex.tv/pms/servers.xml", "myPlex:servers" );
  114. plex_sendApiCmd($hash, "https://plex.tv/devices.xml", "myPlex:devices" );
  115. }
  116. plex_getToken($hash);
  117. plex_startDiscovery($hash);
  118. plex_startTimelineListener($hash);
  119. plex_sendApiCmd( $hash, "http://$hash->{server}:$hash->{port}/servers", "servers" ) if( $hash->{server} );
  120. return undef;
  121. }
  122. sub
  123. plex_sendDiscover($)
  124. {
  125. my ($hash) = @_;
  126. my $name = $hash->{NAME};
  127. my $pname = $hash->{PNAME} || $name;
  128. if( $hash->{multicast} ) {
  129. Log3 $pname, 5, "$name: sending multicast discovery message to $hash->{PORT}";
  130. $hash->{CD}->mcast_send('M-SEARCH * HTTP/1.1', '239.0.0.250:'.$hash->{PORT});
  131. } elsif( $hash->{broadcast} ) {
  132. Log3 $pname, 5, "$name: sending broadcast discovery message to $hash->{PORT}";
  133. my $sin = sockaddr_in($hash->{PORT}, inet_aton('255.255.255.255'));
  134. $hash->{CD}->send('M-SEARCH * HTTP/1.1', 0, $sin );
  135. } else {
  136. Log3 $pname, 2, "$name: can't send unknown discovery message type to $hash->{PORT}";
  137. }
  138. RemoveInternalTimer($hash, "plex_sendDiscover");
  139. if( $hash->{interval} ) {
  140. InternalTimer(gettimeofday()+$hash->{interval}, "plex_sendDiscover", $hash, 0);
  141. }
  142. }
  143. sub
  144. plex_closeSocket($)
  145. {
  146. my ($hash) = @_;
  147. my $name = $hash->{NAME};
  148. if( !$hash->{CD} ) {
  149. my $pname = $hash->{PNAME} || $name;
  150. Log3 $pname, 2, "$name: trying to close a non socket hash";
  151. return undef;
  152. }
  153. RemoveInternalTimer($hash);
  154. close($hash->{CD});
  155. delete($hash->{CD});
  156. delete($selectlist{$name});
  157. delete($hash->{FD});
  158. }
  159. sub
  160. plex_newChash($$$)
  161. {
  162. my ($hash,$socket,$chash) = @_;
  163. $chash->{TYPE} = $hash->{TYPE};
  164. $chash->{NR} = $devcount++;
  165. $chash->{phash} = $hash;
  166. $chash->{PNAME} = $hash->{NAME};
  167. $chash->{CD} = $socket;
  168. $chash->{FD} = $socket->fileno();
  169. $chash->{PORT} = $socket->sockport if( $socket->sockport );
  170. $chash->{TEMPORARY} = 1;
  171. $attr{$chash->{NAME}}{room} = 'hidden';
  172. $defs{$chash->{NAME}} = $chash;
  173. $selectlist{$chash->{NAME}} = $chash;
  174. }
  175. sub
  176. plex_startDiscovery($)
  177. {
  178. my ($hash) = @_;
  179. my $name = $hash->{NAME};
  180. return undef if( $hash->{server} );
  181. return undef if( $hash->{client} );
  182. return undef if( $hash->{machineIdentifier} );
  183. return undef if( !$plex_hasMulticast );
  184. plex_stopDiscovery($hash);
  185. return undef if( AttrVal($name, "disable", 0 ) == 1 );
  186. # udp multicast for servers
  187. if( my $socket = IO::Socket::Multicast->new(Proto => 'udp', Timeout => 5, ReuseAddr=>1, ReusePort=>defined(&SO_REUSEPORT)?1:0) ) {
  188. my $chash = plex_newChash( $hash, $socket,
  189. {NAME=>"$name:serverDiscoveryMcast", STATE=>'discovering', multicast => 1} );
  190. $hash->{helper}{discoverServersMcast} = $chash;
  191. $chash->{PORT} = 32414;
  192. $chash->{interval} = 10;
  193. #plex_sendDiscover($chash);
  194. InternalTimer(gettimeofday()+$chash->{interval}/2, "plex_sendDiscover", $chash, 0);
  195. Log3 $name, 3, "$name: multicast server discovery started";
  196. } else {
  197. Log3 $name, 3, "$name: failed to start multicast server discovery: $@";
  198. InternalTimer(gettimeofday()+10, "plex_startDiscovery", $hash, 0);
  199. }
  200. # udp broadcast for servers
  201. if( my $socket = new IO::Socket::INET ( Proto => 'udp', Broadcast => 1, ) ) {
  202. my $chash = plex_newChash( $hash, $socket,
  203. {NAME=>"$name:serverDiscoveryBcast", STATE=>'discovering', broadcast => 1} );
  204. $hash->{helper}{discoverServersBcast} = $chash;
  205. $chash->{PORT} = 32414;
  206. $chash->{interval} = 10;
  207. plex_sendDiscover($chash);
  208. Log3 $name, 3, "$name: broadcast server discovery started";
  209. } else {
  210. Log3 $name, 3, "$name: failed to start broadcast server discovery: $@";
  211. InternalTimer(gettimeofday()+10, "plex_startDiscovery", $hash, 0);
  212. }
  213. # udp multicast for clients
  214. if( my $socket = IO::Socket::Multicast->new(Proto=>'udp', ReuseAddr=>1, ReusePort=>defined(&SO_REUSEPORT)?1:0) ) {
  215. $socket->mcast_add('239.0.0.250');
  216. my $chash = plex_newChash( $hash, $socket,
  217. {NAME=>"$name:clientDiscoveryMcast", STATE=>'discovering', multicast => 1} );
  218. $hash->{helper}{discoverClientsMcast} = $chash;
  219. $chash->{PORT} = 32412;
  220. $chash->{interval} = 10;
  221. #plex_sendDiscover($chash);
  222. InternalTimer(gettimeofday()+$chash->{interval}/2, "plex_sendDiscover", $chash, 0);
  223. Log3 $name, 3, "$name: multicast client discovery started";
  224. } else {
  225. Log3 $name, 3, "$name: failed to start multicast client discovery: $@";
  226. InternalTimer(gettimeofday()+10, "plex_startDiscovery", $hash, 0);
  227. }
  228. # udp broadcast for clients
  229. if( my $socket = new IO::Socket::INET ( Proto => 'udp', Broadcast => 1, ) ) {
  230. my $chash = plex_newChash( $hash, $socket,
  231. {NAME=>"$name:clientDiscoveryBcast", STATE=>'discovering', broadcast => 1} );
  232. $hash->{helper}{discoverClientsBcast} = $chash;
  233. $chash->{PORT} = 32412;
  234. $chash->{interval} = 10;
  235. plex_sendDiscover($chash);
  236. Log3 $name, 3, "$name: broadcast client discovery started";
  237. } else {
  238. Log3 $name, 3, "$name: failed to start broadcast client discovery: $@";
  239. InternalTimer(gettimeofday()+10, "plex_startDiscovery", $hash, 0);
  240. }
  241. # listen for udp mulicast HELLO and BYE messages from PHT
  242. if( my $socket = IO::Socket::Multicast->new(Proto=>'udp', LocalPort=>32413, ReuseAddr=>1, ReusePort=>defined(&SO_REUSEPORT)?1:0) ) {
  243. $socket->mcast_add('239.0.0.250');
  244. my $chash = plex_newChash( $hash, $socket,
  245. {NAME=>"$name:clientDiscoveryPHT", STATE=>'listening', multicast => 1} );
  246. $hash->{helper}{discoverClientsListen} = $chash;
  247. Log3 $name, 3, "$name: pht client discovery started";
  248. } else {
  249. Log3 $name, 3, "$name: failed to pht start client listener";
  250. InternalTimer(gettimeofday()+10, "plex_startDiscovery", $hash, 0);
  251. }
  252. # listen for udp multicast server UPDATE messages (playerAdd, playerDel)
  253. # if( my $socket = IO::Socket::Multicast->new(Proto=>'udp', LocalPort=>32415, ReuseAddr=>1, ReusePort=>defined(&SO_REUSEPORT)?1:0) ) {
  254. # $socket->mcast_add('239.0.0.250');
  255. #
  256. # my $chash = plex_newChash( $hash, $socket,
  257. # {NAME=>"$name:clientDiscovery4", STATE=>'discovering', multicast => 1} );
  258. #
  259. # $hash->{helper}{discoverClients4} = $chash;
  260. #
  261. # Log3 $name, 3, "$name: client discovery4 started";
  262. #
  263. # } else {
  264. # Log3 $name, 3, "$name: failed to start client discovery4: $@";
  265. #
  266. # InternalTimer(gettimeofday()+10, "plex_startDiscovery", $hash, 0);
  267. # }
  268. if( AttrVal($name, 'responder', undef) ) {
  269. # respond to multicast client discovery messages
  270. if( my $socket = IO::Socket::Multicast->new(Proto=>'udp', LocalPort=>32412, ReuseAddr=>1, ReusePort=>defined(&SO_REUSEPORT)?1:0) ) {
  271. $socket->mcast_add('239.0.0.250');
  272. my $chash = plex_newChash( $hash, $socket,
  273. {NAME=>"$name:clientDiscoveryResponderMcast", STATE=>'listening', multicast => 1} );
  274. $hash->{helper}{clientDiscoveryResponderMcast} = $chash;
  275. Log3 $name, 3, "$name: multicast client discovery responder started";
  276. } else {
  277. Log3 $name, 3, "$name: failed to start multicast client discovery responder: $@";
  278. InternalTimer(gettimeofday()+10, "plex_startDiscovery", $hash, 0);
  279. }
  280. # respond to broadcast client discovery messages
  281. #if( my $socket = new IO::Socket::INET ( Proto => 'udp', Broadcast => 1, LocalAddr => '0.0.0.0', LocalPort => 32412, ReuseAddr=>1) ) {
  282. # my $chash = plex_newChash( $hash, $socket,
  283. # {NAME=>"$name:clientDiscoveryResponderBcast", STATE=>'listening', broadcast => 1} );
  284. # $hash->{helper}{clientDiscoveryResponderBcast} = $chash;
  285. # Log3 $name, 3, "$name: broadcast client discovery responder started";
  286. #} else {
  287. # Log3 $name, 3, "$name: failed to start broadcast client discovery responder: $@";
  288. # InternalTimer(gettimeofday()+10, "plex_startDiscovery", $hash, 0);
  289. #}
  290. }
  291. readingsSingleUpdate($hash, 'state', 'running', 1 );
  292. }
  293. sub
  294. plex_stopDiscovery($)
  295. {
  296. my ($hash) = @_;
  297. my $name = $hash->{NAME};
  298. RemoveInternalTimer($hash, "plex_startDiscovery");
  299. if( my $chash = $hash->{helper}{discoverServersMcast} ) {
  300. my $cname = $chash->{NAME};
  301. plex_closeSocket($chash);
  302. delete($defs{$cname});
  303. delete $hash->{helper}{discoverServersMcast};
  304. Log3 $name, 3, "$name: multicast server discovery stoped";
  305. }
  306. if( my $chash = $hash->{helper}{discoverServersBcast} ) {
  307. my $cname = $chash->{NAME};
  308. plex_closeSocket($chash);
  309. delete($defs{$cname});
  310. delete $hash->{helper}{discoverServersBcast};
  311. Log3 $name, 3, "$name: broadcast server discovery stoped";
  312. }
  313. if( my $chash = $hash->{helper}{discoverClientsMcast} ) {
  314. my $cname = $chash->{NAME};
  315. plex_closeSocket($chash);
  316. delete($defs{$cname});
  317. delete $hash->{helper}{discoverClientsMcast};
  318. Log3 $name, 3, "$name: multicast client discovery stoped";
  319. }
  320. if( my $chash = $hash->{helper}{discoverClientsBcast} ) {
  321. my $cname = $chash->{NAME};
  322. plex_closeSocket($chash);
  323. delete($defs{$cname});
  324. delete $hash->{helper}{discoverClientsBcast};
  325. Log3 $name, 3, "$name: broadcast client discovery stoped";
  326. }
  327. if( my $chash = $hash->{helper}{discoverClientsListen} ) {
  328. my $cname = $chash->{NAME};
  329. plex_closeSocket($chash);
  330. delete($defs{$cname});
  331. delete $hash->{helper}{discoverClientsListen};
  332. Log3 $name, 3, "$name: pht client listener stoped";
  333. }
  334. if( my $chash = $hash->{helper}{discoverClients4} ) {
  335. my $cname = $chash->{NAME};
  336. plex_closeSocket($chash);
  337. delete($defs{$cname});
  338. delete $hash->{helper}{discoverClients4};
  339. Log3 $name, 3, "$name: client discovery4 stoped";
  340. }
  341. if( my $chash = $hash->{helper}{clientDiscoveryResponderMcast} ) {
  342. my $cname = $chash->{NAME};
  343. plex_closeSocket($chash);
  344. delete($defs{$cname});
  345. delete $hash->{helper}{clientDiscoveryResponderMcast};
  346. Log3 $name, 3, "$name: multicast client discovery responder stoped";
  347. }
  348. }
  349. sub
  350. plex_sendSubscription($$)
  351. {
  352. my ($hash,$ip) = @_;
  353. return undef if( !$hash );
  354. my $name = $hash->{NAME};
  355. my $phash = $hash->{phash};
  356. return undef if( !$phash );
  357. my $entry = $phash->{clients}{$ip};
  358. return undef if( !$entry );
  359. my $pname = $hash->{PNAME};
  360. if( !$hash->{subscriptionsTo}{$ip} ) {
  361. $hash->{subscriptionsTo}{$ip} = $ip;
  362. Log3 $pname, 4, "$name: adding timeline subscription for $ip";
  363. } else {
  364. Log3 $pname, 5, "$name: sending subscribe message to $ip:$entry->{port}";
  365. }
  366. plex_sendApiCmd( $phash, "http://$ip:$entry->{port}/player/timeline/subscribe?protocol=http&port=$hash->{PORT}", "subscribe" );
  367. }
  368. sub
  369. plex_removeSubscription($$)
  370. {
  371. my ($hash,$ip) = @_;
  372. return undef if( !$hash );
  373. my $name = $hash->{NAME};
  374. return undef if( !$hash->{subscriptionsTo}{$ip} );
  375. my $phash = $hash->{phash};
  376. return undef if( !$phash );
  377. my $entry = $phash->{clients}{$ip};
  378. return undef if( !$entry );
  379. my $pname = $hash->{PNAME};
  380. Log3 $pname, 4, "$name: removing timeline subscription for $ip";
  381. plex_sendApiCmd( $phash, "http://$ip:$entry->{port}/player/timeline/unsubscribe?", "unsubscribe" ) if( $entry->{online} );
  382. delete $hash->{subscriptionsTo}{$ip};
  383. if( !%{$hash->{subscriptionsTo}} ) {
  384. $phash->{commandID} = 0;
  385. }
  386. if( my $chash = $hash->{helper}{timelineListener} ) {
  387. foreach my $key ( keys %{$chash->{connections}} ) {
  388. my $hash = $chash->{connections}{$key};
  389. my $name = $hash->{NAME};
  390. next if( !$hash->{machineIdentifier} );
  391. next if( $hash->{machineIdentifier} ne $entry->{machineIdentifier} );
  392. plex_closeSocket($hash);
  393. delete($defs{$name});
  394. delete($chash->{connections}{$name});
  395. }
  396. }
  397. }
  398. sub
  399. plex_refreshSubscriptions($)
  400. {
  401. my ($hash) = @_;
  402. my $name = $hash->{NAME};
  403. my $pname = $hash->{PNAME};
  404. Log3 $pname, 4, "$name: refreshing timeline subscriptions" if( %{$hash->{subscriptionsTo}} );
  405. foreach my $ip ( keys %{$hash->{subscriptionsTo}} ) {
  406. plex_sendSubscription($hash, $ip);
  407. }
  408. RemoveInternalTimer($hash,"plex_refreshSubscriptions");
  409. if( $hash->{interval} ) {
  410. InternalTimer(gettimeofday()+$hash->{interval}, "plex_refreshSubscriptions", $hash, 0);
  411. }
  412. }
  413. my $lastCommandID;
  414. sub
  415. plex_sendTimelines($$)
  416. {
  417. my ($hash,$commandID) = @_;
  418. if( ref($hash) ne 'HASH' ) {
  419. my ($name) = split( ':', $hash, 2 );
  420. $hash = $defs{$name};
  421. }
  422. my $name = $hash->{NAME};
  423. $commandID = $lastCommandID if( !$commandID );
  424. $lastCommandID = $commandID;
  425. return undef if( !$hash->{subscriptionsFrom} );
  426. foreach my $key ( keys %{$hash->{subscriptionsFrom}} ) {
  427. my $addr = $hash->{subscriptionsFrom}{$key};
  428. my $chash;
  429. if( $hash->{helper}{subscriptionsFrom}{$key} ) {
  430. $chash = $hash->{helper}{subscriptionsFrom}{$key};
  431. } elsif( my $socket = IO::Socket::INET->new(PeerAddr=>$addr, Timeout=>2, Blocking=>1, ReuseAddr=>1) ) {
  432. $chash = plex_newChash( $hash, $socket,
  433. {NAME=>"$name:timelineSubscription:$addr", STATE=>'opened', timeline=>1} );
  434. Log3 $name, 3, "$name: timeline subscription opened";
  435. $hash->{helper}{subscriptionsFrom}{$key} = $chash;
  436. $chash->{machineIdentifier} = $key;
  437. $chash->{commandID} = $commandID;
  438. }
  439. plex_sendTimeline($chash);
  440. }
  441. $hash->{interval} = 60;
  442. $hash->{interval} = 2 if( $hash->{sonos}{status} && $hash->{sonos}{status} eq 'playing' );
  443. RemoveInternalTimer("$name:sendTimelines");
  444. if( $hash->{interval} ) {
  445. InternalTimer(gettimeofday()+$hash->{interval}, 'plex_sendTimelines', "$name:sendTimelines", 0);
  446. }
  447. }
  448. sub
  449. plex_sendTimeline($)
  450. {
  451. my ($hash) = @_;
  452. my $name = $hash->{NAME};
  453. my $phash = $hash->{phash};
  454. my $pname = $hash->{PNAME};
  455. return undef if( !$hash->{CD} );
  456. Log3 $pname, 4, "$name: refreshing timeline status";
  457. my $xml = { MediaContainer => { size => 1,
  458. machineIdentifier => $phash->{id},
  459. Timeline => { state => $phash->{sonos}{status},
  460. type => 'music',
  461. volume => 100, },
  462. }, };
  463. $xml->{MediaContainer}{commandID} = $hash->{commandID} if( defined($hash->{commandID}) );
  464. if( !$phash->{sonos} || !$phash->{sonos}{playqueue}{size} || $phash->{sonos}{playqueue}{size} < 2 ) {
  465. $xml->{MediaContainer}{Timeline}{controllable} = 'volume,stop,playPause';
  466. } else {
  467. $xml->{MediaContainer}{Timeline}{controllable} = 'volume,stop,playPause,skipNext,skipPrevious';
  468. }
  469. if( !$phash->{sonos} || $phash->{sonos}{status} eq 'stopped' ) {
  470. $xml->{MediaContainer}{Timeline}{location} = 'navigation';
  471. } else {
  472. $xml->{MediaContainer}{Timeline}{location} = 'fullScreenMusic';
  473. $xml->{MediaContainer}{Timeline}{mediaIndex} = $phash->{sonos}{currentTrack}+1;
  474. $xml->{MediaContainer}{Timeline}{playQueueID} = $phash->{sonos}{playqueue}{playQueueID} if( $phash->{sonos}{playqueue}{playQueueID} );
  475. $xml->{MediaContainer}{Timeline}{containerKey} = $phash->{sonos}{containerKey} if( $phash->{sonos}{containerKey} );
  476. $xml->{MediaContainer}{Timeline}{machineIdentifier} = $phash->{sonos}{machineIdentifier};
  477. my $tracks = $phash->{sonos}{playqueue}{Track};
  478. my $track = $tracks->[$phash->{sonos}{currentTrack}];
  479. $xml->{MediaContainer}{Timeline}{duration} = $track->{duration};
  480. $xml->{MediaContainer}{Timeline}{seekRange} = "0-$track->{duration}";
  481. $xml->{MediaContainer}{Timeline}{key} = $track->{key};
  482. $xml->{MediaContainer}{Timeline}{ratingKey} = $track->{ratingKey};
  483. $xml->{MediaContainer}{Timeline}{playQueueItemID} = $track->{playQueueItemID};
  484. if( $phash->{sonos}{status} eq 'playing' ) {
  485. $phash->{sonos}{currentTime} += time() - $phash->{sonos}{updateTime};
  486. if( $phash->{sonos}{currentTime} >= $track->{duration}/1000 ) {
  487. if( !$phash->{sonos}{playqueue}{size} || $phash->{sonos}{playqueue}{size} < 2 ) {
  488. fhem( "set $phash->{id} stop" );
  489. } else {
  490. fhem( "set $phash->{id} skipNext" );
  491. }
  492. return undef;
  493. }
  494. }
  495. $phash->{sonos}{updateTime} = time();
  496. $xml->{MediaContainer}{Timeline}{time} = $phash->{sonos}{currentTime}*1000;
  497. }
  498. my $body = '<?xml version="1.0" encoding="utf-8" ?>';
  499. $body .= "\n";
  500. $body .= XMLout( $xml, KeyAttr => { }, RootName => undef );
  501. $body =~ s/^ //gm;
  502. #Log 1, $body;
  503. my $ret = "POST /:/timeline HTTP/1.1\r\n";
  504. $ret .= plex_hash2header( { 'Host' => $hash->{CD}->peerhost .':'. $hash->{CD}->peerport,
  505. #'Host' => '10.0.1.45:32500',
  506. #'Host' => '10.0.1.17:32400',
  507. 'Accept' => '*/*',
  508. 'X-Plex-Client-Capabilities' => 'audioDecoders=mp3',
  509. 'X-Plex-Client-Identifier' => $phash->{id},
  510. 'X-Plex-Device-Name' => $phash->{fhemHostname},
  511. 'X-Plex-Platform' => $^O,
  512. 'X-Plex-Version' => '0.0.0',
  513. 'X-Plex-Provides' => 'player',
  514. 'Content-Length' => length($body),
  515. #'Content-Range' => 'bytes 0-/-1',
  516. #'Connection' => 'Close',
  517. 'Connection' => 'Keep-Alive',
  518. #'Content-Type' => 'text/xml;charset=utf-8',
  519. 'Content-Type' => 'application/x-www-form-urlencoded',
  520. #'X-Plex-Http-Pipeline' => 'infinite',
  521. } );
  522. $ret .= "\r\n";
  523. $ret .= $body;
  524. #Log 1, $ret;
  525. syswrite($hash->{CD}, $ret );
  526. }
  527. sub
  528. plex_startTimelineListener($)
  529. {
  530. my ($hash) = @_;
  531. my $name = $hash->{NAME};
  532. return undef if( $hash->{server} && $modules{plex}{defptr}{MASTER} );
  533. return undef if( $hash->{client} && $modules{plex}{defptr}{MASTER} );
  534. return undef if( $hash->{machineIdentifier} );
  535. plex_stopTimelineListener($hash);
  536. return undef if( AttrVal($name, "disable", 0 ) == 1 );
  537. my $port = AttrVal($name, 'httpPort', 0);
  538. if( my $socket = IO::Socket::INET->new(LocalPort=>$port, Listen=>10, Blocking=>0, ReuseAddr=>1) ) {
  539. my $chash = plex_newChash( $hash, $socket,
  540. {NAME=>"$name:timelineListener", STATE=>'accepting'} );
  541. $chash->{connections} = {};
  542. $chash->{subscriptionsTo} = {};
  543. $hash->{helper}{timelineListener} = $chash;
  544. Log3 $name, 3, "$name: timeline listener started";
  545. $chash->{interval} = 30;
  546. plex_refreshSubscriptions($chash);
  547. } else {
  548. Log3 $name, 3, "$name: failed to start timeline listener on port $port $@";
  549. InternalTimer(gettimeofday()+10, "plex_startTimelineListener", $hash, 0);
  550. }
  551. }
  552. sub
  553. plex_stopTimelineListener($)
  554. {
  555. my ($hash) = @_;
  556. my $name = $hash->{NAME};
  557. RemoveInternalTimer($hash, "plex_startTimelineListener");
  558. if( my $chash = $hash->{helper}{timelineListener} ) {
  559. my $cname = $chash->{NAME};
  560. foreach my $key ( keys %{$chash->{connections}} ) {
  561. my $hash = $chash->{connections}{$key};
  562. my $name = $hash->{NAME};
  563. plex_closeSocket($hash);
  564. delete($defs{$name});
  565. delete($chash->{connections}{$name});
  566. }
  567. plex_closeSocket($chash);
  568. delete($defs{$cname});
  569. delete $hash->{helper}{timelineListener};
  570. Log3 $name, 3, "$name: timeline listener stoped";
  571. }
  572. }
  573. sub
  574. plex_Undefine($$)
  575. {
  576. my ($hash, $arg) = @_;
  577. plex_stopTimelineListener($hash);
  578. plex_stopWebsockets($hash);
  579. plex_stopDiscovery($hash);
  580. delete $modules{plex}{defptr}{MASTER} if( $modules{plex}{defptr}{MASTER} == $hash ) ;
  581. delete $modules{plex}{defptr}{$hash->{server}} if( $hash->{server} );
  582. delete $modules{plex}{defptr}{$hash->{client}} if( $hash->{client} );
  583. delete $modules{plex}{defptr}{$hash->{machineIdentifier}} if( $hash->{machineIdentifier} );
  584. return undef;
  585. }
  586. sub
  587. plex_Set($$@)
  588. {
  589. my ($hash, $name, $cmd, @params) = @_;
  590. $hash->{".triggerUsed"} = 1;
  591. my $list = '';
  592. if( $hash->{'myPlex-servers'} ) {
  593. if( $cmd eq 'autocreate' ) {
  594. return "usage: autocreate <server>" if( !$params[0] );
  595. if( $hash->{'myPlex-servers'}{Server} ) {
  596. foreach my $entry (@{$hash->{'myPlex-servers'}{Server}}) {
  597. if( $entry->{localAddresses} eq $params[0] || $entry->{machineIdentifier} eq $params[0] ) {
  598. #Log 1, Dumper $entry;
  599. my $define = "$entry->{machineIdentifier} plex $entry->{address}";
  600. if( my $cmdret = CommandDefine(undef,$define) ) {
  601. return $cmdret;
  602. }
  603. my $chash = $defs{$entry->{machineIdentifier}};
  604. $chash->{token} = $entry->{accessToken};
  605. fhem( "setreading $entry->{machineIdentifier} .token $entry->{accessToken}" );
  606. return undef;
  607. }
  608. }
  609. }
  610. return "unknown server: $params[0]";
  611. }
  612. $list .= 'autocreate ';
  613. }
  614. if( my $entry = plex_serverOf($hash, $cmd, !$hash->{machineIdentifier}) ) {
  615. my @params = @params;
  616. $cmd = shift @params if( $cmd eq $entry->{address} );
  617. $cmd = shift @params if( $cmd eq $entry->{machineIdentifier} );
  618. my $ip = $entry->{address};
  619. if( $cmd eq 'refreshToken' ) {
  620. delete $hash->{token};
  621. plex_getToken($hash);
  622. return undef;
  623. }
  624. return "server $ip not online" if( $cmd ne '?' && !$entry->{online} );
  625. if( $cmd eq 'playlistCreate' ) {
  626. return "usage: playlistCreate <name>" if( !$params[0] );
  627. return undef;
  628. } elsif( $cmd eq 'playlistAdd' ) {
  629. my $server = plex_serverOf($hash, $params[0], 1);
  630. return "unknown server" if( !$server );
  631. shift @params if( $params[0] eq $server->{address} );
  632. my $playlist = shift(@params);
  633. return "usage: [<server>] playlistAdd <key> <keys>" if( !$params[0] );
  634. foreach my $key ( @params ) {
  635. plex_addToPlaylist($hash, $server, $playlist, $key);
  636. }
  637. return undef;
  638. } elsif( $cmd eq 'playlistRemove' ) {
  639. #my $server = plex_serverOf($hash, $params[0], 1);
  640. #return "unknown server" if( !$server );
  641. #shift @params if( $params[0] eq $server->{address} );
  642. #my $playlist = shift(@params);
  643. #return "usage: [<server>] playlistRemove <key> <keys>" if( !$params[0] );
  644. #foreach my $key ( @params ) {
  645. # plex_removeFromPlaylist($hash, $server, $playlist, $key);
  646. #}
  647. } elsif( $cmd eq 'unwatched' || $cmd eq 'watched' ) {
  648. return "usage: unwatched <keys>" if( !@params );
  649. $cmd = $cmd eq 'watched' ? 'scrobble' : 'unscrobble';
  650. foreach my $key ( @params ) {
  651. $key =~ s'^/library/metadata/'';
  652. plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/:/$cmd?key=$key&identifier=com.plexapp.plugins.library", $cmd );
  653. }
  654. return undef;
  655. } elsif( $cmd eq 'smapiRegister' ) {
  656. return "first use the httpPort attribute to configure a fixed http port" if( !AttrVal($name, 'httpPort', 0) );
  657. return plex_publishToSonos($name, 'PLEX', $params[0]);
  658. }
  659. $list .= 'playlistCreate playlistAdd playlistRemove ';
  660. $list .= 'smapiRegister ' if( $hash->{helper}{timelineListener} );
  661. $list .= 'unwatched watched ';
  662. }
  663. if( my $entry = plex_clientOf($hash, $cmd) ) {
  664. my @params = @params;
  665. $cmd = shift @params if( $cmd eq $entry->{address} );
  666. my $ip = $entry->{address};
  667. return "client $ip not online" if( $cmd ne '?' && !$entry->{online} );
  668. if( ($cmd eq 'playMedia' || $cmd eq 'resume' ) && $params[0] ) {
  669. my $server = plex_serverOf($hash, $params[0], 1);
  670. return "unknown server" if( !$server );
  671. shift @params if( $params[0] eq $server->{address} );
  672. my $offset = '';
  673. if( $cmd eq 'resume' ) {
  674. my $xml = plex_sendApiCmd( $hash, "http://$server->{address}:$server->{port}$params[0]", '#raw', 1 );
  675. if( $xml && $xml->{Video} ) {
  676. $offset = "&offset=$xml->{Video}[0]{viewOffset}" if( $xml->{Video}[0]{viewOffset} );
  677. }
  678. }
  679. plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/player/playback/playMedia?key=$params[0]&machineIdentifier=$server->{machineIdentifier}&address=$server->{address}&port=$server->{port}$offset", "playback" );
  680. return undef;
  681. } elsif( $cmd eq 'mirror' ) {
  682. return "mirror not supported" if( $hash->{protocolCapabilities} && $hash->{protocolCapabilities} !~ m/\bmirror\b/ );
  683. return "usage: mirror <key>" if( !$params[0] );
  684. my $server = plex_serverOf($hash, $params[0], 1);
  685. return "unknown server" if( !$server );
  686. shift @params if( $params[0] eq $server->{address} );
  687. plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/player/mirror/details?key=$params[0]&machineIdentifier=$server->{machineIdentifier}&address=$server->{address}&port=$server->{port}", "mirror" );
  688. return undef;
  689. } elsif( lc($cmd) eq 'play' && $params[0] ) {
  690. return "usage: play <key>" if( !$params[0] );
  691. my $server = plex_serverOf($hash, $params[0], 1);
  692. return "unknown server" if( !$server );
  693. shift @params if( $params[0] eq $server->{address} );
  694. return plex_play($hash, $entry, $server, $params[0] );
  695. return undef;
  696. } elsif( $cmd eq 'pause' || $cmd eq 'play' || $cmd eq 'resume' || $cmd eq 'stop'
  697. || $cmd eq 'skipNext' || $cmd eq 'skipPrevious' || $cmd eq 'stepBack' || $cmd eq 'stepForward' ) {
  698. return "$cmd not supported" if( $cmd ne 'pause' && $cmd ne 'play' && $cmd ne 'resume'
  699. && $hash->{controllable} && $hash->{controllable} !~ m/\b$cmd\b/ );
  700. if( ($cmd eq 'playMedia' || $cmd eq 'resume') && $hash->{STATE} eq 'stopped' ) {
  701. my $key = ReadingsVal($name,'key', undef);
  702. return 'no current media key' if( !$key );
  703. my $server = ReadingsVal($name,'server', undef);
  704. return 'no current server' if( !$server );
  705. my $entry = plex_serverOf($hash, $server, 1);
  706. return "unknown server: $server" if( !$entry );
  707. CommandSet( undef, "$hash->{NAME} $cmd $entry->{address} $key" );
  708. return undef;
  709. }
  710. if( $cmd eq 'pause' ) {
  711. return undef if( $hash->{STATE} !~ m/playing/ );
  712. } elsif( $cmd eq 'play' ) {
  713. return undef if( $hash->{STATE} =~ m/playing/ );
  714. } elsif( $cmd eq 'resume' ) {
  715. return undef if( $hash->{STATE} =~ m/playing/ );
  716. $cmd = 'play';
  717. }
  718. my $type = $params[0];
  719. $type = $hash->{currentMediaType} if( !$type );
  720. $type = "type=$type" if( $type );
  721. $type = "" if( !$type );
  722. plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/player/playback/$cmd?$type", "playback" );
  723. return undef;
  724. } elsif( $cmd eq 'seekTo' ) {
  725. return "$cmd not supported" if( $hash->{controllable} && $hash->{controllable} !~ m/\b$cmd\b/ );
  726. return "usage: $cmd <value>" if( !defined($params[0]) );
  727. $params[0] =~ s/[^\d]//g;
  728. my $type = $params[1];
  729. $type = $hash->{currentMediaType} if( !$type );
  730. $type = "type=$type" if( $type );
  731. $type = "" if( !$type );
  732. plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/player/playback/seekTo?$type&offset=$params[0]", "parameters" );
  733. return undef;
  734. } elsif( $cmd eq 'volume' || $cmd eq 'shuffle' || $cmd eq 'repeat' ) {
  735. return "$cmd not supported" if( $hash->{controllable} && $hash->{controllable} !~ m/\b$cmd\b/ );
  736. return "usage: $cmd <value>" if( !defined($params[0]) );
  737. $params[0] =~ s/[^\d]//g;
  738. return "usage: $cmd [0/1]" if( $cmd eq 'shuffle' && ($params[0] < 0 || $params[0] > 1) );
  739. return "usage: $cmd [0/1/2]" if( $cmd eq 'repeat' && ($params[0] < 0 || $params[0] > 2) );
  740. return "usage: $cmd [0-100]" if( $cmd eq 'volume' && ($params[0] < 0 || $params[0] > 100) );
  741. my $type = $params[1];
  742. $type = $hash->{currentMediaType} if( !$type );
  743. $type = "type=$type" if( $type );
  744. $type = "" if( !$type );
  745. plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/player/playback/setParameters?$type&$cmd=$params[0]", "parameters" );
  746. return undef;
  747. } elsif( $cmd eq 'home' || $cmd eq 'music' ) {
  748. plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/player/navigation/$cmd?", "navigation" );
  749. return undef;
  750. } elsif( $cmd eq 'unwatched' || $cmd eq 'watched' ) {
  751. my $key = ReadingsVal($name,'key', undef);
  752. return 'no current media key' if( !$key );
  753. my $server = ReadingsVal($name,'server', undef);
  754. return 'no current server' if( !$server );
  755. plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/player/playback/stop?type=video", "playback" ) if( $cmd == 'watched' );
  756. my $entry = plex_serverOf($hash, $server, 1);
  757. return "unknown server: $server" if( !$entry );
  758. $cmd = $cmd eq 'watched' ? 'scrobble' : 'unscrobble';
  759. $key =~ s'^/library/metadata/'';
  760. plex_sendApiCmd( $hash, "http://$entry->{address}:$entry->{port}/:/$cmd?key=$key&identifier=com.plexapp.plugins.library", $cmd );
  761. return undef;
  762. }
  763. $list .= 'playMedia ' if( !$hash->{controllable} || $hash->{controllable} =~ m/\bplayPause\b/ );
  764. $list .= 'play ' if( $hash->{protocolCapabilities} && $hash->{protocolCapabilities} =~ m/\bplayqueues\b/ );
  765. $list .= 'resume:noArg ' if( !$hash->{controllable} || $hash->{controllable} =~ m/\bplayPause\b/ );
  766. $list .= 'pause:noArg ' if( $hash->{controllable} && $hash->{controllable} =~ m/\bplayPause\b/ );;
  767. $list .= 'stop:noArg ' if( $hash->{controllable} && $hash->{controllable} =~ m/\bstop\b/ );;
  768. $list .= 'skipNext:noArg ' if( $hash->{controllable} && $hash->{controllable} =~ m/\bskipNext\b/ );;
  769. $list .= 'skipPrevious:noArg ' if( $hash->{controllable} && $hash->{controllable} =~ m/\bskipPrevious\b/ );;
  770. $list .= 'stepBack:noArg ' if( $hash->{controllable} && $hash->{controllable} =~ m/\bstepBack\b/ );;
  771. $list .= 'stepForward:noArg ' if( $hash->{controllable} && $hash->{controllable} =~ m/\bstepForward\b/ );;
  772. $list .= 'seekTo ' if( $hash->{controllable} && $hash->{controllable} =~ m/\bseekTo\b/ );;
  773. $list .= 'mirror ' if( !$hash->{controllable} || $hash->{controllable} =~ m/\bmirror\b/ );
  774. $list .= 'volume:slider,0,1,100 ' if( $hash->{controllable} && $hash->{controllable} =~ m/\bvolume\b/ );
  775. $list .= 'repeat ' if( $hash->{controllable} && $hash->{controllable} =~ m/\brepeat\b/ );
  776. $list .= 'shuffle ' if( $hash->{controllable} && $hash->{controllable} =~ m/\bshuffle\b/ );
  777. $list .= 'home:noArg music:noArg ';
  778. $list .= 'unwatched:noArg watched:noArg ';
  779. }
  780. if( $modules{plex}{defptr}{MASTER} && $hash == $modules{plex}{defptr}{MASTER} ) {
  781. if( $cmd eq 'restartDiscovery' ) {
  782. plex_startDiscovery($hash);
  783. return undef;
  784. } elsif( $cmd eq 'subscribe' ) {
  785. return 'usage: subscribe <id|ip>' if( !$params[0] );
  786. my $client = plex_clientOf( $hash, $params[0] );
  787. return "no client found for $params[0]" if( !$client );
  788. plex_sendSubscription($hash->{helper}{timelineListener}, $client->{address});
  789. return undef;
  790. } elsif( $cmd eq 'unsubscribe' ) {
  791. return 'usage: unsubscribe <id|ip>' if( !$params[0] );
  792. my $client = plex_clientOf( $hash, $params[0] );
  793. return "no client found for $params[0]" if( !$client );
  794. plex_removeSubscription($hash->{helper}{timelineListener}, $client->{address});
  795. return undef;
  796. } elsif( $cmd eq 'offline' ) {
  797. return 'usage: offline <id|ip>' if( !$params[0] );
  798. my $client = plex_clientOf( $hash, $params[0] );
  799. return "no client found for $params[0]" if( !$client );
  800. $client->{online} = 1;
  801. plex_disappeared($hash, 'client', $client->{address});
  802. return undef;
  803. } elsif( $cmd eq 'online' ) {
  804. return 'usage: online <id|ip>' if( !$params[0] );
  805. my $client = plex_clientOf( $hash, $params[0] );
  806. return "no client found for $params[0]" if( !$client );
  807. $client->{online} = 0;
  808. plex_discovered($hash, 'client', $client->{address}, $client);
  809. return undef;
  810. } elsif( $cmd eq 'showAccount' ) {
  811. my $user = AttrVal($name, 'user', undef);
  812. my $password = AttrVal($name, 'password', undef);
  813. return 'no user set' if( !$user );
  814. return 'no password set' if( !$password );
  815. $user = plex_decrypt( $user );
  816. $password = plex_decrypt( $password );
  817. return "$user: $password";
  818. } elsif( $cmd eq 'refreshToken' ) {
  819. delete $hash->{token};
  820. plex_getToken($hash);
  821. return undef;
  822. }
  823. $list .= 'restartDiscovery:noArg subscribe unsubscribe showAccount:noArg ';
  824. }
  825. $list =~ s/ $//;
  826. return "Unknown argument $cmd, choose one of $list";
  827. }
  828. sub
  829. plex_deviceList($$)
  830. {
  831. my ($hash, $type) = @_;
  832. my $ret = '';
  833. my $entries = $hash->{$type};
  834. $ret .= "$type from discovery:\n";
  835. $ret .= sprintf( "%16s %19s %4s %-23s %s\n", 'ip', 'updatedAt', 'onl.', 'name', 'machineIdentifier' );
  836. foreach my $ip ( keys %{$entries} ) {
  837. my $entry = $entries->{$ip};
  838. $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} );
  839. }
  840. if( $type eq 'servers' && $hash->{'myPlex-servers'} ) {
  841. $ret .= "\n";
  842. $ret .= "$type from myPlex:\n";
  843. if( $hash->{'myPlex-servers'}{Server} ) {
  844. $ret .= sprintf( "%16s %19s %-23s %1s %s\n", 'ip', 'updatedAt', 'name', 'o', 'machineIdentifier' );
  845. foreach my $entry (@{$hash->{'myPlex-servers'}{Server}}) {
  846. #next if( !$entry->{owned} );
  847. $entry->{owned} = 0 if( !defined($entry->{owned}) );
  848. $entry->{localAddresses} = '' if( !$entry->{localAddresses} );
  849. $entry->{address} = '' if( !$entry->{address} );
  850. $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} );
  851. }
  852. }
  853. }
  854. if( $type eq 'clients' && $hash->{'myPlex-devices'} ) {
  855. $ret .= "\n";
  856. $ret .= "$type from myPlex:\n";
  857. if( $hash->{'myPlex-devices'}{Device} ) {
  858. $ret .= sprintf( "%16s %19s %-25s %-20s %-40s %s\n", 'ip', 'lastSeenAt', 'name', 'product', 'clientIdentifier', 'provides' );
  859. foreach my $entry (@{$hash->{'myPlex-devices'}{Device}}) {
  860. next if( !$entry->{provides} );
  861. #next if( !$entry->{localAddresses} );
  862. $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} );
  863. }
  864. }
  865. }
  866. return $ret;
  867. }
  868. sub
  869. plex_makeLink($$$$;$)
  870. {
  871. my ($hash, $cmd, $parentSection, $key, $txt) = @_;
  872. return $txt if( !$key );
  873. $txt = $key if( !$txt );
  874. if( defined($parentSection) && $parentSection eq '' && $key !~ '^/' ) {
  875. $cmd = "get $hash->{NAME} $cmd /library/sections/$key";
  876. } elsif( defined($parentSection) && $key !~ '^/' ) {
  877. $cmd = "get $hash->{NAME} $cmd $parentSection/$key";
  878. } elsif( $key !~ '^/' ) {
  879. $cmd = "get $hash->{NAME} $cmd /library/metadata/$key";
  880. } else {
  881. $cmd = "get $hash->{NAME} $cmd $key";
  882. }
  883. return $txt if( !$FW_ME );
  884. return "<a style=\"cursor:pointer\" onClick=\"FW_cmd(\\\'$FW_ME$FW_subdir?XHR=1&cmd=$cmd\\\')\">$txt</a>";
  885. }
  886. sub
  887. plex_makeImage($$$$)
  888. {
  889. my ($hash, $server, $url, $size) = @_;
  890. return '' if( !$url );
  891. my $token = $server->{accessToken};
  892. $token = $hash->{token} if( !$token );
  893. my $ret .= "<img src=\"http://$server->{address}:$server->{port}/photo/:/transcode?X-Plex-Token=$token&url=".
  894. urlEncode("127.0.0.1:32400$url?X-Plex-Token=$token")
  895. ."&width=$size&height=$size\">\n";
  896. return $ret;
  897. }
  898. sub
  899. plex_mediaList2($$$$;$)
  900. {
  901. my ($hash, $type, $xml, $items, $cmd) = @_;
  902. if( $items ) {
  903. if( 0 && !$xml->{sortAsc} ) {
  904. my @items;
  905. if( $xml->{Track} ) {
  906. @items = sort { $a->{index} <=> $b->{index} } @{$items};
  907. } else {
  908. @items = sort { $a->{title} cmp $b->{title} } @{$items};
  909. }
  910. $items = \@items;
  911. }
  912. }
  913. my $ret;
  914. if( $type eq 'Directory' ) {
  915. #Log 1, Dumper $items;
  916. $ret .= "\n" if( $ret );
  917. $ret .= "$type\n";
  918. $ret .= sprintf( "%-35s %-10s %s\n", 'key', 'type', 'title' );
  919. foreach my $item (@{$items}) {
  920. $item->{type} = '' if( !$item->{type} );
  921. $item->{title} = encode('UTF-8', $item->{title});
  922. $ret .= plex_makeLink($hash, 'ls', $xml->{parentSection}, $item->{key}, sprintf( "%-35s %-10s %s", $item->{key}, $item->{type}, $item->{title} ) );
  923. $ret .= " ($item->{year})" if( $item->{year} );
  924. $ret .= "\n";
  925. }
  926. }
  927. if( $type eq 'Playlist' ) {
  928. $ret .= "\n" if( $ret );
  929. $ret .= "$type\n";
  930. $ret .= sprintf( "%-35s %-10s %s\n", 'key', 'type', 'title' );
  931. foreach my $item (@{$items}) {
  932. $item->{type} = '' if( !$item->{type} );
  933. $item->{title} = encode('UTF-8', $item->{title});
  934. $ret .= plex_makeLink($hash, 'ls', $xml->{parentSection}, $item->{key}, sprintf( "%-35s %-10s %s\n", $item->{key}, $item->{type}, $item->{title} ) );
  935. #$ret .= plex_makeImage($hash, $server, $xml->{composite}, 100);
  936. }
  937. }
  938. if( $type eq 'Video' ) {
  939. $ret .= "\n" if( $ret );
  940. $ret .= "$type\n";
  941. $ret .= sprintf( "%-35s %-10s nr %s\n", 'key', 'type', 'title' );
  942. foreach my $item (@{$items}) {
  943. $item->{title} = encode('UTF-8', $item->{title});
  944. if( defined($item->{index}) ) {
  945. $ret .= plex_makeLink($hash, 'detail', $xml->{parentSection}, $item->{key}, sprintf( "%-35s %-10s %3i %s", $item->{key}, $item->{type}, $item->{index}, $item->{title} ) );
  946. $ret .= plex_makeLink($hash,'detail', undef, $item->{grandparentKey}, " ($item->{grandparentTitle}" ) if( $item->{grandparentTitle} );
  947. #$ret .= " ($item->{year})" if( $item->{year} );
  948. $ret .= sprintf(": S%02iE%02i",$item->{parentIndex}, $item->{index} ) if( $item->{parentIndex} );
  949. $ret .= ")" if( $item->{grandparentTitle} );
  950. } else {
  951. $ret .= plex_makeLink($hash,'detail', $xml->{parentSection}, $item->{key}, sprintf( "%-35s %-10s %s", $item->{key}, $item->{type}, $item->{title} ) );
  952. }
  953. if( $cmd && $cmd eq 'files'
  954. && $item->{Media} && $item->{Media}[0]{Part} ) {
  955. $ret .= " ($item->{Media}[0]{Part}[0]{file})";
  956. }
  957. $ret .= "\n";
  958. }
  959. }
  960. if( $type eq 'Track' ) {
  961. $ret .= "\n" if( $ret );
  962. $ret .= "$type\n";
  963. $ret .= sprintf( "%-35s %-10s nr %s\n", 'key', 'type', 'title' );
  964. foreach my $item (@{$items}) {
  965. $item->{title} = encode('UTF-8', $item->{title});
  966. $ret .= sprintf( "%-35s %-10s %3i %s\n", $item->{key}, $item->{type}, $item->{index}, $item->{title} );
  967. }
  968. }
  969. return $ret;
  970. }
  971. sub
  972. plex_mediaList($$$;$)
  973. {
  974. my ($hash, $server, $xml, $cmd) = @_;
  975. #Log 1, Dumper $xml;
  976. return $xml if( ref($xml) ne 'HASH' );
  977. $xml->{librarySectionTitle} = encode('UTF-8', $xml->{librarySectionTitle}) if( $xml->{librarySectionTitle} );
  978. $xml->{title} = encode('UTF-8', $xml->{title}) if( $xml->{title} );
  979. $xml->{title1} = encode('UTF-8', $xml->{title1}) if( $xml->{title1} );
  980. $xml->{title2} = encode('UTF-8', $xml->{title2}) if( $xml->{title2} );
  981. $xml->{title3} = encode('UTF-8', $xml->{title3}) if( $xml->{title3} );
  982. my $ret = '';
  983. $ret .= plex_makeImage($hash, $server, $xml->{thumb}, 100);
  984. $ret .= plex_makeImage($hash, $server, $xml->{composite}, 100);
  985. $ret .= "$xml->{librarySectionTitle}: " if( $xml->{librarySectionTitle} );
  986. $ret .= plex_makeLink($hash, 'detail', undef, $xml->{ratingKey}, "$xml->{title} ") if( $xml->{title} );
  987. $ret .= plex_makeLink($hash, 'detail', undef, $xml->{grandparentRatingKey}, "$xml->{title1} ") if( $xml->{title1} );
  988. $ret .= plex_makeLink($hash, 'detail', undef, $xml->{key}, "; $xml->{title2} ") if( $xml->{title2} );
  989. $ret .= "; $xml->{title3} " if( $xml->{title3} );
  990. $ret .= "\n";
  991. $ret .= plex_mediaList2( $hash, 'Directory', $xml, $xml->{Directory} ) if( $xml->{Directory} );
  992. $ret .= plex_mediaList2( $hash, 'Playlist', $xml, $xml->{Playlist} ) if( $xml->{Playlist} );
  993. $ret .= plex_mediaList2( $hash, 'Video', $xml, $xml->{Video}, $cmd ) if( $xml->{Video} );
  994. $ret .= plex_mediaList2( $hash, 'Track', $xml, $xml->{Track} ) if( $xml->{Track} );
  995. if( !$xml->{Directory} && !$xml->{Playlist} && !$xml->{Video} && !$xml->{Track} ) {
  996. return $xml->{head}[0]{title}[0] if( ref $xml->{head} eq 'ARRAY' && ref $xml->{head}[0]{title} eq 'ARRAY' );
  997. return "unknown media type";
  998. }
  999. return $ret;
  1000. }
  1001. sub
  1002. plex_mediaDetail2($$$$)
  1003. {
  1004. my ($hash, $server, $xml, $items) = @_;
  1005. #Log 1, Dumper $xml;
  1006. if( $items ) {
  1007. if( 0 && !$xml->{sortAsc} ) {
  1008. my @items = sort { $a->{index} <=> $b->{index} } @{$items};
  1009. #my @items = sort { $a->{title} cmp $b->{title} } @{$items};
  1010. $items = \@items;
  1011. }
  1012. }
  1013. $xml->{viewGroup} = encode('UTF-8', $xml->{viewGroup}) if( $xml->{viewGroup} );
  1014. my $ret = '';
  1015. foreach my $item (@{$items}) {
  1016. $item->{grandparentTitle} = encode('UTF-8', $item->{grandparentTitle}) if( $item->{grandparentTitle} );
  1017. $item->{parentTitle} = encode('UTF-8', $item->{parentTitle}) if( $item->{parentTitle} );
  1018. $item->{title} = encode('UTF-8', $item->{title}) if( $item->{title} );
  1019. $item->{summary} = encode('UTF-8', $item->{summary}) if( $item->{summary} );
  1020. $ret .= "\n" if( $ret && (!$xml->{viewGroup} || ($xml->{viewGroup} ne 'track' && $xml->{viewGroup} ne 'secondary') ) );
  1021. if( $item->{type} eq 'playlist' ) {
  1022. $ret .= sprintf( "%s ", $item->{title} ) if( $item->{title} );
  1023. $ret .= "\n";
  1024. $ret .= plex_makeImage($hash, $server, $item->{composite}, 250);
  1025. $ret .= "\n";
  1026. $ret .= sprintf( "%s ", $item->{playlistType} ) if( $item->{playlistType} );
  1027. $ret .= sprintf( "%s ", plex_timestamp2date($item->{addedAt}) ) if( $item->{addedAt} );
  1028. $ret .= sprintf( "items: %i ", $item->{leafCount} ) if( $item->{leafCount} && $item->{leafCount} > 1 );
  1029. $ret .= sprintf( "viewCount: %i ", $item->{viewCount} ) if( $item->{viewCount} );
  1030. $ret .= "\n";
  1031. } elsif( $item->{type} eq 'album' || $item->{type} eq 'artist' || $item->{type} eq 'show' || $item->{type} eq 'season' ) {
  1032. $ret .= plex_makeLink($hash, 'detail', undef, $item->{grandparentRatingKey}, "$item->{grandparentTitle}: ") if( $item->{grandparentTitle} );
  1033. $ret .= plex_makeLink($hash, 'detail', undef, $item->{parentRatingKey}, "$item->{parentTitle}: ") if( $item->{parentTitle} );
  1034. $ret .= sprintf( "%s ", $item->{title} ) if( $item->{title} );
  1035. $ret .= sprintf("(S%02iE%02i)",$item->{parentIndex}, $item->{index} ) if( $item->{parentIndex} && $item->{type} ne 'season' );
  1036. #$ret .= sprintf("(S%02i)", $item->{index} ) if( $item->{index} && $item->{type} eq 'season' );
  1037. $ret .= "\n";
  1038. $ret .= plex_makeImage($hash, $server, $item->{thumb}, 250);
  1039. $ret .= "\n";
  1040. if( $item->{Genre} ) {
  1041. foreach my $genre ( @{$item->{Genre}}) {
  1042. $ret .= sprintf( "%s ", $genre->{tag} ) if( $genre->{tag} );
  1043. }
  1044. $ret .= ' ';
  1045. }
  1046. $ret .= sprintf( "%s ", $item->{contentRating} ) if( $item->{contentRating} );
  1047. $ret .= sprintf( "%s ", $item->{rating} ) if( $item->{rating} );
  1048. $ret .= sprintf( "%i ", $item->{year} ) if( $item->{year} );
  1049. $ret .= sprintf( "%s ", plex_timestamp2date($item->{addedAt}) ) if( $item->{addedAt} );
  1050. $ret .= sprintf( "items: %i ", $item->{leafCount} ) if( $item->{leafCount} && $item->{leafCount} > 1 );
  1051. $ret .= sprintf( "viewCount: %i ", $item->{viewCount} ) if( $item->{viewCount} );
  1052. $ret .= "\n";
  1053. } elsif( $item->{type} eq 'track' ) {
  1054. $ret .= sprintf("(Disk %02i Track %02i) ",$item->{parentIndex}, $item->{index} ) if( $item->{parentIndex} );
  1055. $ret .= sprintf("%2i ",$item->{index}, $item->{index} ) if( !$item->{parentIndex} );
  1056. $ret .= plex_sec2hms($item->{duration}/1000);
  1057. $ret .= " ";
  1058. $ret .= sprintf( "%s: ", $item->{grandparentTitle} ) if( !$xml->{title1} && $item->{grandparentTitle} );
  1059. $ret .= sprintf( "%s: ", $item->{parentTitle} ) if( !$xml->{title2} && $item->{parentTitle} );
  1060. $ret .= sprintf( "%s ", $item->{title} ) if( $item->{title} );
  1061. #$ret .= "\n";
  1062. $ret .= "\n";
  1063. $ret .= plex_makeImage($hash, $server, $item->{thumb}, 250);
  1064. #$ret .= "\n";
  1065. $ret .= sprintf( "%s ", $item->{contentRating} ) if( $item->{contentRating} );
  1066. $ret .= sprintf( "%i ", $item->{year} ) if( $item->{year} );
  1067. #$ret .= sprintf( "%s ", plex_timestamp2date($item->{addedAt}) ) if( $item->{addedAt} );
  1068. #$ret .= sprintf( "viewCount: %i ", $item->{viewCount} ) if( $item->{viewCount} );
  1069. #$ret .= "\n";
  1070. } elsif( $item->{type} eq 'episode' || $item->{type} eq 'movie' ) {
  1071. $ret .= plex_makeLink($hash, 'detail', undef, $item->{grandparentRatingKey}, "$item->{grandparentTitle}: ") if( $item->{grandparentTitle} );
  1072. $ret .= plex_makeLink($hash, 'detail', undef, $item->{parentKey}, "; $item->{parentTitle} ") if( $item->{parentTitle} );
  1073. $ret .= sprintf( "%s ", $item->{title} ) if( $item->{title} );
  1074. $ret .= sprintf("(S%02iE%02i)",$item->{parentIndex}, $item->{index} ) if( defined($item->{parentIndex}) );
  1075. $ret .= sprintf("(Episode %02i)",$item->{index}, $item->{index} ) if( !defined($item->{parentIndex}) && $item->{index} );
  1076. $ret .= " ";
  1077. $ret .= plex_sec2hms($item->{duration}/1000);
  1078. $ret .= "\n";
  1079. $ret .= plex_makeImage($hash, $server, $item->{thumb}, 250);
  1080. $ret .= "\n";
  1081. $ret .= sprintf( "%s ", $item->{contentRating} ) if( $item->{contentRating} );
  1082. $ret .= sprintf( "%s ", $item->{rating} ) if( $item->{rating} );
  1083. $ret .= sprintf( "%i ", $item->{year} ) if( $item->{year} );
  1084. $ret .= sprintf( "%s ", plex_timestamp2date($item->{addedAt}) ) if( $item->{addedAt} );
  1085. $ret .= sprintf( "viewCount: %i ", $item->{viewCount} ) if( $item->{viewCount} );
  1086. $ret .= "\n";
  1087. } elsif( $item->{type} ) {
  1088. $ret .= "unknown item type: $item->{type}\n";
  1089. } else {
  1090. $ret .= sprintf( "%-35s %-10s %s\n", $item->{key}, $item->{title} );
  1091. }
  1092. if( !$xml->{viewGroup} || ($xml->{viewGroup} ne 'track' && $xml->{viewGroup} ne 'secondary') ) {
  1093. if( my $mhash = $modules{plex}{defptr}{MASTER} ) {
  1094. if( my $clients = $mhash->{clients} ) {
  1095. $ret .= "\nplay: ";
  1096. foreach my $ip ( keys %{$clients} ) {
  1097. my $client = $clients->{$ip};
  1098. next if( !$client->{online} );
  1099. my $cmd = 'play';
  1100. my $key = $item->{key};
  1101. $key =~ s/.children$//;
  1102. $cmd = "set $hash->{NAME} $client->{address} $cmd $key";
  1103. $ret .= "<a style=\"cursor:pointer\" onClick=\"FW_cmd(\\\'$FW_ME$FW_subdir?XHR=1&cmd=$cmd\\\')\">$ip</a> ";
  1104. }
  1105. $ret .= "\n\n";
  1106. }
  1107. }
  1108. }
  1109. $ret .= $item->{summary} ."\n" if( $item->{summary} );
  1110. }
  1111. return $ret;
  1112. }
  1113. sub
  1114. plex_mediaDetail($$$)
  1115. {
  1116. my ($hash, $server, $xml) = @_;
  1117. return $xml if( ref($xml) ne 'HASH' );
  1118. $xml->{title} = encode('UTF-8', $xml->{title}) if( $xml->{title} );
  1119. $xml->{title1} = encode('UTF-8', $xml->{title1}) if( $xml->{title1} );
  1120. $xml->{title2} = encode('UTF-8', $xml->{title2}) if( $xml->{title2} );
  1121. $xml->{summary} = encode('UTF-8', $xml->{summary}) if( $xml->{summary} );
  1122. #Log 1, Dumper $xml;
  1123. my $ret = '';
  1124. $ret .= plex_makeImage($hash, $server, $xml->{thumb}, 250);
  1125. $ret .= plex_makeLink($hash, 'detail', undef, $xml->{ratingKey}, "$xml->{title} ") if( $xml->{title} );
  1126. $ret .= sprintf( "%s: ", $xml->{title1} ) if( $xml->{title1} );
  1127. $ret .= sprintf( "%s: ", $xml->{title2} ) if( $xml->{title2} );
  1128. $ret .= sprintf( "(%s)\n", $xml->{parentYear} ) if( $xml->{parentYear} );
  1129. $ret .= $xml->{summary} ."\n" if( $xml->{summary} );
  1130. $ret .= plex_mediaDetail2( $hash, $server, $xml, $xml->{Directory} ) if( $xml->{Directory} );
  1131. $ret .= plex_mediaDetail2( $hash, $server, $xml, $xml->{Playlist} ) if( $xml->{Playlist} );
  1132. $ret .= plex_mediaDetail2( $hash, $server, $xml, $xml->{Video} ) if( $xml->{Video} );
  1133. $ret .= plex_mediaDetail2( $hash, $server, $xml, $xml->{Track} ) if( $xml->{Track} );
  1134. if( !$xml->{Directory} && !$xml->{Playlist} && !$xml->{Video} && !$xml->{Track} ) {
  1135. Log 1, Dumper $xml;
  1136. return "unknown media type";
  1137. }
  1138. return $ret;
  1139. }
  1140. sub
  1141. plex_Get($$@)
  1142. {
  1143. my ($hash, $name, $cmd, @params) = @_;
  1144. my $list = '';
  1145. if( my $hash = $modules{plex}{defptr}{MASTER} ) {
  1146. if( $cmd eq 'servers' || $cmd eq 'clients' ) {
  1147. if( my $entry = plex_serverOf($hash, $cmd, !$hash->{machineIdentifier}) ) {
  1148. plex_sendApiCmd( $hash, "http://$entry->{address}:$entry->{port}/clients", "clients" );
  1149. }
  1150. return plex_deviceList($hash, $cmd );
  1151. } elsif( $cmd eq 'pin' ) {
  1152. return plex_getPinForToken($hash);
  1153. }
  1154. $list .= 'clients:noArg servers:noArg pin:noArg ';
  1155. }
  1156. if( my $entry = plex_serverOf($hash, $cmd, !$hash->{machineIdentifier}) ) {
  1157. my @params = @params;
  1158. $cmd = shift @params if( $cmd eq $entry->{address} );
  1159. $cmd = shift @params if( $cmd eq $entry->{machineIdentifier} );
  1160. if( $cmd eq 'servers' ) {
  1161. return plex_deviceList($hash, 'servers' );
  1162. } elsif( $cmd eq 'clients' ) {
  1163. return plex_deviceList($hash, 'clients' );
  1164. } elsif( $cmd eq 'pin' ) {
  1165. return plex_getPinForToken($hash);
  1166. }
  1167. my $ip = $entry->{address};
  1168. return "server $ip not online" if( $cmd ne '?' && !$entry->{online} );
  1169. my $param = shift( @params );
  1170. if( !$param ) {
  1171. $param = '';
  1172. }
  1173. if( $cmd eq 'sections' || $cmd eq 'ls' || $cmd eq 'files' ) {
  1174. $param = "/$param" if( $param && $param !~ '^/' );
  1175. my $ret;
  1176. if( $param =~ m'/playlists' ) {
  1177. $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}$param", 'sections', $hash->{CL} || 1, $entry->{accessToken} );
  1178. } elsif( $param =~ m'^/library' ) {
  1179. $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}$param", "sections:$param $cmd", $hash->{CL} || 1, $entry->{accessToken} );
  1180. } else {
  1181. $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/library/sections$param", "sections:$param $cmd", $hash->{CL} || 1, $entry->{accessToken} );
  1182. }
  1183. return $ret;
  1184. } elsif( $cmd eq 'search' ) {
  1185. return "usage: search <keywords>" if( !$param );
  1186. $param .= ' '. join( ' ', @params ) if( @params );
  1187. $param = urlEncode( $param );
  1188. my $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/search?query=$param", 'search', $hash->{CL} || 1 );
  1189. return $ret;
  1190. } elsif( $cmd eq 'playlists' ) {
  1191. $param = "/$param" if( $param && $param !~ '^/' );
  1192. $param = '' if( !$param );
  1193. $param =~ s'^/playlists'';
  1194. my $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/playlists$param", "playlists", $hash->{CL} || 1 );
  1195. return $ret;
  1196. } elsif( $cmd eq 'sessions' ) {
  1197. my $xml = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/status/sessions", 'sessions', 1 );
  1198. return undef if( !$xml );
  1199. return Dumper $xml;
  1200. } elsif( $cmd eq 'identity' ) {
  1201. my $xml = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/identity", 'identity', 1 );
  1202. return undef if( !$xml );
  1203. return Dumper $xml;
  1204. } elsif( $cmd eq 'detail' ) {
  1205. return "usage: detail <key>" if( !$param );
  1206. my $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}$param", 'detail', $hash->{CL} || 1 );
  1207. return $ret;
  1208. } elsif( lc($cmd) eq 'ondeck' ) {
  1209. my $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/library/onDeck", 'onDeck', $hash->{CL} || 1 );
  1210. return $ret;
  1211. } elsif( lc($cmd) eq 'recentlyadded' ) {
  1212. my $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/library/recentlyAdded", 'recentlyAdded', $hash->{CL} || 1 );
  1213. return $ret;
  1214. } elsif( $cmd eq 'm3u' || $cmd eq 'pls' ) {
  1215. return "usage: $cmd <key>" if( !$param );
  1216. $param = "/library/metadata/$param" if( $param !~ '^/' );
  1217. my $ret;
  1218. if( $param =~ m'/playlists' ) {
  1219. $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}$param", "#$cmd:$entry->{machineIdentifier}", $hash->{CL} || 1 );
  1220. } else {
  1221. $ret = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}$param", "#$cmd:$entry->{machineIdentifier}", $hash->{CL} || 1 );
  1222. }
  1223. return $ret;
  1224. }
  1225. $list .= 'identity:noArg ls files search sessions:noArg detail onDeck:noArg recentlyAdded:noArg playlists:noArg ';
  1226. $list .= 'servers:noArg pin:noArg ' if( $list !~ m/\bservers\b/ );
  1227. }
  1228. if( my $entry = plex_clientOf($hash, $cmd) ) {
  1229. my @params = @params;
  1230. $cmd = shift @params if( $cmd eq $entry->{address} );
  1231. $cmd = shift @params if( $cmd eq $entry->{machineIdentifier} );
  1232. my $key = ReadingsVal($name,'key', undef);
  1233. my $server = ReadingsVal($name,'server', undef);
  1234. if( $cmd eq 'detail' ) {
  1235. return 'no current media key' if( !$key );
  1236. return 'no current server' if( !$server );
  1237. my $entry = plex_serverOf($hash, $server, 1);
  1238. return "unknown server: $server" if( !$entry );
  1239. my $ret = plex_sendApiCmd( $hash, "http://$entry->{address}:$entry->{port}$key", 'detail', $hash->{CL} || 1 );
  1240. return $ret;
  1241. }
  1242. my $ip = $entry->{address};
  1243. return "client $ip not online" if( $cmd ne '?' && !$entry->{online} );
  1244. if( $cmd eq 'resources' ) {
  1245. my $xml = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/resources", 'resources', 1 );
  1246. return undef if( !$xml );
  1247. return Dumper $xml;
  1248. } elsif( $cmd eq 'timeline' ) {
  1249. my $xml = plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/player/timeline/poll?&wait=0", 'timeline', 1 );
  1250. return undef if( !$xml );
  1251. return Dumper $xml;
  1252. }
  1253. $list .= 'detail:noArg ';
  1254. $list .= 'resources:noArg timeline:noArg ';
  1255. }
  1256. $list =~ s/ $//;
  1257. return "Unknown argument $cmd, choose one of $list";
  1258. }
  1259. sub
  1260. plex_encrypt($)
  1261. {
  1262. my ($decoded) = @_;
  1263. my $key = getUniqueId();
  1264. my $encoded;
  1265. return $decoded if( $decoded =~ /^crypt:(.*)/ );
  1266. for my $char (split //, $decoded) {
  1267. my $encode = chop($key);
  1268. $encoded .= sprintf("%.2x",ord($char)^ord($encode));
  1269. $key = $encode.$key;
  1270. }
  1271. return 'crypt:'. $encoded;
  1272. }
  1273. sub
  1274. plex_decrypt($)
  1275. {
  1276. my ($encoded) = @_;
  1277. my $key = getUniqueId();
  1278. my $decoded;
  1279. $encoded = $1 if( $encoded =~ /^crypt:(.*)/ );
  1280. for my $char (map { pack('C', hex($_)) } ($encoded =~ /(..)/g)) {
  1281. my $decode = chop($key);
  1282. $decoded .= chr(ord($char)^ord($decode));
  1283. $key = $decode.$key;
  1284. }
  1285. return $decoded;
  1286. }
  1287. sub
  1288. plex_Attr($$$)
  1289. {
  1290. my ($cmd, $name, $attrName, $attrVal) = @_;
  1291. my $orig = $attrVal;
  1292. $attrVal = int($attrVal) if($attrName eq "interval");
  1293. $attrVal = 60 if($attrName eq "interval" && $attrVal < 60 && $attrVal != 0);
  1294. my $hash = $defs{$name};
  1295. if( $attrName eq 'disable' ) {
  1296. if( $cmd eq "set" && $attrVal ) {
  1297. plex_stopTimelineListener($hash);
  1298. plex_stopWebsockets($hash);
  1299. plex_stopDiscovery($hash);
  1300. foreach my $ip ( keys %{$hash->{clients}} ) {
  1301. $hash->{clients}{$ip}{online} = 0;
  1302. }
  1303. readingsSingleUpdate($hash, 'state', 'disabled', 1 );
  1304. } else {
  1305. readingsSingleUpdate($hash, 'state', 'running', 1 );
  1306. $attr{$name}{$attrName} = 0;
  1307. plex_startDiscovery($hash);
  1308. plex_startTimelineListener($hash);
  1309. }
  1310. } elsif( $attrName eq 'httpPort' ) {
  1311. plex_stopTimelineListener($hash);
  1312. plex_startTimelineListener($hash);
  1313. } elsif( $attrName eq 'responder' ) {
  1314. if( $cmd eq "set" && $attrVal ) {
  1315. $attr{$name}{$attrName} = 1;
  1316. plex_startDiscovery($hash);
  1317. } else {
  1318. $attr{$name}{$attrName} = 0;
  1319. plex_startDiscovery($hash);
  1320. }
  1321. } elsif( $attrName eq 'user' ) {
  1322. if( $cmd eq "set" && $attrVal ) {
  1323. $attrVal = plex_encrypt($attrVal);
  1324. if( $attr{$name}{'user'} && $attr{$name}{'password'} ) {
  1325. delete $hash->{token};
  1326. plex_getToken($hash);
  1327. }
  1328. }
  1329. } elsif( $attrName eq 'password' ) {
  1330. if( $cmd eq "set" && $attrVal ) {
  1331. $attrVal = plex_encrypt($attrVal);
  1332. if( $attr{$name}{'user'} && $attr{$name}{'password'} ) {
  1333. delete $hash->{token};
  1334. plex_getToken($hash);
  1335. }
  1336. }
  1337. } elsif( $attrName eq 'fhemIP' ) {
  1338. if( $cmd eq "set" && $attrVal ) {
  1339. $hash->{fhemIP} = $attrVal;
  1340. } else {
  1341. $hash->{fhemIP} = plex_getLocalIP();
  1342. }
  1343. }
  1344. if( $cmd eq "set" ) {
  1345. if( $attrVal && $orig ne $attrVal ) {
  1346. $attr{$name}{$attrName} = $attrVal;
  1347. return $attrName ." set to ". $attrVal if( $init_done );
  1348. }
  1349. }
  1350. return;
  1351. }
  1352. sub
  1353. plex_getToken($)
  1354. {
  1355. my ($hash) = @_;
  1356. my $name = $hash->{NAME};
  1357. return $hash->{token} if( $hash->{token} );
  1358. my $user = AttrVal($name, 'user', undef);
  1359. my $password = AttrVal($name, 'password', undef);
  1360. return '' if( !$user );
  1361. return '' if( !$password );
  1362. $user = plex_decrypt( $user );
  1363. $password = plex_decrypt( $password );
  1364. my $url = 'https://plex.tv/users/sign_in.xml';
  1365. Log3 $name, 4, "$name: requesting $url";
  1366. my $param = {
  1367. url => $url,
  1368. method => 'POST',
  1369. timeout => 5,
  1370. noshutdown => 0,
  1371. hash => $hash,
  1372. key => 'token',
  1373. header => { 'X-Plex-Provides' => 'controller',
  1374. 'X-Plex-Client-Identifier' => $hash->{id},
  1375. 'X-Plex-Platform' => $^O,
  1376. #'X-Plex-Device' => 'FHEM',
  1377. 'X-Plex-Device-Name' => $hash->{fhemHostname},
  1378. 'X-Plex-Product' => 'FHEM',
  1379. 'X-Plex-Version' => '0.0', },
  1380. data => { 'user[login]' => $user, 'user[password]' => $password },
  1381. };
  1382. $param->{callback} = \&plex_parseHttpAnswer;
  1383. HttpUtils_NonblockingGet( $param );
  1384. return undef;
  1385. }
  1386. sub
  1387. plex_getPinForToken($)
  1388. {
  1389. my ($hash) = @_;
  1390. my $name = $hash->{NAME};
  1391. RemoveInternalTimer($hash, "plex_getTokenOfPin");
  1392. my $url = 'https://plex.tv/pins.xml';
  1393. Log3 $name, 4, "$name: requesting $url";
  1394. my $param = {
  1395. url => $url,
  1396. method => 'POST',
  1397. timeout => 5,
  1398. noshutdown => 0,
  1399. hash => $hash,
  1400. key => 'getPinForToken',
  1401. header => { 'X-Plex-Provides' => 'controller',
  1402. 'X-Plex-Client-Identifier' => $hash->{id},
  1403. 'X-Plex-Platform' => $^O,
  1404. #'X-Plex-Device' => 'FHEM',
  1405. 'X-Plex-Device-Name' => $hash->{fhemHostname},
  1406. 'X-Plex-Product' => 'FHEM',
  1407. 'X-Plex-Version' => '0.0', },
  1408. };
  1409. $param->{cl} = $hash->{CL} if( ref($hash->{CL}) eq 'HASH' );
  1410. $param->{callback} = \&plex_parseHttpAnswer;
  1411. HttpUtils_NonblockingGet( $param );
  1412. return undef;
  1413. }
  1414. sub
  1415. plex_getTokenOfPin($)
  1416. {
  1417. my ($hash) = @_;
  1418. my $name = $hash->{NAME};
  1419. RemoveInternalTimer($hash, "plex_getTokenOfPin");
  1420. Log3 $name, 2, "$name: no PIN" if( !$hash->{PIN} );
  1421. return undef if( !$hash->{PIN} );
  1422. return undef if( !$hash->{PIN_ID} );
  1423. my $url = "https://plex.tv/pins/$hash->{PIN_ID}.xml";
  1424. Log3 $name, 4, "$name: requesting $url";
  1425. my $param = {
  1426. url => $url,
  1427. method => 'GET',
  1428. timeout => 5,
  1429. noshutdown => 0,
  1430. hash => $hash,
  1431. key => 'tokenOfPin',
  1432. header => { 'X-Plex-Provides' => 'controller',
  1433. 'X-Plex-Client-Identifier' => $hash->{id},
  1434. 'X-Plex-Platform' => $^O,
  1435. #'X-Plex-Device' => 'FHEM',
  1436. 'X-Plex-Device-Name' => $hash->{fhemHostname},
  1437. 'X-Plex-Product' => 'FHEM',
  1438. 'X-Plex-Version' => '0.0', },
  1439. };
  1440. $param->{callback} = \&plex_parseHttpAnswer;
  1441. HttpUtils_NonblockingGet( $param );
  1442. return undef;
  1443. }
  1444. sub
  1445. plex_sendApiCmd($$$;$$)
  1446. {
  1447. my ($hash,$url,$key,$blocking,$token) = @_;
  1448. $token = $hash->{token} if( !$token && $hash->{token} );
  1449. my $name = $hash->{NAME};
  1450. if( $url =~ m/.player./ ) {
  1451. my $mhash = $modules{plex}{defptr}{MASTER};
  1452. $mhash = $hash if( !$mhash );
  1453. ++$mhash->{commandID};
  1454. $url .= "&commandID=$mhash->{commandID}";
  1455. }
  1456. Log3 $name, 4, "$name: requesting $url";
  1457. my $address;
  1458. my $port;
  1459. if( $url =~ m'//([^:]*):(\d*)' ) {
  1460. $address = $1;
  1461. $port = $2;
  1462. }
  1463. #X-Plex-Platform (Platform name, eg iOS, MacOSX, Android, LG, etc)
  1464. #X-Plex-Platform-Version (Operating system version, eg 4.3.1, 10.6.7, 3.2)
  1465. #X-Plex-Provides (one or more of [player, controller, server])
  1466. #X-Plex-Product (Plex application name, eg Laika, Plex Media Server, Media Link)
  1467. #X-Plex-Version (Plex application version number)
  1468. #X-Plex-Device (Device name and model number, eg iPhone3,2, Motorola XOOM™, LG5200TV)
  1469. #X-Plex-Client-Identifier (UUID, serial number, or other number unique per device)
  1470. my $param = {
  1471. url => $url,
  1472. timeout => 5,
  1473. noshutdown => 1,
  1474. httpversion => '1.1',
  1475. hash => $hash,
  1476. key => $key,
  1477. address => $address,
  1478. port => $port,
  1479. header => { 'X-Plex-Provides' => 'controller',
  1480. 'X-Plex-Client-Identifier' => $hash->{id},
  1481. 'X-Plex-Platform' => $^O,
  1482. #'X-Plex-Device' => 'FHEM',
  1483. 'X-Plex-Device-Name' => $hash->{fhemHostname},
  1484. 'X-Plex-Product' => 'FHEM',
  1485. 'X-Plex-Version' => '0.0', },
  1486. };
  1487. $param->{header}{'X-Plex-Token'} = $token if( $token );
  1488. if( my $entry = plex_entryOfIP($hash, 'client', $address) ) {
  1489. $param->{header}{'X-Plex-Target-Client-Identifier'} = $entry->{machineIdentifier} if( $entry->{machineIdentifier} );
  1490. }
  1491. $param->{cl} = $blocking if( ref($blocking) eq 'HASH' );
  1492. if( $blocking && (!ref($blocking) || !$blocking->{canAsyncOutput}) ) {
  1493. my($err,$data) = HttpUtils_BlockingGet( $param );
  1494. return $err if( $err );
  1495. $param->{blocking} = 1;
  1496. return( plex_parseHttpAnswer( $param, $err, $data ) );
  1497. }
  1498. $param->{callback} = \&plex_parseHttpAnswer;
  1499. HttpUtils_NonblockingGet( $param );
  1500. return undef;
  1501. }
  1502. sub
  1503. plex_play($$$$)
  1504. {
  1505. my ($hash, $client, $server,$key) = @_;
  1506. my $name = $hash->{NAME};
  1507. my $url;
  1508. if ($key =~ m/\bplaylists\b/) { #play playlist
  1509. $key =~ s/[^0-9]//g;
  1510. $url = "http://$server->{address}:$server->{port}/playQueues?type=&playlistID=$key";
  1511. $url .= "&shuffle=0&repeat=0&includeChapters=1&includeRelated=1";
  1512. } else { # play album or single track
  1513. $key = "/library/metadata/$key" if( $key !~ '^/' );
  1514. my $xml = plex_sendApiCmd( $hash, "http://$server->{address}:$server->{port}$key", '#raw', 1, $server->{accessToken} );
  1515. #Log 1, Dumper $xml;
  1516. if( !$xml || !$xml->{librarySectionUUID} ) {
  1517. return $xml->{head}[0]{title}[0] if( ref $xml->{head} eq 'ARRAY' && ref $xml->{head}[0]{title} eq 'ARRAY' );
  1518. return "item not found";
  1519. }
  1520. $url = "http://$server->{address}:$server->{port}/playQueues?type=&uri=". urlEncode( "library://$xml->{librarySectionUUID}/item/$key" );
  1521. $url .= "&shuffle=0&repeat=0&includeChapters=1&includeRelated=1";
  1522. }
  1523. Log3 $name, 4, "$name: requesting $url";
  1524. my $address;
  1525. my $port;
  1526. if( $url =~ m'//([^:]*):(\d*)' ) {
  1527. $address = $1;
  1528. $port = $2;
  1529. }
  1530. #X-Plex-Platform (Platform name, eg iOS, MacOSX, Android, LG, etc)
  1531. #X-Plex-Platform-Version (Operating system version, eg 4.3.1, 10.6.7, 3.2)
  1532. #X-Plex-Provides (one or more of [player, controller, server])
  1533. #X-Plex-Product (Plex application name, eg Laika, Plex Media Server, Media Link)
  1534. #X-Plex-Version (Plex application version number)
  1535. #X-Plex-Device (Device name and model number, eg iPhone3,2, Motorola XOOM™, LG5200TV)
  1536. #X-Plex-Client-Identifier (UUID, serial number, or other number unique per device)
  1537. my $param = {
  1538. url => $url,
  1539. method => 'POST',
  1540. timeout => 5,
  1541. noshutdown => 1,
  1542. httpversion => '1.1',
  1543. hash => $hash,
  1544. key => 'playAlbum',
  1545. album => $key,
  1546. client => $client,
  1547. server => $server,
  1548. address => $address,
  1549. port => $port,
  1550. header => { 'X-Plex-Provides' => 'controller',
  1551. 'X-Plex-Client-Identifier' => $hash->{id},
  1552. 'X-Plex-Platform' => $^O,
  1553. #'X-Plex-Device' => 'FHEM',
  1554. 'X-Plex-Device-Name' => $hash->{fhemHostname},
  1555. 'X-Plex-Product' => 'FHEM',
  1556. 'X-Plex-Version' => '0.0', },
  1557. };
  1558. $param->{header}{'X-Plex-Token'} = $hash->{token} if( $hash->{token} );
  1559. $param->{header}{'X-Plex-Token'} = $server->{accessToken} if( $server->{accessToken} );
  1560. if( my $entry = plex_entryOfIP($hash, 'client', $address) ) {
  1561. $param->{header}{'X-Plex-Target-Client-Identifier'} = $entry->{machineIdentifier} if( $entry->{machineIdentifier} );
  1562. }
  1563. $param->{callback} = \&plex_parseHttpAnswer;
  1564. HttpUtils_NonblockingGet( $param );
  1565. return undef;
  1566. }
  1567. sub
  1568. plex_addToPlaylist($$$$)
  1569. {
  1570. my ($hash, $server,$playlist,$key) = @_;
  1571. my $name = $hash->{NAME};
  1572. $playlist = "/playlists/$playlist" if( $playlist !~ '^/' );
  1573. $playlist .= "/items" if( $playlist !~ '/items$' );
  1574. $key = "/library/metadata/$key" if( $key !~ '^/' );
  1575. my $xml = plex_sendApiCmd( $hash, "http://$server->{address}:$server->{port}$key", '#raw', 1 );
  1576. #Log 1, Dumper $xml;
  1577. return "item not found" if( !$xml || !$xml->{librarySectionUUID} );
  1578. my $url = "http://$server->{address}:$server->{port}$playlist?uri=". urlEncode( "library://$xml->{librarySectionUUID}/directory$key" );
  1579. Log3 $name, 4, "$name: requesting $url";
  1580. my $address;
  1581. my $port;
  1582. if( $url =~ m'//([^:]*):(\d*)' ) {
  1583. $address = $1;
  1584. $port = $2;
  1585. }
  1586. #X-Plex-Platform (Platform name, eg iOS, MacOSX, Android, LG, etc)
  1587. #X-Plex-Platform-Version (Operating system version, eg 4.3.1, 10.6.7, 3.2)
  1588. #X-Plex-Provides (one or more of [player, controller, server])
  1589. #X-Plex-Product (Plex application name, eg Laika, Plex Media Server, Media Link)
  1590. #X-Plex-Version (Plex application version number)
  1591. #X-Plex-Device (Device name and model number, eg iPhone3,2, Motorola XOOM™, LG5200TV)
  1592. #X-Plex-Client-Identifier (UUID, serial number, or other number unique per device)
  1593. my $param = {
  1594. url => $url,
  1595. method => 'PUT',
  1596. timeout => 5,
  1597. noshutdown => 1,
  1598. httpversion => '1.1',
  1599. hash => $hash,
  1600. key => 'addToPlaylist',
  1601. server => $server,
  1602. address => $address,
  1603. port => $port,
  1604. header => { 'X-Plex-Provides' => 'controller',
  1605. 'X-Plex-Client-Identifier' => $hash->{id},
  1606. 'X-Plex-Platform' => $^O,
  1607. #'X-Plex-Device' => 'FHEM',
  1608. 'X-Plex-Device-Name' => $hash->{fhemHostname},
  1609. 'X-Plex-Product' => 'FHEM',
  1610. 'X-Plex-Version' => '0.0', },
  1611. };
  1612. $param->{header}{'X-Plex-Token'} = $hash->{token} if( $hash->{token} );
  1613. $param->{header}{'X-Plex-Token'} = $server->{accessToken} if( $server->{accessToken} );
  1614. if( my $entry = plex_entryOfIP($hash, 'client', $address) ) {
  1615. $param->{header}{'X-Plex-Target-Client-Identifier'} = $entry->{machineIdentifier} if( $entry->{machineIdentifier} );
  1616. }
  1617. $param->{callback} = \&plex_parseHttpAnswer;
  1618. HttpUtils_NonblockingGet( $param );
  1619. return undef;
  1620. }
  1621. sub plex_entryOfID($$$);
  1622. sub plex_entryOfIP($$$);
  1623. sub
  1624. plex_entryOfID($$$)
  1625. {
  1626. my ($hash,$type,$id) = @_;
  1627. return undef if( !$id );
  1628. $hash->{$type.'s'} = {} if( !$hash->{$type.'s'} );
  1629. my $entries = $hash->{$type.'s'};
  1630. foreach my $ip ( keys %{$entries} ) {
  1631. return $entries->{$ip} if( $entries->{$ip}{machineIdentifier} && $entries->{$ip}{machineIdentifier} eq $id );
  1632. return $entries->{$ip} if( $entries->{$ip}{resourceIdentifier} && $entries->{$ip}{resourceIdentifier} eq $id );
  1633. }
  1634. if( $type eq 'server' ) {
  1635. if( $hash->{'myPlex-servers'}{Server} ) {
  1636. foreach my $entry (@{$hash->{'myPlex-servers'}{Server}}) {
  1637. if( $id eq $entry->{machineIdentifier} ) {
  1638. $entry->{online} = 1;
  1639. return $entry;
  1640. }
  1641. }
  1642. }
  1643. }
  1644. if( my $mhash = $modules{plex}{defptr}{MASTER} ) {
  1645. return plex_entryOfID($mhash,$type,$id) if( $mhash != $hash );
  1646. }
  1647. return undef;
  1648. }
  1649. sub
  1650. plex_entryOfIP($$$)
  1651. {
  1652. my ($hash,$type,$ip) = @_;
  1653. return undef if( !$ip );
  1654. $hash->{$type.'s'} = {} if( !$hash->{$type.'s'} );
  1655. my $entries = $hash->{$type.'s'};
  1656. foreach my $key ( keys %{$entries} ) {
  1657. return $entries->{$key} if( $entries->{$key}{address} eq $ip );
  1658. }
  1659. if( $type eq 'server' ) {
  1660. if( $hash->{'myPlex-servers'}{Server} ) {
  1661. foreach my $entry (@{$hash->{'myPlex-servers'}{Server}}) {
  1662. if( $ip eq $entry->{address} ) {
  1663. $entry->{online} = 1;
  1664. return $entry;
  1665. }
  1666. }
  1667. }
  1668. }
  1669. if( my $mhash = $modules{plex}{defptr}{MASTER} ) {
  1670. return plex_entryOfIP($mhash,$type,$ip) if( $mhash != $hash );
  1671. }
  1672. return undef;
  1673. }
  1674. sub
  1675. plex_serverOf($$;$)
  1676. {
  1677. my ($hash,$server,$only) = @_;
  1678. my $entry;
  1679. $entry = plex_entryOfID($hash, 'server', $hash->{currentServer} ) if( $hash->{currentServer} );
  1680. $entry = plex_entryOfIP($hash, 'server', $server) if( $server && $server =~ m/^\d+\.\d+\.\d+\.\d+$/ );
  1681. $entry = plex_entryOfID($hash, 'server', $server) if( $server && !$entry );
  1682. $entry = plex_entryOfIP($hash, 'server', $hash->{server} ) if( !$entry );
  1683. $entry = plex_entryOfID($hash, 'server', $hash->{machineIdentifier} ) if( !$entry );
  1684. $entry = plex_entryOfID($hash, 'server', $hash->{resourceIdentifier} ) if( !$entry );
  1685. if( !$entry && $only ) {
  1686. if( my $mhash = $modules{plex}{defptr}{MASTER} ) {
  1687. #Log 1, Dumper $mhash;
  1688. my @keys = keys(%{$modules{plex}{defptr}{MASTER}{servers}});
  1689. if( @keys == 1 ) {
  1690. $entry = $modules{plex}{defptr}{MASTER}{servers}{$keys[0]};
  1691. }
  1692. } elsif( $hash->{server} && $hash->{servers} ) {
  1693. my @keys = keys(%{$hash->{servers}});
  1694. if( @keys == 1 ) {
  1695. $entry = $hash->{servers}{$keys[0]};
  1696. }
  1697. }
  1698. }
  1699. return $entry;
  1700. }
  1701. sub
  1702. plex_clientOf($$)
  1703. {
  1704. my ($hash,$client) = @_;
  1705. if( my $chash = $defs{$client} ) {
  1706. $client = $chash->{machineIdentifier} if( $chash->{machineIdentifier} );
  1707. }
  1708. my $entry;
  1709. $entry = plex_entryOfIP($hash, 'client', $client) if( $client =~ m/^\d+\.\d+\.\d+\.\d+$/ );
  1710. $entry = plex_entryOfID($hash, 'client', $client) if( !$entry );
  1711. $entry = plex_entryOfIP($hash, 'client', $hash->{client} ) if( !$entry );
  1712. $entry = plex_entryOfID($hash, 'client', $hash->{machineIdentifier} ) if( !$entry );
  1713. $entry = plex_entryOfID($hash, 'client', $hash->{resourceIdentifier} ) if( !$entry );
  1714. return $entry;
  1715. }
  1716. sub
  1717. plex_msg2hash($;$)
  1718. {
  1719. my ($string,$keep) = @_;
  1720. my %hash = ();
  1721. if( $string !~ m/\r/ ) {
  1722. $string =~ s/\n/\r\n/g;
  1723. }
  1724. foreach my $line (split("\r\n", $string)) {
  1725. my ($key,$value) = split( ": ", $line );
  1726. next if( !$value );
  1727. if( !$keep ) {
  1728. $key =~ s/-//g;
  1729. $key = lcfirst( $key );
  1730. }
  1731. $value =~ s/^ //;
  1732. $hash{$key} = $value;
  1733. }
  1734. return \%hash;
  1735. }
  1736. sub
  1737. plex_hash2header($)
  1738. {
  1739. my ($hash) = @_;
  1740. return $hash if( ref($hash) ne 'HASH' );
  1741. my $header;
  1742. foreach my $key (keys %{$hash}) {
  1743. #$header .= "\r\n" if( $header );
  1744. $header .= "$key: $hash->{$key}\r\n";
  1745. }
  1746. return $header;
  1747. }
  1748. sub
  1749. plex_hash2form($)
  1750. {
  1751. my ($hash) = @_;
  1752. return $hash if( ref($hash) ne 'HASH' );
  1753. my $form;
  1754. foreach my $key (keys %{$hash}) {
  1755. $form .= "&" if( $form );
  1756. $form .= "$key=".urlEncode($hash->{$key});
  1757. }
  1758. return $form;
  1759. }
  1760. sub
  1761. plex_discovered($$$$)
  1762. {
  1763. my ($hash, $type, $ip, $entry) = @_;
  1764. my $name = $hash->{NAME};
  1765. if( !$type ) {
  1766. $type = 'server' if( $hash->{servers}{$ip} || ($hash->{server} && $hash->{server} eq $ip) );
  1767. $type = 'client' if( $hash->{clients}{$ip} || ($hash->{client} && $hash->{client} eq $ip) );
  1768. return undef if( !$type );
  1769. }
  1770. $hash->{$type.'s'} = {} if( !$hash->{$type.'s'} );
  1771. my $entries = $hash->{$type.'s'};
  1772. my $new;
  1773. $new = 1 if( !$entries->{$ip} || !$entries->{$ip}{online}
  1774. || !$entries->{$ip}{port} || !$entry->{port} || $entries->{$ip}{port} ne $entry->{port} );
  1775. if( $new ) {
  1776. $entry->{machineIdentifier} = $entry->{resourceIdentifier} if( $entry->{resourceIdentifier} && !$entry->{machineIdentifier} );
  1777. my $type = ucfirst( $type );
  1778. if( my $ignored = AttrVal($name, "ignored${type}s", '' ) ) {
  1779. if( $ignored =~ m/\b$ip\b/ ) {
  1780. Log3 $name, 5, "$name: ignoring $type $ip";
  1781. return undef;
  1782. } elsif( $entry->{machineIdentifier} && $ignored =~ m/\b$entry->{machineIdentifier}\b/ ) {
  1783. Log3 $name, 5, "$name: ignoring $type $entry->{machineIdentifier}";
  1784. return undef;
  1785. }
  1786. }
  1787. $entries->{$ip} = $entry;
  1788. $entries->{$ip}{online} = 1;
  1789. } else {
  1790. @{$entries->{$ip}}{ keys %{$entry} } = values %{$entry};
  1791. }
  1792. $entry = $entries->{$ip};
  1793. $entry->{address} = $ip;
  1794. $entry->{updatedAt} = gettimeofday();
  1795. if( $type eq 'client' && $entry->{machineIdentifier} ) {
  1796. if( my $chash = $modules{plex}{defptr}{$entry->{machineIdentifier}} ) {
  1797. readingsBeginUpdate($chash);
  1798. readingsBulkUpdate($chash, 'presence', 'present' ) if( ReadingsVal($chash->{NAME}, 'presence', '') ne 'present' );
  1799. readingsBulkUpdate($chash, 'state', 'appeared' ) if( ReadingsVal($chash->{NAME}, 'state', '') eq 'disappeared' );
  1800. readingsEndUpdate($chash, 1);
  1801. #$chash->{name} = $entry->{name};
  1802. $chash->{product} = $entry->{product};
  1803. $chash->{version} = $entry->{version};
  1804. $chash->{platform} = $entry->{platform};
  1805. $chash->{deviceClass} = $entry->{deviceClass};
  1806. $chash->{platformVersion} = $entry->{platformVersion};
  1807. $chash->{protocolCapabilities} = $entry->{protocolCapabilities};
  1808. }
  1809. }
  1810. if( $type eq 'server' ) {
  1811. Log3 $name, 3, "$name: $type discovered: $ip" if( $new );
  1812. if( $new && $entry->{port} ) {
  1813. plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/clients", "clients" );
  1814. }
  1815. plex_requestNotifications( $hash, $entry );
  1816. } elsif( $type eq 'client' ) {
  1817. Log3 $name, 3, "$name: $type discovered: $ip" if( $new );
  1818. if( $new && $entry->{port} ) {
  1819. plex_sendApiCmd( $hash, "http://$ip:$entry->{port}/resources", "resources" );
  1820. }
  1821. } else {
  1822. Log3 $name, 2, "$name: discovered unknown type: $type";
  1823. }
  1824. }
  1825. sub
  1826. plex_disappeared($$$)
  1827. {
  1828. my ($hash, $type, $ip) = @_;
  1829. my $name = $hash->{NAME};
  1830. if( !$type ) {
  1831. $type = 'server' if( $hash->{servers}{$ip} || ($hash->{server} && $hash->{server} eq $ip) );
  1832. $type = 'client' if( $hash->{clients}{$ip} || ($hash->{client} && $hash->{client} eq $ip) );
  1833. return undef if( !$type );
  1834. }
  1835. $hash->{$type.'s'} = {} if( !$hash->{$type.'s'} );
  1836. my $entries = $hash->{$type.'s'};
  1837. my $new;
  1838. $new = 1 if( !$entries->{$ip} || $entries->{$ip}{online} );
  1839. $entries->{$ip} = {} if( !$entries->{$ip} );
  1840. $entries->{$ip}{online} = 0;
  1841. my $machineIdentifier = $entries->{$ip}{machineIdentifier};
  1842. if( $type eq 'client' && $new && $machineIdentifier ) {
  1843. delete $hash->{subscriptionsFrom}{$machineIdentifier};
  1844. if( my $chash = $hash->{helper}{subscriptionsFrom}{$machineIdentifier} ) {
  1845. plex_closeSocket( $chash );
  1846. delete($defs{$chash->{NAME}});
  1847. delete $hash->{helper}{subscriptionsFrom}{$machineIdentifier};
  1848. }
  1849. if( my $chash = $modules{plex}{defptr}{$machineIdentifier} ) {
  1850. delete $chash->{controllable};
  1851. delete $chash->{currentMediaType};
  1852. readingsBeginUpdate($chash);
  1853. readingsBulkUpdate($chash, 'presence', 'absent' );
  1854. readingsBulkUpdate($chash, 'state', 'disappeared' );
  1855. readingsEndUpdate($chash, 1);
  1856. 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 ) );
  1857. }
  1858. }
  1859. if( $type eq 'server' ) {
  1860. Log3 $name, 3, "$name: $type disappeared: $ip" if( $new );
  1861. } elsif( $type eq 'client' ) {
  1862. Log3 $name, 3, "$name: $type disappeared: $ip" if( $new );
  1863. plex_removeSubscription($hash->{helper}{timelineListener}, $ip);
  1864. } else {
  1865. Log3 $name, 2, "$name: unknown type $type disappeared";
  1866. }
  1867. }
  1868. sub
  1869. plex_requestNotifications($$)
  1870. {
  1871. my ($hash,$server) = @_;
  1872. my $name = $hash->{NAME};
  1873. return if( $hash->{helper}{websockets}{$server->{machineIdentifier}} );
  1874. if( my $socket = IO::Socket::INET->new(PeerAddr=>"$server->{address}:$server->{port}", Timeout=>2, Blocking=>1, ReuseAddr=>1) ) {
  1875. my $chash = plex_newChash( $hash, $socket,
  1876. {NAME=>"$name:websocket:$server->{machineIdentifier}", STATE=>'listening', websocket=>0} );
  1877. $chash->{address} = $server->{address};
  1878. $chash->{machineIdentifier} = $server->{machineIdentifier};
  1879. Log3 $name, 3, "$name: notification websocket opened to $server->{address}";
  1880. $hash->{helper}{websockets}{$server->{machineIdentifier}} = $chash;
  1881. my $ret = "GET /:/websockets/notifications HTTP/1.1\r\n";
  1882. $ret .= plex_hash2header( { 'Host' => "$server->{address}:$server->{port}",
  1883. 'X-Plex-Token' => $server->{accessToken}?$server->{accessToken}:$hash->{token},
  1884. 'Upgrade' => 'websocket',
  1885. 'Connection' => 'Upgrade',
  1886. 'Pragma' => 'no-cache',
  1887. 'Cache-Control' => 'no-cache',
  1888. 'Sec-WebSocket-Key' => 'RkhFTQ==',
  1889. 'Sec-WebSocket-Version' => '13',
  1890. } );
  1891. $ret .= "\r\n";
  1892. #Log 1, $ret;
  1893. syswrite($chash->{CD}, $ret );
  1894. } else {
  1895. Log3 $name, 2, "$name: failed to open notification websocket to $server->{address}";
  1896. }
  1897. }
  1898. sub
  1899. plex_closeNotifications($)
  1900. {
  1901. my ($hash,$server) = @_;
  1902. my $name = $hash->{NAME};
  1903. }
  1904. sub
  1905. plex_stopWebsockets($)
  1906. {
  1907. my ($hash,$server) = @_;
  1908. my $name = $hash->{NAME};
  1909. return if( !$hash->{helper}{websockets} );
  1910. foreach my $key ( keys %{$hash->{helper}{websockets}} ) {
  1911. my $chash = $hash->{helper}{websockets}{$key};
  1912. my $cname = $chash->{NAME};
  1913. plex_closeSocket($chash);
  1914. delete($hash->{servers}{$chash->{address}}{sessions});
  1915. delete($hash->{helper}{websockets}{$key});
  1916. delete($defs{$cname});
  1917. }
  1918. Log3 $name, 3, "$name: websockets stoped";
  1919. }
  1920. sub
  1921. plex_readingsBulkUpdateIfChanged($$$)
  1922. {
  1923. my ($hash,$reading,$value) = @_;
  1924. readingsBulkUpdate($hash, $reading, $value ) if( defined($value) && $value ne ReadingsVal($hash->{NAME}, $reading, '') );
  1925. }
  1926. sub
  1927. plex_parseTimeline($$$)
  1928. {
  1929. my ($hash,$id,$xml) = @_;
  1930. my $name = $hash->{NAME};
  1931. if( !$id ) {
  1932. Log3 $name, 2, "$name: can't parse timeline for unknown device";
  1933. return undef if( !$id );
  1934. }
  1935. my $chash = $modules{plex}{defptr}{$id};
  1936. if( !$chash ) {
  1937. my $cname = $id;
  1938. $cname =~ s/-//g;
  1939. my $define = "$cname plex $id";
  1940. if( my $cmdret = CommandDefine(undef,$define) ) {
  1941. Log3 $name, 1, "$name: Autocreate: An error occurred while creating device for id '$id': $cmdret";
  1942. return undef;
  1943. }
  1944. CommandAttr(undef, "$cname room plex");
  1945. if( my $entry = plex_entryOfID($hash, 'client', $id ) ) {
  1946. CommandAttr(undef, "$cname alias ".$entry->{product});
  1947. }
  1948. $chash = $modules{plex}{defptr}{$id};
  1949. }
  1950. readingsBeginUpdate($chash);
  1951. plex_readingsBulkUpdateIfChanged($chash, 'location', $xml->{location} );
  1952. my $state;
  1953. my $entries;
  1954. delete $chash->{time};
  1955. delete $chash->{seekRange};
  1956. delete $chash->{controllable};
  1957. foreach my $entry (@{$xml->{Timeline}}) {
  1958. next if( !$entry->{state} );
  1959. my $key = $entry->{key};
  1960. if( $key && $key ne ReadingsVal($chash->{NAME}, 'key', '') ) {
  1961. $chash->{currentServer} = $entry->{machineIdentifier};
  1962. readingsBulkUpdate($chash, 'key', $key );
  1963. readingsBulkUpdate($chash, 'server', $entry->{machineIdentifier} );
  1964. my $server = plex_entryOfID($hash, 'server', $entry->{machineIdentifier} );
  1965. $server = $entry if( !$server );
  1966. plex_sendApiCmd( $hash, "http://$server->{address}:$server->{port}$key", "#update:$chash->{NAME}" );
  1967. }
  1968. plex_readingsBulkUpdateIfChanged($chash, 'volume', $entry->{volume} ) if( $entry->{controllable} && $entry->{controllable} =~ m/\bvolume\b/ );
  1969. $chash->{controllable} = $entry->{controllable} if( $entry->{controllable} );
  1970. if( $entry->{type} ) {
  1971. $entries->{ $entry->{type} } = $entry;
  1972. }
  1973. my $time = $entry->{time};
  1974. if( defined($time) ) {
  1975. # if( !$chash->{helper}{time} || abs($time - $chash->{helper}{time}) > 2000 ) {
  1976. # plex_readingsBulkUpdateIfChanged($chash, 'time', plex_sec2hms($time/1000) );
  1977. #
  1978. # $chash->{helper}{time} = $time;
  1979. # }
  1980. $chash->{time} = $time;
  1981. }
  1982. $chash->{seekRange} = $entry->{seekRange} if( $entry->{seekRange} && $entry->{seekRange} ne "0-0" );
  1983. $state .= ' ' if( $state );
  1984. $state .= "$entry->{type}:$entry->{state}";
  1985. #$state = undef if( $state && $entry->{continuing} );
  1986. }
  1987. $state = 'stopped' if( !$state );
  1988. $state = $1 if( $state =~ /^[\w]*:(stopped)$/ );
  1989. if( $state =~ '\w*:(\w*) \w*:(\w*) .*:(\w*)' ) {
  1990. $state = $1 if( $1 eq $2 && $2 eq $3 );
  1991. }
  1992. if( $state =~ '(\w*):(playing|paused)' ) {
  1993. $chash->{currentMediaType} = $1;
  1994. if( defined($entries->{$1}) ) {
  1995. $chash->{controllable} = $entries->{$1}->{controllable} if ( defined($entries->{$1}->{controllable}) );
  1996. plex_readingsBulkUpdateIfChanged($chash, 'repeat', $entries->{$1}->{repeat} );
  1997. plex_readingsBulkUpdateIfChanged($chash, 'shuffle', $entries->{$1}->{shuffle} );
  1998. plex_readingsBulkUpdateIfChanged($chash, 'playQueueID', $entries->{$1}->{playQueueID} );
  1999. plex_readingsBulkUpdateIfChanged($chash, 'playQueueItemID', $entries->{$1}->{playQueueItemID} );
  2000. }
  2001. } else {
  2002. delete $chash->{currentMediaType};
  2003. #FIXME: move after stop event
  2004. 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 ) );
  2005. }
  2006. plex_readingsBulkUpdateIfChanged($chash, 'state', $state );
  2007. readingsEndUpdate($chash, 1);
  2008. }
  2009. sub
  2010. plex_getDataForSMAPI($$$)
  2011. {
  2012. my ($hash,$server,$key) = @_;
  2013. my $name = $hash->{NAME};
  2014. my ($seconds) = gettimeofday();
  2015. foreach my $key ( keys %{$hash->{helper}{SMAPIcache}} ) {
  2016. delete $hash->{helper}{SMAPIcache}{$key} if( $seconds - $hash->{helper}{SMAPIcache}{$key}{timestamp} > 10 );
  2017. }
  2018. my $xml;
  2019. if( !$hash->{helper}{SMAPIcache}{$key} ) {
  2020. Log 1, "get: $key";
  2021. if( $key =~ m'^/library' ) {
  2022. $xml = plex_sendApiCmd( $hash, "http://$server->{address}:$server->{port}$key", '#raw', 1 );
  2023. } else {
  2024. $xml = plex_sendApiCmd( $hash, "http://$server->{address}:$server->{port}/library/sections$key", '#raw', 1 );
  2025. return undef if( !$xml || ref($xml) ne 'HASH' );
  2026. if( $key eq '' && $xml->{Directory} ) {
  2027. my $section;
  2028. foreach my $item (@{$xml->{Directory}}) {
  2029. if( $item->{type} && $item->{type} eq 'artist' ) {
  2030. if( $section ) {
  2031. $section = undef;
  2032. last;
  2033. } else {
  2034. $section = $item->{key};
  2035. }
  2036. }
  2037. }
  2038. if( $section ) {
  2039. Log3 $name, 4, "$name: found only one music section, using this as root";
  2040. $xml = plex_sendApiCmd( $hash, "http://$server->{address}:$server->{port}/library/sections/$section", '#raw', 1 );
  2041. } else {
  2042. Log3 $name, 4, "$name: found multiple music sections";
  2043. }
  2044. }
  2045. }
  2046. return undef if( !$xml || ref($xml) ne 'HASH' );
  2047. if( $xml->{Directory} ) {
  2048. for(my $i = int(@{$xml->{Directory}}); $i >= 0; --$i) {
  2049. my $item = $xml->{Directory}[$i];
  2050. # at the toplevel only care about music sections
  2051. if( !$key && $item->{type} && $item->{type} ne 'artist' ) {
  2052. splice @{$xml->{Directory}}, $i, 1;
  2053. --$xml->{size};
  2054. next;
  2055. }
  2056. # ignore search nodes
  2057. if( $item->{key} =~ /^search/ ) {
  2058. splice @{$xml->{Directory}}, $i, 1;
  2059. --$xml->{size};
  2060. next;
  2061. }
  2062. }
  2063. }
  2064. my ($seconds) = gettimeofday();
  2065. $hash->{helper}{SMAPIcache}{$key} = { value => $xml, timestamp => $seconds };
  2066. } else {
  2067. Log 1, "cached: $key";
  2068. my ($seconds) = gettimeofday();
  2069. $hash->{helper}{SMAPIcache}{$key}{value}{timestamp} = $seconds;
  2070. $xml = $hash->{helper}{SMAPIcache}{$key}{value}
  2071. }
  2072. Log3 $name, 5, "$name: got:". Dumper $xml;
  2073. return $xml;
  2074. }
  2075. sub
  2076. plex_metadataResponseForSMAPI($$$$$)
  2077. {
  2078. my ($hash,$request,$server,$key,$xml) = @_;
  2079. my $name = $hash->{NAME};
  2080. return undef if( !$request || ref($request) ne 'HASH' );
  2081. return undef if( !$server || ref($server) ne 'HASH' );
  2082. return undef if( !$xml || ref($xml) ne 'HASH' );
  2083. my $type;
  2084. if( $request->{getMetadata} ) {
  2085. $type = 'getMetadata';
  2086. } elsif( $request->{getExtendedMetadata} ) {
  2087. $type = 'getExtendedMetadata';
  2088. } else {
  2089. return undef;
  2090. }
  2091. my $index = $request->{$type}{index};
  2092. my $count = $request->{$type}{count};
  2093. my $body;
  2094. $body .= '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">';
  2095. $body .= ' <s:Body>';
  2096. $body .= ' <'.$type.'Response xmlns="http://www.sonos.com/Services/1.1">';
  2097. $body .= ' <'.$type.'Result>';
  2098. my $i = 0;
  2099. my $total = $xml->{size};
  2100. $total = 0 if( !$total );
  2101. if( $xml->{Directory} ) {
  2102. foreach my $item (@{$xml->{Directory}}) {
  2103. if( $i < $index ) {
  2104. ++$i;
  2105. next;
  2106. }
  2107. my $title = $item->{titleSort};
  2108. $title = $item->{title};# if( !$title );
  2109. $title =~ s/&/&amp;/g;
  2110. $body .= '<mediaCollection>';
  2111. $body .= " <title>$title</title>";
  2112. $body .= " <id>$item->{key}</id>" if( $item->{key} =~ '^/' );
  2113. $body .= " <id>$key/$item->{key}</id>" if( $item->{key} !~ '^/' );
  2114. $body .= " <albumArtURI>http://$server->{address}:$server->{port}$item->{thumb}</albumArtURI>" if( $item->{thumb} );
  2115. $body .= ' <canScroll>true</canScroll>';
  2116. if( $item->{type} eq 'album' ) {
  2117. $body .= '<canPlay>true</canPlay>';
  2118. $body .= '<itemType>album</itemType>';
  2119. } elsif( $item->{type} eq 'artist' ) {
  2120. $body .= '<canPlay>true</canPlay>';
  2121. $body .= '<itemType>artist</itemType>';
  2122. } elsif( $item->{type} eq 'genre' ) {
  2123. $body .= '<canPlay>true</canPlay>';
  2124. $body .= '<itemType>genre</itemType>';
  2125. } else {
  2126. $body .= '<itemType>collection</itemType>';
  2127. }
  2128. $body .= '</mediaCollection>';
  2129. last if( ++$i >= $index + $count );
  2130. }
  2131. } elsif( $xml->{Track} ) {
  2132. foreach my $item (@{$xml->{Track}}) {
  2133. if( $i < $index ) {
  2134. ++$i;
  2135. next;
  2136. }
  2137. $item->{title} =~ s/&/&amp;/g;
  2138. $item->{parentTitle} =~ s/&/&amp;/g;
  2139. $item->{grandparentTitle} =~ s/&/&amp;/g;
  2140. $body .= '<mediaMetadata>';
  2141. $body .= " <title>$item->{title}</title>";
  2142. $body .= " <id>$item->{key}</id>" if( $item->{key} =~ '^/' );
  2143. $body .= " <id>$key/$item->{key}</id>" if( $item->{key} !~ '^/' );
  2144. $body .= ' <mimeType>audio/mp3</mimeType>';
  2145. $body .= ' <itemType>track</itemType>';
  2146. $body .= ' <trackMetadata>';
  2147. $body .= " <album>$item->{parentTitle}</album>";
  2148. $body .= " <albumId>$item->{parentKey}</albumId>";
  2149. $body .= " <artist>$item->{grandparentTitle}</artist>";
  2150. $body .= " <artistId>$item->{grandparentKey}</artistId>";
  2151. $body .= " <trackNumber>$item->{index}</trackNumber>";
  2152. $body .= " <duration>". int($item->{duration}/1000) ."</duration>";
  2153. $body .= " <albumArtURI>http://$server->{address}:$server->{port}$item->{parentThumb}</albumArtURI>" if( $item->{parentThumb} );
  2154. $body .= ' </trackMetadata>';
  2155. $body .= '</mediaMetadata>';
  2156. last if( ++$i >= $index + $count );
  2157. }
  2158. }
  2159. $body .= " <total>$total</total>";
  2160. $body .= " <index>$index</index>";
  2161. $body .= " <count>". ($i-$index) ."</count>";
  2162. $body .= ' </'.$type.'Result>';
  2163. $body .= ' </'.$type.'Response>';
  2164. $body .= ' </s:Body>';
  2165. $body .= '</s:Envelope>';
  2166. #Log 1, $body;
  2167. my $ret = "HTTP/1.1 200 OK\r\n";
  2168. $ret .= plex_hash2header( { 'Connection' => 'Close',
  2169. 'Content-Type' => 'text/xml; charset=utf-8',
  2170. 'Content-Length' => length($body),
  2171. } );
  2172. $ret .= "\r\n";
  2173. $ret .= $body;
  2174. #Log 1, $ret;
  2175. return $ret;
  2176. }
  2177. sub
  2178. plex_getScrollindicesForSMAPI($$)
  2179. {
  2180. my ($hash,$xml) = @_;
  2181. my $name = $hash->{NAME};
  2182. my $indices ='';
  2183. my $last;
  2184. my $i = 0;
  2185. if( $xml->{Directory} ) {
  2186. foreach my $item (@{$xml->{Directory}}) {
  2187. my $title = $item->{titleSort};
  2188. $title = $item->{title} if( !$title );
  2189. my $current = uc(substr($title, 0, 1));
  2190. return '' if( $last && ord($last) > ord($current ) );
  2191. if( $current =~ /[A-Z]/ && (!$last || $current ne $last) ) {
  2192. $indices .= ',' if( $indices );
  2193. $indices .= "$current,$i";
  2194. $last = $current;
  2195. }
  2196. ++$i;
  2197. }
  2198. }
  2199. return $indices;
  2200. }
  2201. sub
  2202. plex_handleSMAPI($$)
  2203. {
  2204. my ($hash,$msg) = @_;
  2205. my $name = $hash->{NAME};
  2206. my $handled;
  2207. my $server = plex_serverOf($hash, $hash->{machineIdentifier}, !$hash->{machineIdentifier});
  2208. if( !$server ) {
  2209. Log3 $name, 2, "$name: no server found for SMAPI request";
  2210. return undef;
  2211. }
  2212. if( $msg =~ m/^(.*?)\r?\n\r?\n(.*)$/s ) {
  2213. my $header = $1;
  2214. my $body = $2;
  2215. #Log 1, $header;
  2216. #Log 1, $body;
  2217. if( my $xml = eval { XMLin( $body, KeyAttr => {}, ForceArray => 0 ); } ) {
  2218. if( my $body = $xml->{'s:Body'} ) {
  2219. Log3 $name, 4, "$name: got soap request:". Dumper $body;
  2220. if( $body->{getMetadata} ) {
  2221. $handled = 1;
  2222. #Log 1, Dumper $body;
  2223. my $key = $body->{getMetadata}{id};
  2224. $key = '' if( $key eq 'root' );
  2225. $key = "/$key" if( $key && $key !~ '^/' );
  2226. my $xml = plex_getDataForSMAPI($hash, $server, $key);
  2227. #Log 1, Dumper $xml;
  2228. return plex_metadataResponseForSMAPI($hash, $body, $server, $key, $xml);
  2229. } elsif( $body->{getExtendedMetadata} ) {
  2230. $handled = 1;
  2231. #Log 1, Dumper $body;
  2232. my $key = $body->{getExtendedMetadata}{id};
  2233. $key = "" if( $key eq 'root' );
  2234. $key = "/$key" if( $key && $key !~ '^/' );
  2235. my $xml = plex_getDataForSMAPI($hash, $server, $key);
  2236. return plex_metadataResponseForSMAPI($hash, $body, $server, $key, $xml);
  2237. } elsif( $body->{getScrollIndices} ) {
  2238. $handled = 1;
  2239. if( my $key = $body->{getScrollIndices}{id} ) {
  2240. $key = "/$key" if( $key && $key !~ '^/' );
  2241. my $xml = plex_getDataForSMAPI($hash, $server, $key);
  2242. return undef if( !$xml || ref($xml) ne 'HASH' );
  2243. my $body;
  2244. $body .= '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">';
  2245. $body .= ' <s:Body>';
  2246. $body .= ' <getScrollIndicesResponse xmlns="http://www.sonos.com/Services/1.1">';
  2247. $body .= ' <getScrollIndicesResult>';
  2248. $body .= plex_getScrollindicesForSMAPI($hash,$xml);
  2249. $body .= ' </getScrollIndicesResult>';
  2250. $body .= ' </getScrollIndicesResponse>';
  2251. $body .= ' </s:Body>';
  2252. $body .= '</s:Envelope>';
  2253. my $ret = "HTTP/1.1 200 OK\r\n";
  2254. $ret .= plex_hash2header( { 'Connection' => 'Close',
  2255. 'Content-Type' => 'text/xml; charset=utf-8',
  2256. 'Content-Length' => length($body),
  2257. } );
  2258. $ret .= "\r\n";
  2259. $ret .= $body;
  2260. #Log 1, $ret;
  2261. return $ret;
  2262. }
  2263. } elsif( $body->{getMediaMetadata} ) {
  2264. $handled = 1;
  2265. if( my $key = $body->{getMediaMetadata}{id} ) {
  2266. $key = "/$key" if( $key && $key !~ '^/' );
  2267. my $xml = plex_getDataForSMAPI($hash, $server, $key);
  2268. return undef if( !$xml || ref($xml) ne 'HASH' );
  2269. my $body;
  2270. $body .= '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">';
  2271. $body .= ' <s:Body>';
  2272. $body .= ' <getMediaMetadataResponse xmlns="http://www.sonos.com/Services/1.1">';
  2273. $body .= ' <getMediaMetadataResult>';
  2274. if( $xml->{Track} ) {
  2275. foreach my $item (@{$xml->{Track}}) {
  2276. $item->{title} =~ s/&/&amp;/g;
  2277. $item->{parentTitle} =~ s/&/&amp;/g;
  2278. $item->{grandparentTitle} =~ s/&/&amp;/g;
  2279. $body .= "<title>$item->{title}</title>";
  2280. $body .= "<id>$item->{key}</id>" if( $item->{key} =~ '^/' );
  2281. $body .= "<id>$key/$item->{key}</id>" if( $item->{key} !~ '^/' );
  2282. $body .= '<mimeType>audio/mp3</mimeType>';
  2283. $body .= '<itemType>track</itemType>';
  2284. $body .= '<trackMetadata>';
  2285. $body .= " <album>$item->{parentTitle}</album>";
  2286. $body .= " <albumId>$item->{parentKey}</albumId>";
  2287. $body .= " <artist>$item->{grandparentTitle}</artist>";
  2288. $body .= " <artistId>$item->{grandparentKey}</artistId>";
  2289. $body .= " <trackNumber>$item->{index}</trackNumber>";
  2290. $body .= " <duration>". int($item->{duration}/1000) ."</duration>";
  2291. $body .= " <albumArtURI>http://$server->{address}:$server->{port}$item->{parentThumb}</albumArtURI>" if( $item->{parentThumb} );
  2292. $body .= '</trackMetadata>';
  2293. }
  2294. }
  2295. $body .= ' </getMediaMetadataResult>';
  2296. $body .= ' </getMediaMetadataResponse>';
  2297. $body .= ' </s:Body>';
  2298. $body .= '</s:Envelope>';
  2299. my $ret = "HTTP/1.1 200 OK\r\n";
  2300. $ret .= plex_hash2header( { 'Connection' => 'Close',
  2301. 'Content-Type' => 'text/xml; charset=utf-8',
  2302. 'Content-Length' => length($body),
  2303. } );
  2304. $ret .= "\r\n";
  2305. $ret .= $body;
  2306. #Log 1, $ret;
  2307. return $ret;
  2308. }
  2309. } elsif( $body->{getMediaURI} ) {
  2310. $handled = 1;
  2311. if( my $key = $body->{getMediaURI}{id} ) {
  2312. my $xml = plex_getDataForSMAPI($hash, $server, $key);
  2313. return undef if( !$xml || ref($xml) ne 'HASH' );
  2314. my $body;
  2315. $body .= '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">';
  2316. $body .= ' <s:Body>';
  2317. $body .= ' <getMediaURIResponse xmlns="http://www.sonos.com/Services/1.1">';
  2318. $body .= ' <getMediaURIResult>';
  2319. if( $xml->{Track} ) {
  2320. foreach my $item (@{$xml->{Track}}) {
  2321. if( $item->{Media} && $item->{Media}[0]{Part} ) {
  2322. $body .= "http://$server->{address}:$server->{port}$item->{Media}[0]{Part}[0]{key}";
  2323. #$body .= "&X-Plex-Token=$hash->{token}" if( $hash->{token} );
  2324. last;
  2325. }
  2326. }
  2327. }
  2328. $body .= ' </getMediaURIResult>';
  2329. if( $hash->{token} ) {
  2330. $body .= '<httpHeaders>';
  2331. $body .= ' <httpHeader>';
  2332. $body .= ' <header>X-Plex-Token</header>';
  2333. $body .= " <value>$hash->{token}</value>";
  2334. $body .= ' </httpHeader>';
  2335. $body .= '</httpHeaders>';
  2336. }
  2337. $body .= ' </getMediaMetadataResponse>';
  2338. $body .= ' </s:Body>';
  2339. $body .= '</s:Envelope>';
  2340. my $ret = "HTTP/1.1 200 OK\r\n";
  2341. $ret .= plex_hash2header( { 'Connection' => 'Close',
  2342. 'Content-Type' => 'text/xml; charset=utf-8',
  2343. 'Content-Length' => length($body),
  2344. } );
  2345. $ret .= "\r\n";
  2346. $ret .= $body;
  2347. #Log 1, $ret;
  2348. return $ret;
  2349. }
  2350. } elsif( $body->{getLastUpdate} ) {
  2351. $handled = 1;
  2352. my ($seconds) = gettimeofday();
  2353. my $body;
  2354. $body .= '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">';
  2355. $body .= ' <s:Body>';
  2356. $body .= ' <getLastUpdateResponse xmlns="http://www.sonos.com/Services/1.1">';
  2357. $body .= ' <getLastUpdateResult>';
  2358. $body .= " <catalog>$seconds</catalog>";
  2359. $body .= ' <favorites></favorites>';
  2360. $body .= ' <pollInterval>120</pollInterval>';
  2361. $body .= ' </getLastUpdateResult>';
  2362. $body .= ' </getLastUpdateResponse>';
  2363. $body .= ' </s:Body>';
  2364. $body .= '</s:Envelope>';
  2365. my $ret = "HTTP/1.1 200 OK\r\n";
  2366. $ret .= plex_hash2header( { 'Connection' => 'Close',
  2367. 'Content-Type' => 'text/xml; charset=utf-8',
  2368. 'Content-Length' => length($body),
  2369. } );
  2370. $ret .= "\r\n";
  2371. $ret .= $body;
  2372. #Log 1, $ret;
  2373. return $ret;
  2374. }
  2375. Log3 $name, 2, "$name: unhandled soap request:". Dumper $body if( !$handled );
  2376. return undef;
  2377. }
  2378. }
  2379. }
  2380. Log3 $name, 2, "$name: unhandled message: $msg" if( !$handled );
  2381. return undef;
  2382. }
  2383. sub
  2384. plex_Parse($$;$$$)
  2385. {
  2386. my ($hash,$msg,$peerhost,$peerport,$sockport) = @_;
  2387. my $name = $hash->{NAME};
  2388. Log3 $name, 5, "$name: from: $peerhost" if( $peerhost );
  2389. Log3 $name, 5, "$name: $msg";
  2390. my $handled = 0;
  2391. if( $peerhost ) { #from broadcast
  2392. if( $msg =~ '^HTTP/1.\d 200 OK' ) {
  2393. my $params = plex_msg2hash($msg);
  2394. if( $params->{'contentType'} eq 'plex/media-server' ) {
  2395. $handled = 1;
  2396. plex_discovered($hash, 'server', $peerhost, $params );
  2397. } elsif( $params->{'contentType'} eq 'plex/media-player' ) {
  2398. return undef if( $peerhost eq $hash->{fhemIP} && $hash->{clients}{$peerhost}{online} );
  2399. $handled = 1;
  2400. plex_discovered($hash, 'client', $peerhost, $params );
  2401. }
  2402. } elsif( $msg =~ '^([\w\-]+) \* HTTP/1.\d' ) {
  2403. my $type = $1;
  2404. my $params = plex_msg2hash($msg);
  2405. if( $type eq 'HELLO' ) {
  2406. $handled = 1;
  2407. plex_discovered($hash, 'client', $peerhost, $params );
  2408. } elsif( $type eq 'BYE' ) {
  2409. plex_disappeared($hash, 'client', $peerhost );
  2410. $handled = 1;
  2411. } elsif( $type eq 'UPDATE' ) {
  2412. if( $params->{parameters} =~ m/playerAdd=(.*)/ ) {
  2413. $handled = 1;
  2414. my $ip = $peerhost;
  2415. if( $hash->{servers}{$ip}{port} ) {
  2416. plex_sendApiCmd( $hash, "http://$ip:$hash->{servers}{$ip}{port}/clients", "clients" );
  2417. }
  2418. } elsif( $params->{parameters} =~ m/playerDel=(.*)/ ) {
  2419. my $ip = $1;
  2420. $handled = 1;
  2421. if( !$hash->{clients}{$ip} || $hash->{clients}{$ip}{product} ne 'Plex Home Theater' ) {
  2422. plex_disappeared($hash, 'client', $ip );
  2423. }
  2424. }
  2425. } elsif( $type eq 'M-SEARCH' ) {
  2426. $handled = 1;
  2427. if( $peerhost eq $hash->{fhemIP} && $hash->{clients}{$peerhost}{online} ) {
  2428. if( $hash->{helper}{discoverClientsMcast} && $hash->{helper}{discoverClientsMcast}->{CD}->sockport() == $peerport ) {
  2429. #Log3 $name, 5, "$name: ignoring multicast M-Search from self ($peerhost:$peerport)";
  2430. return undef;
  2431. }
  2432. if( $hash->{helper}{discoverClientsBcast} && $hash->{helper}{discoverClientsBcast}->{CD}->sockport() == $peerport ) {
  2433. #Log3 $name, 5, "$name: ignoring broadcast M-Search from self ($peerhost:$peerport)";
  2434. return undef;
  2435. }
  2436. }
  2437. #Log3 $name, 5, "$name: received from: $peerhost:$peerport to $sockport: $msg";
  2438. my $msg = "HTTP/1.0 200 OK\r\n";
  2439. $msg .= plex_hash2header( { 'Content-Type' => 'plex/media-player',
  2440. 'Resource-Identifier' => $hash->{id},
  2441. 'Name' => $hash->{fhemHostname},
  2442. #'Host' => $hash->{fhemIP},
  2443. 'Port' => $hash->{helper}{timelineListener}{PORT},
  2444. #'Updated-At' => 1447614540,
  2445. 'Product' => 'FHEM SONOS Proxy',
  2446. 'Version' => '0.0.0',
  2447. #'Protocol' => 'plex',
  2448. 'Protocol-Version' => 1,
  2449. 'Protocol-Capabilities' => 'playback,timeline', } );
  2450. $msg .= "\r\n";
  2451. my $sin = sockaddr_in($peerport, inet_aton($peerhost));
  2452. $hash->{helper}{clientDiscoveryResponderMcast}->{CD}->send($msg, 0, $sin );
  2453. }
  2454. }
  2455. } elsif( $msg =~ '^GET\s*([^\s]*)\s*HTTP/1.\d' ) {
  2456. my $request = $1;
  2457. if( $msg =~ m/^(.*?)\r?\n\r?\n(.*)$/s ) {
  2458. my $header = $1;
  2459. my $body = $2;
  2460. my $params;
  2461. if( $request =~ m/^([^?]*)(\?(.*))?/ ) {
  2462. #$request = $1;
  2463. if( $3 ) {
  2464. foreach my $param (split("&", $3)) {
  2465. my ($key,$value) = split("=",$param);
  2466. $params->{$key} = $value;
  2467. }
  2468. }
  2469. }
  2470. $header = plex_msg2hash($header, 1);
  2471. my $ret;
  2472. if( $request =~ m'^/resources' ) {
  2473. $handled = 1;
  2474. Log3 $name, 4, "$name: answering $request";
  2475. my $xml = { MediaContainer => [ {Player => { title => $hash->{fhemHostname},
  2476. protocol => 'plex',
  2477. protocolVersion =>'1',
  2478. protocolCapabilities => 'playback,timeline,skipNext,skipPrevious',
  2479. machineIdentifier => $hash->{id},
  2480. product => 'FHEM SONOS Proxy',
  2481. platform => $^O,
  2482. platformVersion => '0.0.0',
  2483. deviceClass => 'pc',
  2484. deviceProtocol => 'sonos' } }] };
  2485. my $body = '<?xml version="1.0" encoding="utf-8" ?>';
  2486. $body .= "\n";
  2487. $body .= XMLout( $xml, KeyAttr => { }, RootName => undef );
  2488. $ret = "HTTP/1.1 200 OK\r\n";
  2489. $ret .= plex_hash2header( { 'Connection' => 'Close',
  2490. 'X-Plex-Client-Identifier' => $hash->{id},
  2491. 'Content-Type' => 'text/xml;charset=utf-8',
  2492. 'Content-Length' => length($body), } );
  2493. $ret .= "\r\n";
  2494. $ret .= $body;
  2495. }
  2496. my $entry = plex_entryOfID($hash, 'client', $header->{'X-Plex-Client-Identifier'} );
  2497. if( $entry ) {
  2498. my $addr = "$entry->{address}:$entry->{port}";
  2499. if( $request =~ m'^/player/timeline/subscribe' ) {
  2500. $handled = 1;
  2501. Log3 $name, 4, "$name: answering $request";
  2502. $hash->{subscriptionsFrom}{$header->{'X-Plex-Client-Identifier'}} = $addr;
  2503. plex_sendTimelines($hash, $params->{commandID});
  2504. $ret = "HTTP/1.1 200 OK\r\n";
  2505. $ret .= plex_hash2header( { 'Connection' => 'Close',
  2506. 'X-Plex-Client-Identifier' => $hash->{id},
  2507. 'Content-Type' => 'text/xml;charset=utf-8',
  2508. 'Content-Length' => 0, } );
  2509. $ret .= "\r\n";
  2510. } elsif( $request =~ m'^/player/timeline/unsubscribe' ) {
  2511. $handled = 1;
  2512. Log3 $name, 4, "$name: answering $request";
  2513. delete $hash->{subscriptionsFrom}{$header->{'X-Plex-Client-Identifier'}};
  2514. if( my $chash = $hash->{helper}{subscriptionsFrom}{$header->{'X-Plex-Client-Identifier'}} ) {
  2515. plex_closeSocket( $chash );
  2516. delete($defs{$chash->{NAME}});
  2517. delete $hash->{helper}{subscriptionsFrom}{$header->{'X-Plex-Client-Identifier'}};
  2518. }
  2519. $ret = "HTTP/1.1 200 OK\r\n";
  2520. $ret .= plex_hash2header( { 'Connection' => 'Close',
  2521. 'X-Plex-Client-Identifier' => $hash->{id},
  2522. 'Content-Type' => 'text/xml;charset=utf-8',
  2523. 'Content-Length' => 0, } );
  2524. $ret .= "\r\n";
  2525. } elsif( $request =~ m'^/player/mirror/details' ) {
  2526. $handled = 1;
  2527. Log3 $name, 4, "$name: answering $request";
  2528. if( my $chash = $hash->{helper}{subscriptionsFrom}{$header->{'X-Plex-Client-Identifier'}} ) {
  2529. $chash->{commandID} = $params->{commandID};
  2530. }
  2531. $ret = "HTTP/1.1 200 OK\r\n";
  2532. $ret .= plex_hash2header( { 'Connection' => 'Close',
  2533. 'X-Plex-Client-Identifier' => $hash->{id},
  2534. 'Content-Type' => 'text/xml;charset=utf-8',
  2535. 'Content-Length' => 0, } );
  2536. $ret .= "\r\n";
  2537. } elsif( $request =~ m'^/player/playback/playMedia' ) {
  2538. delete $hash->{sonos}{playqueue};
  2539. delete $hash->{sonos}{containerKey} ;
  2540. delete $hash->{sonos}{machineIdentifier};
  2541. my $entry = plex_entryOfID($hash, 'server', $params->{machineIdentifier} );
  2542. if( $params->{containerKey} ) {
  2543. my ($containerKey) = split( '\?', $params->{containerKey}, 2 );
  2544. return "HTTP/1.1 400 Bad Request\r\n\r\n" if( !$containerKey);
  2545. my $xml = plex_sendApiCmd( $hash, "http://$entry->{address}:$entry->{port}$containerKey", '#raw', 1 );
  2546. return undef if( !$xml || ref($xml) ne 'HASH' );
  2547. $hash->{sonos}{playqueue} = $xml;
  2548. $hash->{sonos}{containerKey} = $containerKey;
  2549. } elsif( my $key = $params->{key} ) {
  2550. my $xml = plex_sendApiCmd( $hash, "http://$entry->{address}:$entry->{port}$key", '#raw', 1 );
  2551. return undef if( !$xml || ref($xml) ne 'HASH' || !$xml->{Track} );
  2552. $hash->{sonos}{playqueue} = ();
  2553. $hash->{sonos}{playqueue}{size} = 1;
  2554. $hash->{sonos}{playqueue}{Track} = $xml->{Track};
  2555. }
  2556. $hash->{sonos}{machineIdentifier} = $params->{machineIdentifier};
  2557. $hash->{sonos}{currentTrack} = 0;
  2558. $hash->{sonos}{updateTime} = time();
  2559. $hash->{sonos}{currentTime} = 0;
  2560. $hash->{sonos}{status} = 'playing';
  2561. $handled = 1;
  2562. Log3 $name, 4, "$name: answering $request";
  2563. my $tracks = $hash->{sonos}{playqueue}{Track};
  2564. my $track = $tracks->[$hash->{sonos}{currentTrack}];
  2565. my $server = plex_entryOfID($hash, 'server', $hash->{sonos}{machineIdentifier});
  2566. fhem( "set sonos_Esszimmer playURI http://$server->{address}:$server->{port}$track->{Media}[0]{Part}[0]{key}" );
  2567. plex_sendTimelines($hash, $params->{commandID});
  2568. $ret = "HTTP/1.1 200 OK\r\n";
  2569. $ret .= plex_hash2header( { 'Connection' => 'Close',
  2570. 'X-Plex-Client-Identifier' => $hash->{id},
  2571. 'Content-Type' => 'text/xml;charset=utf-8',
  2572. 'Content-Length' => 0, } );
  2573. $ret .= "\r\n";
  2574. } elsif( $request =~ m'^/player/playback/setParameters' ) {
  2575. $handled = 1;
  2576. Log3 $name, 4, "$name: answering $request";
  2577. plex_sendTimelines($hash, $params->{commandID});
  2578. $ret = "HTTP/1.1 200 OK\r\n";
  2579. $ret .= plex_hash2header( { 'Connection' => 'Close',
  2580. 'X-Plex-Client-Identifier' => $hash->{id},
  2581. 'Content-Type' => 'text/xml;charset=utf-8',
  2582. 'Content-Length' => 0, } );
  2583. $ret .= "\r\n";
  2584. } elsif( $request =~ m'^/player/playback/(\w*)' ) {
  2585. my $cmd = $1;
  2586. $handled = 1;
  2587. Log3 $name, 4, "$name: answering $request";
  2588. return "HTTP/1.1 400 Bad Request\r\n\r\n" if( !$hash->{sonos}{playqueue} );
  2589. if( $cmd eq 'play' ) {
  2590. $cmd = 'playing';
  2591. fhem( "set sonos_Esszimmer play" );
  2592. } elsif( $cmd eq 'pause' ) {
  2593. $cmd = 'paused';
  2594. fhem( "set sonos_Esszimmer pause" );
  2595. } elsif( $cmd eq 'stop' ) {
  2596. $cmd = 'stopped' if( $cmd eq 'stop' );
  2597. fhem( "set sonos_Esszimmer stop" );
  2598. } elsif( $cmd eq 'skipNext' ) {
  2599. $cmd = 'playing';
  2600. $hash->{sonos}{currentTrack}++;
  2601. $hash->{sonos}{currentTrack} = 0 if( $hash->{sonos}{currentTrack} > $hash->{sonos}{playqueue}{size}-1 );
  2602. $hash->{sonos}{updateTime} = time();
  2603. $hash->{sonos}{currentTime} = 0;
  2604. my $server = plex_entryOfID($hash, 'server', $hash->{sonos}{machineIdentifier});
  2605. my $tracks = $hash->{sonos}{playqueue}{Track};
  2606. my $track = $tracks->[$hash->{sonos}{currentTrack}];
  2607. fhem( "set sonos_Esszimmer playURI http://$server->{address}:$server->{port}$track->{Media}[0]{Part}[0]{key}" );
  2608. } elsif( $cmd eq 'skipPrevious' ) {
  2609. $cmd = 'playing';
  2610. if( $hash->{sonos}{currentTime} < 10 ) {
  2611. $hash->{sonos}{currentTrack}--;
  2612. $hash->{sonos}{currentTrack} = $hash->{sonos}{playqueue}{size} - 1 if( $hash->{sonos}{currentTrack} < 0 );
  2613. my $server = plex_entryOfID($hash, 'server', $hash->{sonos}{machineIdentifier});
  2614. my $tracks = $hash->{sonos}{playqueue}{Track};
  2615. my $track = $tracks->[$hash->{sonos}{currentTrack}];
  2616. fhem( "set sonos_Esszimmer playURI http://$server->{address}:$server->{port}$track->{Media}[0]{Part}[0]{key}" );
  2617. }
  2618. $hash->{sonos}{updateTime} = time();
  2619. $hash->{sonos}{currentTime} = 0;
  2620. } elsif( $cmd eq 'seekTo' ) {
  2621. $cmd = $hash->{sonos}{status};
  2622. $hash->{sonos}{updateTime} = time();
  2623. $hash->{sonos}{currentTime} = int($params->{offset} / 1000);
  2624. fhem( "set sonos_Esszimmer currentTrackPosition ". plex_sec2hms(int($params->{offset} / 1000) ) );
  2625. }
  2626. $hash->{sonos}{updateTime} = time() if( $cmd eq 'playing' && $hash->{sonos}{status} ne 'playing' );
  2627. $hash->{sonos}{status} = $cmd;
  2628. plex_sendTimelines($hash, $params->{commandID});
  2629. $ret = "HTTP/1.1 200 OK\r\n";
  2630. $ret .= plex_hash2header( { 'Connection' => 'Close',
  2631. 'X-Plex-Client-Identifier' => $hash->{id},
  2632. 'Content-Type' => 'text/xml;charset=utf-8',
  2633. 'Content-Length' => 0, } );
  2634. $ret .= "\r\n";
  2635. }
  2636. }
  2637. if( !$handled ) {
  2638. $peerhost = $peerhost ? " from $peerhost" : '';
  2639. Log3 $name, 2, "$name: unhandled request: $msg";
  2640. }
  2641. return $ret;
  2642. }
  2643. } elsif( $msg =~ '^POST /:/timeline\?? HTTP/1.\d' ) {
  2644. #Log 1, $msg;
  2645. if( $msg =~ m/^(.*?)\r?\n\r?\n(.*)$/s ) {
  2646. my $header = $1;
  2647. my $body = $2;
  2648. if( !$body ) {
  2649. $handled = 1;
  2650. Log3 $name, 5, "$name: empty timeline received";
  2651. } elsif( $body !~ m/^<.*>$/ms ) {
  2652. $handled = 1;
  2653. Log3 $name, 2, "$name: unknown timeline content: $body";
  2654. } else {
  2655. $handled = 1;
  2656. my $header = plex_msg2hash($header, 1);
  2657. my $id = $header->{'X-Plex-Client-Identifier'};
  2658. if( !$id ) {
  2659. my $entry = plex_entryOfIP($hash, 'client', $peerhost);
  2660. $id = $entry->{machineIdentifier};
  2661. }
  2662. #Log 1, ">>$body<<";
  2663. my $xml = eval { XMLin( $body, KeyAttr => {}, ForceArray => 1 ); };
  2664. Log3 $name, 2, "$name: xml error: $@" if( $@ );
  2665. return undef if( !$xml );
  2666. plex_parseTimeline($hash, $id, $xml);
  2667. }
  2668. }
  2669. } elsif( $msg =~ '^POST /SMAPI HTTP/1.\d' ) {
  2670. return plex_handleSMAPI($hash, $msg);
  2671. }
  2672. if( !$handled ) {
  2673. $peerhost = $peerhost ? " from $peerhost" : '';
  2674. Log3 $name, 2, "$name: unhandled message$peerhost: $msg";
  2675. }
  2676. return undef;
  2677. }
  2678. sub
  2679. plex_sec2hms($)
  2680. {
  2681. my ($sec) = @_;
  2682. my $s = $sec % 60;
  2683. $sec = int( $sec / 60 );
  2684. my $m = $sec % 60;
  2685. $sec = int( $sec / 60 );
  2686. my $h = $sec % 24;
  2687. return sprintf("%02d:%02d:%02d", $h, $m, $s);
  2688. }
  2689. sub
  2690. plex_timestamp2date($)
  2691. {
  2692. my @t = localtime(shift);
  2693. return sprintf("%04d-%02d-%02d",
  2694. $t[5]+1900, $t[4]+1, $t[3]);
  2695. }
  2696. sub
  2697. plex_parseHttpAnswer($$$)
  2698. {
  2699. my ($param, $err, $data) = @_;
  2700. my $hash = $param->{hash};
  2701. my $name = $hash->{NAME};
  2702. if( $err ) {
  2703. if( $param->{key} eq 'publishToSonos' ) {
  2704. if( $param->{cl} && $param->{cl}{canAsyncOutput} ) {
  2705. asyncOutput( $param->{cl}, "SMAPI registration for $param->{player}: failed\n" );
  2706. }
  2707. } elsif( $err =~ m/Connection refused$/ || $err =~ m/timed out$/ || $err =~ m/empty answer received$/ ) {
  2708. if( !$param->{retry} || $param->{retry} < 1 ) {
  2709. ++$param->{retry};
  2710. delete $param->{conn};
  2711. Log3 $name, 4, "$name: http request ($param->{url}) failed: $err; retrying";
  2712. if( $param->{url} =~ m/.player./ ) {
  2713. ++$hash->{commandID};
  2714. $param->{url} =~ s/commandID=\d*/commandID=$hash->{commandID}/;
  2715. }
  2716. Log3 $name, 5, " ($param->{url})";
  2717. RemoveInternalTimer($hash);
  2718. InternalTimer(gettimeofday()+5, "HttpUtils_NonblockingGet", $param, 0);
  2719. return;
  2720. }
  2721. }
  2722. Log3 $name, 2, "$name: http request ($param->{url}) failed: $err";
  2723. plex_disappeared($hash, undef, $param->{address} ) if( $param->{retry} );
  2724. return undef;
  2725. return $err;
  2726. }
  2727. Log3 $name, 5, "$name: received $data";
  2728. return undef if( !$data );
  2729. $data = encode('UTF-8', $data );
  2730. if( $data =~ m/^<!DOCTYPE html>(.*)/ ) {
  2731. if( $param->{key} eq 'tokenOfPin' ) {
  2732. delete $hash->{PIN};
  2733. delete $hash->{PIN_ID};
  2734. delete $hash->{PIN_EXPIRES};
  2735. Log3 $name, 2, "$name: PIN expired";
  2736. return undef;
  2737. }
  2738. Log3 $name, 2, "$name: failed: $1";
  2739. return undef;
  2740. } elsif( $data =~ m/200 OK/ ) {
  2741. Log3 $name, 5, "$name: http request ($param->{url}) received code : $data";
  2742. return undef;
  2743. } elsif( $data !~ m/^<.*>$/ms ) {
  2744. Log3 $name, 2, "$name: http request ($param->{url}) unknown content: $data";
  2745. return undef;
  2746. }
  2747. #Log 1, $param->{url};
  2748. #Log 1, Dumper $xml;
  2749. my $handled = 0;
  2750. #Log 1, $data;
  2751. my $xml = eval { XMLin( $data, KeyAttr => {}, ForceArray => 1 ); };
  2752. Log3 $name, 2, "$name: xml error: $@" if( $@ );
  2753. return undef if( !$xml );
  2754. if( $param->{key} eq 'token' ) {
  2755. $handled = 1;
  2756. $hash->{token} = $xml->{'authenticationToken'};
  2757. readingsSingleUpdate($hash, '.token', $hash->{token}, 0 );
  2758. CommandSave(undef,undef) if( AttrVal( "autocreate", "autosave", 1 ) );
  2759. Log3 $name, 3, "$name: got token from user/password";
  2760. plex_sendApiCmd($hash, "https://plex.tv/pms/servers.xml", "myPlex:servers" );
  2761. plex_sendApiCmd($hash, "https://plex.tv/devices.xml", "myPlex:devices" );
  2762. #https://plex.tv/pms/resources.xml?includeHttps=1
  2763. } elsif( $param->{key} eq 'getPinForToken' ) {
  2764. $handled = 1;
  2765. delete $hash->{PIN};
  2766. delete $hash->{PIN_ID};
  2767. delete $hash->{PIN_EXPIRES};
  2768. $hash->{PIN} = $xml->{code}[0] if( $xml->{code} );
  2769. $hash->{PIN_ID} = $xml->{id}[0]{content} if( $xml->{id} );
  2770. $hash->{PIN_EXPIRES} = $xml->{'expires-at'}[0]{content} if( $xml->{'expires-at'} );
  2771. Log3 $name, 2, "$name: PIN: $hash->{PIN}";
  2772. #plex_sendApiCmd($hash, "https://plex.tv/pms/servers.xml", "myPlex:servers" );
  2773. #plex_sendApiCmd($hash, "https://plex.tv/devices.xml", "myPlex:devices" );
  2774. #https://plex.tv/pms/resources.xml?includeHttps=1
  2775. if( $param->{cl} && $param->{cl}{canAsyncOutput} ) {
  2776. asyncOutput( $param->{cl}, "PIN: $hash->{PIN}\n" );
  2777. plex_getTokenOfPin($hash);
  2778. }
  2779. } elsif( $param->{key} eq 'tokenOfPin' ) {
  2780. $handled = 1;
  2781. RemoveInternalTimer($hash, "plex_getTokenOfPin");
  2782. if( $xml->{auth_token}[0] && !ref($xml->{auth_token}[0]) ) {
  2783. delete $hash->{PIN};
  2784. delete $hash->{PIN_ID};
  2785. delete $hash->{PIN_EXPIRES};
  2786. $hash->{token} = $xml->{auth_token}[0];
  2787. readingsSingleUpdate($hash, '.token', $hash->{token}, 0 );
  2788. CommandSave(undef,undef) if( AttrVal( "autocreate", "autosave", 1 ) );
  2789. Log3 $name, 3, "$name: got token from pin";
  2790. plex_sendApiCmd($hash, "https://plex.tv/pms/servers.xml", "myPlex:servers" );
  2791. plex_sendApiCmd($hash, "https://plex.tv/devices.xml", "myPlex:devices" );
  2792. } else {
  2793. InternalTimer(gettimeofday()+4, "plex_getTokenOfPin", $hash, 0);
  2794. }
  2795. } elsif( $param->{key} eq 'clients' ) {
  2796. $handled = 1;
  2797. foreach my $entry (@{$xml->{Server}}) {
  2798. #next if( $entry->{address} eq $hash->{fhemIP}
  2799. # && $hash->{helper}{timelineListener} && $hash->{helper}{timelineListener}->{PORT} == $entry->{port} );
  2800. plex_discovered($hash, 'client', $entry->{address}, $entry);
  2801. }
  2802. } elsif( $param->{key} eq 'servers' ) {
  2803. $handled = 1;
  2804. foreach my $entry (@{$xml->{Server}}) {
  2805. my $ip = $entry->{address};
  2806. $ip = $param->{address} if( !$ip );
  2807. $entry->{port} = $param->{port} if( !$entry->{port} );
  2808. plex_discovered($hash, 'server', $ip, $entry);
  2809. }
  2810. } elsif( $param->{key} eq 'resources' ) {
  2811. $handled = 1;
  2812. foreach my $entry (@{$xml->{Server}}) {
  2813. my $ip = $entry->{address};
  2814. $ip = $param->{address} if( !$ip );
  2815. $entry->{port} = $param->{port} if( !$entry->{port} );
  2816. plex_discovered($hash, 'server', $ip, $entry);
  2817. }
  2818. foreach my $entry (@{$xml->{Player}}) {
  2819. my $ip = $entry->{address};
  2820. $ip = $param->{address} if( !$ip );
  2821. $entry->{port} = $param->{port} if( !$entry->{port} );
  2822. plex_discovered($hash, 'client', $ip, $entry);
  2823. plex_sendSubscription($hash->{helper}{timelineListener}, $ip) if( $entry->{protocolCapabilities} && $entry->{protocolCapabilities} =~ m/timeline/);
  2824. }
  2825. } elsif( $param->{key} eq 'detail' ) {
  2826. $handled = 1;
  2827. my $server = plex_entryOfIP($hash, 'server', $param->{address});
  2828. my $ret = plex_mediaDetail( $hash, $server, $xml );
  2829. #Log 1, Dumper $xml;
  2830. if( $param->{cl} && $param->{cl}->{TYPE} eq 'FHEMWEB' ) {
  2831. $ret =~ s/&/&amp;/g;
  2832. $ret =~ s/'/&apos;/g;
  2833. $ret =~ s/\n/<br>/g;
  2834. $ret = "<pre>$ret</pre>" if( $ret =~ m/ / );
  2835. $ret = "<html>$ret</html>";
  2836. } else {
  2837. $ret =~ s/<a[^>]*>//g;
  2838. $ret =~ s/<\/a>//g;
  2839. $ret =~ s/<img[^>]*>\n//g;
  2840. $ret .= "\n";
  2841. }
  2842. if( $param->{cl} && $param->{cl}{canAsyncOutput} ) {
  2843. #Log 1, $ret;
  2844. asyncOutput( $param->{cl}, $ret );
  2845. } elsif( $param->{blocking} ) {
  2846. return $ret;
  2847. }
  2848. return undef;
  2849. } elsif( $param->{key} eq 'onDeck'
  2850. || $param->{key} eq 'playlists'
  2851. || $param->{key} eq 'recentlyAdded'
  2852. || $param->{key} eq 'search'
  2853. || $param->{key} =~ m'sections(:(\S*))?( (.*))?' ) {
  2854. $handled = 1;
  2855. my $cmd = $4;
  2856. $xml->{parentSection} = $2;
  2857. my $server = plex_entryOfIP($hash, 'server', $param->{address});
  2858. my $ret = plex_mediaList( $hash, $server, $xml, $cmd );
  2859. if( $param->{cl} && $param->{cl}->{TYPE} eq 'FHEMWEB' ) {
  2860. $ret =~ s/&/&amp;/g;
  2861. $ret =~ s/'/&apos;/g;
  2862. $ret =~ s/\n/<br>/g;
  2863. $ret = "<pre>$ret</pre>" if( $ret =~ m/ / );
  2864. $ret = "<html>$ret</html>";
  2865. } else {
  2866. $ret =~ s/<a[^>]*>//g;
  2867. $ret =~ s/<\/a>//g;
  2868. $ret =~ s/<img[^>]*>//g;
  2869. $ret .= "\n";
  2870. }
  2871. if( $param->{cl} ) {
  2872. #Log 1, $ret;
  2873. asyncOutput( $param->{cl}, $ret ."\n" );
  2874. } elsif( $param->{blocking} ) {
  2875. return $ret;
  2876. }
  2877. return undef;
  2878. } elsif( $param->{key} eq 'playAlbum' ) {
  2879. $handled = 1;
  2880. my $client = $param->{client};
  2881. my $server = $param->{server};
  2882. my $queue = $xml->{playQueueID};
  2883. my $key = $param->{album};
  2884. my $url = "http://$client->{address}:$client->{port}/player/playback/playMedia?key=$key&offset=0";
  2885. $url .= "&machineIdentifier=$server->{machineIdentifier}&protocol=http&address=$server->{address}&port=$server->{port}";
  2886. $url .= "&containerKey=/playQueues/$queue?own=1&window=200";
  2887. plex_sendApiCmd( $hash, $url, "playback" );
  2888. } elsif( $param->{key} eq 'timeline' ) {
  2889. $handled = 1;
  2890. my $id = $xml->{machineIdentifier};
  2891. if( !$id ) {
  2892. my $entry = plex_entryOfIP($hash, 'client', $param->{address});
  2893. $id = $entry->{machineIdentifier};
  2894. }
  2895. plex_parseTimeline($hash, $id, $xml);
  2896. } elsif( $param->{key} eq 'subscribe' ) {
  2897. $handled = 1;
  2898. my $id = $xml->{machineIdentifier};
  2899. if( !$id ) {
  2900. my $entry = plex_entryOfIP($hash, 'client', $param->{address});
  2901. $id = $entry->{machineIdentifier};
  2902. }
  2903. #plex_parseTimeline($hash, $id, $xml);
  2904. } elsif( $param->{key} =~ m/#update:(.*)/ ) {
  2905. $handled = 1;
  2906. my $chash = $defs{$1};
  2907. return undef if( !$chash );
  2908. #Log 1, Dumper $xml;
  2909. #Log 1, Dumper $param;
  2910. if( $xml->{librarySectionTitle} ne ReadingsVal($chash->{NAME}, 'section', '' ) ) {
  2911. CommandDeleteReading( undef, "$chash->{NAME} currentAlbum|currentArtist|episode|series|track" );
  2912. }
  2913. readingsBeginUpdate($chash);
  2914. plex_readingsBulkUpdateIfChanged($chash, 'section', $xml->{librarySectionTitle} );
  2915. if( $xml->{Video} ) {
  2916. foreach my $entry (@{$xml->{Video}}) {
  2917. plex_readingsBulkUpdateIfChanged($chash, 'type', $entry->{type} );
  2918. plex_readingsBulkUpdateIfChanged($chash, 'series', $entry->{grandparentTitle} );
  2919. plex_readingsBulkUpdateIfChanged($chash, 'currentTitle', $entry->{title} );
  2920. if( $entry->{parentThumb} ) {
  2921. plex_readingsBulkUpdateIfChanged($chash, 'cover', "http://$param->{address}:$param->{port}$entry->{parentThumb}" );
  2922. } elsif( $entry->{grandparentThumb} ) {
  2923. plex_readingsBulkUpdateIfChanged($chash, 'cover', "http://$param->{address}:$param->{port}$entry->{grandparentThumb}" );
  2924. } else {
  2925. plex_readingsBulkUpdateIfChanged($chash, 'cover', "http://$param->{address}:$param->{port}$entry->{thumb}" );
  2926. }
  2927. plex_readingsBulkUpdateIfChanged($chash, 'episode', sprintf("S%02iE%02i",$entry->{parentIndex}, $entry->{index} ) ) if( $entry->{parentIndex} );
  2928. if( !$chash->{duration} || $chash->{duration} != $entry->{duration} ) {
  2929. $chash->{duration} = $entry->{duration};
  2930. plex_readingsBulkUpdateIfChanged($chash, 'duration', plex_sec2hms($entry->{duration}/1000) );
  2931. }
  2932. }
  2933. } elsif( $xml->{Track} ) {
  2934. foreach my $entry (@{$xml->{Track}}) {
  2935. plex_readingsBulkUpdateIfChanged($chash, 'type', $entry->{type} );
  2936. plex_readingsBulkUpdateIfChanged($chash, 'currentArtist', $entry->{grandparentTitle} );
  2937. plex_readingsBulkUpdateIfChanged($chash, 'currentAlbum', $entry->{parentTitle} );
  2938. plex_readingsBulkUpdateIfChanged($chash, 'currentTitle', $entry->{title} );
  2939. plex_readingsBulkUpdateIfChanged($chash, 'track', $entry->{index} );
  2940. if( $entry->{parentThumb} ) {
  2941. plex_readingsBulkUpdateIfChanged($chash, 'cover', "http://$param->{address}:$param->{port}$entry->{parentThumb}" );
  2942. } elsif( $entry->{grandparentThumb} ) {
  2943. plex_readingsBulkUpdateIfChanged($chash, 'cover', "http://$param->{address}:$param->{port}$entry->{grandparentThumb}" );
  2944. } else {
  2945. plex_readingsBulkUpdateIfChanged($chash, 'cover', "http://$param->{address}:$param->{port}$entry->{thumb}" );
  2946. }
  2947. if( !$chash->{duration} || $chash->{duration} != $entry->{duration} ) {
  2948. $chash->{duration} = $entry->{duration};
  2949. plex_readingsBulkUpdateIfChanged($chash, 'duration', plex_sec2hms($entry->{duration}/1000) );
  2950. }
  2951. }
  2952. }
  2953. readingsEndUpdate($chash, 1);
  2954. } elsif( $param->{key} =~ m/myPlex:servers/ ) {
  2955. $handled = 1;
  2956. $hash->{'myPlex-servers'} = $xml;
  2957. foreach my $server (@{$xml->{Server}}) {
  2958. if( $hash->{server} && $server->{address} eq $hash->{server} ) {
  2959. my $entry = $server;
  2960. my $ip = $entry->{address};
  2961. $ip = $param->{address} if( !$ip );
  2962. $entry->{port} = $param->{port} if( !$entry->{port} );
  2963. if( my $entry = plex_serverOf($hash, $entry->{machineIdentifier}, !$hash->{machineIdentifier}) ) {
  2964. $entry->{address} = $server->{address};
  2965. $entry->{port} = $server->{port};
  2966. }
  2967. #plex_discovered($hash, 'server', $ip, $entry);
  2968. } elsif( my $entry = plex_entryOfID($hash, 'server', $server->{machineIdentifier} ) ) {
  2969. }
  2970. if( my $chash = $modules{plex}{defptr}{$server->{machineIdentifier}} ) {
  2971. }
  2972. }
  2973. } elsif( $param->{key} =~ m/myPlex:devices/ ) {
  2974. $handled = 1;
  2975. $hash->{'myPlex-devices'} = $xml;
  2976. foreach my $device (@{$xml->{Device}}) {
  2977. if( my $entry = plex_entryOfID($hash, 'server', $device->{clientIdentifier} ) ) {
  2978. }
  2979. if( my $entry = plex_entryOfID($hash, 'client', $device->{clientIdentifier} ) ) {
  2980. }
  2981. if( my $chash = $modules{plex}{defptr}{$device->{clientIdentifier}} ) {
  2982. }
  2983. }
  2984. } elsif( $param->{key} eq 'sessions' ) {
  2985. $handled = 1;
  2986. if( my $server = plex_serverOf($hash, $param->{host}) ) {
  2987. delete $server->{sessions};
  2988. foreach my $type ( keys %{$xml} ) {
  2989. next if( ref($xml->{$type}) ne 'ARRAY' );
  2990. foreach my $item (@{$xml->{$type}}) {
  2991. $server->{sessions}{$item->{sessionKey}} = $item;
  2992. }
  2993. }
  2994. }
  2995. } elsif( $param->{key} =~ m/#m3u:(.*)/ ) {
  2996. my $entry = plex_entryOfID($hash, 'server', $1);
  2997. $handled = 1;
  2998. my $items;
  2999. $items = $xml->{Directory} if( $xml->{Directory} );
  3000. $items =$xml->{Playlist} if( $xml->{Playlist} );
  3001. $items = $xml->{Video} if( $xml->{Video} );
  3002. $items = $xml->{Track} if( $xml->{Track} );
  3003. my $artist = '';
  3004. $artist = $xml->{grandparentTitle} if( $xml->{grandparentTitle} );
  3005. my $album = '';
  3006. $album = $xml->{parentTitle} if( $xml->{parentTitle} );
  3007. my $ret = "#EXTM3U\n";
  3008. if( $entry && $items ) {
  3009. foreach my $item (@{$items}) {
  3010. $ret .= '#EXTINF:'. int($item->{duration}/1000) .",$artist - $album - $item->{title}\n";
  3011. if( $item->{Media} && $item->{Media}[0]{Part} ) {
  3012. $ret .= "http://$entry->{address}:$entry->{port}$item->{Media}[0]{Part}[0]{key}\n";
  3013. }
  3014. }
  3015. }
  3016. if( $param->{cl} && $param->{cl}{canAsyncOutput} ) {
  3017. #Log 1, $ret;
  3018. asyncOutput( $param->{cl}, $ret );
  3019. } elsif( $param->{blocking} ) {
  3020. return $ret;
  3021. }
  3022. } elsif( $param->{key} =~ m/#pls:(.*)/ ) {
  3023. my $entry = plex_entryOfID($hash, 'server', $1);
  3024. $handled = 1;
  3025. my $items;
  3026. $items = $xml->{Directory} if( $xml->{Directory} );
  3027. $items =$xml->{Playlist} if( $xml->{Playlist} );
  3028. $items = $xml->{Video} if( $xml->{Video} );
  3029. $items = $xml->{Track} if( $xml->{Track} );
  3030. my $artist = '';
  3031. $artist = $xml->{grandparentTitle} if( $xml->{grandparentTitle} );
  3032. my $album = '';
  3033. $album = $xml->{parentTitle} if( $xml->{parentTitle} );
  3034. my $ret = "[playlist]\n";
  3035. if( $entry && $items ) {
  3036. my $i = 0;
  3037. foreach my $item (@{$items}) {
  3038. ++$i;
  3039. if( $item->{Media} && $item->{Media}[0]{Part} ) {
  3040. $ret .= "File$i=http://$entry->{address}:$entry->{port}$item->{Media}[0]{Part}[0]{key}\n";
  3041. }
  3042. $ret .= "Title$i=$artist - $album - $item->{title}\n";
  3043. $ret .= "Length$i=". int($item->{duration}/1000) ."\n";
  3044. }
  3045. $ret .= "NumberOfEntries=". $i ."\n";
  3046. $ret .= "Version=2\n";
  3047. }
  3048. if( $param->{cl} && $param->{cl}{canAsyncOutput} ) {
  3049. #Log 1, $ret;
  3050. asyncOutput( $param->{cl}, $ret );
  3051. } elsif( $param->{blocking} ) {
  3052. return $ret;
  3053. }
  3054. } elsif( $param->{key} eq 'publishToSonos' ) {
  3055. $handled = 1;
  3056. if( $param->{cl} && $param->{cl}{canAsyncOutput} ) {
  3057. asyncOutput( $param->{cl}, "SMAPI registration for $param->{player}: $xml->{body}[0]\n" );
  3058. }
  3059. } elsif( $param->{key} eq '#raw' ) {
  3060. $handled = 1;
  3061. return $xml if( $param->{blocking} );
  3062. } elsif( $xml->{code} && $xml->{status} ) {
  3063. $handled = 1;
  3064. if( $xml->{code} == 200 ) {
  3065. Log3 $name, 5, "$name: http request ($param->{url}) received code $xml->{code}: $xml->{status}";
  3066. } else {
  3067. Log3 $name, 2, "$name: http request ($param->{url}) received code $xml->{code}: $xml->{status}";
  3068. }
  3069. }
  3070. if( !$handled ) {
  3071. Log3 $name, 2, "$name: unhandled message '$param->{key}': ". Dumper $xml;
  3072. }
  3073. return $xml if( $param->{blocking} );
  3074. }
  3075. sub
  3076. plex_Read($)
  3077. {
  3078. my ($hash) = @_;
  3079. my $name = $hash->{NAME};
  3080. my $len;
  3081. my $buf;
  3082. if( $hash->{multicast} || $hash->{broadcast} ) {
  3083. my $phash = $hash->{phash};
  3084. $len = $hash->{CD}->recv($buf, 1024);
  3085. if( !defined($len) || !$len ) {
  3086. Log 1, "!!!!!!!!!!";
  3087. return;
  3088. }
  3089. my $peerhost = $hash->{CD}->peerhost;
  3090. my $peerport = $hash->{CD}->peerport;
  3091. my $sockport = $hash->{CD}->sockport;
  3092. plex_Parse($phash, $buf, $peerhost, $peerport, $sockport);
  3093. } elsif( $hash->{timeline} ) {
  3094. $len = sysread($hash->{CD}, $buf, 10240);
  3095. #Log 1, "1:$len: $buf";
  3096. my $peerhost = $hash->{CD}->peerhost;
  3097. my $peerport = $hash->{CD}->peerport;
  3098. if( !defined($len) || !$len ) {
  3099. plex_closeSocket( $hash );
  3100. delete($defs{$name});
  3101. if( my $entry = plex_clientOf($hash->{phash}, $peerhost) ) {
  3102. delete($hash->{phash}{helper}{subscriptionsFrom}{$entry->{machineIdentifier}});
  3103. }
  3104. return undef;
  3105. }
  3106. #Log 1, "timeline ($peerhost:$peerport): $buf";
  3107. return undef;
  3108. } elsif( defined($hash->{websocket}) ) {
  3109. my $pname = $hash->{PNAME} || $name;
  3110. $len = sysread($hash->{CD}, $buf, 10240);
  3111. #Log 1, "2:$len: $buf";
  3112. my $peerhost = $hash->{CD}->peerhost;
  3113. my $peerport = $hash->{CD}->peerport;
  3114. my $close = 0;
  3115. if( !defined($len) || !$len ) {
  3116. $close = 1;
  3117. } elsif( $hash->{websocket} ) {
  3118. $hash->{buf} .= $buf;
  3119. do {
  3120. my $fin = (ord(substr($hash->{buf},0,1)) & 0x80)?1:0;
  3121. my $op = (ord(substr($hash->{buf},0,1)) & 0x0F);
  3122. my $mask = (ord(substr($hash->{buf},1,1)) & 0x80)?1:0;
  3123. my $len = (ord(substr($hash->{buf},1,1)) & 0x7F);
  3124. my $i = 2;
  3125. if( $len == 126 ) {
  3126. $len = unpack( 'n', substr($hash->{buf},$i,2) );
  3127. $i += 2;
  3128. } elsif( $len == 127 ) {
  3129. $len = unpack( 'q', substr($hash->{buf},$i,8) );
  3130. $i += 8;
  3131. }
  3132. if( $mask ) {
  3133. $i += 4;
  3134. }
  3135. #Log 1, "$fin $op $mask $len";
  3136. #FIXME: hande !$fin
  3137. return if( $len > length($hash->{buf})-$i );
  3138. my $data = substr($hash->{buf}, $i, $len);
  3139. $hash->{buf} = substr($hash->{buf},$i+$len);
  3140. #Log 1, ">>>$data<<<";
  3141. if( $data eq '?' ) {
  3142. #ignore keepalive
  3143. } elsif( $op == 0x01 ) {
  3144. my $obj = eval { decode_json($data) };
  3145. if( $obj ) {
  3146. Log3 $pname, 5, "$pname: websocket data: ". Dumper $obj;
  3147. my $phash = $hash->{phash};
  3148. my $handled = 0;
  3149. if( $obj->{NotificationContainer} ) {
  3150. $obj = $obj->{NotificationContainer};
  3151. if( $obj->{type} eq 'update.statechange' ) {
  3152. $handled = 1;
  3153. Log3 $pname, 4, "$pname: update available $obj->{AutoUpdateNotification}[0]{fixed}";
  3154. }
  3155. } elsif( $obj->{_elementType} && $obj->{_elementType} eq 'NotificationContainer' ) {
  3156. if( $obj->{type} eq 'playing' ) {
  3157. $handled = 1;
  3158. my $cname;
  3159. my $session_info_requested;
  3160. if( my $session = $obj->{_children}[0]{sessionKey} ) {
  3161. if( my $server = plex_serverOf($phash, $peerhost) ) {
  3162. if( my $session = $server->{sessions}{$session} ) {
  3163. if( my $chash = $modules{plex}{defptr}{$session->{Player}[0]{machineIdentifier}} ) {
  3164. $cname = $chash->{NAME};
  3165. #Log 1, Dumper $obj;
  3166. readingsBeginUpdate($chash);
  3167. my $key = $obj->{_children}[0]{key};
  3168. if( $key && $key ne ReadingsVal($chash->{NAME}, 'key', '') ) {
  3169. $chash->{currentServer} = $server->{machineIdentifier};
  3170. readingsBulkUpdate($chash, 'key', $key );
  3171. readingsBulkUpdate($chash, 'server', $server->{machineIdentifier} );
  3172. plex_sendApiCmd( $phash, "http://$server->{address}:$server->{port}$key", "#update:$chash->{NAME}" );
  3173. }
  3174. my $time = $obj->{_children}[0]{viewOffset};
  3175. if( defined($time) ) {
  3176. # if( !$chash->{helper}{time} || abs($time - $chash->{helper}{time}) > 2000 ) {
  3177. # plex_readingsBulkUpdateIfChanged($chash, 'time', plex_sec2hms($time/1000) );
  3178. #
  3179. # $chash->{helper}{time} = $time;
  3180. # }
  3181. $chash->{time} = $time;
  3182. }
  3183. plex_readingsBulkUpdateIfChanged($chash, 'state', $obj->{_children}[0]{state} );
  3184. readingsEndUpdate($chash, 1);
  3185. } else {
  3186. Log3 $pname, 3, "$pname: unknown player: $session->{Player}[0]{machineIdentifier}";
  3187. }
  3188. } else {
  3189. Log3 $pname, 3, "$pname: new session $obj->{_children}[0]{sessionKey}";
  3190. $session_info_requested = 1;
  3191. plex_sendApiCmd( $phash, "http://$server->{address}:$server->{port}/status/sessions", 'sessions' );
  3192. }
  3193. }
  3194. } else {
  3195. Log3 $pname, 3, "$pname: no session in notifcation ";
  3196. }
  3197. if( !$session_info_requested ) {
  3198. if( $obj->{_children}[0]{state} eq 'playing'
  3199. || $obj->{_children}[0]{state} eq 'stopped' ) {
  3200. if( !$cname || $obj->{_children}[0]{key} ne ReadingsVal($cname, 'key', '' ) ) {
  3201. if( my $server = plex_serverOf($phash, $peerhost) ) {
  3202. plex_sendApiCmd( $phash, "http://$server->{address}:$server->{port}/status/sessions", 'sessions' );
  3203. }
  3204. }
  3205. }
  3206. }
  3207. } elsif( $obj->{type} eq 'status' ) {
  3208. $handled = 1;
  3209. #Log 1, Dumper $obj;
  3210. DoTrigger( $pname, "$obj->{_children}[0]{notificationName}: $obj->{_children}[0]{title}" );
  3211. }
  3212. }
  3213. if( $obj->{type} ) {
  3214. Log3 $pname, 4, "$pname: unhandled websocket text type: $obj->{type}: $data" if( !$handled );
  3215. } else {
  3216. Log3 $pname, 4, "$pname: unhandled websocket data: $data" if( !$handled );
  3217. }
  3218. } else {
  3219. Log3 $pname, 2, "$pname: unhandled websocket text $data";
  3220. }
  3221. } else {
  3222. Log3 $pname, 2, "$pname: unhandled websocket data: $data";
  3223. }
  3224. } while( $hash->{buf} && !$close );
  3225. } elsif( $buf =~ m'^HTTP/1.1 101 Switching Protocols'i ) {
  3226. $hash->{websocket} = 1;
  3227. my $buf = plex_msg2hash($buf, 1);
  3228. Log3 $pname, 3, "$pname: notification websocket: Switching Protocols ok";
  3229. } else {
  3230. $close = 1;
  3231. Log3 $pname, 2, "$pname: notification websocket: Switching Protocols failed";
  3232. }
  3233. if( $close ) {
  3234. my $phash = $hash->{phash};
  3235. plex_closeSocket( $hash );
  3236. delete($phash->{helper}{websockets}{$hash->{machineIdentifier}});
  3237. delete($phash->{servers}{$hash->{address}}{sessions});
  3238. delete($defs{$name});
  3239. }
  3240. return undef;
  3241. } elsif ( $hash->{phash} ) {
  3242. my $phash = $hash->{phash};
  3243. my $pname = $hash->{PNAME};
  3244. if( $phash->{helper}{timelineListener} == $hash ) {
  3245. my @clientinfo = $hash->{CD}->accept();
  3246. if( !@clientinfo ) {
  3247. Log3 $name, 1, "Accept failed ($name: $!)" if($! != EAGAIN);
  3248. return undef;
  3249. }
  3250. $hash->{CONNECTS}++;
  3251. my ($port, $iaddr) = sockaddr_in($clientinfo[1]);
  3252. my $caddr = inet_ntoa($iaddr);
  3253. my $chash = plex_newChash( $phash, $clientinfo[0],
  3254. {NAME=>"$name:$port", STATE=>'listening'} );
  3255. $chash->{buf} = '';
  3256. $hash->{connections}{$chash->{NAME}} = $chash;
  3257. Log3 $name, 5, "$name: timeline sender $caddr connected to $port";
  3258. return;
  3259. }
  3260. $len = sysread($hash->{CD}, $buf, 10240);
  3261. #Log 1, "2:$len: $buf";
  3262. do {
  3263. my $close = 1;
  3264. if( $len ) {
  3265. $hash->{buf} .= $buf;
  3266. return if $hash->{buf} !~ m/^(.*?)\r?\n\r?\n(.*)?$/s;
  3267. my $header = $1;
  3268. my $body = $2;
  3269. my $content_length;
  3270. my $length = length($body);
  3271. if( $header =~ m/Content-Length:\s*(\d+)/si ) {
  3272. $content_length = $1;
  3273. return if( $length < $content_length );
  3274. if( $header !~ m/Connection: Close/si ) {
  3275. $close = 0;
  3276. Log3 $pname, 5, "$name: keepalive";
  3277. #syswrite($hash->{CD}, "HTTP/1.1 200 OK\r\nConnection: Keep-Alive\r\nContent-Length: 0\r\n\r\n" );
  3278. if( $length > $content_length ) {
  3279. $buf = substr( $body, $content_length );
  3280. $hash->{buf} = "$header\r\n\r\n". substr( $body, 0, $content_length );
  3281. } else {
  3282. $buf ='';
  3283. }
  3284. if( !$hash->{machineIdentifier} && $header =~ m/X-Plex-Client-Identifier:\s*(.*)/i ) {
  3285. $hash->{machineIdentifier} = $1;
  3286. }
  3287. } else {
  3288. Log3 $pname, 5, "$name: close";
  3289. #syswrite($hash->{CD}, "HTTP/1.1 200 OK\r\nConnection: Close\r\n\r\n" );
  3290. }
  3291. } elsif( $length == 0 && $header =~ m/^GET/ ) {
  3292. $buf = '';
  3293. } else {
  3294. return;
  3295. }
  3296. }
  3297. Log3 $pname, 4, "$name: disconnected" if( !$len );
  3298. my $ret;
  3299. $ret = plex_Parse($phash, $hash->{buf}) if( $hash->{buf} );
  3300. if( $len ) {
  3301. my $add_header;
  3302. if( !$ret || $ret !~ m/^HTTP/si ) {
  3303. $add_header .= "HTTP/1.1 200 OK\r\n";
  3304. }
  3305. if( !$ret || $ret !~ m/Connection:/si ) {
  3306. if( $close ) {
  3307. $add_header .= "Connection: Close\r\n";
  3308. } else {
  3309. $add_header .= "Connection: Keep-Alive\r\n";
  3310. }
  3311. }
  3312. if( !$ret ) {
  3313. $add_header .= "Content-Length: 0\r\n";
  3314. }
  3315. if( $add_header ) {
  3316. Log3 $pname, 5, "$name: add header: $add_header";
  3317. syswrite($hash->{CD}, $add_header);
  3318. }
  3319. if( $ret ) {
  3320. syswrite($hash->{CD}, $ret);
  3321. if( $ret !~ m/Connection: Close/si ) {
  3322. $close = 0;
  3323. Log3 $pname, 5, "$name: keepalive";
  3324. }
  3325. } else {
  3326. syswrite($hash->{CD}, "\r\n" );
  3327. }
  3328. }
  3329. $hash->{buf} = $buf;
  3330. $buf = '';
  3331. if( $close || !$len ) {
  3332. plex_closeSocket( $hash );
  3333. delete($defs{$name});
  3334. delete($hash->{phash}{helper}{timelineListener}{connections}{$hash->{NAME}});
  3335. return;
  3336. }
  3337. } while( $hash->{buf} );
  3338. }
  3339. return undef;
  3340. }
  3341. sub
  3342. plex_publishToSonos(;$$$)
  3343. {
  3344. my ($hash,$service,$player) = @_;
  3345. $hash = $modules{plex}{defptr}{MASTER} if( !$hash && defined($modules{plex}{defptr}{MASTER}) );
  3346. $hash = $defs{$hash} if( ref($hash) ne 'HASH' );
  3347. return 'no plex device found' if( !$hash );
  3348. my $name = $hash->{NAME};
  3349. return 'no timeline listener started' if( !$hash->{helper}{timelineListener} );
  3350. $service = 'PLEX' if( !$service );
  3351. my $i = 0;
  3352. foreach my $d (devspec2array("TYPE=SONOSPLAYER")) {
  3353. next if( $player && $d !~ /$player/ );
  3354. my $location = ReadingsVal($d,'location',undef);
  3355. my $ip = ($location =~ m/https?:..([\d.]*)/)[0];
  3356. next if( !$ip );
  3357. my $url = "http://$ip:1400/customsd";
  3358. Log3 $name, 4, "$name: requesting $url";
  3359. my $fhem_base_url = "http://$hash->{fhemIP}:$hash->{helper}{timelineListener}{PORT}";
  3360. my $data = plex_hash2form( { 'sid' => '246',
  3361. 'name' => $service,
  3362. 'uri' => "$fhem_base_url/SMAPI",
  3363. 'secureUri' => "$fhem_base_url/SMAPI",
  3364. 'pollInterval' => '1200',
  3365. 'authType' => 'Anonymous',
  3366. 'containerType' => 'MService',
  3367. #'presentationMapVersion' => '1',
  3368. #'presentationMapUri' => "$fhem_base_url/sonos/presentationMap.xml",
  3369. #'stringsVersion' => '5',
  3370. #'stringsUri' => "$fhem_base_url/sonos/strings.xml",
  3371. } );
  3372. $data .= "&caps=search";
  3373. $data .= "&caps=ucPlaylists";
  3374. $data .= "&caps=extendedMD";
  3375. my $param = {
  3376. url => $url,
  3377. method => 'POST',
  3378. timeout => 10,
  3379. noshutdown => 0,
  3380. hash => $hash,
  3381. key => 'publishToSonos',
  3382. player => $d,
  3383. data => $data,
  3384. };
  3385. $param->{cl} = $hash->{CL} if( ref($hash->{CL}) eq 'HASH' );
  3386. $param->{callback} = \&plex_parseHttpAnswer;
  3387. HttpUtils_NonblockingGet( $param );
  3388. ++$i;
  3389. }
  3390. if( !$i && $player ) {
  3391. my $url = "http://$player:1400/customsd";
  3392. Log3 $name, 4, "$name: requesting $url";
  3393. my $fhem_base_url = "http://$hash->{fhemIP}:$hash->{helper}{timelineListener}{PORT}";
  3394. my $data = plex_hash2form( { 'sid' => '246',
  3395. 'name' => $service,
  3396. 'uri' => "$fhem_base_url/SMAPI",
  3397. 'secureUri' => "$fhem_base_url/SMAPI",
  3398. 'pollInterval' => '1200',
  3399. 'authType' => 'Anonymous',
  3400. 'containerType' => 'MService',
  3401. #'presentationMapVersion' => '1',
  3402. #'presentationMapUri' => "$fhem_base_url/sonos/presentationMap.xml",
  3403. #'stringsVersion' => '5',
  3404. #'stringsUri' => "$fhem_base_url/sonos/strings.xml",
  3405. } );
  3406. $data .= "&caps=search";
  3407. $data .= "&caps=ucPlaylists";
  3408. $data .= "&caps=extendedMD";
  3409. my $param = {
  3410. url => $url,
  3411. method => 'POST',
  3412. timeout => 10,
  3413. noshutdown => 0,
  3414. hash => $hash,
  3415. key => 'publishToSonos',
  3416. player => $player,
  3417. data => $data,
  3418. };
  3419. $param->{cl} = $hash->{CL} if( ref($hash->{CL}) eq 'HASH' );
  3420. $param->{callback} = \&plex_parseHttpAnswer;
  3421. HttpUtils_NonblockingGet( $param );
  3422. ++$i;
  3423. }
  3424. return 'no sonos players found' if( !$i );
  3425. return "send SMAPI registration to $i players";
  3426. return undef;
  3427. }
  3428. 1;
  3429. =pod
  3430. =item summary control and receive events from PLEX players
  3431. =item summary_DE Steuern und &uuml;berwachen von PLEX Playern
  3432. =begin html
  3433. <a name="plex"></a>
  3434. <h3>plex</h3>
  3435. <ul>
  3436. This module allows you to control and receive events from plex.<br><br>
  3437. <br><br>
  3438. Notes:
  3439. <ul>
  3440. <li>IO::Socket::Multicast is needed to use server and client autodiscovery.</li>
  3441. <li>As far as possible alle get and set commands are non-blocking.
  3442. Any output is displayed asynchronous and is using fhemweb popup windows.</li>
  3443. </ul>
  3444. <br><br>
  3445. <a name="plex_Define"></a>
  3446. <b>Define</b>
  3447. <ul>
  3448. <code>define &lt;name&gt; plex [&lt;server&gt;]</code>
  3449. <br><br>
  3450. </ul>
  3451. <a name="plex_Set"></a>
  3452. <b>Set</b>
  3453. <ul>
  3454. <li>play [&lt;server&gt; [&lt;item&gt;]]<br>
  3455. </li>
  3456. <li>resume [&lt;server&gt;] &lt;item&gt;]<br>
  3457. </li>
  3458. <li>pause [&lt;type&gt;]</li>
  3459. <li>stop [&lt;type&gt;]</li>
  3460. <li>skipNext [&lt;type&gt;]</li>
  3461. <li>skipPrevious [&lt;type&gt;]</li>
  3462. <li>stepBack [&lt;type&gt;]</li>
  3463. <li>stepForward [&lt;type&gt;]</li>
  3464. <li>seekTo &lt;value&gt; [&lt;type&gt;]</li>
  3465. <li>volume &lt;value&gt; [&lt;type&gt;]</li>
  3466. <li>shuffle 0|1 [&lt;type&gt;]</li>
  3467. <li>repeat 0|1|2 [&lt;type&gt;]</li>
  3468. <li>mirror [&lt;server&gt;] &lt;item&gt;<br>
  3469. show preplay screen for &lt;item&gt;</li>
  3470. <li>home</li>
  3471. <li>music</li>
  3472. <li>showAccount<br>
  3473. display obfuscated user and password in cleartext</li>
  3474. <li>playlistCreate [&lt;server&gt;] &lt;name&gt;</li>
  3475. <li>playlistAdd [&lt;server&gt;] &lt;key&gt; &lt;keys&gt;</li>
  3476. <li>playlistRemove [&lt;server&gt;] &lt;key&gt; &lt;keys&gt;</li>
  3477. <li>unwatched [[&lt;server&gt;] &lt;items&gt;]</li>
  3478. <li>watched [[&lt;server&gt;] &lt;items&gt;]</li>
  3479. <li>autocreate &lt;machineIdentifier&gt;<br>
  3480. create device for remote/shared server</li>
  3481. </ul><br>
  3482. <a name="plex_Get"></a>
  3483. <b>Get</b>
  3484. <ul>
  3485. <li>[&lt;server&gt;] ls [&lt;path&gt;]<br>
  3486. browse the media library. eg:<br><br>
  3487. <b><code>get &lt;plex&gt; ls</code></b>
  3488. <pre> Plex Library
  3489. key type title
  3490. 1 artist Musik
  3491. 2 ...</pre><br>
  3492. <b><code>get &lt;plex&gt; ls /1</code></b>
  3493. <pre> Musik
  3494. key type title
  3495. all All Artists
  3496. albums By Album
  3497. collection By Collection
  3498. decade By Decade
  3499. folder By Folder
  3500. genre By Genre
  3501. year By Year
  3502. recentlyAdded Recently Added
  3503. search?type=9 Search Albums...
  3504. search?type=8 Search Artists...
  3505. search?type=10 Search Tracks...</pre><br>
  3506. <b><code>get &lt;plex&gt; ls /1/albums</code></b>
  3507. <pre> Musik ; By Album
  3508. key type title
  3509. /library/metadata/133999/children album ...
  3510. /library/metadata/134207/children album ...
  3511. /library/metadata/168437/children album ...
  3512. /library/metadata/82906/children album ...
  3513. ...</pre><br>
  3514. <b><code>get &lt;plex&gt; ls /library/metadata/133999/children</code></b>
  3515. <pre> ...</pre><br>
  3516. <br>if used from fhemweb album art can be displayed and keys and other items are klickable.<br><br>
  3517. </li>
  3518. <li>[&lt;server&gt;] search &lt;keywords&gt;<br>
  3519. search the media library for items that match &lt;keywords&gt;</li>
  3520. <li>[&lt;server&gt;] onDeck<br>
  3521. list the global onDeck items</li>
  3522. <li>[&lt;server&gt;] recentlyAdded<br>
  3523. list the global recentlyAdded items</li>
  3524. <li>[&lt;server&gt;] detail &lt;key&gt;<br>
  3525. show detail information for media item &lt;key&gt;</li>
  3526. <li>[&lt;server&gt;] playlists<br>
  3527. list playlists</li>
  3528. <li>[&lt;server&gt;] m3u [album]<br>
  3529. creates an album playlist in m3u format. can be used with other players like sonos.</li>
  3530. <li>[&lt;server&gt;] pls [album]<br>
  3531. creates an album playlist in pls format. can be used with other players like sonos.</li>
  3532. <li>clients<br>
  3533. list the known clients</li>
  3534. <li>servers<br>
  3535. list the known servers</li>
  3536. <li>pin<br>
  3537. get a pin for authentication at <a href="https://plex.tv/pin">https://plex.tv/pin</a></li>
  3538. </ul><br>
  3539. <a name="plex_Attr"></a>
  3540. <b>Attr</b>
  3541. <ul>
  3542. <li>httpPort</li>
  3543. <li>ignoredClients</li>
  3544. <li>ignoredServers</li>
  3545. <li>removeUnusedReadings</li>
  3546. <li>user</li>
  3547. <li>password<br>
  3548. user and password of a myPlex account. required if plex home is used. both are stored obfuscated</li>
  3549. </ul>
  3550. </ul><br>
  3551. =end html
  3552. =cut