73_MPD.pm 86 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532
  1. ################################################################
  2. #
  3. # $Id: 73_MPD.pm 13247 2017-01-26 20:20:08Z Wzut $
  4. #
  5. # (c) 2014 Copyright: Wzut
  6. # All rights reserved
  7. #
  8. # FHEM Forum : http://forum.fhem.de/index.php/topic,18517.msg400328.html#msg400328
  9. #
  10. # This code is free software; you can redistribute it and/or modify
  11. # it under the terms of the GNU General Public License as published by
  12. # the Free Software Foundation; either version 2 of the License, or
  13. # (at your option) any later version.
  14. # The GNU General Public License can be found at
  15. # http://www.gnu.org/copyleft/gpl.html.
  16. # A copy is found in the textfile GPL.txt and important notices to the license
  17. # from the author is found in LICENSE.txt distributed with these scripts.
  18. # This script is distributed in the hope that it will be useful,
  19. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. # GNU General Public License for more details.
  22. ################################################################
  23. # Version 1.5 26.01.17 add playlist bookmarks, change XML to JSON
  24. # Version 1.43 - 18.01.17 add channelUp and channelDown
  25. # Version 1.42 - 15.01.17 add Cover and playlist_json
  26. # Version 1.41 - 12.01.17 add rawTitle
  27. # Version 1.4 - 11.01.17 add mute, ctp, seekcur, Fix LWP:: , album cover
  28. # Version 1.32 - 03.01.17
  29. # Version 1.31 - 30.12.16
  30. # Version 1.3 - 14.12.16
  31. # Version 1.2 - 10.04.16
  32. # Version 1.1 - 03.02.16
  33. # Version 1.01 - 18.08.14
  34. # add set toggle command
  35. # Version 1.0 - 21.02.14
  36. # add german doc , readings & state times only on change, devStateIcon
  37. # Version 0.95 - 17.02.14
  38. # add command set IdleNow
  39. # Version 0.9 - 15.02.14
  40. # Version 0.8 - 01.02.14 , first version
  41. package main;
  42. use strict;
  43. use warnings;
  44. use Time::HiRes qw(gettimeofday);
  45. use URI::Escape;
  46. use POSIX;
  47. use Blocking; # http://www.fhemwiki.de/wiki/Blocking_Call
  48. use IO::Socket;
  49. use Getopt::Std;
  50. use HttpUtils;
  51. use HTML::Entities;
  52. use Cwd 'abs_path';
  53. use JSON;
  54. sub MPD_html($);
  55. my %gets = (
  56. "music:noArg" => "",
  57. "playlists:noArg" => "",
  58. "playlistinfo:noArg" => "",
  59. "statusRequest:noArg" => "",
  60. "currentsong:noArg" => "",
  61. "outputs:noArg" => "",
  62. "bookmarks:noArg" => ""
  63. );
  64. my %sets = (
  65. "play" => "",
  66. "clear:noArg" => "",
  67. "stop:noArg" => "",
  68. "pause:noArg" => "",
  69. "previous:noArg" => "",
  70. "next:noArg" => "",
  71. "random:noArg" => "",
  72. "repeat:noArg" => "",
  73. "volume:slider,0,1,100" => "",
  74. "volumeUp:noArg" => "",
  75. "volumeDown:noArg" => "",
  76. "playlist" => "",
  77. "playfile" => "",
  78. "updateDb:noArg" => "",
  79. "mpdCMD" => "",
  80. "reset:noArg" => "",
  81. "single:noArg" => "",
  82. "IdleNow:noArg" => "",
  83. "toggle:noArg" => "",
  84. "clear_readings:noArg" => "",
  85. "mute:on,off,toggle" => "",
  86. "seekcur" => "",
  87. "forward:noArg" => "",
  88. "rewind:noArg" => "",
  89. "channel" => "",
  90. "channelUp:noArg" => "",
  91. "channelDown:noArg" => "",
  92. "save_bookmark:noArg" => "",
  93. "load_bookmark" => "",
  94. );
  95. use constant clb => "command_list_begin\n";
  96. use constant cle => "status\nstats\ncurrentsong\ncommand_list_end";
  97. use constant lfm_artist => "http://ws.audioscrobbler.com/2.0/?method=artist.getinfo&format=json&api_key=";
  98. use constant lfm_album => "http://ws.audioscrobbler.com/2.0/?method=album.getinfo&format=json&api_key=";
  99. my @Cover;
  100. ###################################
  101. sub MPD_Initialize($)
  102. {
  103. my ($hash) = @_;
  104. my $name = $hash->{NAME};
  105. $hash->{GetFn} = "MPD_Get";
  106. $hash->{SetFn} = "MPD_Set";
  107. $hash->{DefFn} = "MPD_Define";
  108. $hash->{UndefFn} = "MPD_Undef";
  109. $hash->{ShutdownFn} = "MPD_Undef";
  110. $hash->{AttrFn} = "MPD_Attr";
  111. $hash->{AttrList} = "disable:0,1 password loadMusic:0,1 loadPlaylists:0,1 volumeStep:1,2,5,10 titleSplit:1,0 ".
  112. "timeout waits stateMusic:0,1 statePlaylists:0,1 lastfm_api_key image_size:-1,0,1,2,3 ".
  113. "cache artist_summary:0,1 artist_content:0,1 player:mpd,mopidy,forked-daapd ".
  114. "unknown_artist_image bookmarkDir autoBookmark:0,1 seekStepThreshold seekStep seekStepSmall ".
  115. "no_playlistcollection:0,1 ".$readingFnAttributes;
  116. $hash->{FW_summaryFn} = "MPD_summaryFn";
  117. }
  118. sub MPD_updateConfig($)
  119. {
  120. # this routine is called 5 sec after the last define of a restart
  121. # this gives FHEM sufficient time to fill in attributes
  122. my ($hash) = @_;
  123. my $name = $hash->{NAME};
  124. if (!$init_done)
  125. {
  126. RemoveInternalTimer($hash);
  127. InternalTimer(gettimeofday()+5,"MPD_updateConfig", $hash, 0);
  128. return;
  129. }
  130. my $error;
  131. $hash->{".playlist"} = "";
  132. $hash->{".playlists"} = "";
  133. $hash->{".musiclist"} = "";
  134. $hash->{".music"} = "";
  135. $hash->{".outputs"} = "";
  136. $hash->{".lasterror"} = "";
  137. $hash->{PRESENCE} = "absent";
  138. $hash->{".volume"} = -1;
  139. $hash->{".artist"} = "";
  140. $hash->{".album"} = "";
  141. $hash->{".playlist_crc"} = 0;
  142. $hash->{helper}{playlistcollection}{val} = -1;
  143. $hash->{".password"} = AttrVal($name, "password", "");
  144. $hash->{TIMEOUT} = AttrVal($name, "timeout", 2);
  145. $hash->{".sMusicL"} = AttrVal($name, "stateMusic", 1);
  146. $hash->{".sPlayL"} = AttrVal($name, "statePlaylists", 1);
  147. $hash->{".apikey"} = AttrVal($name, "lastfm_api_key", "f3a26c7c8b4c4306bc382557d5c04ad5");
  148. $hash->{".player"} = AttrVal($name, "player", "mpd");
  149. delete($gets{"music:noArg"}) if ($hash->{".player"} eq "mopidy");
  150. ## kommen wir via reset Kommando ?
  151. if ($hash->{".reset"})
  152. {
  153. $hash->{".reset"} = 0;
  154. RemoveInternalTimer($hash);
  155. if(defined($hash->{IPID}))
  156. {
  157. BlockingKill($hash->{helper}{RUNNING_PID});
  158. Log3 $name,4, "$name, Idle Kill PID : ".$hash->{IPID};
  159. delete $hash->{helper}{RUNNING_PID};
  160. delete $hash->{IPID};
  161. Log3 $name,4,"$name, Reset done";
  162. }
  163. }
  164. if (IsDisabled($name))
  165. {
  166. readingsSingleUpdate($hash,"state","disabled",1);
  167. return undef;
  168. }
  169. MPD_ClearReadings($hash); # beim Starten etwas aufräumen
  170. readingsBeginUpdate($hash);
  171. readingsBulkUpdate($hash,"playlist_json","");
  172. readingsBulkUpdate($hash,"playlist_num","-1");
  173. readingsBulkUpdate($hash,"playlistname","");
  174. readingsBulkUpdate($hash,"playlistinfo","");
  175. readingsBulkUpdate($hash,"playlistcollection","");
  176. readingsEndUpdate($hash,0);
  177. MPD_Outputs_Status($hash);
  178. mpd_cmd($hash, clb.cle);
  179. if ($hash->{".volume"} eq "0")
  180. { # ist Mute aktiv oder soll sie mit Absicht 0 sein ?
  181. # neuen Restore Wert zu Sicherheit erfinden
  182. $hash->{".mute"} = 50;
  183. }
  184. else
  185. { # wir haben irgend eine Lautstärke
  186. $hash->{".mute"} = -1;
  187. if (ReadingsVal($name,"mute","on") eq "on")
  188. { # das passt so nicht zusammen !
  189. readingsSingleUpdate($hash,"mute","off",1);
  190. }
  191. }
  192. if ((AttrVal($name, "icon_size", -1) > -1) && (AttrVal($name, "cache", "") ne ""))
  193. {
  194. my $cache = AttrVal($name, "cache", "");
  195. unless(-e ("./www/".$cache) or mkdir ("./www/".$cache))
  196. {
  197. #Verzeichnis anlegen gescheitert
  198. Log3 $name,3,"$name, Could not create directory: www/$cache";
  199. }
  200. #else {Log3 $name,4,"$name, lastfm cache = www/$cache";}
  201. }
  202. if (MPD_try_idle($hash))
  203. {
  204. # Playlisten und Musik Dir laden ?
  205. # nicht bei Player mopidy, listall wird von ihm nicht unterstützt !
  206. if ((AttrVal($name, "loadMusic", "1") eq "1") && !$error && ($hash->{".player"} ne "mopidy"))
  207. {
  208. $error = mpd_cmd($hash, "i|listall|music");
  209. Log3 $name,3,"$name, error loading music -> $error" if ($error);
  210. readingsSingleUpdate($hash,"last_error",$error,1) if ($error);
  211. }
  212. if ((AttrVal($name, "loadPlaylists", "1") eq "1") && !$error)
  213. {
  214. #$error = mpd_cmd($hash, "i|lsinfo|playlists");
  215. $error = mpd_cmd($hash, "i|listplaylists|playlists");
  216. Log3 $name,3,"$name, error loading playlists -> $error" if ($error);
  217. readingsSingleUpdate($hash,"last_error",$error,1) if ($error);
  218. }
  219. }
  220. else { readingsSingleUpdate($hash,"state","error",1);}
  221. return undef;
  222. }
  223. sub MPD_Define($$)
  224. {
  225. my ($hash, $def) = @_;
  226. my $name = $hash->{NAME};
  227. my @a = split("[ \t][ \t]*", $def);
  228. return "Usage: define <name> $name [<MPD ip-address>] [<MPD port-nr>]" if(int(@a) > 4);
  229. $hash->{HOST} = (defined($a[2])) ? $a[2] : "127.0.0.1";
  230. $hash->{PORT} = (defined($a[3])) ? $a[3] : "6600" ;
  231. $hash->{".reset"} = 0;
  232. Log3 $name,3,"$name, Device defined.";
  233. readingsSingleUpdate($hash,"state","defined",1);
  234. $attr{$name}{devStateIcon} = 'play:rc_PLAY:stop stop:rc_STOP:play pause:rc_PAUSE:pause error:icoBlitz' unless (exists($attr{$name}{devStateIcon}));
  235. $attr{$name}{icon} = 'it_radio' unless (exists($attr{$name}{icon}));
  236. $attr{$name}{titleSplit} = '1' unless (exists($attr{$name}{titleSplit}));
  237. $attr{$name}{player} = 'mpd' unless (exists($attr{$name}{player}));
  238. $attr{$name}{loadPlaylists}= '1' unless (exists($attr{$name}{loadPlaylists}));
  239. $attr{$name}{unknown_artist_image} = '/fhem/icons/1px-spacer' unless (exists($attr{$name}{unknown_artist_image}));
  240. #$attr{$name}{cache} = 'lfm' unless (exists($attr{$name}{cache}));
  241. #$attr{$name}{loadMusic} = '1' unless (exists($attr{$name}{loadMusic})) && ($attr{$name}{player} ne 'mopidy');
  242. #DevIo_CloseDev($hash); das wird irgendwann auch mal kommen ... :)
  243. $hash->{DeviceName} = $hash->{HOST}.":".$hash->{PORT};
  244. RemoveInternalTimer($hash);
  245. InternalTimer(gettimeofday()+5, "MPD_updateConfig", $hash, 0);
  246. return undef;
  247. }
  248. sub MPD_Undef ($$)
  249. {
  250. my ($hash, $arg) = @_;
  251. RemoveInternalTimer($hash);
  252. if(defined($hash->{helper}{RUNNING_PID}))
  253. {
  254. BlockingKill($hash->{helper}{RUNNING_PID});
  255. }
  256. return undef;
  257. }
  258. sub MPD_Attr (@)
  259. {
  260. my ($cmd, $name, $attrName, $attrVal) = @_;
  261. my $hash = $defs{$name};
  262. my $error;
  263. if ($cmd eq "set")
  264. {
  265. if ($attrName eq "timeout")
  266. {
  267. if (int($attrVal) < 1) {$attrVal = 1;}
  268. $hash->{TIMEOUT} = $attrVal;
  269. $_[3] = $attrVal;
  270. }
  271. elsif ($attrName eq "password")
  272. {
  273. $hash->{".password"} = $attrVal;
  274. $_[3] = $attrVal;
  275. }
  276. elsif (($attrName eq "disable") && ($attrVal == 1))
  277. {
  278. readingsSingleUpdate($hash,"state","disabled",1);
  279. $_[3] = $attrVal;
  280. }
  281. elsif (($attrName eq "disable") && ($attrVal == 0))
  282. {
  283. $attr{$name}{disable} = $attrVal;
  284. readingsSingleUpdate($hash,"state","reset",1);
  285. $hash->{".reset"} = 1;
  286. MPD_updateConfig($hash);
  287. }
  288. elsif ($attrName eq "statePlaylists")
  289. {
  290. $attr{$name}{statePlaylists} = $attrVal;
  291. $hash->{".sPlayL"}=$attrVal;
  292. }
  293. elsif ($attrName eq "stateMusic")
  294. {
  295. $attr{$name}{stateMusic} = $attrVal;
  296. $hash->{".sMusicL"}=$attrVal;
  297. }
  298. elsif ($attrName eq "player")
  299. {
  300. $attr{$name}{player} = $attrVal;
  301. $hash->{".player"}=$attrVal;
  302. }
  303. elsif ($attrName eq "bookmarkDir")
  304. {
  305. my $abs_path = abs_path($attrVal);
  306. unless(-e ($abs_path ) or mkdir ($abs_path ))
  307. {
  308. $error = "could not access or create bookmark directory $abs_path";
  309. #Verzeichnis anlegen gescheitert
  310. Log3 $name,3,"$name, error $error";
  311. readingsSingleUpdate($hash,"last_error",$error,1);
  312. return $error;
  313. }
  314. $_[3] = $abs_path; # Absoluten Pfad im Attribut speichern.
  315. }
  316. elsif ($attrName eq "cache")
  317. {
  318. unless(-e ("./www/".$attrVal) or mkdir ("./www/".$attrVal))
  319. {
  320. $error = "could not access or create directory: www/$attrVal";
  321. #Verzeichnis anlegen gescheitert
  322. Log3 $name,3,"$name, error $error";
  323. readingsSingleUpdate($hash,"last_error",$error,1);
  324. return $error;
  325. }
  326. $_[3] = $attrVal;
  327. }
  328. }
  329. elsif ($cmd eq "del")
  330. {
  331. if ($attrName eq "disable")
  332. {
  333. $attr{$name}{disable} = 0;
  334. readingsSingleUpdate($hash,"state","reset",1);
  335. $hash->{".reset"}=1;
  336. MPD_updateConfig($hash);
  337. }
  338. elsif ($attrName eq "statePlaylists") { $hash->{".sPlayL"} = 1; }
  339. elsif ($attrName eq "stateMusic") { $hash->{".sMusicL"} = 1; }
  340. elsif ($attrName eq "player") { $hash->{".player"} = "mpd"; }
  341. }
  342. return undef;
  343. }
  344. sub MPD_ClearReadings($)
  345. {
  346. my ($hash)= @_;
  347. readingsBeginUpdate($hash);
  348. if ($hash->{".player"} eq "forked-daapd")
  349. {
  350. readingsBulkUpdate($hash,"albumartistsort","");
  351. readingsBulkUpdate($hash,"artistsort","");
  352. }
  353. #readingsBulkUpdate($hash,"albumartist","");
  354. readingsBulkUpdate($hash,"Album","");
  355. readingsBulkUpdate($hash,"Artist","");
  356. readingsBulkUpdate($hash,"file","");
  357. readingsBulkUpdate($hash,"Genre","");
  358. readingsBulkUpdate($hash,"Last-Modified","");
  359. readingsBulkUpdate($hash,"Title","");
  360. readingsBulkUpdate($hash,"Name","");
  361. readingsBulkUpdate($hash,"Date","");
  362. readingsBulkUpdate($hash,"Track","");
  363. readingsBulkUpdate($hash,"Cover","");
  364. readingsBulkUpdate($hash,"artist_summary","") if (AttrVal($hash->{NAME}, "artist_summary",""));
  365. readingsBulkUpdate($hash,"artist_content","") if (AttrVal($hash->{NAME}, "artist_content",""));
  366. readingsEndUpdate($hash, 1);
  367. return;
  368. }
  369. sub MPD_Set($@)
  370. {
  371. my ($hash, @a)= @_;
  372. my $name= $hash->{NAME};
  373. my $ret ;
  374. my $channel_cmd = 0;
  375. return join(" ", sort keys %sets) if(@a < 2);
  376. return undef if(IsDisabled($name));
  377. my $cmd = $a[1];
  378. return join(" ", sort keys %sets) if ($cmd eq "?");
  379. if ($cmd eq "mpdCMD")
  380. {
  381. my $sub;
  382. shift @a;
  383. shift @a;
  384. $sub = join (" ", @a);
  385. return $name." ".$sub.":\n".mpd_cmd($hash, "i|$sub|x");
  386. }
  387. my $subcmd = (defined($a[2])) ? $a[2] : "";
  388. return undef if ($subcmd eq '---'); # erster Eintrag im select Feld ignorieren
  389. my $step = int(AttrVal($name, "volumeStep", 5)); # vllt runtersetzen auf default = 2 ?
  390. my $vol_now = int($hash->{".volume"});
  391. my $vol_new;
  392. if ($cmd eq "reset") { $hash->{".reset"} = 1; MPD_updateConfig($hash); return undef;}
  393. if ($cmd eq "pause") { $ret = mpd_cmd($hash, clb."pause\n".cle); return $ret; }
  394. if ($cmd eq "update") { $ret = mpd_cmd($hash, clb."update\n".cle); return $ret; }
  395. if ($cmd eq "stop")
  396. {
  397. readingsBeginUpdate($hash);
  398. readingsBulkUpdate($hash,"artist_image",AttrVal($name,"unknown_artist_image","/fhem/icons/1px-spacer"));
  399. readingsBulkUpdate($hash,"artist_image_html","");
  400. readingsBulkUpdate($hash,"album_image",AttrVal($name,"unknown_artist_image","/fhem/icons/1px-spacer"));
  401. readingsBulkUpdate($hash,"album_image_html","");
  402. readingsBulkUpdate($hash,"audio","");
  403. readingsBulkUpdate($hash,"bitrate","");
  404. readingsBulkUpdate($hash,"rawTitle","");
  405. readingsBulkUpdate($hash,"Album","");
  406. readingsBulkUpdate($hash,"Track","");
  407. readingsBulkUpdate($hash,"Cover","");
  408. readingsBulkUpdate($hash,"elapsed","");
  409. readingsEndUpdate($hash, 1);
  410. $ret = mpd_cmd($hash, clb."stop\n".cle);
  411. return $ret;
  412. }
  413. if ($cmd eq "toggle")
  414. {
  415. $ret = mpd_cmd($hash, clb."play\n".cle) if (($hash->{STATE} eq "stop") || ($hash->{STATE} eq "pause"));
  416. if ($hash->{STATE} eq "play")
  417. {
  418. readingsBeginUpdate($hash);
  419. readingsBulkUpdate($hash,"artist_image",AttrVal($name,"unknown_artist_image","/fhem/icons/1px-spacer"));
  420. readingsBulkUpdate($hash,"artist_image_html","");
  421. readingsBulkUpdate($hash,"album_image",AttrVal($name,"unknown_artist_image","/fhem/icons/1px-spacer"));
  422. readingsBulkUpdate($hash,"album_image_html","");
  423. readingsBulkUpdate($hash,"Cover","");
  424. readingsEndUpdate($hash, 1);
  425. $ret = mpd_cmd($hash, clb."stop\n".cle);
  426. }
  427. }
  428. if ($cmd eq "previous")
  429. {
  430. if (ReadingsNum($name,"song",0) > 0)
  431. {
  432. MPD_ClearReadings($hash);
  433. return mpd_cmd($hash, clb."previous\n".cle);
  434. }
  435. else { return undef; }
  436. }
  437. if ($cmd eq "next")
  438. {
  439. if (ReadingsNum($name,"nextsong",0) != ReadingsNum($name,"song",0))
  440. #if ($hash->{READINGS}{"nextsong"}{VAL} != $hash->{READINGS}{"song"}{VAL})
  441. {
  442. MPD_ClearReadings($hash);
  443. return mpd_cmd($hash, clb."next\n".cle);
  444. }
  445. else { return undef; }
  446. }
  447. if ($cmd eq "random")
  448. {
  449. my $rand = ($hash->{READINGS}{random}{VAL}) ? "0" : "1";
  450. $ret = mpd_cmd($hash, clb."random $rand\n".cle);
  451. }
  452. if ($cmd eq "repeat")
  453. {
  454. my $rep = ($hash->{READINGS}{repeat}{VAL}) ? "0" : "1";
  455. $ret = mpd_cmd($hash, clb."repeat $rep\n".cle);
  456. }
  457. if ($cmd eq "single")
  458. {
  459. my $single = ($hash->{READINGS}{single}{VAL}) ? "0" : "1";
  460. $ret = mpd_cmd($hash, clb."single $single\n".cle);
  461. }
  462. if ($cmd eq "clear")
  463. {
  464. MPD_ClearReadings($hash);
  465. $ret = mpd_cmd($hash, clb."clear\n".cle);
  466. $hash->{".music"} = "";
  467. $hash->{".playlist"} = "";
  468. }
  469. if ($cmd eq "volume")
  470. {
  471. if (int($subcmd) > 100) { $vol_new = "100"; }
  472. elsif (int($subcmd) < 0) { $vol_new = "0"; }
  473. else { $vol_new = $subcmd; }
  474. # sollte nun zwischen 0 und 100 sein
  475. }
  476. if ($cmd eq "volumeUp") { $vol_new = (($vol_now + $step) <= 100) ? $vol_now+$step : "100"; }
  477. if ($cmd eq "volumeDown") { $vol_new = (($vol_now - $step) >= 0) ? $vol_now-$step : " 0"; }
  478. if ($cmd eq "mute")
  479. {
  480. my $mute_state = ReadingsVal($name,"mute","off");
  481. my $mute = $mute_state;
  482. if (($subcmd eq "on") && ($mute_state eq "off")){ $vol_new = "0"; $hash->{".mute"} = $vol_now; $mute="on"; }
  483. elsif (($subcmd eq "off") && ($mute_state eq "on")) { $vol_new = $hash->{".mute"}; $hash->{".mute"} = -1; $mute="off"; }
  484. elsif ($subcmd eq "toggle")
  485. {
  486. if ($mute_state eq "on")
  487. { $vol_new = $hash->{".mute"}; $hash->{".mute"} = -1; $mute="off";}
  488. else
  489. { $vol_new = "0"; $hash->{".mute"} = $vol_now; $mute="on";}
  490. }
  491. readingsSingleUpdate($hash,"mute",$mute,1) if ($mute ne $mute_state);
  492. }
  493. # muessen wir die Laustärke verändern ?
  494. if (defined($vol_new))
  495. {
  496. $ret = mpd_cmd($hash, clb."setvol $vol_new\n".cle);
  497. }
  498. # einfaches Play bzw Play Listenplatz Nr. ?
  499. if ($cmd eq "play")
  500. {
  501. MPD_ClearReadings($hash);
  502. $ret = mpd_cmd($hash,clb."play $subcmd\n".cle);
  503. }
  504. if($cmd eq "forward" || $cmd eq "rewind")
  505. {
  506. my ($elapsed,$total) = split (":",ReadingsVal($name,"time",""));
  507. $total=int($total);
  508. return undef if( !defined($total) || $total <= 0);
  509. my $percent = $elapsed / $total;
  510. my $step = 0.01*(0.01*AttrVal($name,"seekStepThreshold",0) > $percent ? AttrVal($name,"seekStepSmall",3) : AttrVal($name,"seekStep",7));
  511. $percent +=$step if $cmd eq "forward";
  512. $percent -=$step if $cmd eq "rewind";
  513. $percent = 0 if $percent < 0;
  514. $percent = 0.99 if $percent > 0.99;
  515. $cmd = "seekcur";
  516. $subcmd=int($percent*$total);
  517. }
  518. if ($cmd eq "seekcur")
  519. {
  520. if ((int($hash->{SUBVERSION}) < 20) && (AttrVal($name,"player","mpd") eq "mpd"))
  521. {
  522. $ret = "command $cmd needs a MPD version of 0.20.0 or greater ! (is ".$hash->{VERSION}.")";
  523. Log3 $name,3,"$name,$ret";
  524. readingsSingleUpdate($hash,"last_error",$ret,1);
  525. }
  526. else
  527. {
  528. if($subcmd=~/^(?:(?:([01]?\d|2[0-3]):)?([0-5]?\d):)?([0-5]?\d)$/) # Matches valid time given as [[hh:]mm:]ss
  529. {
  530. $subcmd=(defined($1) ? $1 : 0)*3600+(defined($2) ? $2 : 0)*60+$3; # Sekunden ausrechnen
  531. }
  532. else { $subcmd--; $subcmd++; } # sicherstellen das subcmd numerisch ist
  533. if ( $subcmd > 0 )
  534. { $ret = mpd_cmd($hash,clb."seekcur $subcmd\n".cle) ; } # ungetestet !
  535. else { $ret = undef; }
  536. }
  537. }
  538. if ($cmd eq "IdleNow")
  539. {
  540. return "$name: sorry, a Idle process is always running with pid ".$hash->{IPID} if(defined($hash->{IPID}));
  541. MPD_try_idle($hash);
  542. return undef;
  543. }
  544. if ($cmd eq "clear_readings")
  545. {
  546. MPD_ClearReadings($hash);
  547. return undef;
  548. }
  549. # die ersten beiden brauchen wir nicht mehr
  550. shift @a;
  551. shift @a;
  552. # den Rest als ein String
  553. $subcmd = join(" ",@a);
  554. if ($cmd eq "save_bookmark")
  555. {
  556. return "unknown playlist !" if ($hash->{".playlist"} eq "");
  557. return "you can't save bookmarks on unknown playlists or radio streams !" if (ReadingsVal($name,"currentTrackProvider","Radio") eq "Radio");
  558. my $bm_dir = AttrVal($name, "bookmarkDir","");
  559. return "please set attribute bookmarkDir first, saving is disabled !" if ($bm_dir eq "");
  560. my $state;
  561. $state->{playlistname} = $hash->{".playlist"};
  562. $state->{songnumber} = ReadingsNum($name,"Pos",0);
  563. $state->{songposition} = ReadingsVal($name,"time","")=~/^(\d+):\d+$/ ? $1 : 0; # get elapsed seconds from time reading
  564. my $fname = $hash->{".playlist"};
  565. #$fname =~ s/[^A-Za-z0-9\-\.]//g; # ensure to use only valid characters for filenames
  566. $fname = $bm_dir."/".$fname;
  567. if (!open (FILE , "> ".$fname))
  568. {
  569. $ret = "save_bookmark error saving $fname : ".$!;
  570. readingsSingleUpdate($hash,"last_error",$ret,1);
  571. Log3 $name, 2, "$name, $ret";
  572. }
  573. else
  574. {
  575. print FILE encode_json($state);
  576. close(FILE);
  577. Log3 $name, 4, "$name, save_bookmark ".$subcmd."saved bookmark $fname";
  578. }
  579. if ($ret)
  580. {
  581. Log3 $name, 3, "$name, $ret";
  582. readingsSingleUpdate($hash,"last_error",$ret,1);
  583. return $ret ;
  584. }
  585. }
  586. if ($cmd eq "load_bookmark")
  587. {
  588. return "unknown playlist" if (($hash->{".playlist"} eq "") && ($subcmd eq ""));
  589. return "you can't load bookmarks for radio streams !" if ((ReadingsVal($name,"currentTrackProvider","Radio") eq "Radio") && ($subcmd eq ""));
  590. my $bm_dir = AttrVal($name, "bookmarkDir","");
  591. return format_get_output("Get Bookmark","attribute bookmarkDir not set, loading disabled") if ($bm_dir eq "");
  592. my $state;
  593. my $data;
  594. my $fname = ($subcmd eq "") ? $hash->{".playlist"} : $subcmd;
  595. #$fname =~ s/[^A-Za-z0-9\-\.]//g; # ensure to use only valid characters for filenames
  596. $fname = $bm_dir."/".$fname;
  597. if (-e $fname) # gibt es überhaupt eine Bookmark ?
  598. {
  599. if (!open (FILE , $fname))
  600. {
  601. $ret = "error reading $fname: ".$!;
  602. Log3 $name, 4, "$name, $ret";
  603. readingsSingleUpdate($hash,"last_error",$ret,1);
  604. }
  605. else
  606. {
  607. while(<FILE>){ $data = $data.$_;}
  608. close (FILE);
  609. eval { $state = decode_json($data); 1; } or do
  610. {
  611. $ret = "invalid content in playlist bookmark file";
  612. Log3 $name, 2, "$name, $ret";
  613. readingsSingleUpdate($hash,"last_error",$ret,1);
  614. };
  615. }
  616. if (!$ret) # bis jetzt wohl ohne Fehler ...
  617. {
  618. $state->{songnumber} += 0; # ensure it is numeric
  619. $state->{songposition}+= 0;
  620. # wechsele auf die neue Playliste ?
  621. Log3 $name, 4, "$name, load_bookmark $fname - Song:".$state->{songnumber}." - Pos:".$state->{songposition};
  622. if ($subcmd ne "")
  623. {
  624. $subcmd = $state->{playlistname}."|".$state->{songnumber}."|".$state->{songposition};
  625. $cmd = "playlist";
  626. Log3 $name, 4, "$name, load_bookmark new cmd -> playlist ".$subcmd;
  627. }
  628. else
  629. {
  630. Log3 $name, 4, "$name, load_bookmark new cmd -> play & seekcur";
  631. $ret = mpd_cmd($hash,"play ".$state->{songnumber});
  632. $ret .= mpd_cmd($hash,"seekcur ".$state->{songposition}) if (($state->{songposition} > 9) && (int($hash->{SUBVERSION}) > 19));
  633. }
  634. }
  635. } else { $ret = "no bookmark $fname found"; }
  636. if ($ret)
  637. {
  638. Log3 $name, 3, "$name, $ret";
  639. readingsSingleUpdate($hash,"last_error",$ret,1);
  640. return $ret ;
  641. }
  642. }
  643. if ($cmd eq "channel")
  644. {
  645. if ( $subcmd <= $hash->{helper}{playlistcollection}{val})
  646. {
  647. $cmd = "playlist";
  648. $subcmd = $hash->{helper}{playlistcollection}{$subcmd};
  649. $channel_cmd = 1;
  650. }
  651. }
  652. if ($cmd eq "channelUp")
  653. {
  654. my $i = ReadingsNum($name,"playlist_num",-1);
  655. $i++;
  656. if ($i <= $hash->{helper}{playlistcollection}{val})
  657. {
  658. $cmd = "playlist";
  659. $subcmd = $hash->{helper}{playlistcollection}{$i};
  660. $channel_cmd = 1;
  661. }
  662. }
  663. if ($cmd eq "channelDown")
  664. {
  665. my $i = ReadingsNum($name,"playlist_num",0);
  666. $i--;
  667. if ($i > -1)
  668. {
  669. $cmd = "playlist";
  670. $subcmd = $hash->{helper}{playlistcollection}{$i};
  671. $channel_cmd = 1;
  672. }
  673. }
  674. if ($cmd eq "playlist")
  675. {
  676. return "$name : no playlist name !" if (!$subcmd);
  677. my ($list,$song,$pos) = split("\\|",$subcmd);
  678. $song = "0" if (!$song);
  679. $pos = "0" if (!$pos);
  680. my $error;
  681. my $nr = -1;
  682. # ToDo : prüfen ob es Liste überhaupt gibt !
  683. # nicht schön aber vermeidet den Fehler
  684. # PERL WARNING: main::MPD_Set() called too early to check prototype at ./FHEM/73_MPD.pm line 771, <$fh> line 412
  685. MPD_SaveBookmark($hash) if(AttrVal($name,"autoBookmark","0") eq "1");
  686. MPD_ClearReadings($hash);
  687. $hash->{".music"} = "";
  688. $hash->{".playlist"} = $list; # interne Playlisten Verwaltung
  689. readingsSingleUpdate($hash,"playlistname",$subcmd,1);
  690. $ret = mpd_cmd($hash, clb."stop\nclear\nload \"$list\"\nplay $song\n".cle);
  691. if (int($pos > 9))
  692. {
  693. $ret .= mpd_cmd($hash,"seekcur ".$pos) if (int($hash->{SUBVERSION}) > 19);
  694. }
  695. # welche Listen Nr ?
  696. for (my $i=0; $i <= $hash->{helper}{playlistcollection}{val}; $i++)
  697. {
  698. $nr = $i if ($hash->{helper}{playlistcollection}{$i} eq $subcmd);
  699. }
  700. readingsSingleUpdate($hash,"playlist_num",$nr,1) if ($nr > -1);
  701. my $json = ReadingsVal($name,"playlist_json","");
  702. if ($json ne "")
  703. {
  704. undef(@Cover);
  705. my $result;
  706. eval {
  707. $result = decode_json($json);
  708. 1;
  709. } or do
  710. {
  711. $error = "invalid playlist_json: $json";
  712. Log3 $name, 3, "$name, $error";
  713. readingsSingleUpdate($hash,"last_error",$error,1);
  714. return $error;
  715. };
  716. foreach (@$result)
  717. {
  718. push(@Cover, $_->{'Cover'});
  719. Log3 $name, 5, "$name, Cover : ".$_->{'Cover'};
  720. }
  721. }
  722. return $ret;
  723. }
  724. if ($cmd eq "playfile")
  725. {
  726. return "$name, no File !" if (!$subcmd);
  727. MPD_ClearReadings($hash);
  728. $hash->{".playlist"} = "";
  729. readingsSingleUpdate($hash,"playlistname","",1);
  730. $hash->{".music"} = $subcmd; # interne Song Verwaltung
  731. $ret = mpd_cmd($hash, clb."stop\nclear\nadd \"$subcmd\"\nplay\n".cle);
  732. }
  733. if ($cmd eq "updateDb")
  734. {
  735. $ret = mpd_cmd($hash, clb."rescan\n".cle);
  736. }
  737. if ($cmd eq "mpd_event")
  738. {
  739. my (@arr) = split("\\|",$subcmd);
  740. $subcmd = $arr[0];
  741. readingsSingleUpdate($hash,"mpd_event",$subcmd,1);
  742. Log3 $name,4,"$name mpd event : ".$subcmd;
  743. mpd_cmd($hash, clb.cle) if ($subcmd ne "playlist");
  744. shift @arr;
  745. MPD_NewPlaylist($hash,join ("\n", @arr));
  746. # if ((ReadingsVal($name,"currentTrackProvider","Radio") eq "Radio") || (ReadingsVal($name,"playlist_json","") eq ""));
  747. return undef;
  748. }
  749. if (substr($cmd,0,13) eq "outputenabled")
  750. {
  751. my $oid = substr($cmd,13,1);
  752. if ($subcmd eq "1")
  753. {
  754. $ret = mpd_cmd($hash, "i|enableoutput $oid|x");
  755. Log3 $name , 5, "enableoutput $oid | $subcmd";
  756. }
  757. else
  758. {
  759. $ret = mpd_cmd($hash, "i|disableoutput $oid|x");
  760. Log3 $name , 5 ,"disableoutput $oid | $subcmd";
  761. }
  762. MPD_Outputs_Status($hash);
  763. }
  764. return $ret;
  765. }
  766. sub MPD_Get($@)
  767. {
  768. my ($hash, @a)= @_;
  769. my $name= $hash->{NAME};
  770. my $ret;
  771. my $cmd;
  772. return "get $name needs at least one argument" if(int(@a) < 2);
  773. $cmd = $a[1];
  774. return(MPD_html($hash)) if ($cmd eq "webrc");
  775. return "no get cmd on a disabled device !" if(IsDisabled($name));
  776. if ($cmd eq "playlists")
  777. {
  778. $hash->{".playlists"} = "";
  779. #mpd_cmd($hash, "i|lsinfo|playlists");
  780. mpd_cmd($hash, "i|listplaylists|playlists");
  781. return format_get_output("Playlists",$hash->{".playlists"});
  782. }
  783. if ($cmd eq "music")
  784. {
  785. return "Command is not supported by player mopidy !" if ($hash->{".player"} eq "mopidy");
  786. $hash->{".musiclist"} = "";
  787. mpd_cmd($hash, "i|listall|music");
  788. return format_get_output("Music",$hash->{".musiclist"});
  789. }
  790. if ($cmd eq "statusRequest")
  791. {
  792. mpd_cmd($hash, clb.cle);
  793. $ret = mpd_cmd($hash, "i|".clb.cle."|x|s");
  794. return format_get_output("Status Request", $ret) if($ret);
  795. return undef;
  796. }
  797. if ($cmd eq "bookmarks")
  798. {
  799. my $bm_dir = AttrVal($name, "bookmarkDir","");
  800. return format_get_output("Get Bookmarks","attribute bookmarkDir not set !") if ($bm_dir eq "");
  801. opendir(DIR,$bm_dir);
  802. while(my $d = readdir(DIR)) {$ret .= $d."\n" if (($d ne "..") && ($d ne "."));}
  803. closedir(DIR);
  804. return format_get_output("Get Bookmarks",$ret) if($ret);
  805. return undef;
  806. }
  807. if ($cmd eq "outputs")
  808. {
  809. MPD_Outputs_Status($hash);
  810. return format_get_output("Outputs", $hash->{".outputs"});
  811. }
  812. return format_get_output("Current Song", mpd_cmd($hash, "i|currentsong|x")) if ($cmd eq "currentsong");
  813. return format_get_output("Playlist Info",mpd_cmd($hash, "i|playlistinfo|x")) if ($cmd eq "playlistinfo");
  814. return "$name get with unknown argument $cmd, choose one of " . join(" ", sort keys %gets);
  815. }
  816. sub format_get_output($$)
  817. {
  818. my ($head,$ret)= @_;
  819. my $width = 10;
  820. my @arr = split("\n",$ret);
  821. #my @sort = sort(@arr);
  822. foreach(@arr) { $width = length($_) if(length($_) > $width); }
  823. return $head."\n".("-" x $width)."\n".$ret;
  824. }
  825. sub MPD_Outputs_Status($)
  826. {
  827. my ($hash)= @_;
  828. my $name = $hash->{NAME};
  829. $hash->{".outputs"} = mpd_cmd($hash, "i|outputs|x");
  830. my @outp = split("\n" , $hash->{".outputs"});
  831. readingsBeginUpdate($hash);
  832. my $outpid = "0";
  833. foreach (@outp)
  834. {
  835. my @val = split(": " , $_);
  836. Log3 $name, 4 ,"$name: MPD_Outputs_Status -> $val[0] = $val[1]";
  837. $outpid = ($val[0] eq "outputid") ? $val[1] : $outpid;
  838. readingsBulkUpdate($hash,$val[0].$outpid,$val[1]) if ($val[0] ne "outputid");
  839. $sets{$val[0].$outpid.":0,1"} = "" if ($val[0] eq "outputenabled");
  840. }
  841. readingsEndUpdate($hash, 1);
  842. }
  843. sub mpd_cmd($$)
  844. {
  845. my ($hash,$a)= @_;
  846. my $output = "";
  847. my $sp;
  848. my $artist;
  849. my $album;
  850. my $name_ = "";
  851. my $title = "";
  852. my $exot = "";
  853. my $rawTitle;
  854. my $name = $hash->{NAME};
  855. my $old_plists = $hash->{".playlists"};
  856. $hash->{VERSION} = undef;
  857. $hash->{SUBVERSION} = undef;
  858. $hash->{PRESENCE} = "absent";
  859. my $sock = IO::Socket::INET->new(
  860. PeerHost => $hash->{HOST},
  861. PeerPort => $hash->{PORT},
  862. Proto => 'tcp',
  863. Timeout => $hash->{TIMEOUT});
  864. if (!$sock)
  865. {
  866. readingsBeginUpdate($hash);
  867. readingsBulkUpdate($hash,"state","error");
  868. readingsBulkUpdate($hash,"last_error",$!);
  869. readingsBulkUpdate($hash,"presence","absent"); # MPD ist wohl tot :(
  870. readingsEndUpdate($hash, 1 );
  871. Log3 $name, 2 , "$name, cmd error : ".$!;
  872. return $!;
  873. }
  874. while (<$sock>) # MPD rede mit mir , egal was ;)
  875. { last if $_ ; } # end of output.
  876. chomp $_;
  877. return "not a valid mpd server, welcome string was: ".$_ if $_ !~ /^OK MPD (.+)$/;
  878. $hash->{PRESENCE} = "present";
  879. my ($b , $c) = split("OK MPD " , $_);
  880. $hash->{VERSION} = $c;
  881. (undef,$hash->{SUBVERSION},undef) = split("\\.",$c);
  882. # ok, now we're connected - let's issue the commands.
  883. if ($hash->{".password"} ne "")
  884. {
  885. # lets try to authenticate with a password
  886. print $sock "password ".$hash->{".password"}."\r\n";
  887. while (<$sock>) { last if $_ ; } # end of output.
  888. chomp;
  889. if ($_ !~ /^OK$/)
  890. {
  891. print $sock "close\n";
  892. close($sock);
  893. readingsSingleUpdate($hash,"last_error",$_,1);
  894. return "password auth failed : ".$_ ;
  895. }
  896. }
  897. my @commands = split("\\|" , $a);
  898. if ($commands[0] ne "i")
  899. { # start Ausgabe nach Readings oder Internals
  900. readingsBeginUpdate($hash);
  901. readingsBulkUpdate($hash,"presence","present"); # MPD lebt
  902. foreach (@commands)
  903. {
  904. my $cmd = $_;
  905. print $sock "$cmd\r\n";
  906. Log3 $name, 5 , "$name, mpd_cmd[1] -> $cmd";
  907. while (<$sock>)
  908. {
  909. chomp $_;
  910. return "MPD_Msg ACK ERROR ".$_ if $_ =~ s/^ACK //; # oops - error.
  911. last if $_ =~ /^OK/; # end of output.
  912. Log3 $name, 5 , "$name, rec: ".$_;
  913. ($b , $c , $exot) = split(": " , $_);
  914. if ($b && defined($c)) # ist das ein Reading ?
  915. {
  916. ;
  917. if ($b eq "volume") { $hash->{".volume"} = $c; } # Sonderfall volume
  918. $artist = $c if ($b eq "Artist");
  919. $album = $c if ($b eq "Album");
  920. $name_ = $c if ($b eq "Name");
  921. if ($b eq "Title")
  922. {
  923. $rawTitle = $_;
  924. $rawTitle =~ s/Title: //;
  925. readingsBulkUpdate($hash,"rawTitle",$rawTitle);
  926. $title = $c;
  927. $c =~ s/\+\+\+//; # +++ = Tickermeldungen
  928. $c =~ s/feat./ \/ /;
  929. $c .= " - ".$exot if (defined($exot) && ($exot ne "")); # Sonderfall Bayern 3
  930. $sp = index($c, " - ");
  931. if (AttrVal($name, "titleSplit", 1) && ($sp>0)) # wer nicht mag solls eben abschalten
  932. {
  933. $artist = substr($c,0,$sp);
  934. readingsBulkUpdate($hash,"Artist",$artist);
  935. readingsBulkUpdate($hash,"Title",substr($c,$sp+3));
  936. $title = substr($c,$sp+3);
  937. }
  938. else { readingsBulkUpdate($hash,"Title",$c); } # kein Titel Split
  939. }
  940. else { readingsBulkUpdate($hash,$b,$c); } # irgendwas aber kein Titel
  941. } # defined $c
  942. } # while
  943. } # foreach
  944. readingsBulkUpdate($hash,"currentTrackProvider",($name_) ? "Radio" : "Bibliothek") if ($artist && $title);
  945. readingsEndUpdate($hash, 1 );
  946. if (AttrVal($name, "image_size", -1) > -1)
  947. {
  948. MPD_get_artist_info($hash, urlEncode($artist)) if ($artist);
  949. MPD_get_album_info($hash, urlEncode($album)) if ($album);
  950. }
  951. if (ReadingsVal($name,"playlist_json","") ne "")
  952. {
  953. my $pos = ReadingsNum($name,"Pos",-1);
  954. readingsSingleUpdate($hash,"Cover",$Cover[$pos],1) if($pos > -1) && defined($Cover[$pos]);
  955. }
  956. } # Ende der Ausgabe Readings und Internals, ab jetzt folgt nur noch Bildschirmausgabe
  957. else
  958. { # start internes cmd
  959. print $sock $commands[1]."\r\n";
  960. Log3 $name, 5 , "$name, mpd_cmd[2] -> ".$commands[1];
  961. my $d;
  962. while (<$sock>)
  963. {
  964. return "mpd_Msg ACK ERROR ".$_ if $_ =~ s/^ACK //; # oops - error.
  965. last if $_ =~ /^OK/; # end of output.
  966. $sp = index($_, ": ");
  967. $b = substr($_,0,$sp);
  968. $c = substr($_,$sp+2);
  969. if (($b eq "file" ) && ($commands[2] eq "music")) {$hash->{".musiclist"} .= $c; } # Titelliste füllen
  970. elsif (($b eq "playlist" ) && ($commands[2] eq "playlists")) {$hash->{".playlists"} .= $c; } # Playliste füllen
  971. if ($commands[2] eq "x") { $output .= $_; }
  972. } # while
  973. if (defined($commands[3]))
  974. {
  975. my @arr = split("\n",$output);
  976. @arr = sort(@arr);
  977. $output = join("\n",@arr);
  978. }
  979. } # end internes cmd
  980. print $sock "close\n";
  981. close($sock);
  982. if ($hash->{".playlists"} ne $old_plists) # haben sich sich die Listen geändert ?
  983. {
  984. $hash->{".playlists"} =~ s/\n+\z//;
  985. $old_plists = $hash->{".playlists"};
  986. my @arr = split("\n",$old_plists);
  987. my $i = 0 ;;
  988. foreach (@arr)
  989. {
  990. $hash->{helper}{playlistcollection}{$i} = $_;
  991. $i++;
  992. }
  993. $hash->{helper}{playlistcollection}{val} = $i-1;
  994. $old_plists =~ tr/\n/\:/; # TabletUI will diese Art der Liste
  995. readingsSingleUpdate($hash,"playlistcollection", $old_plists,1) if (!AttrVal($name,"no_playlistcollection",""));
  996. Log3 $name ,5 ,"$name, new playlistcollection -> $old_plists";
  997. }
  998. return $output; # falls es einen gibt , wenn nicht - auch gut ;)
  999. } # end mpd_msg
  1000. sub MPD_IdleStart($)
  1001. {
  1002. my ($name) = @_;
  1003. return unless(defined($name));
  1004. my $hash = $defs{$name};
  1005. my $old_event = "";
  1006. my $telnetPort = undef;
  1007. my $output;
  1008. # Suche das Telnet Device ohne Passwort
  1009. # Code geklaut aus Blocking.pm :)
  1010. foreach my $d (sort keys %defs)
  1011. {
  1012. my $h = $defs{$d};
  1013. next if(!$h->{TYPE} || $h->{TYPE} ne "telnet" || $h->{SNAME});
  1014. next if($attr{$d}{SSL} || AttrVal($d, "allowfrom", "127.0.0.1") ne "127.0.0.1");
  1015. next if($h->{DEF} !~ m/^\d+( global)?$/);
  1016. next if($h->{DEF} =~ m/IPV6/);
  1017. my %cDev = ( SNAME=>$d, TYPE=>$h->{TYPE}, NAME=>$d.time() );
  1018. next if(Authenticate(\%cDev, undef) == 2); # Needs password
  1019. $telnetPort = $defs{$d}{"PORT"};
  1020. last;
  1021. }
  1022. return $name."|no telnet port without password found" if (!$telnetPort);
  1023. my $sock = IO::Socket::INET->new(
  1024. PeerHost => $hash->{HOST},
  1025. PeerPort => $hash->{PORT},
  1026. Proto => 'tcp',
  1027. Timeout => $hash->{TIMEOUT});
  1028. return $name."|IdleStart: $!" if (!$sock);
  1029. while (<$sock>) { last if $_ ; }
  1030. chomp $_;
  1031. return $name."|not a valid mpd server, welcome string was: ".$_ if $_ !~ /^OK MPD (.+)$/;
  1032. if ($hash->{".password"} ne "")
  1033. {
  1034. # lets try to authenticate with a password
  1035. print $sock "password ".$hash->{".password"}."\r\n";
  1036. while (<$sock>)
  1037. {
  1038. last if $_ ; # end of output.
  1039. }
  1040. chomp;
  1041. if ($_ !~ /^OK$/)
  1042. {
  1043. print $sock "close\n";
  1044. close($sock);
  1045. return $name."|mpd password auth failed : ".$_;
  1046. }
  1047. }
  1048. # Waits until there is a noteworthy change in one or more of MPD's subsystems.
  1049. # As soon as there is one, it lists all changed systems in a line in the format changed: SUBSYSTEM,
  1050. # where SUBSYSTEM is one of the following:
  1051. # - database: the song database has been modified after update.
  1052. # - update: a database update has started or finished. If the database was modified during the update, the database event is also emitted.
  1053. # - stored_playlist: a stored playlist has been modified, renamed, created or deleted
  1054. # +- playlist: the current playlist has been modified
  1055. # +- player: the player has been started, stopped or seeked
  1056. # +- mixer: the volume has been changed
  1057. # - output: an audio output has been enabled or disabled
  1058. # +- options: options like repeat, random, crossfade, replay gain
  1059. # - sticker: the sticker database has been modified.
  1060. # - subscription: a client has subscribed or unsubscribed to a channel
  1061. # - message: a message was received on a channel this client is subscribed to; this event is only emitted when the queue is empty
  1062. my $sock2 = IO::Socket::INET->new(
  1063. PeerHost => "127.0.0.1",
  1064. PeerPort => $telnetPort,
  1065. Proto => 'tcp',
  1066. Timeout => 2);
  1067. return $name."|Idle send: ".$! if (!$sock2);
  1068. print $sock2 "get $name statusRequest\nexit\n";
  1069. close ($sock2);
  1070. print $sock "idle\n";
  1071. my $step = 0;
  1072. while (<$sock>)
  1073. {
  1074. if ($_) # es hat sich was getan.
  1075. {
  1076. chomp $_;
  1077. if ($_ =~ s/^ACK //) # oops - error.
  1078. {
  1079. print $sock "close\n";
  1080. close($sock);
  1081. return $name."|ACK ERROR : ".$_;
  1082. }
  1083. $_ =~s/changed: //g;
  1084. if (($_ ne $old_event) && ($_ ne "OK") && (index($_,": ") == -1))
  1085. {
  1086. $output .= ($old_event eq "") ? $_ : "+".$_;
  1087. $old_event = $_;
  1088. $step=0;
  1089. }
  1090. elsif (index($_,": ") > -1){
  1091. $output .= "|".$_;
  1092. $step=1;
  1093. }
  1094. else #if ($_ eq "OK")
  1095. {
  1096. #print $sock "idle\n";
  1097. print $sock "idle\n" if($step) ;
  1098. print $sock "playlistinfo\n" if(!$step) ;
  1099. $step=2 if ($step==1);
  1100. } # OK
  1101. } # $_
  1102. if ((($old_event eq "player") ||
  1103. ($old_event eq "playlist")||
  1104. ($old_event eq "mixer") ||
  1105. #($old_event eq "options"))
  1106. ($old_event eq "options"))&& ($step==2)
  1107. ) # muessen wir den Parentprozess informieren ?
  1108. {
  1109. $sock2 = IO::Socket::INET->new(
  1110. PeerHost => "127.0.0.1",
  1111. PeerPort => $telnetPort,
  1112. Proto => 'tcp',
  1113. Timeout => 2);
  1114. return $name."|Idle_loop send: ".$! if (!$sock2);
  1115. Log3 $name,5,"Output : $output";
  1116. print $sock2 "set $name mpd_event $output\nexit\n";
  1117. close($sock2);
  1118. $old_event = "";
  1119. $output = "";
  1120. $step=0;
  1121. }
  1122. } #while
  1123. #print $sock "close\n";
  1124. close($sock);
  1125. #xxx
  1126. return $name."|socket error";
  1127. }
  1128. sub MPD_IdleDone($)
  1129. {
  1130. my ($string) = @_;
  1131. return unless(defined($string));
  1132. my @r = split("\\|",$string);
  1133. my $hash = $defs{$r[0]};
  1134. my $ret = (defined($r[1])) ? $r[1] : "unknow error";
  1135. my $name = $hash->{NAME};
  1136. Log3 $name, 5,"$name, IdleDone -> $string";
  1137. delete($hash->{helper}{RUNNING_PID});
  1138. delete $hash->{IPID};
  1139. readingsBeginUpdate($hash);
  1140. readingsBulkUpdate($hash,"state","error");
  1141. readingsBulkUpdate($hash,"last_error",$ret);
  1142. readingsBulkUpdate($hash,"presence","absent");
  1143. readingsEndUpdate($hash, 1 );
  1144. Log3 $name, 4 , "$name, idle error -> $ret";
  1145. return if(IsDisabled($name));
  1146. RemoveInternalTimer($hash);
  1147. InternalTimer(gettimeofday()+AttrVal($name, "waits", 60), "MPD_try_idle", $hash, 0);
  1148. return;
  1149. }
  1150. sub MPD_try_idle($)
  1151. {
  1152. my ($hash) = @_;
  1153. my $name = $hash->{NAME};
  1154. my $waits = AttrVal($name, "waits", 60);
  1155. $hash->{helper}{RUNNING_PID} = BlockingCall("MPD_IdleStart",$name, "MPD_IdleDone", $hash) unless(exists($hash->{helper}{RUNNING_PID}));
  1156. if ($hash->{helper}{RUNNING_PID})
  1157. {
  1158. $hash->{IPID} = $hash->{helper}{RUNNING_PID}{pid};
  1159. Log3 $name, 4 , $name.", Idle new PID : ".$hash->{IPID};
  1160. RemoveInternalTimer($hash);
  1161. if ($^O !~ /Win/) # was könnte man bei Windows tun ?
  1162. {
  1163. InternalTimer(gettimeofday()+$waits, "MPD_watch_idle", $hash, 0); # starte die Überwachung
  1164. }
  1165. return 1;
  1166. }
  1167. else
  1168. {
  1169. my $error = "Idle Start failed, waiting $waits seconds for next try";
  1170. Log3 $name, 4 , "$name, $error";
  1171. readingsSingleUpdate($hash,"last_error",$error,1);
  1172. RemoveInternalTimer($hash);
  1173. InternalTimer(gettimeofday()+$waits, "MPD_try_idle", $hash, 0);
  1174. return 0;
  1175. }
  1176. }
  1177. sub MPD_watch_idle($)
  1178. {
  1179. # Lebt denn der Idle Prozess überhaupt noch ?
  1180. my ($hash) = @_;
  1181. my $name = $hash->{NAME};
  1182. RemoveInternalTimer($hash);
  1183. return if (IsDisabled($name));
  1184. return if (!defined($hash->{IPID}));
  1185. my $waits = AttrVal($name, "waits", 60);
  1186. my $cmd = "ps -e | grep '".$hash->{IPID}." '";
  1187. my $result = qx($cmd);
  1188. if (index($result,"perl") == -1)
  1189. {
  1190. Log3 $name, 2 , $name.", cant find idle PID ".$hash->{IPID}." in process list !";
  1191. BlockingKill($hash->{helper}{RUNNING_PID});
  1192. delete $hash->{helper}{RUNNING_PID};
  1193. delete $hash->{IPID};
  1194. InternalTimer(gettimeofday()+2, "MPD_try_idle", $hash, 0);
  1195. return;
  1196. }
  1197. else
  1198. {
  1199. Log3 $name, 5 , $name.", idle PID ".$hash->{IPID}." found";
  1200. if ((ReadingsVal($name,"presence","") eq "present") &&
  1201. ($hash->{STATE} eq "play") &&
  1202. (ReadingsVal($name,"currentTrackProvider","") ne "Radio")
  1203. )
  1204. {
  1205. # Wichtig um das Readings elapsed aktuell zu halten (TabletUI)
  1206. mpd_cmd($hash, "status");
  1207. readingsSingleUpdate($hash,"playlistname",$hash->{".playlist"},1) if ($hash->{READINGS}{"playlistname"}{VAL} ne $hash->{".playlist"});
  1208. }
  1209. }
  1210. InternalTimer(gettimeofday()+$waits, "MPD_watch_idle", $hash, 0);
  1211. return;
  1212. }
  1213. sub MPD_get_artist_info ($$)
  1214. {
  1215. my ($hash, $artist) = @_;
  1216. my $name = $hash->{NAME};
  1217. return undef if (($hash->{'.artist'} eq $artist) || ($artist eq ""));
  1218. $hash->{'.artist'} = $artist;
  1219. my $data;
  1220. my $cache = AttrVal($name,"cache",""); # default
  1221. my $param = {
  1222. url => lfm_artist.$hash->{'.apikey'}."&artist=".$artist,
  1223. timeout => 5,
  1224. hash => $hash,
  1225. header => "User-Agent: Mozilla/5.0\r\nAccept: application/json\r\nAccept-Charset: utf-8",
  1226. method => "GET",
  1227. callback => \&MPD_lfm_artist_info
  1228. };
  1229. if ((-e "www/$cache/".$artist.".json") && ($cache ne ""))
  1230. {
  1231. Log3 $name ,4,"$name, artist file ".$artist.".json already exist";
  1232. if (!open (FILE , "www/$cache/".$artist.".json"))
  1233. {
  1234. my $error = "error reading ".$artist.".json : ".$!;
  1235. Log3 $name, 2, "$name, $error";
  1236. readingsSingleUpdate($hash,"last_error",$error,1);
  1237. $hash->{JSON} = 0;
  1238. }
  1239. else
  1240. {
  1241. while(<FILE>){ $data = $data.$_;}
  1242. close (FILE);
  1243. MPD_lfm_artist_info($param,"",$data,'local');
  1244. }
  1245. }
  1246. else # json von lastfm holen
  1247. {
  1248. Log3 $name ,4,"$name, new artist ".$artist." , try to get it from Last.fm";
  1249. HttpUtils_NonblockingGet($param);
  1250. }
  1251. return undef;
  1252. }
  1253. sub MPD_get_album_info ($$)
  1254. {
  1255. my ($hash, $album) = @_;
  1256. my $name = $hash->{NAME};
  1257. return undef if (($hash->{'.album'} eq $album) || ($album eq ""));
  1258. $hash->{'.album'} = $album;
  1259. my $artist = $hash->{'.artist'};
  1260. my $data;
  1261. my $cache = AttrVal($name,"cache",""); # default
  1262. my $param = {
  1263. url => lfm_album.$hash->{'.apikey'}."&album=".$album."&artist=".$artist,
  1264. timeout => 5,
  1265. hash => $hash,
  1266. header => "User-Agent: Mozilla/5.0\r\nAccept: application/json\r\nAccept-Charset: utf-8",
  1267. method => "GET",
  1268. callback => \&MPD_lfm_album_info
  1269. };
  1270. my $fname = "www/$cache/".$artist."_".$album.".json";
  1271. if (-e $fname && ($cache ne ""))
  1272. {
  1273. Log3 $name ,4,"$name, album file $fname already exist";
  1274. if (!open (FILE , $fname))
  1275. {
  1276. my $error = "error reading $fname : $!";
  1277. Log3 $name, 2, "$name, $error";
  1278. readingsSingleUpdate($hash,"last_error",$error,1);
  1279. $hash->{JSON} = 0;
  1280. }
  1281. else
  1282. {
  1283. while(<FILE>){ $data = $data.$_;}
  1284. close (FILE);
  1285. MPD_lfm_album_info($param,"",$data,'local');
  1286. }
  1287. }
  1288. else # json von lastfm holen
  1289. {
  1290. $fname = $artist."_".$album.".json";
  1291. Log3 $name ,4,"$name, new album $fname , try to get it from Last.fm";
  1292. HttpUtils_NonblockingGet($param);
  1293. }
  1294. return undef;
  1295. }
  1296. sub MPD_lfm_artist_info(@)
  1297. {
  1298. my ($param, $err, $data, $local) = @_;
  1299. my $hash = $param->{hash};
  1300. my $name = $hash->{NAME};
  1301. my $artist = $hash->{'.artist'};
  1302. my $size = AttrVal($name,"image_size",0); # default
  1303. my $cache = AttrVal($name,"cache","");
  1304. my $url;
  1305. return if ($size < 0);
  1306. if (!$data || $err)
  1307. {
  1308. Log3 $name ,3,"$name, error got artist info [$artist] from Last.fm -> $err";
  1309. MPD_artist_image($hash,"/fhem/icons/10px-kreis-rot","");
  1310. return undef;
  1311. }
  1312. if (!$local) {Log3 $name,4,"$name, new json data for $artist from Last.fm";}
  1313. if ($cache ne "")
  1314. {
  1315. # json lokal speichern ?
  1316. if (-e "www/$cache/".$hash->{'.artist'}.".json")
  1317. {
  1318. Log3 $name ,5,"$name, artist ".$artist." already exist";
  1319. $hash->{JSON} = 1;
  1320. }
  1321. else
  1322. {
  1323. if (!open (FILE , ">"."www/$cache/".$artist.".json"))
  1324. {
  1325. my $error = "error saving ".$artist.".json : ".$!;
  1326. Log3 $name, 2, "$name, $error";
  1327. readingsSingleUpdate($hash,"last_error",$error,1);
  1328. $hash->{JSON} = 0;
  1329. }
  1330. else
  1331. {
  1332. print FILE $data;
  1333. close(FILE);
  1334. $hash->{JSON} = 1;
  1335. }
  1336. }
  1337. }
  1338. my $res;
  1339. eval { $res = decode_json($data); 1; } or do
  1340. {
  1341. my $error = "invalid file content in file ".$artist.".json";
  1342. Log3 $name, 3, "$name, $error";
  1343. readingsSingleUpdate($hash,"last_error",$error,1);
  1344. return undef;
  1345. };
  1346. my $hw="width='32' height='32'";
  1347. $hw="width='64' height='64'" if ($size == 1);
  1348. $hw="width='174' height='174'" if ($size == 2);
  1349. $hw="width='300' height='300'" if ($size == 3);
  1350. if ((exists $res->{'artist'}->{'bio'}->{'summary'}) && AttrVal($name,"artist_summary",0))
  1351. {
  1352. readingsSingleUpdate($hash,"artist_summary",$res->{'artist'}->{'bio'}->{'summary'},1);
  1353. }
  1354. if ((exists $res->{'artist'}->{'bio'}->{'content'}) && AttrVal($name,"artist_content",0))
  1355. {
  1356. readingsSingleUpdate($hash,"artist_content",$res->{'artist'}->{'bio'}->{'content'},1);
  1357. }
  1358. if (exists $res->{'artist'}->{'image'}[$size]->{'#text'})
  1359. {
  1360. $url = $res->{'artist'}->{'image'}[$size]->{'#text'};
  1361. }
  1362. if (!$cache || !$hash->{JSON}) # cache verwenden ?
  1363. {
  1364. if ($url)
  1365. {
  1366. if (index($url,"http") < 0)
  1367. {
  1368. MPD_artist_image($hash,"/fhem/icons/10px-kreis-rot","");
  1369. my $error = "falsche info URL : $url";
  1370. readingsSingleUpdate($hash,"last_error",$error,1);
  1371. Log3 $name,1,"$name, $error";
  1372. return undef;
  1373. }
  1374. Log3 $name,4,"$name, try to get image from $url";
  1375. MPD_artist_image($hash,$url,$hw);
  1376. }
  1377. else
  1378. {
  1379. MPD_artist_image($hash,"/fhem/icons/10px-kreis-rot", "");
  1380. Log3 $name,4,"$name, no picture on Last.fm";
  1381. }
  1382. return undef;
  1383. } # kein cache verwenden
  1384. if ($url)
  1385. {
  1386. $hash->{'.suffix'} = substr($url,-4);
  1387. if (length($hash->{'.suffix'})>3)
  1388. {
  1389. my $fname = $hash->{'.artist'}."_$size".$hash->{'.suffix'};
  1390. if (-e "www/$cache/".$fname)
  1391. {
  1392. Log3 $name ,4,"$name, artist image ".$fname." local found";
  1393. MPD_artist_image($hash,"/fhem/$cache/".$fname,$hw);
  1394. return undef;
  1395. }
  1396. Log3 $name ,4,"$name, no local artist image ".$fname." found, try to get it from Last.fm";
  1397. $param = {
  1398. url => $url,
  1399. timeout => 5,
  1400. hash => $hash,
  1401. header => "User-Agent: Mozilla/5.0\r\nAccept: application/json\r\nAccept-Charset: utf-8",
  1402. method => "GET",
  1403. callback => \&MPD_lfm_artist_image
  1404. };
  1405. HttpUtils_NonblockingGet($param);
  1406. MPD_artist_image($hash,"/fhem/icons/10px-kreis-gelb","");
  1407. return undef;
  1408. }# zu kurz
  1409. }# gibt es nicht oder ist leer
  1410. MPD_artist_image($hash,"/fhem/icons/10px-kreis-rot","");
  1411. Log3 $name ,4,"$name, image infos missing , delete old json";
  1412. unlink ("www/$cache/".$artist.".json");
  1413. # keine Image Infos vorhanden !
  1414. return undef;
  1415. }
  1416. sub MPD_lfm_album_info(@)
  1417. {
  1418. my ($param, $err, $data, $local) = @_;
  1419. my $hash = $param->{hash};
  1420. my $name = $hash->{NAME};
  1421. my $artist= $hash->{'.artist'};
  1422. my $album = $hash->{'.album'};
  1423. my $size = AttrVal($name,"image_size",0); # default
  1424. my $cache = AttrVal($name,"cache","");
  1425. my $url;
  1426. return if ($size < 0);
  1427. if (!$data || $err)
  1428. {
  1429. Log3 $name ,3,"$name, error got album info from Last.fm -> $err";
  1430. MPD_album_image($hash,"/fhem/icons/10px-kreis-rot","");
  1431. return undef;
  1432. }
  1433. if (!$local) {Log3 $name,4,"$name, new json data for $album from Last.fm";}
  1434. my $fname = "www/$cache/".$artist."_".$album.".json";
  1435. if ($cache ne "")
  1436. {
  1437. # json lokal speichern ?
  1438. if (-e $fname)
  1439. {
  1440. Log3 $name ,5,"$name, album $fname already exist";
  1441. }
  1442. else
  1443. {
  1444. if (!open (FILE , "> ".$fname))
  1445. {
  1446. my $error = "error saving $fname : ".$!;
  1447. readingsSingleUpdate($hash,"last_error",$error,1);
  1448. Log3 $name, 2, "$name, $error";
  1449. }
  1450. else
  1451. {
  1452. print FILE $data;
  1453. close(FILE);
  1454. }
  1455. }
  1456. }
  1457. my $res;
  1458. eval { $res = decode_json($data); 1; } or do
  1459. {
  1460. my $error = "invalid file content in file ".$album.".json";
  1461. Log3 $name, 3, "$name, $error";
  1462. readingsSingleUpdate($hash,"last_error",$error,1);
  1463. return undef;
  1464. };
  1465. my $hw="width='32' height='32'";
  1466. $hw="width='64' height='64'" if ($size == 1);
  1467. $hw="width='174' height='174'" if ($size == 2);
  1468. $hw="width='300' height='300'" if ($size == 3);
  1469. if (!$cache || !$hash->{JSON}) # cache verwenden ?
  1470. {
  1471. if (exists $res->{'album'}->{'image'}[$size]->{'#text'})
  1472. {
  1473. $url = $res->{'album'}->{'image'}[$size]->{'#text'};
  1474. if (index($url,"http") < 0)
  1475. {
  1476. MPD_album_image($hash,"/fhem/icons/10px-kreis-rot","");
  1477. my $error = "falsche info URL : $url";
  1478. readingsSingleUpdate($hash,"last_error",$error,1);
  1479. Log3 $name,1,"$name, $error";
  1480. return undef;
  1481. }
  1482. MPD_album_image($hash,$url,$hw);
  1483. }
  1484. else
  1485. {
  1486. MPD_album_image($hash,"/fhem/icons/10px-kreis-rot", "");
  1487. Log3 $name,4,"$name, unknown album";
  1488. }
  1489. return undef;
  1490. } # kein cache verwenden
  1491. if (exists $res->{'album'}->{'image'}[$size]->{'#text'})
  1492. {
  1493. $url = $res->{'album'}->{'image'}[$size]->{'#text'};
  1494. $hash->{'.suffix'} = substr($url,-4);
  1495. my $fname = $artist."_".$album."_".$size.$hash->{'.suffix'};
  1496. if (-e "www/".$cache."/".$fname)
  1497. {
  1498. Log3 $name ,4,"$name, album image ".$fname." local found";
  1499. MPD_album_image($hash,"/fhem/".$cache."/".$fname,$hw);
  1500. return undef;
  1501. }
  1502. Log3 $name ,4,"$name, no local album image ".$fname." found, try to get it from Last.fm";
  1503. $param = {
  1504. url => $url,
  1505. timeout => 5,
  1506. hash => $hash,
  1507. header => "User-Agent: Mozilla/5.0\r\nAccept: application/json\r\nAccept-Charset: utf-8",
  1508. method => "GET",
  1509. callback => \&MPD_lfm_album_image
  1510. };
  1511. HttpUtils_NonblockingGet($param);
  1512. MPD_album_image($hash,"/fhem/icons/10px-kreis-gelb","");
  1513. }
  1514. return undef;
  1515. }
  1516. sub MPD_artist_image($$$)
  1517. {
  1518. my ($hash, $im , $hw) = @_;
  1519. readingsBeginUpdate($hash);
  1520. readingsBulkUpdate($hash,"artist_image_html","<img src='$im' $hw />");
  1521. readingsBulkUpdate($hash,"artist_image","$im");
  1522. readingsBulkUpdate($hash,"album_image_html","");
  1523. readingsBulkUpdate($hash,"album_image","");
  1524. readingsEndUpdate($hash, 1);
  1525. return;
  1526. }
  1527. sub MPD_album_image($$$)
  1528. {
  1529. my ($hash, $im , $hw) = @_;
  1530. readingsBeginUpdate($hash);
  1531. readingsBulkUpdate($hash,"album_image_html","<img src='$im' $hw />");
  1532. readingsBulkUpdate($hash,"album_image","$im");
  1533. readingsEndUpdate($hash, 1);
  1534. return;
  1535. }
  1536. sub MPD_lfm_artist_image(@)
  1537. {
  1538. my ($param, $err, $data) = @_;
  1539. my $hash = $param->{hash};
  1540. my $name = $hash->{NAME};
  1541. my $artist= $hash->{'.artist'};
  1542. my $cache = AttrVal($name,"cache","");
  1543. my $size = AttrVal($name,"image_size",0);
  1544. my $hw="width='32' height='32'";
  1545. $hw="width='64' height='64'" if ($size == 1);
  1546. $hw="width='174' height='174'" if ($size == 2);
  1547. $hw="width='300' height='300'" if ($size == 3);
  1548. my $fname = $artist."_".$size.$hash->{'.suffix'};
  1549. $artist = urlDecode($artist);
  1550. if($err ne "")
  1551. {
  1552. my $error = "error to get artist image [$artist] while requesting ".$param->{url}." - $err";
  1553. readingsSingleUpdate($hash,"last_error",$error,1);
  1554. Log3 $name, 3, "$name, $error";
  1555. }
  1556. elsif(($data ne "") && ($data =~ /PNG/i))
  1557. {
  1558. Log3 $name,4,"$name, got new image $fname from Last.fm";
  1559. if (!open(FILE, "> www/$cache/$fname"))
  1560. {
  1561. Log3 $name, 2, "$name, error saving image $fname : ".$!;
  1562. MPD_artist_image($hash,"/fhem/icons/10px-kreis-rot"," ");
  1563. return undef;
  1564. }
  1565. binmode(FILE);
  1566. print FILE $data;
  1567. close(FILE);
  1568. MPD_artist_image($hash,"/fhem/$cache/".$fname,$hw);
  1569. return undef;
  1570. }
  1571. Log3 $name,3,"$name, empty or invalid artist image [$artist] from Last.fm";
  1572. # ToDo : da nochmal genau reinsehen !
  1573. #system("cp www/$cache/".$hash->{'.artist'}.".json www/$cache/".$hash->{'.artist'}.".def");
  1574. unlink ("www/$cache/$fname");
  1575. MPD_artist_image($hash,"/fhem/icons/10px-kreis-rot","");
  1576. return undef;
  1577. }
  1578. sub MPD_lfm_album_image(@)
  1579. {
  1580. my ($param, $err, $data) = @_;
  1581. my $hash = $param->{hash};
  1582. my $name = $hash->{NAME};
  1583. my $artist= $hash->{'.artist'};
  1584. my $album = $hash->{'.album'};
  1585. my $cache = AttrVal($name,"cache","");
  1586. my $size = AttrVal($name,"image_size",0);
  1587. my $hw="width='32' height='32'";
  1588. my $error;
  1589. $hw="width='64' height='64'" if ($size == 1);
  1590. $hw="width='174' height='174'" if ($size == 2);
  1591. $hw="width='300' height='300'" if ($size == 3);
  1592. my $fname = $artist."_".$album."_".$size.$hash->{'.suffix'};
  1593. $artist = urlDecode($artist);
  1594. $album = urlDecode($album);
  1595. if($err ne "")
  1596. {
  1597. $error = "error to get album image [$album] for artist [$artist] while requesting ".$param->{url}." - $err";
  1598. readingsSingleUpdate($hash,"last_error",$error,1);
  1599. Log3 $name, 3, "$name, $error";
  1600. }
  1601. elsif(($data ne "") && ($data =~ /PNG/i))
  1602. {
  1603. Log3 $name,4,"$name, got new image $fname for $album from Last.fm";
  1604. if (!open(FILE, "> www/".$cache."/".$fname))
  1605. {
  1606. $error = "error saving image $fname : ".$!;
  1607. readingsSingleUpdate($hash,"last_error",$error,1);
  1608. Log3 $name, 2, "$name, $error";
  1609. MPD_album_image($hash,"/fhem/icons/10px-kreis-rot"," ");
  1610. return undef;
  1611. }
  1612. binmode(FILE);
  1613. print FILE $data;
  1614. close(FILE);
  1615. MPD_album_image($hash,"/fhem/$cache/".$fname,$hw);
  1616. return undef;
  1617. }
  1618. Log3 $name,3,"$name, empty or invalid image for album [$album] from Last.fm";
  1619. unlink ("www/".$cache."/".$fname);
  1620. MPD_album_image($hash,"/fhem/icons/10px-kreis-rot","");
  1621. return undef;
  1622. }
  1623. sub MPD_NewPlaylist($$)
  1624. {
  1625. my ($hash, $list) = @_;
  1626. my $name = $hash->{NAME};
  1627. my $crc = unpack ("%16C*",$list);
  1628. Log3 $name,5,"$name, new Playlist in -> $list";
  1629. return if ($crc == $hash->{".playlist_crc"});
  1630. Log3 $name,4,"$name, new CRC : $crc";
  1631. $hash->{".playlist_crc"} = $crc;
  1632. $list =~ s/"/\\"/g;
  1633. $list = "\n".$list;
  1634. my @artist = ($list=~/\nArtist:\s(.*)\n/g);
  1635. my @title = ($list=~/\nTitle:\s(.*)\n/g);
  1636. my @album = ($list=~/\nAlbum:\s(.*)\n/g);
  1637. my @time = ($list=~/\nTime:\s(.*)\n/g);
  1638. my @file = ($list=~/\nfile:\s(.*)\n/g);
  1639. my @track = ($list=~/\nTrack:\s(.*)\n/g);
  1640. my @albumUri = ($list=~/\nX-AlbumUri:\s(.*)\n/g); # von Mopidy ?
  1641. my $ret = '[';
  1642. my $lastUri = '';
  1643. my $url;
  1644. my $error;
  1645. my $lastcover;
  1646. # Radiostream ohne Artist ?
  1647. if (!@artist && @title && AttrVal($name, "titleSplit", 1))
  1648. {
  1649. $title[0] =~ s/: / - /;
  1650. $title[0] =~ s/\+\+\+//;
  1651. $title[0] =~ s/feat./ \/ /;
  1652. my $sp = index($title[0], " - ");
  1653. if ($sp>0)
  1654. {
  1655. $artist[0] = substr($title[0],0,$sp);
  1656. $title[0] = substr($title[0],$sp+3);
  1657. }
  1658. }
  1659. for my $i (0 .. $#artist)
  1660. {
  1661. $lastcover = AttrVal($name,"unknown_artist_image","/fhem/icons/1px-spacer"); # default
  1662. if (defined($albumUri[$i]))
  1663. {
  1664. if ( $lastUri ne $albumUri[$i])
  1665. {
  1666. $lastUri = $albumUri[$i];
  1667. eval "use LWP::UserAgent";
  1668. if($@)
  1669. {
  1670. $error = "please install LWP::UserAgent to get album cover from spotify.com";
  1671. Log3 $name,3,"$name, $error";
  1672. readingsBeginUpdate($hash);
  1673. readingsBulkUpdate($hash,"playlistinfo","");
  1674. readingsBulkUpdate($hash,"last_error",$error);
  1675. readingsEndUpdate($hash,1);
  1676. }
  1677. else
  1678. {
  1679. #eval "use JSON qw( decode_json )";
  1680. #if($@)
  1681. #{
  1682. #$error = "please install JSON to decode cover";
  1683. #Log3 $name, 3,"$name, $error";
  1684. #readingsBeginUpdate($hash);
  1685. #readingsBulkUpdate($hash,"last_error",$error);
  1686. #readingsBulkUpdate($hash,"playlistinfo","");
  1687. #readingsEndUpdate($hash,1);
  1688. #}
  1689. #else
  1690. #{
  1691. my $ua = LWP::UserAgent->new( ssl_opts => { verify_hostname => 1 } );
  1692. my $response = $ua->get("https://embed.spotify.com/oembed/?url=".$albumUri[$i]);
  1693. my $data = '';
  1694. if ( $response->is_success )
  1695. {
  1696. $data = $response->decoded_content;
  1697. eval
  1698. {
  1699. $url = decode_json( $data );
  1700. $lastcover = $url->{'thumbnail_url'};
  1701. 1;
  1702. } or do
  1703. {
  1704. $error = "invalid JSON: $data";
  1705. Log3 $name, 3, "$name, $error";
  1706. readingsBeginUpdate($hash);
  1707. readingsBulkUpdate($hash,"last_error",$error);
  1708. readingsBulkUpdate($hash,"playlistinfo","");
  1709. readingsEndUpdate($hash,1);
  1710. }
  1711. } #
  1712. #} # JSON
  1713. } # LWP
  1714. } #$lastUri ne $albumUri[$i]
  1715. } # defined($albumUri[$i]), vesuchen wir es mit Last.fm
  1716. elsif (AttrVal($name,"image_size",-1) > -1 && (AttrVal($name,"cache","") ne ""))
  1717. {
  1718. my $cache = AttrVal($name,"cache","");
  1719. my $size = AttrVal($name,"image_size",0);
  1720. if (-e "www/$cache/".urlEncode($artist[$i])."_".$size.".png")
  1721. { $lastcover = "/fhem/www/".$cache."/".urlEncode($artist[$i])."_".$size.".png"; }
  1722. }
  1723. $ret .= '{"Artist":"'.$artist[$i].'",';
  1724. $ret .= '"Title":';
  1725. $ret .= (defined($title[$i])) ? '"'.$title[$i].'",' : '"",';
  1726. $ret .= '"Album":';
  1727. $ret .= (defined($album[$i])) ? '"'.$album[$i].'",' : '"",';
  1728. $ret .= '"Time":';
  1729. $ret .= (defined($time[$i])) ? '"'.$time[$i].'",' : '"",';
  1730. $ret .= '"File":';
  1731. $ret .= (defined($file[$i])) ? '"'.$file[$i].'",' : '"",';
  1732. $ret .= '"Track":';
  1733. $ret .= (defined($track[$i])) ? '"'.$track[$i].'",' : '"",';
  1734. $ret .= '"Cover":"'.$lastcover.'"}';
  1735. $ret .= ',' if ($i<$#artist);
  1736. }
  1737. $ret .= ']';
  1738. $ret =~ s/;//g;
  1739. $ret =~ s/\\n//g;
  1740. Log3 $name,5,"$name, new Playlist out -> $ret";
  1741. Log3 $name,5,"$name, list : $list" if ($ret eq "[]");
  1742. readingsSingleUpdate($hash,"playlistinfo",$ret,1);
  1743. return;
  1744. }
  1745. ###############################################
  1746. sub MPD_html($) {
  1747. my ($hash)= @_;
  1748. my $name = $hash->{NAME};
  1749. my $playlist = $hash->{".playlist"};
  1750. my $playlists = $hash->{".playlists"};
  1751. my $musiclist = $hash->{".musiclist"};
  1752. my $music = $hash->{".music"};
  1753. my $volume = (defined($hash->{".volume"})) ? $hash->{".volume"} : "???";
  1754. my $len = (defined($hash->{READINGS}{"playlistlength"}{VAL})) ? $hash->{READINGS}{"playlistlength"}{VAL} : "--";
  1755. my $html;
  1756. my @list;
  1757. my $sel = "";
  1758. my $pos = (defined($hash->{READINGS}{"song"}{VAL}) && $len) ? $hash->{READINGS}{"song"}{VAL} : "--";
  1759. $pos .= "/";
  1760. $pos .= ($len) ? $len : "0";
  1761. $html = "<div class=\"remotecontrol\" id=\"$name\">";
  1762. $html .= "<table class=\"rc_body\" border=0>";
  1763. if ($playlists||$music)
  1764. {
  1765. if ($playlists) {
  1766. $html .= "<tr><td colspan=\"5\" align=center><select id=\"".$name."_List\" name=\"".$name."_List\" class=\"dropdown\" onchange=\"FW_cmd('/fhem?XHR=1&cmd.$name=set $name playlist ' + this.options[this.selectedIndex].value)\" style=\"font-size:11px;\">";
  1767. $html .= "<optgroup label=\"Playlists\">";
  1768. $html .= "<option>---</option>";
  1769. @list = sort(split ("\n",$playlists));
  1770. foreach (@list)
  1771. {
  1772. $sel = ($_ eq $playlist) ? " selected" : "";
  1773. $html .= "<option ".$sel." value=\"".uri_escape($_)."\">".$_."</option>";
  1774. }
  1775. $html .= "</optgroup></select></td></tr>";
  1776. }
  1777. if ($musiclist) {
  1778. $html .= "<tr><td colspan=\"5\" align=center><select id=\"".$name."_List\" name=\"".$name."_List\" class=\"dropdown\" onchange=\"FW_cmd('/fhem?XHR=1&cmd.$name=set $name playfile ' + this.options[this.selectedIndex].value)\" style=\"font-size:11px;\">";
  1779. $html .= "<optgroup label=\"Music\">";
  1780. $html .= "<option>---</option>";
  1781. @list = sort(split ("\n",$musiclist));
  1782. foreach (@list)
  1783. {
  1784. $sel = ($_ eq $music) ? " selected" : "";
  1785. $html .= "<option ".$sel." value=\"".uri_escape($_)."\">".$_."</option>";
  1786. }
  1787. $html .= "</optgroup></select></td></tr>";
  1788. }
  1789. }
  1790. $html .= "<tr><td>&nbsp;</td>";
  1791. $html .= "<td class=\"rc_button\"><a onClick=\"FW_cmd('/fhem?XHR=1&cmd.$name=set $name play')\"><img src=\"/fhem/icons/remotecontrol/black_btn_PLAY\" title=\"PLAY\"></a></td>";
  1792. $html .= "<td class=\"rc_button\"><a onClick=\"FW_cmd('/fhem?XHR=1&cmd.$name=set $name pause')\"><img src=\"/fhem/icons/remotecontrol/black_btn_PAUSE\" title=\"PAUSE\"></a></td>";
  1793. $html .= "<td class=\"rc_button\"><a onClick=\"FW_cmd('/fhem?XHR=1&cmd.$name=set $name stop')\"><img src=\"/fhem/icons/remotecontrol/black_btn_STOP\" title=\"STOP\"></a></td>";
  1794. $html .= "<td>&nbsp;</td></tr>";
  1795. $html .= "<tr><td>&nbsp;</td>";
  1796. $html .= "<td class=\"rc_button\"><a onClick=\"FW_cmd('/fhem?XHR=1&cmd.$name=set $name previous')\"><img src=\"/fhem/icons/remotecontrol/black_btn_REWIND\" title=\"PREV\"></a></td>";
  1797. $html .= "<td style=\"font-size:14px; align:center;\">".$pos."</td>";
  1798. $html .= "<td class=\"rc_button\"><a onClick=\"FW_cmd('/fhem?XHR=1&cmd.$name=set $name next')\"><img src=\"/fhem/icons/remotecontrol/black_btn_FF\" title=\"NEXT\"></a></td>";
  1799. $html .= "<td>&nbsp;</td></tr>";
  1800. $html .= "<tr><td>&nbsp;</td>";
  1801. $html .= "<td class=\"rc_button\"><a onClick=\"FW_cmd('/fhem?XHR=1&cmd.$name=set $name volumeDown')\"><img src=\"/fhem/icons/remotecontrol/black_btn_VOLDOWN2\" title=\"VOL -\"></a></td>";
  1802. $html .= "<td style=\"font-size:14px; align:center;\">".$volume."</td>";
  1803. $html .= "<td class=\"rc_button\"><a onClick=\"FW_cmd('/fhem?XHR=1&cmd.$name=set $name volumeUp')\"><img src=\"/fhem/icons/remotecontrol/black_btn_VOLUP2\" title=\"VOL +\"></a></td>";
  1804. $html .= "<td>&nbsp;</td></tr>";
  1805. if ($hash->{".outputs"})
  1806. {
  1807. my @outp = split("\n" , $hash->{".outputs"});
  1808. my $oid;
  1809. my $oname = "";
  1810. my $oen = "";
  1811. my $osel = "";
  1812. foreach (@outp)
  1813. {
  1814. my @val = split(": " , $_);
  1815. $oid = $val[1] if ($val[0] eq "outputid");
  1816. $oname = $val[1] if ($val[0] eq "outputname");
  1817. $oen = $val[1] if ($val[0] eq "outputenabled");
  1818. if ($oen ne "")
  1819. {
  1820. $html .= "<tr>";
  1821. $html .="<td style='font-size:10px;' colspan='5' align='center'>$oid.$oname ";
  1822. $osel = ($oen eq "1") ? "checked" : "";
  1823. $html .="<input type='radio' name='B".$oid." value='1' $osel onclick=\"FW_cmd('/fhem?XHR=1&cmd.$name=set $name mpdCMD enableoutput $oid')\">on&nbsp;";
  1824. $osel = ($oen ne "1") ? "checked" : "";
  1825. $html .="<input type='radio' name='B".$oid." value='0' $osel onclick=\"FW_cmd('/fhem?XHR=1&cmd.$name=set $name mpdCMD disableoutput $oid')\">off</td>";
  1826. $html .="</tr>";
  1827. $oen = "";
  1828. }
  1829. }
  1830. }
  1831. $html .= "</table></div>";
  1832. return $html;
  1833. }
  1834. sub MPD_summaryFn($$$$) {
  1835. my ($FW_wname, $hash, $room, $pageHash) = @_;
  1836. $hash = $defs{$hash};
  1837. my $state = $hash->{STATE};
  1838. my $txt = $state;
  1839. my $name = $hash->{NAME};
  1840. my $playlist = $hash->{".playlist"};
  1841. my $playlists = $hash->{".playlists"};
  1842. my $music = $hash->{".music"};
  1843. my $musiclist = $hash->{".musiclist"};
  1844. my ($icon,$isHtml,$link,$html,@list,$sel);
  1845. ($icon, $link, $isHtml) = FW_dev2image($name);
  1846. $txt = ($isHtml ? $icon : FW_makeImage($icon, $state)) if ($icon);
  1847. $link = "cmd.$name=set $name $link" if ($link);
  1848. $txt = "<a onClick=\"FW_cmd('/fhem?XHR=1&$link&room=$room')\">".$txt."</a>" if ($link);
  1849. my $rname = "";
  1850. my $artist = "";
  1851. my $title = "";
  1852. my $album = "";
  1853. my $hw;
  1854. my $file = (defined($hash->{READINGS}{"file"}{VAL})) ? $hash->{READINGS}{"file"}{VAL}."&nbsp;<br />" : "";
  1855. if (defined($hash->{READINGS}{"Title"}{VAL}))
  1856. { $title = ($hash->{READINGS}{"Title"}{VAL} ne "" ) ? $hash->{READINGS}{"Title"}{VAL}."&nbsp;<br />" : "";}
  1857. if (defined($hash->{READINGS}{"Artist"}{VAL}))
  1858. { $artist = ($hash->{READINGS}{"Artist"}{VAL} ne "") ? $hash->{READINGS}{"Artist"}{VAL}."&nbsp;<br />": "";}
  1859. if (defined($hash->{READINGS}{"Album"}{VAL}))
  1860. { $album = ($hash->{READINGS}{"Album"}{VAL} ne "") ? $hash->{READINGS}{"Album"}{VAL}."&nbsp;" : "";}
  1861. if (defined($hash->{READINGS}{"Name"}{VAL}))
  1862. { $rname = ($hash->{READINGS}{"Name"}{VAL} ne "") ? $hash->{READINGS}{"Name"}{VAL}."&nbsp;<br />" : ""; }
  1863. $html ="<table><tr><td>$txt</td><td>";
  1864. if (($playlists) && $hash->{".sPlayL"})
  1865. {
  1866. $html .= "<select id=\"".$name."_List\" name=\"".$name."_List\" class=\"dropdown\" onchange=\"FW_cmd('/fhem?XHR=1&cmd.$name=set $name playlist ' + this.options[this.selectedIndex].value)\">";
  1867. $html .= "<optgroup label=\"Playlists\">";
  1868. $html .= "<option>---</option>";
  1869. @list = sort( split ("\n",$playlists));
  1870. foreach (@list)
  1871. {
  1872. $sel = ($_ eq $playlist) ? " selected" : "";
  1873. $html .= "<option ".$sel." value=\"".uri_escape($_)."\">".$_."</option>";
  1874. }
  1875. $html .= "</optgroup></select><br/>";
  1876. }
  1877. if (($musiclist) && $hash->{".sMusicL"})
  1878. {
  1879. $html .= "<select id=\"".$name."_List\" name=\"".$name."_List\" class=\"dropdown\" onchange=\"FW_cmd('/fhem?XHR=1&cmd.$name=set $name playfile ' + this.options[this.selectedIndex].value)\">";
  1880. $html .= "<optgroup label=\"Music\">";
  1881. $html .= "<option>---</option>";
  1882. @list = sort (split ("\n",$musiclist));
  1883. foreach (@list)
  1884. {
  1885. $sel = ($_ eq $music) ? " selected" : "";
  1886. $html .= "<option ".$sel." value=\"".uri_escape($_)."\">".$_."</option>";
  1887. }
  1888. $html .= "</optgroup></select>";
  1889. }
  1890. $html.= "</td><td>";
  1891. if ($rname.$artist.$title.$album ne "")
  1892. {
  1893. $html .= (($state eq "play") || ($state eq "pause")) ? $rname.$artist.$title.$album : "&nbsp;";
  1894. if ((ReadingsVal($name,"artist_image","") ne "") && (($state eq "play") || ($state eq "pause")))
  1895. {
  1896. $hw = (index(ReadingsVal($name,"artist_image",""),"icon") == -1) ? " width='32' height='32'" : "";
  1897. $html .= "</td><td><img src='".$hash->{READINGS}{"artist_image"}{VAL}."' alt='".$hash->{'.artist'}."' $hw/>";
  1898. }
  1899. if ((ReadingsVal($name,"album_image","") ne "") && (($state eq "play") || ($state eq "pause")))
  1900. {
  1901. $hw = (index(ReadingsVal($name,"album_image",""),"icon") == -1) ? " width='32' height='32'" : "";
  1902. $html .= "</td><td><img src='".ReadingsVal($name,"album_image","")."' $hw />";
  1903. }
  1904. }
  1905. else
  1906. {
  1907. $html .= (($state eq "play") || ($state eq "pause")) ? $file : "&nbsp;";
  1908. }
  1909. $html .= "</td></tr></table>";
  1910. return $html;
  1911. }
  1912. sub MPD_SaveBookmark($)
  1913. {
  1914. my ($hash)= @_;
  1915. my $name= $hash->{NAME};
  1916. MPD_Set($hash,$name,"save_bookmark","auto");
  1917. return;
  1918. }
  1919. 1;
  1920. =pod
  1921. =item device
  1922. =item summary controls MPD or Mopidy music server
  1923. =item summary_DE steuert den MPD oder Mopidy Musik Server
  1924. =begin html
  1925. <a name="MPD"></a>
  1926. <h3>MPD</h3>
  1927. FHEM module to control a MPD (or Mopidy) like the MPC (MPC = Music Player Command, the command line interface to the <a href='http://en.wikipedia.org/wiki/Music_Player_Daemon'>Music Player Daemon</a> )<br>
  1928. To install a MPD on a Raspberry Pi you will find a lot of documentation at the web e.g. http://www.forum-raspberrypi.de/Thread-tutorial-music-player-daemon-mpd-und-mpc-auf-dem-raspberry-pi in german<br>
  1929. FHEM Forum : <a href='http://forum.fhem.de/index.php/topic,18517.0.html'>Modul f&uuml;r MPD</a> ( in german )<br>
  1930. Modul requires JSON -> sudo apt-get install libjson-perl <br>
  1931. If you are using Mopidy with Spotify support you may also need LWP::UserAgent -> sudo apt-get install libwww-perl<br>
  1932. <ul>
  1933. <a name="MPDdefine"></a>
  1934. <b>Define</b>
  1935. <ul>
  1936. define &lt;name&gt; MPD &lt;IP MPD Server | default localhost&gt; &lt;Port MPD Server | default 6600&gt;<br>
  1937. Example:<br>
  1938. <pre>
  1939. define myMPD MPD 192.168.0.99 7000
  1940. </pre>
  1941. if FHEM and MPD a running on the same device :
  1942. <pre>
  1943. define myMPD MPD
  1944. </pre>
  1945. </ul>
  1946. <br>
  1947. <a name="MPDset"></a>
  1948. <b>Set</b><ul>
  1949. <code>set &lt;name&gt; &lt;what&gt;</code>
  1950. <br>&nbsp;<br>
  1951. Currently, the following commands are defined.<br>
  1952. &nbsp;<br>
  1953. play => like MPC play , start playing song in playlist<br>
  1954. clear => like MPC clear , delete MPD playlist<br>
  1955. stop => like MPC stop, stops playing <br>
  1956. pause => like MPC pause<br>
  1957. previous => like MPC previous, play previous song in playlist<br>
  1958. next => like MPC next, play next song in playlist<br>
  1959. random => like MPC random, toggel on/off<br>
  1960. repeat => like MPC repeat, toggel on/off<br>
  1961. toggle => toggles from play to stop or from stop/pause to play<br>
  1962. updateDb => like MPC update<br>
  1963. volume (%) => like MPC volume %, 0 - 100<br>
  1964. volumeUp => inc volume ( + attr volumeStep size )<br>
  1965. volumeDown => dec volume ( - attr volumeStep size )<br>
  1966. playlist (playlistname|songnumber|position) set playlist on MPD Server. If songnumber and/or postion not defined<br>
  1967. MPD starts playing with the first song at position 0<br>
  1968. playfile (file) => create playlist + add file to playlist + start playing<br>
  1969. IdleNow => send Idle command to MPD and wait for events to return<br>
  1970. reset => reset MPD Modul<br>
  1971. mpdCMD (cmd) => send a command to MPD Server ( <a href='http://www.musicpd.org/doc/protocol/'>MPD Command Ref</a> )<br>
  1972. mute => on,off,toggle<br>
  1973. seekcur (time) => Format: [[hh:]mm:]ss. Not before MPD version 0.20.<br>
  1974. forward => jump forward in the current track as far as defined in the <i>seekStep</i> Attribute, default 7%<br>
  1975. rewind => jump backwards in the current track, as far as defined in the <i>seekStep</i> Attribute, default 7%<br>
  1976. channel (no) => loads the playlist with the given number<br>
  1977. channelUp => loads the next playlist<br>
  1978. channelDown => loads the previous playlist<br>
  1979. save_bookmark => saves the current state of the playlist (track number and position inside the track) for the currently loaded playlist
  1980. This will only work if the playlist was loaded through the module and if the attribute bookmarkDir is set. (not on radio streams !)<br>
  1981. load_bookmark <name> => resumes the previously saved state of the currently loaded playlist and jumps to the associated tracknumber and position inside the track<br>
  1982. </ul>
  1983. <br>
  1984. <a name="MPDget"></a>
  1985. <b>Get</b><ul>
  1986. <code>get &lt;name&gt; &lt;what&gt;</code>
  1987. <br>&nbsp;<br>
  1988. Currently, the following commands are defined.<br>
  1989. music => list all MPD music files in MPD databse<br>
  1990. playlists => list all MPD playlist in MPD databse<br>
  1991. playlistsinfo => show current playlist informations<br>
  1992. webrc => HTML output for a simple Remote Control on FHEM webpage e.g :.<br>
  1993. <pre>
  1994. define &lt;name&gt; weblink htmlCode {fhem("get &lt;name&gt; webrc", 1)}
  1995. attr &lt;name&gt; room MPD
  1996. </pre>
  1997. statusRequest => get MPD status<br>
  1998. currentsong => get infos from current song in playlist<br>
  1999. outputs => get name,id,status about all MPD output devices in /etc/mpd.conf<br>
  2000. bookmarks => list all stored bookmarks<br>
  2001. </ul>
  2002. <br>
  2003. <a name="MPDattr"></a>
  2004. <b>Attributes</b>
  2005. <ul>
  2006. <li>password <pwd>, if password in mpd.conf is set</li>
  2007. <li>loadMusic 1|0 => load titles from MPD database at startup (not supported by modipy)</li>
  2008. <li>loadPlaylists 1|0 => load playlist names from MPD database at startup</li>
  2009. <li>volumeStep 1|2|5|10 => Step size for Volume +/- (default 5)</li>
  2010. <li>titleSplit 1|0 => split title to artist and title if no artist is given in songinfo (e.g. radio-stream default 1)</li>
  2011. <li>timeout (default 1) => timeout in seconds for TCP connection timeout</li>
  2012. <li>waits (default 60) => if idle process ends with error, seconds to wait</li>
  2013. <li>stateMusic 1|0 => show Music DropDown box in web frontend</li>
  2014. <li>statePlaylists 1|0 => show Playlists DropDown box in web frontend</li>
  2015. <li>player mpd|mopidy|forked-daapd => which player is controlled by the module</li>
  2016. <li>Cover Art functions from <a href="http://www.last.fm/"><b>Last.fm</b></a> :</li>
  2017. <li>image_size -1|0|1|2|3 (default -1 = don't use artist images and album cover from Last.fm)<br>
  2018. Last.fm is using diffrent image sizes :<br>
  2019. 0 = 32x32 , 1 = 64x64 , 2 = 174x174 , 3 = 300x300</li>
  2020. <li>artist_content 0|1 => store artist informations in Reading artist_content</li>
  2021. <li>artist_summary 0|1 => stote more artist informations in Reading artist_summary<br>
  2022. Example with readingsGroup :<br>
  2023. <pre>
  2024. define rg_artist readingsGroup &ltMPD name&gt:artist,artist_image_html,artist_summary
  2025. attr rg_artist room MPD
  2026. </pre></li>
  2027. <li>cache (default lfm => /fhem/www/lfm) store artist image and album cover in a local directory</li>
  2028. <li>unknown_artist_image => show this image if no other image is avalible (default : /fhem/icons/1px-spacer)</li>
  2029. <li>bookmarkDir => set a writeable directory here to enable saving and restoring of playlist states using the set bookmark and get bookmark commands</li>
  2030. <li>autoBookmark => set this to 1 to enable automatic loading and saving of playlist states whenever the playlist is changed using this module</li>
  2031. <li>seekStep => set this to define how far the forward and rewind commands jump in the current track. Defaults to 7 if not set</li>
  2032. <li>seekStepSmall (default 1) => set this on top of seekStep to define a smaller step size, if the current playing position is below seekStepThreshold percent. This is useful to skip intro music, e.g. in radio plays or audiobooks.</li>
  2033. <li>seekStepSmallThreshold (default 0) => used to define when seekStep or seekStepSmall is applied. Defaults to 0. If set e.g. to 10, then during the first 10% of a track, forward and rewind are using the seekStepSmall value.</li>
  2034. <li>no_playlistcollection (default 0) => if set to 1 , dont create reading playlistcollection</li>
  2035. </ul>
  2036. <br>
  2037. <b>Readings</b>
  2038. <ul>
  2039. all MPD internal values<br>
  2040. artist_image : (if using Last.fm)<br>
  2041. artist_image_html : (if using Last.fm)<br>
  2042. album_image : (if using Last.fm)<br>
  2043. album_image_html : (if using Last.fm)<br>
  2044. artist_content : (if using Last.fm)<br>
  2045. artist_summary : (if using Last.fm)<br>
  2046. currentTrackProvider : Radio / Bibliothek<br>
  2047. playlistinfo : (TabletUI Medialist)<br>
  2048. playlistcollection : (TabletUI)<br>
  2049. playlistname : (TabletUI) current playlist name<br>
  2050. playlist_num : current playlist number<br>
  2051. playlist_json : (Medialist Modul)<br>
  2052. rawTitle : Title information without changes from the modul
  2053. </ul>
  2054. </ul>
  2055. =end html
  2056. =begin html_DE
  2057. <a name="MPD"></a>
  2058. <h3>MPD</h3>
  2059. <ul>
  2060. FHEM Modul zur Steuerung des MPD (oder Mopidy) &auml;hnlich dem MPC (MPC = Music Player Command, das Kommando Zeilen Interface f&uuml;r den
  2061. <a href='http://en.wikipedia.org/wiki/Music_Player_Daemon'>Music Player Daemon</a> ) (englisch)<br>
  2062. Um den MPD auf einem Raspberry Pi zu installieren finden sich im Internet zahlreiche gute Dokumentaionen
  2063. z.B. <a href="http://www.forum-raspberrypi.de/Thread-tutorial-music-player-daemon-mpd-und-mpc-auf-dem-raspberry-pi"><b>hier</b></a><br>
  2064. Thread im FHEM Forum : <a href='http://forum.fhem.de/index.php/topic,18517.0.html'>Modul f&uuml;r MPD</a><br>
  2065. Das Modul ben&ouml;tigt zwingend JSON, installation z.B. mit <i>sudo apt-get install libjson-perl</i><br>
  2066. <a name="MPDdefine"></a>
  2067. <b>Define</b>
  2068. <ul>
  2069. define &lt;name&gt; MPD &lt;IP MPD Server | default localhost&gt; &lt;Port MPD Server | default 6600&gt;<br>
  2070. Beispiel :<br>
  2071. <ul><pre>
  2072. define myMPD MPD 192.168.0.99 7000
  2073. </pre>
  2074. wenn FHEM und der MPD auf dem gleichen PC laufen :
  2075. <pre>
  2076. define myMPD MPD
  2077. </pre>
  2078. </ul>
  2079. </ul>
  2080. <br>
  2081. <a name="MPDset"></a>
  2082. <b>Set</b><ul>
  2083. <code>set &lt;name&gt; &lt;was&gt;</code>
  2084. <br>&nbsp;<br>
  2085. z.Z. unterst&uuml;tzte Kommandos<br>
  2086. &nbsp;<br>
  2087. play => spielt den aktuellen Titel der MPD internen Playliste<br>
  2088. clear => l&ouml;scht die MPD interne Playliste<br>
  2089. stop => stoppt die Wiedergabe<br>
  2090. pause => Pause an/aus<br>
  2091. previous => spielt den vorherigen Titel in der Playliste<br>
  2092. next => spielt den n&aumlchsten Titel in der Playliste<br>
  2093. random => zuf&auml;llige Wiedergabe an/aus<br>
  2094. repeat => Wiederholung an/aus<br>
  2095. toggle => wechselt von play nach stop bzw. stop/pause nach play<br>
  2096. volume (%) => &auml;ndert die Lautst&auml;rke von 0 - 100%<br>
  2097. volumeUp => Lautst&auml;rke schrittweise erh&ouml;hen , Schrittweite = ( attr volumeStep size )<br>
  2098. volumeDown => Lautst&auml;rke schrittweise erniedrigen , Schrittweite = ( attr volumeStep size )<br>
  2099. playlist (name|SongNr|Position) => lade Playliste <name> aus der MPD Datenbank und starte die Wiedergabe<br>
  2100. Werden SongNr und/oder Position nicht mit &uuml;bergeben, startet die Wiedergabe mit dem ersten Titel (Song=0) am Anfang (Position=0)<br>
  2101. playfile (file) => erzeugt eine MPD interne Playliste mit file als Inhalt und spielt dieses ab<br>
  2102. updateDb => wie MPC update, Update der MPD Datenbank<br>
  2103. reset => reset des FHEM MPD Moduls<br>
  2104. mpdCMD (cmd) => sende cmd direkt zum MPD Server ( siehe auch <a href="http://www.musicpd.org/doc/protocol/">MPD Comm Ref</a> )<br>
  2105. IdleNow => sendet das Kommando idle zum MPD und wartet auf Ereignisse<br>
  2106. clear_readings => l&ouml;scht sehr viele Readings<br>
  2107. mute => on,off,toggle<br>
  2108. seekcur (zeit) => Format: [[hh:]mm:]ss. nicht vor MPD Version 0.20<br>
  2109. forward => Springt im laufenden Track um einen optional per seekStep oder seekStepSmall definierten Wert nach vorne bzw. defaultm&auml;ßig um 7%. <br>
  2110. rewind => Springt so wie bei forward beschrieben entsprechend zur&uuml;ck. <br>
  2111. channel => Wechsele zur Playliste mit der angegebenen Nummer<br>
  2112. channelUp => wechselt zur n&auml;chsten Playliste<br>
  2113. channelDown => wechselt zur vorherigen Playliste<br>
  2114. save_bookmark => speichert den aktuellen Zustand (Tracknummer und Position innerhalb des Tracks für die gerade geladene Playliste<br>.
  2115. dies sunktioniert nur, wenn die Playliste mit dem Modul geladen wurde und wenn das Attribut bookmarkDir gesetzt ist.<br>
  2116. load_bookmark <name> => stellt den zuletzt gespeicherten Zustand (set bookmark) der geladenen Playliste wieder her und springt zum gespeicherten Track und Position<br>
  2117. wird <name> zusätzlich mit übergeben wird zuvor die entsprechend Playliste geladen<br>
  2118. </ul>
  2119. <br>
  2120. <a name="MPDget"></a>
  2121. <b>Get</b><ul>
  2122. <code>get &lt;name&gt; &lt;was&gt;</code>
  2123. <br>&nbsp;<br>
  2124. z.Z. unterst&uuml;tzte Kommandos<br>
  2125. music => zeigt alle Dateien der MPD Datenbank<br>
  2126. playlists => zeigt alle Playlisten der MPD Datenbank<br>
  2127. playlistsinfo => zeigt Informationen der aktuellen Playliste<br>
  2128. webrc => HTML Ausgabe einer einfachen Web Fernbedienung Bsp :.<br>
  2129. <pre>
  2130. define &lt;name&gt; weblink htmlCode {fhem("get &lt;name&gt; webrc", 1)}
  2131. attr &lt;name&gt; room MPD
  2132. </pre>
  2133. statusRequest => hole aktuellen MPD Status<br>
  2134. currentsong => zeigt Informationen zum aktuellen Titel der MPD internen Playliste<br>
  2135. outputs => zeigt Informationen der definierten MPD Ausgabe Kan&auml;le ( aus /etc/mpd.conf )<br>
  2136. bookmarks => zeigt eine Liste aller bisher gespeicherten Bookmarks<br>
  2137. </ul>
  2138. <br>
  2139. <a name="MPDattr"></a>
  2140. <b>Attribute</b>
  2141. <ul>
  2142. <li>password <pwd> => Password falls in der mpd.conf definiert</li>
  2143. <li>loadMusic 1|0 => lade die MPD Titel beim FHEM Start : mpd.conf - music_directory</li>
  2144. <li>loadPlaylists 1|0 => lade die MPD Playlisten beim FHEM Start : mpd.conf - playlist_directory</li>
  2145. <li>volumeStep x => Schrittweite f&uuml;r Volume +/-</li>
  2146. <li>titleSplit 1|0 => zerlegt die aktuelle Titelangabe am ersten Vorkommen von - (BlankMinusBlank) in die zwei Felder Artist und Titel,<br>
  2147. wenn im abgespielten Titel die Interpreten Information nicht verf&uuml;gbar ist (sehr oft bei Radio-Streams default 1)<br>
  2148. Liegen keine Titelangaben vor wird die Ausgabe durch den Namen der Radiostation ersetzt</li>
  2149. <li>timeout (default 1) => Timeoutwert in Sekunden für die Verbindung fhem-mpd</li>
  2150. <li>waits (default 60) => &Uuml;berwachungszeit in Sekunden f&uuml;r den Idle Prozess. In Verbindung mit refresh_song der Aktualisierungs Intervall für die aktuellen Songparamter,<br>
  2151. (z.B. um den Fortschrittsbalken bei TabletUI aktuell zu halten) </li>
  2152. <li>stateMusic 1|0 => zeige Musikliste als DropDown im Webfrontend</li>
  2153. <li>statePlaylists 1|0 => zeige Playlisten als DropDown im Webfrontend</li>
  2154. <li>player mpd|mopidy|forked-daapd (default mpd) => welcher Player wird gesteuert<br>
  2155. <b>ACHTUNG</b> : Mopidy unterst&uuml;tzt nicht alle Kommandos des echten MPD ! (siehe <a href="https://docs.mopidy.com/en/latest/ext/mpd/">Mopidy Dokumentation</a>)</li>
  2156. <li>Cover Art Funktionen von <a href="http://www.last.fm/"><b>last.fm</b></a> :</li>
  2157. <li>image_size -1|0|1|2|3 (default -1 = keine Interpretenbilder und Infos von last.fm verwenden)<br>
  2158. last.fm stellt verschiedene Bildgroessen zur Verfügung :<br>
  2159. 0 = 32x32 , 1 = 64x64 , 2 = 174x174 , 3 = 300x300</li>
  2160. <li>artist_content 0|1 => stellt Interpreteninformation im Reading artist_content zur Verf&uuml;gung</li>
  2161. <li>artist_summary 0|1 => stellt weitere Interpreteninformation im Reading artist_summary zur Verf&uuml;gung<br>
  2162. Beispiel Anzeige mittels readingsGroup :<br>
  2163. <pre>
  2164. define rg_artist readingsGroup &ltMPD name&gt:artist,artist_image_html,artist_summary
  2165. attr rg_artist room MPD
  2166. </pre></li>
  2167. <li>cache (default lfm => /fhem/www/lfm) Zwischenspeicher für die JSON und PNG Dateien<br>
  2168. <b>Wichtig</b> : Der User unter dem der fhem Prozess ausgef&uuml;hrt wird (default fhem) muss Lese und Schreibrechte in diesem Verzeichniss haben !<br>
  2169. Das Verzeichnis sollte auch unterhalb von www liegen, damit der fhem Webserver direkten Zugriff auf die Bilder hat.</li>
  2170. <li>unknown_artist_image => Ersatzimage wenn kein anderes Image zur Verf&uuml;gung steht (default : /fhem/icons/1px-spacer)</li>
  2171. <li>bookmarkDir => ein vom FHEM User les- und beschreibbares Verzeichnis. Wennn dieses definiert wird, ist das Speichern und Wiederherstellen von Playlistzust&auml;nden mit Hilfe von set/get bookmark m&ouml;glich</li>
  2172. <li>autoBookmark => wenn dies auf 1 gesetzt wird, dann werden automatisch Playlistenzust&auml;nde geladen und gespeichert, immer wenn die Playliste mit diesem Modul gewechselt wird</li>
  2173. <li>seekStep => wenn definiert, wird dadurch die Sprungweite von forward und rewind gesetzt. Der Wert gilt als Prozentwert. default: 7</li>
  2174. <li>seekStepSmall => Wenn diesem Attribut kann für den Anfang eines Tracks innerhalb der ersten per seekStepSmall definierten Prozent eine kleinere Sprungweite definiert werden,<br>
  2175. um so z.B. die Intromusik von H&ouml;rspielen oder H&ouml;rb&uuml;chern &uuml;berspringen zu k&ouml;nnen. default: 1</li>
  2176. <li>seekStepSmallThreshold => unterhalb dieses Wertes wird seekStepSmall benutzt, oberhalb seekStep default: 0 (ohne Funktion)</li>
  2177. <li>no_playlistcollection (default 0) => wenn auf 1 gesetzt wird das Reading playlistcollection nicht erzeugt</li>
  2178. </ul>
  2179. <br>
  2180. <b>Readings</b>
  2181. <ul>
  2182. - alle MPD internen Werte<br>
  2183. - vom Modul direkt erzeugte Readings :<br>
  2184. playlistinfo : (TabletUI Medialist)<br>
  2185. playlistcollection : (TabletUI)<br>
  2186. playlistname : (TabletUI)<br>
  2187. artist_image : (bei Nutzung von Last.fm)<br>
  2188. artist_image_html : (bei Nutzung von Last.fm)<br>
  2189. album_image : (bei Nutzung von Last.fm)<br>
  2190. album_image_html : (bei Nutzung von Last.fm)<br>
  2191. artist_content : (bei Nutzung von Last.fm)<br>
  2192. artist_summary : (bei Nutzung von Last.fm)<br>
  2193. playlistinfo : (z.B. f&uuml;r die TabletUI Medialist)<br>
  2194. playlistcollection : (TabletUI) Liste der Playlisten<br>
  2195. playlistname : (TabletUI) Name der aktuellen Playliste aus playlistcollection<br>
  2196. playlist_num : Playlisten Nr. (0 .. n) der aktuellen Playliste aus playlistcollection
  2197. playlist_json : (notwendig f&uuml; das Medialist Modul)<br>
  2198. Cover : Cover Bild zum aktuellen Song aus playlist_json<br>
  2199. currentTrackProvider : Radio / Bibliothek - Unterscheidung Radio Stream oder lokale Datei<br>
  2200. rawTitle : Title Information ohne Ver&auml;nderungen durch das Modul
  2201. </ul>
  2202. </ul>
  2203. =end html_DE
  2204. =cut