73_MPD.pm 85 KB

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