37_Spotify.pm 55 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340
  1. ##############################################################################
  2. # $Id: 37_Spotify.pm 16967 2018-07-09 16:02:50Z neumann $
  3. #
  4. # 37_Spotify.pm
  5. #
  6. # 2017 Oskar Neumann
  7. # oskar.neumann@me.com
  8. #
  9. ##############################################################################
  10. package main;
  11. use strict;
  12. use warnings;
  13. use JSON;
  14. use MIME::Base64;
  15. use List::Util qw/shuffle/;
  16. sub Spotify_Initialize($) {
  17. my ($hash) = @_;
  18. $hash->{DefFn} = 'Spotify_Define';
  19. $hash->{NotifyFn} = 'Spotify_Notify';
  20. $hash->{UndefFn} = 'Spotify_Undefine';
  21. $hash->{SetFn} = 'Spotify_Set';
  22. $hash->{GetFn} = 'Spotify_Get';
  23. #$hash->{AttrFn} = "Spotify_Attr";
  24. $hash->{AttrList} = 'defaultPlaybackDeviceID alwaysStartOnDefaultDevice:0,1 updateInterval updateIntervalWhilePlaying disable:0,1 volumeStep ';
  25. $hash->{AttrList} .= $readingFnAttributes;
  26. $hash->{NOTIFYDEV} = "global";
  27. }
  28. sub Spotify_Define($) {
  29. my ($hash, $def) = @_;
  30. my $name = $hash->{NAME};
  31. my @a = split("[ \t][ \t]*", $def);
  32. my $hintGetVaildPair = "get a valid pair by creating a Spotify app".
  33. " here: https://developer.spotify.com/my-applications/#!/applications/create
  34. (recommendation is to use https://oskar.pw/ as redirect_uri because it displays the temporary access code - ".
  35. "this is safe because the code is useless without your client credentials and expires after a few minutes)";
  36. return 'wrong syntax: define <name> Spotify <client_id> <client_secret> [ <redirect_uri> ]
  37. - '. $hintGetVaildPair
  38. if( @a < 4 );
  39. my $client_id = $a[2];
  40. my $client_secret = $a[3];
  41. return 'invalid client_id / client_secret - '. $hintGetVaildPair
  42. if(length $client_id != 32 || length $client_secret != 32);
  43. $hash->{CLIENT_ID} = $client_id;
  44. $hash->{CLIENT_SECRET} = $client_secret;
  45. $hash->{REDIRECT_URI} = @a > 4 ? $a[4] : 'https://oskar.pw/';
  46. $hash->{helper}{custom_redirect} = @a > 4;
  47. Spotify_loadInternals($hash) if($init_done);
  48. return undef;
  49. }
  50. sub Spotify_Undefine($$) {
  51. my ($hash, $name) = @_;
  52. RemoveInternalTimer($hash);
  53. return undef;
  54. }
  55. sub Spotify_Notify($$) {
  56. my ($own_hash, $dev_hash) = @_;
  57. my $ownName = $own_hash->{NAME}; # own name / hash
  58. return "" if(IsDisabled($ownName)); # Return without any further action if the module is disabled
  59. my $devName = $dev_hash->{NAME}; # Device that created the events
  60. my $events = deviceEvents($dev_hash, 1);
  61. if($devName eq "global" && grep(m/^INITIALIZED|REREADCFG$/, @{$events})) {
  62. Spotify_loadInternals($own_hash);
  63. }
  64. }
  65. sub Spotify_Set($$@) {
  66. my ($hash, $name, $cmd, @args) = @_;
  67. return "\"set $name\" needs at least one argument" unless(defined($cmd));
  68. my $list = '';
  69. if(!defined $hash->{helper}{refresh_token}) {
  70. $list .= ' code';
  71. } else {
  72. $list .= ' playTrackByURI playContextByURI pause:noArg resume:noArg volume:slider,0,1,100 update:noArg';
  73. $list .= ' skipToNext:noArg skipToPrevious:noArg seekToPosition repeat:one,all,off shuffle:on,off transferPlayback volumeFade:slider,0,1,100 playTrackByName playPlaylistByName togglePlayback';
  74. $list .= ' playSavedTracks playRandomTrackFromPlaylistByURI randomPlayPlaylistByURI findTrackByName findArtistByName playArtistByName volumeUp volumeDown';
  75. }
  76. if($cmd eq 'code') {
  77. return "please enter the code obtained from the URL after calling \"get $name authorizationURL\""
  78. if( @args < 1 );
  79. return Spotify_getToken($hash, $args[0]);
  80. }
  81. return Spotify_update($hash, 1) if($cmd eq 'update');
  82. return Spotify_pausePlayback($hash) if($cmd eq 'pause');
  83. return Spotify_resumePlayback($hash, @args > 0 ? join(' ', @args) : undef) if($cmd eq 'resume');
  84. return Spotify_setVolume($hash, 1, $args[0], defined $args[1] ? join(' ', @args[1..$#args]) : undef) if ($cmd eq 'volume');
  85. return Spotify_skipToNext($hash) if ($cmd eq 'skipToNext' || $cmd eq 'skip' || $cmd eq 'next');
  86. return Spotify_skipToPrevious($hash) if ($cmd eq 'skipToPrevious' || $cmd eq 'previous' || $cmd eq 'prev');
  87. return Spotify_seekToPosition($hash, $args[0]) if($cmd eq 'seekToPosition');
  88. return Spotify_setRepeat($hash, $args[0]) if($cmd eq 'repeat');
  89. return Spotify_setShuffle($hash, $args[0]) if($cmd eq 'shuffle');
  90. return Spotify_transferPlayback($hash, @args > 0 ? join(' ', @args) : undef) if($cmd eq 'transferPlayback');
  91. return Spotify_playTrackByURI($hash, \@args, undef) if($cmd eq 'playTrackByURI');
  92. return Spotify_playTrackByName($hash, @args > 0 ? join(' ', @args) : undef) if($cmd eq 'playTrackByName');
  93. return Spotify_playPlaylistByName($hash, @args > 0 ? join(' ', @args) : undef) if($cmd eq 'playPlaylistByName');
  94. return Spotify_playContextByURI($hash, $args[0], $args[1], defined $args[2] ? join(' ', @args[2..$#args]) : undef) if($cmd eq 'playContextByURI');
  95. return Spotify_volumeFade($hash, $args[0], $args[1], $args[2], defined $args[3] ? join(' ', @args[3..$#args]) : undef) if($cmd eq 'volumeFade');
  96. return Spotify_volumeFadeStep($hash) if($cmd eq 'volumeFadeStep');
  97. return Spotify_togglePlayback($hash) if($cmd eq 'toggle' || $cmd eq 'togglePlayback');
  98. return Spotify_playSavedTracks($hash, $args[0], defined $args[1] ? join(' ', @args[1..$#args]) : undef) if($cmd eq 'playSavedTracks');
  99. return Spotify_playRandomTrackFromPlaylistByURI($hash, $args[0], $args[1], defined $args[2] ? join(' ', @args[2..$#args]) : undef) if($cmd eq 'playRandomTrackFromPlaylistByURI');
  100. return Spotify_randomPlayPlaylistByURI($hash, $args[0], $args[1], defined $args[2] ? join(' ', @args[2..$#args]) : undef) if($cmd eq 'randomPlayPlaylistByURI');
  101. return Spotify_findTrackByName($hash, @args > 0 ? join(' ', @args) : undef) if($cmd eq 'findTrackByName');
  102. return Spotify_findArtistByName($hash, @args > 0 ? join(' ', @args) : undef) if($cmd eq 'findArtistByName');
  103. return Spotify_playArtistByName($hash, @args > 0 ? join(' ', @args) : undef) if($cmd eq 'playArtistByName');
  104. return Spotify_volumeStep($hash, $cmd eq 'volumeDown' ? -1 : 1, $args[0], defined $args[1] ? join(' ', @args[1..$#args]) : undef) if($cmd eq 'volumeUp' || $cmd eq 'volumeDown');
  105. return "Unknown argument $cmd, choose one of $list";
  106. }
  107. sub Spotify_Get($$@) {
  108. my ($hash, $name, $cmd, @args) = @_;
  109. my $list = "";
  110. if(!defined $hash->{helper}{refresh_token}) {
  111. $list .= ' authorizationURL:noArg';
  112. } else {
  113. #$list .= ' me:noArg';
  114. }
  115. if($cmd eq "authorizationURL") {
  116. return $hash->{AUTHORIZATION_URL};
  117. }
  118. return "Unknown argument $cmd, choose one of $list";
  119. }
  120. sub Spotify_loadInternals($) {
  121. my ($hash) = @_;
  122. my $name = $hash->{NAME};
  123. $hash->{helper}{authorization_url} = "https://accounts.spotify.com/authorize/?client_id=$hash->{CLIENT_ID}&response_type=code&scope=playlist-read-private%20playlist-read-collaborative%20streaming%20user-library-read%20user-read-private%20user-read-playback-state&redirect_uri=" . urlEncode($hash->{REDIRECT_URI});
  124. $hash->{helper}{refresh_token} = ReadingsVal($name, '.refresh_token', undef);
  125. $hash->{helper}{access_token} = ReadingsVal($name, '.access_token', undef);
  126. $hash->{helper}{expires} = ReadingsVal($name, '.expires', undef);
  127. RemoveInternalTimer($hash);
  128. if(!defined(ReadingsVal($name, '.refresh_token', undef))) {
  129. $hash->{STATE} = 'authorization pending (see instructions)';
  130. $hash->{AUTHORIZATION_URL} = $hash->{helper}{authorization_url};
  131. $hash->{A1_INSTRUCTIONS} = 'Open AUTHORIZATION_URL in your browser and set the code afterwards. Make sure to specify REDIRECT_URI as a redirect_uri in your API application.';
  132. $hash->{A1_INSTRUCTIONS} .= ' It is safe to rely on https://oskar.pw/ as redirect_uri because your code is worthless without the client secret and only valid for a few minutes.
  133. However, feel free to specify any other redirect_uri in the definition and extract the code after being redirected yourself.' if(!$hash->{helper}{custom_redirect});
  134. } else {
  135. $hash->{STATE} = 'connected';
  136. my $pollInterval = $attr{$name}{pollInterval};
  137. $attr{$name}{webCmd} = 'toggle:next:prev:volumeUp:volumeDown' if(!defined $attr{$name}{webCmd});
  138. Spotify_poll($hash) if(defined $hash->{helper}{refresh_token} && !Spotify_isDisabled($hash));
  139. }
  140. }
  141. sub Spotify_getToken($$) { # exchanging code for token
  142. my ($hash, $code) = @_;
  143. my $name = $hash->{NAME};
  144. Log3 $name, 4, "$name: checking access code";
  145. my ($err,$data) = HttpUtils_BlockingGet({
  146. url => "https://accounts.spotify.com/api/token",
  147. method => "POST",
  148. timeout => 5,
  149. noshutdown => 1,
  150. data => {client_id => $hash->{CLIENT_ID}, client_secret => $hash->{CLIENT_SECRET}, grant_type => 'authorization_code', redirect_uri => $hash->{REDIRECT_URI}, 'code' => $code}
  151. });
  152. my $json = eval { JSON->new->utf8(0)->decode($data) };
  153. if(defined $json->{error}) {
  154. my $msg = 'Failed to get access token: ';
  155. if($json->{error_description} =~ /redirect/) {
  156. $msg = $msg . 'Please add '. $hash->{REDIRECT_URI} . ' as a redirect_uri at https://developer.spotify.com/my-applications/#!/applications/';
  157. } else {
  158. $msg = $msg . $json->{error_description};
  159. }
  160. Log3 $name, 3, "$name: $json->{error} - $msg";
  161. return $msg;
  162. }
  163. return "failed to get access token"
  164. if(!defined $json->{refresh_token});
  165. $hash->{helper}{refresh_token} = $json->{refresh_token};
  166. $hash->{helper}{access_token} = $json->{access_token};
  167. $hash->{helper}{expires} = gettimeofday() + $json->{expires_in};
  168. $hash->{helper}{scope} = $json->{scope};
  169. delete $hash->{AUTHORIZATION_URL};
  170. delete $hash->{A1_INSTRUCTIONS};
  171. $hash->{STATE} = "connected";
  172. Spotify_writeTokens($hash);
  173. RemoveInternalTimer($hash);
  174. Spotify_updateMe($hash, 0);
  175. Spotify_poll($hash);
  176. return undef;
  177. }
  178. sub Spotify_writeTokens($) { # save gathered tokens
  179. my ($hash) = @_;
  180. readingsBeginUpdate($hash);
  181. readingsBulkUpdate($hash, '.refresh_token', $hash->{helper}{refresh_token});
  182. readingsBulkUpdateIfChanged($hash, '.access_token', $hash->{helper}{access_token});
  183. readingsBulkUpdate($hash, '.expires', $hash->{helper}{expires});
  184. readingsEndUpdate($hash, 1);
  185. }
  186. sub Spotify_refreshToken($) { # refresh the access token once it is expired
  187. my ($hash) = @_;
  188. my $name = $hash->{NAME};
  189. return 'Failed to refresh access token: refresh token missing' if(!defined $hash->{helper}{refresh_token});
  190. Log3 $name, 4, "$name: refreshing access code";
  191. my ($err,$data) = HttpUtils_BlockingGet({
  192. url => "https://accounts.spotify.com/api/token",
  193. method => "POST",
  194. timeout => 5,
  195. noshutdown => 1,
  196. data => {client_id => $hash->{CLIENT_ID}, client_secret => $hash->{CLIENT_SECRET}, grant_type => 'refresh_token', refresh_token => $hash->{helper}{refresh_token}}
  197. });
  198. my $json = eval { JSON->new->utf8(0)->decode($data) };
  199. if(defined $json->{error}) {
  200. if($json->{error} eq 'invalid_grant') {
  201. $hash->{helper}{refresh_token} = undef;
  202. $hash->{STATE} = 'invalid refresh token';
  203. $hash->{AUTHORIZATION_URL} = $hash->{helper}{authorization_url};
  204. CommandDeleteReading(undef, "$name .*");
  205. }
  206. my $msg = 'Failed to refresh access token: $json->{error_description}';
  207. Log3 $name, 3, "$name: $json->{error} - $msg";
  208. return $msg;
  209. }
  210. return "failed to refresh access token" if(!defined $json->{access_token});
  211. $hash->{helper}{access_token} = $json->{access_token};
  212. $hash->{helper}{expires} = gettimeofday() + $json->{expires_in};
  213. $hash->{helper}{scope} = $json->{scope} if(defined $json->{scope});
  214. Spotify_writeTokens($hash);
  215. Spotify_updateMe($hash, 0);
  216. Spotify_updateDevices($hash, 0);
  217. }
  218. sub Spotify_apiRequest($$$$$) { # any kind of api request
  219. my ($hash, $path, $args, $method, $blocking) = @_;
  220. my $name = $hash->{NAME};
  221. Spotify_refreshToken($hash) if(gettimeofday() >= $hash->{helper}{expires});
  222. if(!defined $hash->{helper}{refresh_token}) {
  223. Log3 $name, 3, "$name: could not execute API request (not authorized)";
  224. return 'You need to be authorized to perform this action.';
  225. }
  226. if(!defined $blocking || !$blocking) {
  227. HttpUtils_NonblockingGet({
  228. url => "https://api.spotify.com/v1/$path",
  229. method => $method,
  230. hash => $hash,
  231. apiPath => $path,
  232. timeout => 5,
  233. noshutdown => 1,
  234. data => $method eq 'PUT' && defined $args ? encode_json $args : $args,
  235. header => "Authorization: Bearer ". $hash->{helper}{access_token},
  236. callback => \&Spotify_dispatch
  237. });
  238. } else {
  239. my ($err,$data) = HttpUtils_BlockingGet({
  240. url => "https://api.spotify.com/v1/$path",
  241. method => $method,
  242. hash => $hash,
  243. apiPath => $path,
  244. timeout => 5,
  245. noshutdown => 1,
  246. data => $method eq 'PUT' && defined $args ? encode_json $args : $args,
  247. header => "Authorization: Bearer ". $hash->{helper}{access_token}
  248. });
  249. return Spotify_dispatch({hash => $hash, apiPath => $path, method => $method}, $err, $data);
  250. }
  251. }
  252. sub Spotify_updateMe($$) { # update user infos
  253. my ($hash, $blocking) = @_;
  254. Spotify_apiRequest($hash, 'me/', undef, 'GET', $blocking);
  255. return undef;
  256. }
  257. sub Spotify_updateDevices($$) { # update devices
  258. my ($hash, $blocking) = @_;
  259. Spotify_apiRequest($hash, 'me/player/devices', undef, 'GET', $blocking);
  260. return undef;
  261. }
  262. sub Spotify_pausePlayback($) { # pause playback
  263. my ($hash) = @_;
  264. my $name = $hash->{NAME};
  265. $hash->{helper}{is_playing} = 0;
  266. readingsSingleUpdate($hash, 'is_playing', 0, 1);
  267. Spotify_apiRequest($hash, 'me/player/pause', {}, 'PUT', 0);
  268. Log3 $name, 4, "$name: pause";
  269. return undef;
  270. }
  271. sub Spotify_resumePlayback($$) { # resume playback
  272. my ($hash, $device_id) = @_;
  273. my $name = $hash->{NAME};
  274. $device_id = Spotify_getTargetDeviceID($hash, $device_id, 0); # resolve target device id
  275. $hash->{helper}{is_playing} = 1;
  276. readingsSingleUpdate($hash, 'is_playing', 1, 1);
  277. Spotify_apiRequest($hash, 'me/player/play' . (defined $device_id ? "?device_id=$device_id" : ''), {}, 'PUT', 0);
  278. Log3 $name, 4, "$name: resume";
  279. return undef;
  280. }
  281. sub Spotify_updatePlaybackStatus($$) { # update the playback status
  282. my ($hash, $blocking) = @_;
  283. Spotify_apiRequest($hash, 'me/player', undef, 'GET', $blocking);
  284. return undef;
  285. }
  286. sub Spotify_setVolume($$$$) { # set the volume
  287. my ($hash, $blocking, $volume, $device_id) = @_;
  288. my $name = $hash->{NAME};
  289. return 'wrong syntax: set <name> volume <percent> [ <device_id / device_name> ]' if(!defined $volume);
  290. delete $hash->{helper}{fading} if($blocking && defined $hash->{helper}{fading}); # stop volumeFade if currently active (override)
  291. $device_id = Spotify_getTargetDeviceID($hash, $device_id, 0); # resolve target device id
  292. Spotify_apiRequest($hash, "me/player/volume?volume_percent=$volume". (defined $device_id ? "&device_id=$device_id" : ''), {}, 'PUT', $blocking);
  293. Log3 $name, 4, "$name: volume $volume" if(!defined $hash->{helper}{fading});
  294. return undef;
  295. }
  296. sub Spotify_skipToNext($) { # skip to next track
  297. my ($hash) = @_;
  298. my $name = $hash->{NAME};
  299. Spotify_apiRequest($hash, 'me/player/next', encode_json {}, 'POST', 0);
  300. Log3 $name, 4, "$name: skipToNext";
  301. return undef;
  302. }
  303. sub Spotify_skipToPrevious($) { # skip to previous track
  304. my ($hash) = @_;
  305. my $name = $hash->{NAME};
  306. Spotify_apiRequest($hash, 'me/player/previous', encode_json {}, 'POST', 0);
  307. Log3 $name, 4, "$name: skipToPrevious";
  308. return undef;
  309. }
  310. sub Spotify_seekToPosition($$) { # seek to position in track
  311. my ($hash, $position) = @_;
  312. my $name = $hash->{NAME};
  313. my (undef, $minutes, $seconds) = $position =~ m/(([0-9]+):)?([0-9]+)/;
  314. return 'wrong syntax: set <name> seekToPosition <position_in_s>' if(!defined $minutes && !defined $seconds);
  315. $position = ($minutes * 60 + $seconds) * 1000;
  316. Spotify_apiRequest($hash, "me/player/seek?position_ms=$position", {}, 'PUT', 0);
  317. return undef;
  318. }
  319. sub Spotify_setRepeat($$) { # set the repeat mode
  320. my ($hash, $mode) = @_;
  321. my $name = $hash->{NAME};
  322. return 'wrong syntax: set <name> repeat <one,all,off>' if(!defined $mode || ($mode ne 'one' && $mode ne 'all' && $mode ne 'off'));
  323. $mode = 'track' if($mode eq 'one');
  324. $mode = 'context' if($mode eq 'all');
  325. my $device_id = Spotify_getTargetDeviceID($hash, undef, 0);
  326. Spotify_apiRequest($hash, "me/player/repeat?state=$mode". (defined $device_id ? "&device_id=$device_id" : ""), {}, 'PUT', 0);
  327. Log3 $name, 4, "$name: repeat $mode";
  328. return undef;
  329. }
  330. sub Spotify_setShuffle($$) { # set the shuffle mode
  331. my ($hash, $mode) = @_;
  332. my $name = $hash->{NAME};
  333. return 'wrong syntax: set <name> shuffle <off,on>' if(!defined $mode || ($mode ne 'on' && $mode ne 'off'));
  334. $mode = $mode eq 'on' ? 'true' : 'false';
  335. my $device_id = Spotify_getTargetDeviceID($hash, undef, 0);
  336. Spotify_apiRequest($hash, "me/player/shuffle?state=$mode". (defined $device_id ? "&device_id=$device_id" : ""), {}, 'PUT', 0);
  337. Log3 $name, 4, "$name: shuffle $mode";
  338. return undef;
  339. }
  340. sub Spotify_transferPlayback($$) { # transfer the current playback to another device
  341. my ($hash, $device_id) = @_;
  342. $device_id = Spotify_getTransferTargetDeviceID($hash, $device_id);
  343. return 'device not found' if(!defined $device_id);
  344. my @device_ids = ($device_id);
  345. Spotify_apiRequest($hash, 'me/player', {device_ids => \@device_ids}, 'PUT', 0);
  346. return undef;
  347. }
  348. sub Spotify_playContextByURI($$$$) { # play a context (playlist, album or artist) using its uri
  349. my ($hash, $uri, $position, $device_id) = @_;
  350. my $name = $hash->{NAME};
  351. return 'wrong syntax: set <name> playContextByURI <album_uri / playlist_uri> [ <nr_of_first_track> ] [ <device_id> ]' if(!defined $uri);
  352. $device_id = $position . (defined $device_id ? " ". $device_id : "") if(defined $position && $position !~ /^[0-9]+$/);
  353. $position = 1 if(!defined $position || $position !~ /^[0-9]+$/);
  354. return Spotify_play($hash, undef, $uri, $position, $device_id);
  355. }
  356. sub Spotify_playTrackByURI($$$) { # play a track by its uri
  357. my ($hash, $uris, $device_id) = @_;
  358. my $name = $hash->{NAME};
  359. return 'wrong syntax: set <name> playTrackByURI <track_uri> ... [ <device_id> ]' if(@{$uris} < 1);
  360. Log3 $name, 4, "$name: track". (@{$uris} > 1 ? "s" : "")." ". join(" ", @{$uris}) if(!defined $hash->{helper}{skipTrackLog});
  361. delete $hash->{helper}{skipTrackLog} if(defined $hash->{helper}{skipTrackLog});
  362. return Spotify_play($hash, $uris, undef, undef, $device_id);
  363. }
  364. sub Spotify_playTrackByName($$) { # play a track by its name using search
  365. my ($hash, $trackname) = @_;
  366. return 'wrong syntax: set <name> playTrackByName <track_name> [ <device_id> ]' if(!defined $trackname);
  367. my @parts = split(" ", $trackname);
  368. my $device_id = Spotify_getTargetDeviceID($hash, $parts[-1], 0) if(@parts > 1); # resolve device id (may be last part of the command)
  369. $trackname = substr($trackname, 0, -length($parts[-1])-1) if(@parts > 1 && defined $device_id); # if last part was indeed the device id, remove it from the track name
  370. Spotify_findTrackByName($hash, $trackname);
  371. my $result = $hash->{helper}{searchResult};
  372. return 'could not find track' if(!defined $result);
  373. my @uris = ($result->{uri});
  374. Spotify_playTrackByURI($hash, \@uris, $device_id);
  375. return undef;
  376. }
  377. sub Spotify_findTrackByName($$) { # finds a track by its name and returns the result in the readings
  378. my ($hash, $trackname, $saveTrack) = @_;
  379. return 'wrong syntax: set <name> findTrackByName <track_name> [ <device_id> ]' if(!defined $trackname);
  380. delete $hash->{helper}{searchResult};
  381. Spotify_apiRequest($hash, 'search?limit=1&type=track&q='. urlEncode($trackname), undef, 'GET', 1);
  382. my $result = $hash->{helper}{dispatch}{json}{tracks}{items}[0];
  383. return 'could not find track' if(!defined $result);
  384. $hash->{helper}{searchResult} = $result;
  385. Spotify_saveTrack($hash, $result, 'search_track', 1);
  386. return undef;
  387. }
  388. sub Spotify_findArtistByName($$) { # finds an artist by its name and returns the result in the readings
  389. my ($hash, $artistname, $saveTrack) = @_;
  390. return 'wrong syntax: set <name> findArtistByName <track_name>' if(!defined $artistname);
  391. delete $hash->{helper}{searchResult};
  392. Spotify_apiRequest($hash, 'search?limit=1&type=artist&q='. urlEncode($artistname), undef, 'GET', 1);
  393. my $result = $hash->{helper}{dispatch}{json}{artists}{items}[0];
  394. return 'could not find artist' if(!defined $result);
  395. $hash->{helper}{searchResult} = $result;
  396. Spotify_saveArtist($hash, $result, 'search_artist', 1);
  397. return undef;
  398. }
  399. sub Spotify_playArtistByName($$) { # play an artist by its name using search
  400. my ($hash, $artistname) = @_;
  401. my $name = $hash->{NAME};
  402. return 'wrong syntax: set <name> playArtistByName <artist_name> [ <device_id> ]' if(!defined $artistname);
  403. my @parts = split(" ", $artistname);
  404. my $device_id = Spotify_getTargetDeviceID($hash, $parts[-1], 0) if(@parts > 1); # resolve device id (may be last part of the command)
  405. $artistname = substr($artistname, 0, -length($parts[-1])-1) if(@parts > 1 && defined $device_id); # if last part was indeed the device id, remove it from the track name
  406. Spotify_findArtistByName($hash, $artistname);
  407. my $result = $hash->{helper}{searchResult};
  408. return 'could not find artist' if(!defined $result);
  409. Spotify_playContextByURI($hash, $result->{uri}, undef, $device_id);
  410. Log3 $name, 4, "$name: artist $result->{uri} ($result->{name})";
  411. return undef;
  412. }
  413. sub Spotify_playPlaylistByName($$) { # play a playlist by its name
  414. my ($hash, $playlistname) = @_;
  415. my $name = $hash->{NAME};
  416. return 'wrong syntax: set <name> playPlaylistByName <playlist_name>' if(!defined $playlistname);
  417. my @parts = split(" ", $playlistname);
  418. my $device_id = Spotify_getTargetDeviceID($hash, $parts[-1], 0) if(@parts > 1); # resolve device id (may be last part of the command)
  419. $playlistname = substr($playlistname, 0, -length($parts[-1])-1) if(@parts > 1 && defined $device_id); # if last part was indeed the device id, remove it from the track name
  420. Spotify_apiRequest($hash, 'search?limit=1&type=playlist&q='. urlEncode($playlistname), undef, 'GET', 1);
  421. my $result = $hash->{helper}{dispatch}{json}{playlists}{items}[0];
  422. return 'could not find playlist' if(!defined $result);
  423. Spotify_playContextByURI($hash, $result->{uri}, undef, $device_id);
  424. Log3 $name, 4, "$name: $result->{uri} ($result->{name})";
  425. return undef;
  426. }
  427. sub Spotify_playSavedTracks($$$) { # play users saved tracks
  428. my ($hash, $first, $device_id) = @_;
  429. my $name = $hash->{NAME};
  430. $device_id = $first . (defined $device_id ? " " . $device_id : "") if(defined $first && $first !~ /^[0-9]+$/);
  431. $first = 1 if(!defined $first || $first !~ /^[0-9]+$/);
  432. Spotify_apiRequest($hash, 'me/tracks?limit=50'. ($first > 50 ? '&offset='. int($first/50)-1 : ''), undef, 'GET', 1); # getting saved tracks
  433. my $result = $hash->{helper}{dispatch}{json}{items};
  434. return 'could not get saved tracks' if(!defined $result);
  435. my @uris = map { $_->{track}{uri} } @{$result};
  436. shift @uris for 1..($first%50-1); # removing first elements users wants to skip
  437. Spotify_playTrackByURI($hash, \@uris, $device_id); # play them
  438. Log3 $name, 4, "$name: saved tracks";
  439. return undef;
  440. }
  441. sub Spotify_playRandomTrackFromPlaylistByURI($$$$) { # select a random track from a given playlist and play it (use case: e.g. alarm clocks)
  442. my ($hash, $uri, $limit, $device_id) = @_;
  443. my $name = $hash->{NAME};
  444. return 'wrong syntax: set <name> playRandomTrackFromPlaylistByURI <playlist_uri> [ <limit> ] [ <device_id> ]' if(!defined $uri);
  445. my ($user_id, $playlist_id) = $uri =~ m/user:(.*):playlist:(.*)/;
  446. return 'invalid playlist_uri' if(!defined $user_id || !defined $playlist_id);
  447. $device_id = $limit . (defined $device_id ? " " . $device_id : "") if(defined $limit && $limit !~ /^[0-9]+$/);
  448. $limit = undef if($limit !~ /^[0-9]+$/);
  449. Spotify_apiRequest($hash, "users/$user_id/playlists/$playlist_id/tracks?fields=items(track(name,uri))". (defined $limit ? "&limit=$limit" : ""), undef, 'GET', 1);
  450. my $result = $hash->{helper}{dispatch}{json}{items};
  451. return 'could not find playlist' if(!defined $result);
  452. my @alltracks = map { $_->{track} } @{$result};
  453. my $selectedTrack = $alltracks[rand @alltracks];
  454. my @uris = ($selectedTrack->{uri});
  455. $hash->{helper}{skipTrackLog} = 1;
  456. Spotify_playTrackByURI($hash, \@uris, $device_id);
  457. Log3 $name, 4, "$name: random track $selectedTrack->{uri} ($selectedTrack->{name}) from $uri";
  458. return undef;
  459. }
  460. sub Spotify_randomPlayPlaylistByURI($$$$) { # play the playlist in random order
  461. my ($hash, $uri, $limit, $device_id) = @_;
  462. my $name = $hash->{NAME};
  463. return 'wrong syntax: set <name> randomPlayPlaylistByURI <playlist_uri> [ <limit> ] [ <device_id> ]' if(!defined $uri);
  464. my ($user_id, $playlist_id) = $uri =~ m/user:(.*):playlist:(.*)/;
  465. return 'invalid playlist_uri' if(!defined $user_id || !defined $playlist_id);
  466. $device_id = $limit . (defined $device_id ? " " . $device_id : "") if(defined $limit && $limit !~ /^[0-9]+$/);
  467. $limit = undef if($limit !~ /^[0-9]+$/);
  468. Spotify_apiRequest($hash, "users/$user_id/playlists/$playlist_id/tracks?fields=items(track(name,uri))". (defined $limit ? "&limit=$limit" : ""), undef, 'GET', 1);
  469. my $result = $hash->{helper}{dispatch}{json}{items};
  470. return 'could not find playlist' if(!defined $result);
  471. my @uris = map { $_->{track}{uri} } @{$result};
  472. @uris = shuffle(@uris);
  473. $hash->{helper}{skipTrackLog} = 1;
  474. Spotify_playTrackByURI($hash, \@uris, $device_id);
  475. Log3 $name, 4, "$name: playing $uri in random order";
  476. return undef;
  477. }
  478. sub Spotify_play($$$$$) { # any play command (colleciton or track)
  479. my ($hash, $uris, $context_uri, $position, $device_id) = @_;
  480. my $name = $hash->{NAME};
  481. my $data = undef;
  482. if(defined $uris) {
  483. if(@{$uris} > 1 && @{$uris}[-1] !~ /spotify:/) {
  484. $device_id = pop @{$uris};
  485. }
  486. $data = {uris => $uris};
  487. } else {
  488. $data = {context_uri => $context_uri};
  489. $data->{offset} = {position => $position-1} if($position > 1);
  490. }
  491. $device_id = Spotify_getTargetDeviceID($hash, $device_id, 1);
  492. Spotify_apiRequest($hash, 'me/player/play'. (defined $device_id ? '?device_id='. $device_id : ''), $data, 'PUT', 1);
  493. Spotify_updatePlaybackStatus($hash, 1);
  494. return undef;
  495. }
  496. sub Spotify_volumeFade($$$$$) { # fade the volume of a device
  497. my ($hash, $targetVolume, $duration, $step, $device_id) = @_;
  498. return 'wrong syntax: set <name> volumeFade <target_volume> [ <duration_s> <percent_per_step> ] [ <device_id> ]' if(!defined $targetVolume);
  499. Spotify_updateDevices($hash, 1); # make sure devices are up to date (a valid start volume is required)
  500. $device_id = $duration . (defined $device_id ? " " . $device_id : "") if(defined $duration && $duration !~ /^[0-9]+$/);
  501. my $startVolume = $hash->{helper}{device_active}{volume_percent};
  502. return 'could not get start volume of active device' if(!defined $startVolume);
  503. $step = 5 if(!defined $step); # fall back to default step if not specified
  504. $duration = 5 if(!defined $duration || $duration !~ /^[0-9]+$/); # fallback to default value if duration is not specified or valid
  505. my $delta = abs($targetVolume - $startVolume);
  506. my $requiredSteps = int($delta/$step);
  507. return Spotify_setVolume($hash, 0, $targetVolume, $device_id) if($requiredSteps == 0); # no steps required, set volume and exit
  508. #Log3 "spotify", 3, "fading volume start $startVolume target $targetVolume duration $duration step $step steps $requiredSteps";
  509. $hash->{helper}{fading}{step} = $step;
  510. $hash->{helper}{fading}{startVolume} = $startVolume;
  511. $hash->{helper}{fading}{targetVolume} = $targetVolume;
  512. $hash->{helper}{fading}{requiredSteps} = $requiredSteps;
  513. $hash->{helper}{fading}{iteration} = 0;
  514. $hash->{helper}{fading}{duration} = $duration;
  515. $hash->{helper}{fading}{device_id} = $device_id;
  516. Spotify_volumeFadeStep($hash);
  517. return undef;
  518. }
  519. sub Spotify_togglePlayback($) { # toggle playback (pause if active, resume otherwise)
  520. my ($hash) = @_;
  521. my $name = $hash->{NAME};
  522. Log3 $name, 4, "$name: togglePlayback";
  523. if($hash->{helper}{is_playing}) {
  524. Spotify_pausePlayback($hash);
  525. } else {
  526. Spotify_resumePlayback($hash, undef);
  527. }
  528. return undef;
  529. }
  530. sub Spotify_volumeStep($$$$) {
  531. my ($hash, $direction, $step, $device_id) = @_;
  532. my $name = $hash->{NAME};
  533. $device_id = $step . (defined $device_id ? " ". $device_id : "") if(defined $step && $step !~ /^[0-9]+$/);
  534. $step = $attr{$name}{volumeStep} if(!defined $step || $step !~ /^[0-9]+$/);
  535. $step = 5 if(!defined $step);
  536. my $nextVolume = undef;
  537. if(defined $device_id) {
  538. my @devices = @{$hash->{helper}{devices}};
  539. foreach my $device (@devices) {
  540. if(defined $device->{id} && $device->{id} eq $device_id) {
  541. $nextVolume = $device->{volume_percent} + $step * $direction;
  542. $device->{volume_percent} = $nextVolume;
  543. }
  544. }
  545. } else {
  546. $nextVolume = $hash->{helper}{device_active}{volume_percent} + $step * $direction;
  547. $hash->{helper}{device_active}{volume_percent} = $nextVolume;
  548. }
  549. return "could not find device" if(!defined $nextVolume);
  550. Spotify_setVolume($hash, 0, $nextVolume, $device_id);
  551. return undef;
  552. }
  553. sub Spotify_getTargetDeviceID { # resolve target device settings
  554. my ($hash, $device_id, $newPlayback) = @_;
  555. my $name = $hash->{NAME};
  556. if(defined $device_id) { # use device id given by user
  557. foreach my $device (@{$hash->{helper}{devices}}) {
  558. return $device->{id} if((defined $device->{id} && $device->{id} eq $device_id) || (defined $device->{name} && lc($device->{name}) eq lc($device_id))); # resolve name to / verify device_id
  559. }
  560. # if not verified, continue to look for target device
  561. }
  562. # no specific device given by user for this command
  563. return Spotify_getTargetDeviceID($hash, $attr{$name}{defaultPlaybackDeviceID}, $newPlayback) if(defined $attr{$name}{defaultPlaybackDeviceID} # use default device or active device
  564. && (
  565. (
  566. defined $attr{$name}{alwaysStartOnDefaultDevice}
  567. && (!$hash->{helper}{is_playing} || $newPlayback)
  568. && $attr{$name}{alwaysStartOnDefaultDevice}
  569. )
  570. || !defined $hash->{helper}{device_active}{id}
  571. )
  572. && (!defined $device_id || $attr{$name}{defaultPlaybackDeviceID} ne $device_id)
  573. );
  574. # no default or active device available
  575. return $hash->{helper}{devices}[0]{id} if($newPlayback && !defined $hash->{helper}{device_active}{id}); # use first device available device on new playback
  576. # if no new playback, trust the user anyway (maybe the device list is outdated)
  577. return undef;
  578. }
  579. sub Spotify_getTransferTargetDeviceID($$) { # get target device id for transfer
  580. my ($hash, $targetdevice_id) = @_;
  581. my $device_id = Spotify_getTargetDeviceID($hash, $targetdevice_id, 1); # resolve to user settings
  582. return $device_id if(defined $targetdevice_id || (defined $device_id && $device_id ne $hash->{helper}{device_active}{id})); # only return if device was specified in command or default device is not active
  583. # target device not found, no (inactive) default device available
  584. Spotify_updateDevices($hash, 1); # make sure devices are up to date
  585. # choose any device that is not active
  586. foreach my $device (@{$hash->{helper}{devices}}) {
  587. return $device->{id} if(!$device->{is_active});
  588. }
  589. return undef;
  590. }
  591. sub Spotify_volumeFadeStep { # do a single fading stemp
  592. my ($hash) = @_;
  593. return if(!defined $hash->{helper}{fading});
  594. my $name = $hash->{NAME};
  595. my $iteration = $hash->{helper}{fading}{iteration};
  596. my $requiredSteps = $hash->{helper}{fading}{requiredSteps};
  597. my $startVolume = $hash->{helper}{fading}{startVolume};
  598. my $targetVolume = $hash->{helper}{fading}{targetVolume};
  599. my $step = $hash->{helper}{fading}{step};
  600. my $isLastStep = $iteration+1 >= $requiredSteps;
  601. my $nextVolume = int($isLastStep ? $targetVolume : $startVolume + ($iteration+1)*$step*($targetVolume < $startVolume ? -1 : 1));
  602. my $deltaBetweenSteps = ($hash->{helper}{fading}{duration}/$requiredSteps); # time in s between each step
  603. #Log3 "spotify", 3, "fading volume step start $startVolume target $targetVolume steps $requiredSteps step $step nextVolume $nextVolume iteration $iteration delta $deltaBetweenSteps";
  604. return if($nextVolume < 0 || $nextVolume > 100);
  605. $hash->{helper}{fading}{iteration}++;
  606. Spotify_setVolume($hash, 0, $nextVolume, $hash->{helper}{fading}{device_id});
  607. if(!$isLastStep) {
  608. InternalTimer(gettimeofday()+$deltaBetweenSteps*($iteration+1), 'Spotify_volumeFadeStep', $hash);
  609. }
  610. delete $hash->{helper}{fading} if($isLastStep);
  611. return undef;
  612. }
  613. sub Spotify_dispatch($$$) {
  614. my ($param, $err, $data) = @_;
  615. my $hash = $param->{hash};
  616. my $name = $hash->{NAME};
  617. my ($path) = split('\?', $param->{apiPath}, 2);
  618. my ($pathpt0, $pathpt1, $pathpt2) = split('/', $path, 3);
  619. my $method = $param->{method};
  620. delete $hash->{helper}{dispatch};
  621. if(!defined($param->{hash})){
  622. Log3 "Spotify", 2, 'Spotify: dispatch fail (hash missing)';
  623. return undef;
  624. }
  625. my $json = eval { JSON->new->utf8(0)->decode($data) };
  626. $hash->{helper}{dispatch}{json} = $json;
  627. #Log3 $name, 3, $name . ' : ' . $hash . $data;
  628. if(defined $json->{error}) {
  629. Log3 $name, 3, "$name: request failed: $json->{error}{message}";
  630. return Spotify_refreshToken($hash) if($json->{error}{message} =~ /expired/);
  631. readingsBeginUpdate($hash);
  632. readingsBulkUpdate($hash, 'error_code', $json->{error}{status}, 1);
  633. readingsBulkUpdate($hash, 'error_description', $json->{error}{message}, 1);
  634. readingsEndUpdate($hash, 1);
  635. return "request failed: $json->{error}{message}";
  636. }
  637. Log3 $name, 4, "$name: dispatch successful $path";
  638. if($path eq 'me/') {
  639. return 'could not get user data' if(!defined $json->{id});
  640. $hash->{helper}{user_id} = $json->{id};
  641. $hash->{helper}{subscription} = $json->{product};
  642. $hash->{helper}{uri} = $json->{uri};
  643. readingsBeginUpdate($hash);
  644. readingsBulkUpdateIfChanged($hash, 'user_id', $json->{id}, 1);
  645. readingsBulkUpdateIfChanged($hash, 'user_country', $json->{country}, 1);
  646. readingsBulkUpdateIfChanged($hash, 'user_subscription', $json->{subscription}, 1);
  647. readingsBulkUpdateIfChanged($hash, 'user_display_name', $json->{display_name}, 1);
  648. readingsBulkUpdateIfChanged($hash, 'user_profile_pic_url', $json->{images}[0]{url}, 1) if(defined $json->{images} && $json->{images} > 0);
  649. readingsBulkUpdateIfChanged($hash, 'user_follower_cnt', $json->{followers}{total}, 1);
  650. readingsEndUpdate($hash, 1);
  651. }
  652. if($path eq 'me/player/devices') {
  653. return 'could not update devices' if(!defined $json->{devices});
  654. delete $hash->{helper}{device_active};
  655. # delete any devices that are out of bounds
  656. if(defined $hash->{helper}{devices}) {
  657. my $index = 1;
  658. foreach my $device (@{$hash->{helper}{devices}}) {
  659. if($index > @{$json->{devices}}) {
  660. CommandDeleteReading(undef, "$name device_". $index ."_.*");
  661. }
  662. $index++;
  663. }
  664. } else {
  665. CommandDeleteReading(undef, "$name device_.*");
  666. }
  667. $hash->{helper}{devices} = $json->{devices};
  668. readingsBeginUpdate($hash);
  669. my $index = 1;
  670. foreach my $device (@{$hash->{helper}{devices}}) {
  671. Spotify_saveDevice($hash, $device, "device_". $index, 0);
  672. if($device->{is_active}) {
  673. Spotify_saveDevice($hash, $device, "device_active", 0);
  674. readingsBulkUpdateIfChanged($hash, 'volume', $device->{volume_percent});
  675. $hash->{helper}{device_active} = $device; # found active device
  676. }
  677. $hash->{helper}{device_default} = $device if(defined $attr{$name}{defaultPlaybackDeviceID} && $device->{id} eq $attr{$name}{defaultPlaybackDeviceID}); # found users default device
  678. $index++;
  679. }
  680. readingsBulkUpdateIfChanged($hash, 'devices_cnt', $index-1, 1);
  681. $hash->{helper}{is_active} = defined $hash->{helper}{device_active};
  682. if(!$hash->{helper}{is_active}) {
  683. Spotify_saveDevice($hash, {id => "none", "name" => "none", "volume_percent" => -1, "type" => "none"}, 'device_active', 0);
  684. $hash->{STATE} = "connected";
  685. readingsBulkUpdateIfChanged($hash, 'is_playing', 0, 1);
  686. }
  687. readingsEndUpdate($hash, 1);
  688. }
  689. if($path eq 'me/player') {
  690. if(!defined $json->{is_playing}) {
  691. $hash->{STATE} = 'connected';
  692. $hash->{helper}{is_playing} = 0;
  693. readingsSingleUpdate($hash, 'is_playing', 0, 1);
  694. return undef;
  695. }
  696. $hash->{helper}{is_active} = defined $json->{device} && $json->{device}{is_active};
  697. $hash->{helper}{is_playing} = $json->{is_playing} && $hash->{helper}{is_active};
  698. $hash->{helper}{repeat} = $json->{repeat_state} eq 'track' ? 'one' : ($json->{repeat_state} eq 'context' ? 'all' : 'off');
  699. $hash->{helper}{shuffle} = $json->{shuffle_state};
  700. $hash->{helper}{progress_ms} = $json->{progress_ms};
  701. $hash->{STATE} = $json->{is_playing} ? 'playing' : 'paused';
  702. readingsBeginUpdate($hash);
  703. readingsBulkUpdateIfChanged($hash, 'is_playing', $hash->{helper}{is_playing} ? 1 : 0, 1);
  704. readingsBulkUpdateIfChanged($hash, 'shuffle', $json->{shuffle_state} ? 'on' : 'off', 1);
  705. readingsBulkUpdateIfChanged($hash, 'repeat', $hash->{helper}{repeat}, 1);
  706. readingsBulkUpdateIfChanged($hash, 'progress_ms', $json->{progress_ms}, 1);
  707. readingsBulkUpdateIfChanged($hash, "progress", h2hms_fmt($json->{progress_ms} / 1000 / 60 / 60), 1);
  708. if(defined $json->{item}) {
  709. my $item = $json->{item};
  710. $hash->{helper}{track} = $item;
  711. Spotify_saveTrack($hash, $item, 'track', 0);
  712. } else {
  713. CommandDeleteReading(undef, "$name track_.*");
  714. }
  715. if($hash->{helper}{is_active}) {
  716. my $device = $json->{device};
  717. $hash->{helper}{device_active} = $device;
  718. readingsBulkUpdateIfChanged($hash, 'volume', $device->{volume_percent});
  719. Spotify_saveDevice($hash, $device, "device_active", 0);
  720. } else {
  721. delete $hash->{helper}{device_active};
  722. Spotify_saveDevice($hash, {id => "none", "name" => "none", "volume_percent" => -1, "type" => "none"}, 'device_active', 0);
  723. $hash->{STATE} = 'connected' if(!defined $json->{device});
  724. }
  725. if($hash->{helper}{is_playing}) {
  726. if(!defined $hash->{helper}{updatePlaybackTimer_next} || $hash->{helper}{updatePlaybackTimer_next} <= gettimeofday()) { # start refresh timer if not already started
  727. my $updateIntervalWhilePlaying = $attr{updateIntervalWhilePlaying};
  728. $updateIntervalWhilePlaying = 10 if(!defined $updateIntervalWhilePlaying);
  729. $hash->{helper}{updatePlaybackTimer_next} = gettimeofday()+$updateIntervalWhilePlaying; # refresh playback status every 15 seconds if currently playing
  730. InternalTimer($hash->{helper}{updatePlaybackTimer_next}, 'Spotify_updatePlaybackStatus', $hash);
  731. }
  732. if(defined $json->{item} && (!defined $hash->{helper}{nextSongTimer} || $hash->{helper}{nextSongTimer} <= gettimeofday())) { # refresh on finish of the song
  733. $hash->{helper}{nextSongTimer} = gettimeofday() + int(($json->{item}{duration_ms} - $json->{progress_ms}) / 1000) + 1;
  734. InternalTimer($hash->{helper}{nextSongTimer}, "Spotify_updatePlaybackStatus", $hash);
  735. }
  736. }
  737. readingsEndUpdate($hash, 1);
  738. return undef;
  739. }
  740. if($path eq 'me/player/volume') {
  741. Spotify_updateDevices($hash, 0) if(!defined $hash->{helper}{fading});
  742. return undef; # do not fall through
  743. }
  744. if(defined $pathpt1 && $pathpt1 eq 'player' && $method ne 'GET') { # on every modification on the player, update playback status
  745. Spotify_updatePlaybackStatus($hash, 1);
  746. InternalTimer(gettimeofday()+2, 'Spotify_updatePlaybackStatus', $hash); # make sure the api is already up to date and lists the changes
  747. }
  748. return undef;
  749. }
  750. sub Spotify_poll($) {
  751. my ($hash) = @_;
  752. my $name = $hash->{NAME};
  753. return if(Spotify_isDisabled($hash));
  754. my $pollInterval = $attr{$name}{updateInterval};
  755. InternalTimer(gettimeofday()+(defined $pollInterval ? $pollInterval : 5*60), "Spotify_poll", $hash);
  756. Spotify_update($hash, 0);
  757. }
  758. sub Spotify_update($$) {
  759. my ($hash, $full) = @_;
  760. Spotify_updateMe($hash, 0) if($full);
  761. Spotify_updateDevices($hash, 0);
  762. Spotify_updatePlaybackStatus($hash, 0);
  763. }
  764. sub Spotify_saveTrack($$$$) { # save a track object to the readings
  765. my ($hash, $track, $prefix, $beginUpdate) = @_;
  766. readingsBeginUpdate($hash) if($beginUpdate);
  767. readingsBulkUpdateIfChanged($hash, $prefix."_name", $track->{name}, 1);
  768. readingsBulkUpdateIfChanged($hash, $prefix."_uri", $track->{uri}, 1);
  769. readingsBulkUpdateIfChanged($hash, $prefix."_popularity", $track->{popularity}, 1);
  770. readingsBulkUpdateIfChanged($hash, $prefix."_duration_ms", $track->{duration_ms}, 1);
  771. readingsBulkUpdateIfChanged($hash, $prefix."_artist_name", $track->{artists}[0]{name}, 1);
  772. readingsBulkUpdateIfChanged($hash, $prefix."_artist_uri", $track->{artists}[0]{uri}, 1);
  773. readingsBulkUpdateIfChanged($hash, $prefix."_album_name", $track->{album}{name}, 1);
  774. readingsBulkUpdateIfChanged($hash, $prefix."_album_uri", $track->{album}{uri}, 1);
  775. readingsBulkUpdateIfChanged($hash, $prefix."_duration", h2hms_fmt($track->{duration_ms} / 1000 / 60 / 60), 1);
  776. my @sizes = ("large", "medium", "small");
  777. my $index = 0;
  778. foreach my $image(@{$track->{album}{images}}) {
  779. readingsBulkUpdateIfChanged($hash, $prefix."_album_cover_". $sizes[$index], $image->{url}, 1);
  780. $index++;
  781. last if($index >= 3);
  782. }
  783. readingsEndUpdate($hash, 1) if($beginUpdate);
  784. }
  785. sub Spotify_saveArtist($$$$) { # save an artist object to the readings
  786. my ($hash, $artist, $prefix, $beginUpdate) = @_;
  787. readingsBeginUpdate($hash) if($beginUpdate);
  788. readingsBulkUpdate($hash, $prefix."_name", $artist->{name}, 1);
  789. readingsBulkUpdate($hash, $prefix."_uri", $artist->{uri}, 1);
  790. readingsBulkUpdate($hash, $prefix."_popularity", $artist->{popularity}, 1);
  791. readingsBulkUpdate($hash, $prefix."_follower_cnt", $artist->{followers}{total}, 1);
  792. readingsBulkUpdate($hash, $prefix."_profile_pic_url", $artist->{images}[0]{url}, 1);
  793. readingsEndUpdate($hash, 1) if($beginUpdate);
  794. }
  795. sub Spotify_saveDevice($$$$) {
  796. my ($hash, $device, $prefix, $beginUpdate) = @_;
  797. readingsBeginUpdate($hash) if($beginUpdate);
  798. readingsBulkUpdateIfChanged($hash, $prefix . '_id', $device->{id}, 1);
  799. readingsBulkUpdateIfChanged($hash, $prefix . '_name', $device->{name}, 1);
  800. readingsBulkUpdateIfChanged($hash, $prefix . '_type', $device->{type}, 1);
  801. readingsBulkUpdateIfChanged($hash, $prefix . '_volume', $device->{volume_percent}, 1) if(defined $device->{volume_percent});
  802. readingsEndUpdate($hash, 1) if($beginUpdate);
  803. }
  804. sub Spotify_isDisabled($) {
  805. my ($hash) = @_;
  806. my $name = $hash->{NAME};
  807. return defined $attr{$name}{disable};
  808. }
  809. 1;
  810. =pod
  811. =item device
  812. =item summary control your Spotify (Connect) playback
  813. =item summary_DE Steuerung von Spotify (Connect)
  814. =begin html
  815. <a name="Spotify"></a>
  816. <h3>Spotify</h3>
  817. <ul>
  818. The <i>Spotify</i> module enables you to control your Spotify (Connect) playback.<br>
  819. To be able to control your music, you need to authorize with the Spotify WEB API. To do that, a <a target="_blank" rel="nofollow" href="https://developer.spotify.com/my-applications/#!/applications/create">Spotify API application</a> is required.<br>
  820. While creating the app, enter any <i>redirect_uri</i>. By default the module will use <a href="https://oskar.pw/" target="_blank">https://oskar.pw/</a> as <i>redirect_uri</i> since the site outputs your temporary authentification code.<br>
  821. It is safe to rely on this site because the code is useless without your client secret and only valid for a few minutes (important: you have to press the <b>add</b> and <b>save</b> button while adding the url).<br>
  822. If you want to use it, make sure to add it as <i>redirect_uri</i> to your app - however, you are free to use any other url and extract the code after signing in yourself.<br>
  823. <br>
  824. <a name="Spotify_define"></a>
  825. <p><b>Define</b></p>
  826. <ul>
  827. <code>define &lt;name&gt; Spotify &lt;client_id&gt; &lt;client_secret&gt; [ &lt;redirect_url&gt; ]</code><br>
  828. </ul>
  829. <br>
  830. <ul>
  831. Example: <code>define Spotify Spotify f88e5f5c2911152d914391592e717738 301b6d1a245e4fe01c2f8b4efd250756</code><br>
  832. </ul>
  833. <br>
  834. Once defined, open up your browser and call the URL displayed in <i>AUTHORIZATION_URL</i>, sign in with spotify and extract the code after being redirected.<br>
  835. If you get a <b>redirect_uri mismatch</b> make sure to either add <a href="https://oskar.pw/" target="_blank">https://oskar.pw/</a> as redirect url or that your url <b>matches exactly</b> with the one defined.<br>
  836. As soon as you obtained the code call <code>set &lt;name&gt; code &lt;code&gt;</code> - your state should change to connected and you are ready to go.<br>
  837. <br>
  838. <a name="Spotify_set"></a>
  839. <p><b>set &lt;required&gt; [ &lt;optional&gt; ]</b></p>
  840. Without a target device given, the active device (or default device if <i>alwaysStartOnDefaultDevice</i> is enabled) will be used.<br>
  841. You can also use the name of the target device instead of the id if it does not contain spaces - where it states <i>&lt;device_id / device_name&gt;</i> spaces are allowed.<br>
  842. If no default device is defined and none is active, it will use the first available device.<br>
  843. You can get a spotify uri by pressing the share button in the spotify (desktop) app on a track/playlist/album.<br><br>
  844. <ul>
  845. <li>
  846. <i>findArtistByName</i><br>
  847. finds an artist using its name and returns the result to the readings
  848. </li>
  849. <li>
  850. <i>findTrackByName</i><br>
  851. finds a track using its name and returns the result to the readings
  852. </li>
  853. <li>
  854. <i>pause</i><br>
  855. pause the current playback
  856. </li>
  857. <li>
  858. <i>playArtistByName &lt;artist_name&gt; [ &lt;device_id&gt; ]</i><br>
  859. plays an artist using its name (uses search)
  860. </li>
  861. <li>
  862. <i>playContextByURI &lt;context_uri&gt; [ &lt;nr_of_start_track&gt; ] [ &lt;device_id / device_name&gt; ]</i><br>
  863. plays a context (playlist, album or artist) using a Spotify URI
  864. </li>
  865. <li>
  866. <i>playPlaylistByName &lt;playlist_name&gt; [ &lt;device_id&gt; ]</i><br>
  867. plays any playlist by providing a name (uses search)
  868. </li>
  869. <li>
  870. <i>playRandomTrackFromPlaylistByURI &lt;playlist_uri&gt; [ &lt;limit&gt; ] [ &lt;device_id / device_name&gt; ]</i><br>
  871. plays a random track from a playlist (only considering the first <i>&lt;limit&gt;</i> songs)
  872. </li>
  873. <li>
  874. <i>playSavedTracks [ &lt;nr_of_start_track&gt; ] [ &lt;device_id / device_name&gt; ]</i><br>
  875. plays the saved tracks (beginning with track <i>&lt;nr_of_start_track&gt;</i>)
  876. </li>
  877. <li>
  878. <i>playTrackByName &lt;track_name&gt; [ &lt;device_id&gt; ]</i><br>
  879. finds a song by its name and plays it
  880. </li>
  881. <li>
  882. <i>playTrackByURI &lt;track_uri&gt; [ &lt;device_id / device_name&gt; ]</i><br>
  883. plays a track using a track uri
  884. </li>
  885. <li>
  886. <i>repeat &lt;track,context,off&gt;</i><br>
  887. sets the repeat mode: either <i>one</i>, <i>all</i> (meaning playlist or album) or <i>off</i>
  888. </li>
  889. <li>
  890. <i>resume [ &lt;device_id / device_name&gt; ]</i><br>
  891. resumes playback (on a device)
  892. </li>
  893. <li>
  894. <i>seekToPosition &lt;position&gt;</i><br>
  895. seeks to the position <i>&lt;position&gt;</i> (in seconds, supported formats: 01:20, 80, 00:20, 20)
  896. </li>
  897. <li>
  898. <i>shuffle &lt;off,on&gt;</i><br>
  899. sets the shuffle mode: either <i>on</i> or <i>off</i>
  900. </li>
  901. <li>
  902. <i>skipToNext</i><br>
  903. skips to the next track
  904. </li>
  905. <li>
  906. <i>skipToPrevious</i><br>
  907. skips to the previous track
  908. </li>
  909. <li>
  910. <i>togglePlayback</i><br>
  911. toggles the playback (resumes if paused, pauses if playing)
  912. </li>
  913. <li>
  914. <i>transferPlayback [ &lt;device_id&gt; ]</i><br>
  915. transfers the current playback to the specified device (or the next inactive device if not specified)
  916. </li>
  917. <li>
  918. <i>update</i><br>
  919. updates playback and devices
  920. </li>
  921. <li>
  922. <i>volume &lt;volume&gt; [ &lt;device_id&gt; ]</i><br>
  923. sets the volume
  924. </li>
  925. <li>
  926. <i>volumeDown [ &lt;step&gt; ] [ &lt;device_id / device_name&gt; ]</i><br>
  927. decreases the volume by <i>step</i> (if not set it uses <i>volumeStep</i>)
  928. </li>
  929. <li>
  930. <i>volumeFade &lt;volume&gt; [ &lt;duration&gt; &lt;step&gt; ] [ &lt;device_id&gt; ]</i><br>
  931. fades the volume
  932. </li>
  933. <li>
  934. <i>volumeDown [ &lt;step&gt; ] [ &lt;device_id / device_name&gt; ]</i><br>
  935. increases the volume by <i>step</i> (if not set it uses <i>volumeStep</i>)
  936. </li>
  937. </ul>
  938. <br>
  939. <a name="Spotify_get"></a>
  940. <p><b>Get</b></p>
  941. <ul>
  942. N/A
  943. </ul>
  944. <br>
  945. <a name="Spotify_attr"></a>
  946. <p><b>Attributes</b></p>
  947. <ul>
  948. <li>
  949. <i>alwaysStartOnDefaultDevice</i><br>
  950. always start new playback on the default device<br>
  951. default: 0
  952. </li>
  953. <li>
  954. <i>defaultPlaybackDeviceID</i><br>
  955. the prefered device by its id or device name<br>
  956. </li>
  957. <li>
  958. <i>disable</i><br>
  959. disables the device<br>
  960. default: 0
  961. </li>
  962. <li>
  963. <i>updateInterval</i><br>
  964. the interval to update your playback status while no music is running (in seconds)<br>
  965. default: 300
  966. </li>
  967. <li>
  968. <i>updateIntervalWhilePlaying</i><br>
  969. the interval to update your playback status while music is running (in seconds)<br>
  970. default: 10
  971. </li>
  972. <li>
  973. <i>volumeStep</i><br>
  974. the value by which the volume is in-/decreased by default (in percent)<br>
  975. default: 5
  976. </li>
  977. </ul>
  978. </ul>
  979. =end html
  980. =begin html_DE
  981. <a name="Spotify"></a>
  982. <h3>Spotify</h3>
  983. <ul>
  984. Das <i>Spotify</i> Modul ermöglicht die Steuerung von Spotify (Connect).<br>
  985. Um die Wiedergabe zu steuern, wird die Spotify WEB API verwendet. Dafür wird eine eigene <a target="_blank" rel="nofollow" href="https://developer.spotify.com/my-applications/#!/applications/create">Spotify API application</a> benötigt.<br>
  986. Während der Erstellung muss eine <i>redirect_uri</i> angegeben - standardmäßig wird vom Modul <a href="https://oskar.pw/" target="_blank">https://oskar.pw/</a> verwendet, da diese Seite nach der Anmeldung den Code in leserlicher Form ausgibt.<br>
  987. Die Seite kann bedenkenlos verwendet werden, da der Code ohne <i>client_secret</i> nutzlos und nur wenige Minuten gültig ist.<br>
  988. Wenn du diese verwenden willst, stelle sicher, diese bei der Erstellung anzugeben (wichtig: das Hinzufügen der URL muss mit <b>add</b> und <b>save</b> bestätigt werden), ansonsten kann jede beliebige andere Seite verwendet werden und der Code aus der URL extrahiert werden.<br>
  989. <br>
  990. <a name="Spotify_define"></a>
  991. <p><b>Define</b></p>
  992. <ul>
  993. <code>define &lt;name&gt; Spotify &lt;client_id&gt; &lt;client_secret&gt; [ &lt;redirect_url&gt; ]</code><br>
  994. </ul>
  995. <br>
  996. <ul>
  997. Beispiel: <code>define Spotify Spotify f88e5f5c2911152d914391592e717738 301b6d1a245e4fe01c2f8b4efd250756</code><br>
  998. </ul>
  999. <br>
  1000. Sobald das Gerät angelegt wurde, muss die <i>AUTHORIZATION_URL</i> im Browser geöffnet werden und die Anmeldung mit Spotify erfolgen.<br>
  1001. Sollte der Fehler <b>redirect_uri mismatch</b> auftauchen, stelle sicher, dass <a href="https://oskar.pw/" target="_blank">https://oskar.pw/</a> als <i>redirect_uri</i> hinzugefügt wurde oder die verwendete URL <b>exakt übereinstimmt</b>.<br>
  1002. Sobald der Anmeldecode ermittelt wurde, führe folgenden Befehl aus: <code>set &lt;name&gt; code &lt;code&gt;</code> - der Status sollte nun auf connected wechseln und das Gerät ist einsatzbereit.<br>
  1003. <br>
  1004. <a name="Spotify_set"></a>
  1005. <p><b>set &lt;required&gt; [ &lt;optional&gt; ]</b></p>
  1006. Wird kein Zielgerät angegeben, wird das aktive (oder das Standard-Gerät, wenn <i>alwaysStartOnDefaultDevice</i> aktiviert ist) verwendet.<br>
  1007. An den Stellen, wo eine <i>&lt;device_id&gt;</i> verlangt wird, kann auch der Gerätename, sofern dieser keine Leerzeichen enthält, verwendet werden. Dort wo es <i>&lt;device_name&gt;</i> heißt, sind auch Leerzeichen im Namen zugelassen.
  1008. Wenn kein aktives oder Standard-Gerät vorhanden ist, wird das erste verfügbare Gerät verwendet.<br>
  1009. Die Spotify URI kann in der (Desktop) App ermittelt werden, wenn man den teilen Knopf bei einem Track/Playlist/Album drückt.<br><br>
  1010. <ul>
  1011. <li>
  1012. <i>findArtistByName</i><br>
  1013. sucht einen Künstler und liefert das Ergebnis in den Readings
  1014. </li>
  1015. <li>
  1016. <i>findTrackByName</i><br>
  1017. sucht einen Track und liefert das Ergebnis in den Readings
  1018. </li>
  1019. <li>
  1020. <i>pause</i><br>
  1021. pausiert die aktuelle Wiedergabe
  1022. </li>
  1023. <li>
  1024. <i>playArtistByName &lt;artist_name&gt; [ &lt;device_id&gt; ]</i><br>
  1025. sucht einen Künstler und spielt dessen Tracks ab
  1026. </li>
  1027. <li>
  1028. <i>playContextByURI &lt;context_uri&gt; [ &lt;nr_of_start_track&gt; ] [ &lt;device_id / device_name&gt; ]</i><br>
  1029. spielt einen Context (Playlist, Album oder Künstler) durch Angabe der URI ab
  1030. </li>
  1031. <li>
  1032. <i>playPlaylistByName &lt;playlist_name&gt; [ &lt;device_id&gt; ]</i><br>
  1033. sucht eine Playlist und spielt diese ab
  1034. </li>
  1035. <li>
  1036. <i>playRandomTrackFromPlaylistByURI &lt;playlist_uri&gt; [ &lt;limit&gt; ] [ &lt;device_id / device_name&gt; ]</i><br>
  1037. spielt einen zufälligen Track aus einer Playlist ab (berücksichtigt nur die ersten <i>&lt;limit&gt;</i> Tracks der Playlist)
  1038. </li>
  1039. <li>
  1040. <i>playSavedTracks [ &lt;nr_of_start_track&gt; ] [ &lt;device_id / device_name&gt; ]</i><br>
  1041. spielt die gespeicherten Tracks ab (beginnend mit Track Nummer <i>&lt;nr_of_start_track&gt;</i>)
  1042. </li>
  1043. <li>
  1044. <i>playTrackByName &lt;track_name&gt; [ &lt;device_id&gt; ]</i><br>
  1045. sucht den Song und spielt ihn ab
  1046. </li>
  1047. <li>
  1048. <i>playTrackByURI &lt;track_uri&gt; [ &lt;device_id / device_name&gt; ]</i><br>
  1049. spielt einen Song durch Angabe der URI ab
  1050. </li>
  1051. <li>
  1052. <i>repeat &lt;track,context,off&gt;</i><br>
  1053. setzt den Wiederholungsmodus: entweder <i>one</i>, <i>all</i> (Playlist, Album, Künstler) oder <i>off</i>
  1054. </li>
  1055. <li>
  1056. <i>resume [ &lt;device_id / device_name&gt; ]</i><br>
  1057. fährt mit der Wiedergabe (auf einem Gerät) fort
  1058. </li>
  1059. <li>
  1060. <i>seekToPosition &lt;position&gt;</i><br>
  1061. spult an die Position <i>&lt;position&gt;</i> (in Sekunden, erlaubte Formate: 01:20, 80, 00:20, 20)
  1062. </li>
  1063. <li>
  1064. <i>shuffle &lt;off,on&gt;</i><br>
  1065. setzt den Shuffle-Modus: entweder <i>on</i> oder <i>off</i>
  1066. </li>
  1067. <li>
  1068. <i>skipToNext</i><br>
  1069. weiter zum nächsten Track
  1070. </li>
  1071. <li>
  1072. <i>skipToPrevious</i><br>
  1073. zurück zum vorherigen Track
  1074. </li>
  1075. <li>
  1076. <i>togglePlayback</i><br>
  1077. toggelt die Wiedergabe (hält an, wenn sie aktiv ist, ansonsten fortsetzen)
  1078. </li>
  1079. <li>
  1080. <i>transferPlayback [ &lt;device_id&gt; ]</i><br>
  1081. überträgt die aktuelle Wiedergabe auf ein anderes Gerät (wenn kein Gerät angegeben wird, wird das nächste inaktive verwendet)
  1082. </li>
  1083. <li>
  1084. <i>update</i><br>
  1085. lädt den aktuellen Zustand neu
  1086. </li>
  1087. <li>
  1088. <i>volume &lt;volume&gt; [ &lt;device_id&gt; ]</i><br>
  1089. setzt die Lautstärke
  1090. </li>
  1091. <li>
  1092. <i>volumeDown [ &lt;step&gt; ] [ &lt;device_id / device_name&gt; ]</i><br>
  1093. verringert die Lautstärke um <i>step</i> (falls nicht gesetzt, um <i>volumeStep</i>)
  1094. </li>
  1095. <li>
  1096. <i>volumeFade &lt;volume&gt; [ &lt;duration&gt; &lt;step&gt; ] [ &lt;device_id&gt; ]</i><br>
  1097. setzt die Lautstärke schrittweise
  1098. </li>
  1099. <li>
  1100. <i>volumeUp [ &lt;step&gt; ] [ &lt;device_id / device_name&gt; ]</i><br>
  1101. erhöht die Lautstärke um <i>step</i> (falls nicht gesetzt, um <i>volumeStep</i>)
  1102. </li>
  1103. </ul>
  1104. <br>
  1105. <a name="Spotify_get"></a>
  1106. <p><b>Get</b></p>
  1107. <ul>
  1108. N/A
  1109. </ul>
  1110. <br>
  1111. <a name="Spotify_attr"></a>
  1112. <p><b>Attributes</b></p>
  1113. <ul>
  1114. <li>
  1115. <i>alwaysStartOnDefaultDevice</i><br>
  1116. startet neue Wiedergabe immer auf dem Standard-Gerät<br>
  1117. default: 0
  1118. </li>
  1119. <li>
  1120. <i>defaultPlaybackDeviceID</i><br>
  1121. das Standard-Gerät durch Angabe der Geräte-ID oder des Geräte-Namens<br>
  1122. </li>
  1123. <li>
  1124. <i>disable</i><br>
  1125. deaktiviert das Gerät<br>
  1126. default: 0
  1127. </li>
  1128. <li>
  1129. <i>updateInterval</i><br>
  1130. Intervall in Sekunden, in dem der Status aktualisiert wird, wenn keine Musik läuft<br>
  1131. default: 300
  1132. </li>
  1133. <li>
  1134. <i>updateIntervalWhilePlaying</i><br>
  1135. Intervall in Sekunden, in dem der Status aktualisiert wird, wenn Musik läuft<br>
  1136. default: 10
  1137. </li>
  1138. <li>
  1139. <i>volumeStep</i><br>
  1140. der Wert, um den die Lautstärke bei volumeUp/volumeDown standardmäßig verändert wird (in Prozent)<br>
  1141. default: 5
  1142. </li>
  1143. </ul>
  1144. </ul>
  1145. =end html_DE
  1146. =cut