diff --git a/fhem/CHANGED b/fhem/CHANGED index 904607680..d54c22ec3 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - feature: 37_Spotify: first release - bugfix: 38_netatmo: fixed blocking connection check on dns error - change: 02_RSS: height/width for rect layout directive - change: 34_ESPEasy: performance tuning, some fixes diff --git a/fhem/FHEM/37_Spotify.pm b/fhem/FHEM/37_Spotify.pm new file mode 100644 index 000000000..1b4f7fedc --- /dev/null +++ b/fhem/FHEM/37_Spotify.pm @@ -0,0 +1,1198 @@ +############################################################################## +# $Id$ +# +# 37_Spotify.pm +# +# 2017 Oskar Neumann +# oskar.neumann@me.com +# +############################################################################## + +package main; + +use strict; +use warnings; + +use JSON; + +use MIME::Base64; + +sub Spotify_Initialize($) { + my ($hash) = @_; + + $hash->{DefFn} = 'Spotify_Define'; + $hash->{NotifyFn} = 'Spotify_Notify'; + $hash->{UndefFn} = 'Spotify_Undefine'; + $hash->{SetFn} = 'Spotify_Set'; + $hash->{GetFn} = 'Spotify_Get'; + #$hash->{AttrFn} = "Spotify_Attr"; + $hash->{AttrList} = 'defaultPlaybackDeviceID alwaysStartOnDefaultDevice:0,1 updateInterval '; + $hash->{AttrList} .= $readingFnAttributes; + $hash->{NOTIFYDEV} = "global"; +} + +sub Spotify_Define($) { + my ($hash, $def) = @_; + my $name = $hash->{NAME}; + my @a = split("[ \t][ \t]*", $def); + my $hintGetVaildPair = "get a valid pair by creating a Spotify app". + " here: https://developer.spotify.com/my-applications/#!/applications/create + (recommendation is to use https://oskar.pw/ as redirect_uri because it displays the temporary access code - ". + "this is safe because the code is useless without your client credentials and expires after a few minutes)"; + + return 'wrong syntax: define Spotify [ ] + - '. $hintGetVaildPair + if( @a < 4 ); + + + my $client_id = $a[2]; + my $client_secret = $a[3]; + + return 'invalid client_id / client_secret - '. $hintGetVaildPair + if(length $client_id != 32 || length $client_secret != 32); + + $hash->{CLIENT_ID} = $client_id; + $hash->{CLIENT_SECRET} = $client_secret; + $hash->{REDIRECT_URI} = @a > 4 ? $a[4] : 'https://oskar.pw/'; + $hash->{helper}{custom_redirect} = @a > 4; + + Spotify_loadInternals($hash) if($init_done); + + return undef; +} + +sub Spotify_Undefine($$) { + my ($hash, $name) = @_; + RemoveInternalTimer($hash); + return undef; +} + +sub Spotify_Notify($$) { + my ($own_hash, $dev_hash) = @_; + my $ownName = $own_hash->{NAME}; # own name / hash + + return "" if(IsDisabled($ownName)); # Return without any further action if the module is disabled + + my $devName = $dev_hash->{NAME}; # Device that created the events + my $events = deviceEvents($dev_hash, 1); + + if($devName eq "global" && grep(m/^INITIALIZED|REREADCFG$/, @{$events})) { + Spotify_loadInternals($own_hash); + } +} + +sub Spotify_Set($$@) { + my ($hash, $name, $cmd, @args) = @_; + + return "\"set $name\" needs at least one argument" unless(defined($cmd)); + + my $list = ''; + + if(!defined $hash->{helper}{refresh_token}) { + $list .= ' code'; + } else { + $list .= ' playTrackByURI playContextByURI pause:noArg resume:noArg volume:slider,0,1,100 update:noArg'; + $list .= ' skipToNext:noArg skipToPrevious:noArg seekToPosition repeat:one,all,off shuffle:on,off transferPlayback volumeFade:slider,0,1,100 playTrackByName playPlaylistByName togglePlayback'; + $list .= ' playSavedTracks playRandomTrackFromPlaylistByURI findTrackByName findArtistByName playArtistByName'; + } + + if($cmd eq 'code') { + return "please enter the code obtained from the URL after calling \"get $name authorizationURL\"" + if( @args < 1 ); + + return Spotify_getToken($hash, $args[0]); + } + + return Spotify_update($hash, 1) if($cmd eq 'update'); + return Spotify_pausePlayback($hash) if($cmd eq 'pause'); + return Spotify_resumePlayback($hash) if($cmd eq 'resume'); + return Spotify_setVolume($hash, 1, $args[0], defined $args[1] ? join(' ', @args[1..$#args]) : undef) if ($cmd eq 'volume'); + return Spotify_skipToNext($hash) if ($cmd eq 'skipToNext' || $cmd eq 'skip' || $cmd eq 'next'); + return Spotify_skipToPrevious($hash) if ($cmd eq 'skipToPrevious' || $cmd eq 'previous' || $cmd eq 'prev'); + return Spotify_seekToPosition($hash, $args[0]) if($cmd eq 'seekToPosition'); + return Spotify_setRepeat($hash, $args[0]) if($cmd eq 'repeat'); + return Spotify_setShuffle($hash, $args[0]) if($cmd eq 'shuffle'); + return Spotify_transferPlayback($hash, @args > 0 ? join(' ', @args) : undef) if($cmd eq 'transferPlayback'); + return Spotify_playTrackByURI($hash, \@args, undef) if($cmd eq 'playTrackByURI'); + return Spotify_playTrackByName($hash, @args > 0 ? join(' ', @args) : undef) if($cmd eq 'playTrackByName'); + return Spotify_playPlaylistByName($hash, @args > 0 ? join(' ', @args) : undef) if($cmd eq 'playPlaylistByName'); + return Spotify_playContextByURI($hash, $args[0], $args[1], $args[2]) if($cmd eq 'playContextByURI'); + return Spotify_volumeFade($hash, $args[0], $args[1], $args[2], defined $args[3] ? join(' ', @args[3..$#args]) : undef) if($cmd eq 'volumeFade'); + return Spotify_togglePlayback($hash) if($cmd eq 'toggle' || $cmd eq 'togglePlayback'); + return Spotify_playSavedTracks($hash, $args[0], defined $args[1] ? join(' ', @args[1..$#args]) : undef) if($cmd eq 'playSavedTracks'); + return Spotify_playRandomTrackFromPlaylistByURI($hash, $args[0], $args[1], defined $args[2] ? join(' ', @args[2..$#args]) : undef) if($cmd eq 'playRandomTrackFromPlaylistByURI'); + return Spotify_findTrackByName($hash, @args > 0 ? join(' ', @args) : undef) if($cmd eq 'findTrackByName'); + return Spotify_findArtistByName($hash, @args > 0 ? join(' ', @args) : undef) if($cmd eq 'findArtistByName'); + return Spotify_playArtistByName($hash, @args > 0 ? join(' ', @args) : undef) if($cmd eq 'playArtistByName'); + + return "Unknown argument $cmd, choose one of $list"; +} + +sub Spotify_Get($$@) { + my ($hash, $name, $cmd, @args) = @_; + + my $list = ""; + + if(!defined $hash->{helper}{refresh_token}) { + $list .= ' authorizationURL:noArg'; + } else { + #$list .= ' me:noArg'; + } + + if($cmd eq "authorizationURL") { + return $hash->{AUTHORIZATION_URL}; + } + + return "Unknown argument $cmd, choose one of $list"; +} + +sub Spotify_loadInternals($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + $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}); + $hash->{helper}{refresh_token} = ReadingsVal($name, '.refresh_token', undef); + $hash->{helper}{access_token} = ReadingsVal($name, '.access_token', undef); + $hash->{helper}{expires} = ReadingsVal($name, '.expires', undef); + + RemoveInternalTimer($hash); + if(!defined(ReadingsVal($name, '.refresh_token', undef))) { + $hash->{STATE} = 'authorization pending (see instructions)'; + $hash->{AUTHORIZATION_URL} = $hash->{helper}{authorization_url}; + $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.'; + $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. + 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}); + } else { + $hash->{STATE} = 'connected'; + my $pollInterval = $attr{$name}{pollInterval}; + $attr{$name}{webCmd} = 'toggle:next:prev' if(!defined $attr{$name}{webCmd}); + InternalTimer(gettimeofday()+(defined $pollInterval ? $pollInterval : 10*60), "Spotify_poll", $hash); + } + + if(defined $hash->{helper}{refresh_token}) { + Spotify_updateMe($hash, 0); + Spotify_updateDevices($hash, 0); + Spotify_updatePlaybackStatus($hash, 0); + } +} + +sub Spotify_getToken($$) { # exchanging code for token + my ($hash, $code) = @_; + my $name = $hash->{NAME}; + + Log3 $name, 4, "$name: checking access code"; + my ($err,$data) = HttpUtils_BlockingGet({ + url => "https://accounts.spotify.com/api/token", + method => "POST", + timeout => 5, + noshutdown => 1, + data => {client_id => $hash->{CLIENT_ID}, client_secret => $hash->{CLIENT_SECRET}, grant_type => 'authorization_code', redirect_uri => $hash->{REDIRECT_URI}, 'code' => $code} + }); + + my $json = eval { JSON->new->utf8(0)->decode($data) }; + if(defined $json->{error}) { + my $msg = 'Failed to get access token: '; + + if($json->{error_description} =~ /redirect/) { + $msg = $msg . 'Please add '. $hash->{REDIRECT_URI} . ' as a redirect_uri at https://developer.spotify.com/my-applications/#!/applications/'; + } else { + $msg = $msg . $json->{error_description}; + } + + Log3 $name, 3, "$name: $json->{error} - $msg"; + return $msg; + } + + return "failed to get access token" + if(!defined $json->{refresh_token}); + + + $hash->{helper}{refresh_token} = $json->{refresh_token}; + $hash->{helper}{access_token} = $json->{access_token}; + $hash->{helper}{expires} = gettimeofday() + $json->{expires_in}; + $hash->{helper}{scope} = $json->{scope}; + delete $hash->{AUTHORIZATION_URL}; + delete $hash->{A1_INSTRUCTIONS}; + $hash->{STATE} = "connected"; + + Spotify_writeTokens($hash); + + Spotify_updateMe($hash, 0); + Spotify_updateDevices($hash, 0); + + return undef; +} + +sub Spotify_writeTokens($) { # save gathered tokens + my ($hash) = @_; + + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, '.refresh_token', $hash->{helper}{refresh_token}); + readingsBulkUpdateIfChanged($hash, '.access_token', $hash->{helper}{access_token}); + readingsBulkUpdate($hash, '.expires', $hash->{helper}{expires}); + readingsEndUpdate($hash, 1); +} + +sub Spotify_refreshToken($) { # refresh the access token once it is expired + my ($hash) = @_; + my $name = $hash->{NAME}; + + return 'Failed to refresh access token: refresh token missing' if(!defined $hash->{helper}{refresh_token}); + + Log3 $name, 4, "$name: refreshing access code"; + my ($err,$data) = HttpUtils_BlockingGet({ + url => "https://accounts.spotify.com/api/token", + method => "POST", + timeout => 5, + noshutdown => 1, + data => {client_id => $hash->{CLIENT_ID}, client_secret => $hash->{CLIENT_SECRET}, grant_type => 'refresh_token', refresh_token => $hash->{helper}{refresh_token}} + }); + + my $json = eval { JSON->new->utf8(0)->decode($data) }; + if(defined $json->{error}) { + if($json->{error} eq 'invalid_grant') { + $hash->{helper}{refresh_token} = undef; + $hash->{STATE} = 'invalid refresh token'; + $hash->{AUTHORIZATION_URL} = $hash->{helper}{authorization_url}; + CommandDeleteReading(undef, "$name .*"); + } + + my $msg = 'Failed to refresh access token: $json->{error_description}'; + Log3 $name, 3, "$name: $json->{error} - $msg"; + return $msg; + } + + return "failed to refresh access token" + if(!defined $json->{access_token}); + + $hash->{helper}{access_token} = $json->{access_token}; + $hash->{helper}{expires} = gettimeofday() + $json->{expires_in}; + $hash->{helper}{scope} = $json->{scope} if(defined $json->{scope}); + + Spotify_writeTokens($hash); + + Spotify_updateMe($hash, 0); + Spotify_updateDevices($hash, 0); +} + +sub Spotify_apiRequest($$$$$) { # any kind of api request + my ($hash, $path, $args, $method, $blocking) = @_; + my $name = $hash->{NAME}; + + Spotify_refreshToken($hash) if(gettimeofday() >= $hash->{helper}{expires}); + if(!defined $hash->{helper}{refresh_token}) { + Log3 $name, 3, "$name: could not execute API request (not authorized)"; + return 'You need to be authorized to perform this action.'; + } + + if(!defined $blocking || !$blocking) { + HttpUtils_NonblockingGet({ + url => "https://api.spotify.com/v1/$path", + method => $method, + hash => $hash, + apiPath => $path, + timeout => 5, + noshutdown => 1, + data => $method eq 'PUT' && defined $args ? encode_json $args : $args, + header => "Authorization: Bearer ". $hash->{helper}{access_token}, + callback => \&Spotify_dispatch + }); + } else { + my ($err,$data) = HttpUtils_BlockingGet({ + url => "https://api.spotify.com/v1/$path", + method => $method, + hash => $hash, + apiPath => $path, + timeout => 5, + noshutdown => 1, + data => $method eq 'PUT' && defined $args ? encode_json $args : $args, + header => "Authorization: Bearer ". $hash->{helper}{access_token} + }); + return Spotify_dispatch({hash => $hash, apiPath => $path, method => $method}, $err, $data); + } +} + +sub Spotify_updateMe($$) { # update user infos + my ($hash, $blocking) = @_; + Spotify_apiRequest($hash, 'me/', undef, 'GET', $blocking); + return undef; +} + +sub Spotify_updateDevices($$) { # update devices + my ($hash, $blocking) = @_; + Spotify_apiRequest($hash, 'me/player/devices', undef, 'GET', $blocking); + return undef; +} + +sub Spotify_pausePlayback($) { # pause playback + my ($hash) = @_; + my $name = $hash->{NAME}; + $hash->{helper}{is_playing} = 0; + Spotify_apiRequest($hash, 'me/player/pause', undef, 'PUT', 0); + Log3 $name, 3, "$name: pause"; + return undef; +} + +sub Spotify_resumePlayback($) { # resume playback + my ($hash) = @_; + my $name = $hash->{NAME}; + $hash->{helper}{is_playing} = 1; + Spotify_apiRequest($hash, 'me/player/play', undef, 'PUT', 0); + Log3 $name, 3, "$name: resume"; + return undef; +} + +sub Spotify_updatePlaybackStatus($$) { # update the playback status + my ($hash, $blocking) = @_; + Spotify_apiRequest($hash, 'me/player', undef, 'GET', $blocking); + return undef; +} + +sub Spotify_setVolume($$$$) { # set the volume + my ($hash, $blocking, $volume, $device_id) = @_; + my $name = $hash->{NAME}; + return 'wrong syntax: set volume [ ]' if(!defined $volume); + + delete $hash->{helper}{fading} if($blocking && defined $hash->{helper}{fading}); # stop volumeFade if currently active (override) + + $device_id = Spotify_getTargetDeviceID($hash, $device_id, 0); # resolve target device id + Spotify_apiRequest($hash, "me/player/volume?volume_percent=$volume". (defined $device_id ? "&device_id=$device_id" : ''), undef, 'PUT', $blocking); + Log3 $name, 3, "$name: volume $volume" if(!defined $hash->{helper}{fading}); + return undef; +} + +sub Spotify_skipToNext($) { # skip to next track + my ($hash) = @_; + my $name = $hash->{NAME}; + Spotify_apiRequest($hash, 'me/player/next', undef, 'POST', 0); + Log3 $name, 3, "$name: skipToNext"; + return undef; +} + +sub Spotify_skipToPrevious($) { # skip to previous track + my ($hash) = @_; + my $name = $hash->{NAME}; + Spotify_apiRequest($hash, 'me/player/previous', undef, 'POST', 0); + Log3 $name, 3, "$name: skipToPrevious"; + return undef; +} + +sub Spotify_seekToPosition($$) { # seek to position in track + my ($hash, $position) = @_; + my $name = $hash->{NAME}; + my (undef, $minutes, $seconds) = $position =~ m/(([0-9]+):)?([0-9]+)/; + return 'wrong syntax: set seekToPosition ' if(!defined $minutes && !defined $seconds); + $position = ($minutes * 60 + $seconds) * 1000; + Spotify_apiRequest($hash, "me/player/seek?position_ms=$position", undef, 'PUT', 0); + return undef; +} + +sub Spotify_setRepeat($$) { # set the repeat mode + my ($hash, $mode) = @_; + my $name = $hash->{NAME}; + return 'wrong syntax: set repeat ' if(!defined $mode || ($mode ne 'one' && $mode ne 'all' && $mode ne 'off')); + $mode = 'track' if($mode eq 'one'); + $mode = 'context' if($mode eq 'all'); + my $device_id = Spotify_getTargetDeviceID($hash, undef, 0); + Spotify_apiRequest($hash, "me/player/repeat?state=$mode". (defined $device_id ? "&device_id=$device_id" : ""), undef, 'PUT', 0); + Log3 $name, 3, "$name: repeat $mode"; + return undef; +} + +sub Spotify_setShuffle($$) { # set the shuffle mode + my ($hash, $mode) = @_; + my $name = $hash->{NAME}; + return 'wrong syntax: set shuffle ' if(!defined $mode || ($mode ne 'on' && $mode ne 'off')); + $mode = $mode eq 'on' ? 'true' : 'false'; + my $device_id = Spotify_getTargetDeviceID($hash, undef, 0); + Spotify_apiRequest($hash, "me/player/shuffle?state=$mode". (defined $device_id ? "&device_id=$device_id" : ""), undef, 'PUT', 0); + Log3 $name, 3, "$name: shuffle $mode"; + return undef; +} + +sub Spotify_transferPlayback($$) { # transfer the current playback to another device + my ($hash, $device_id) = @_; + $device_id = Spotify_getTransferTargetDeviceID($hash, $device_id); + return 'wrong syntax: set transferPlayback [ ]' if(!defined $device_id); + my @device_ids = ($device_id); + Spotify_apiRequest($hash, 'me/player', {device_ids => \@device_ids}, 'PUT', 0); + return undef; +} + +sub Spotify_playContextByURI($$$$) { # play a context (playlist, album or artist) using its uri + my ($hash, $uri, $position, $device_id) = @_; + my $name = $hash->{NAME}; + return 'wrong syntax: set playContextByURI [ ] [ ]' if(!defined $uri); + $position = 1 if(!defined $position || $position !~ /^[0-9]+$/); + + return Spotify_play($hash, undef, $uri, $position, $device_id); +} + +sub Spotify_playTrackByURI($$$) { # play a track by its uri + my ($hash, $uris, $device_id) = @_; + my $name = $hash->{NAME}; + return 'wrong syntax: set playTrackByURI ... [ ]' if(@{$uris} < 1); + Log3 $name, 3, "$name: track". (@{$uris} > 1 ? "s" : "")." ". join(" ", @{$uris}) if(!defined $hash->{helper}{skipTrackLog}); + delete $hash->{helper}{skipTrackLog} if(defined $hash->{helper}{skipTrackLog}); + return Spotify_play($hash, $uris, undef, undef, $device_id); +} + +sub Spotify_playTrackByName($$) { # play a track by its name using search + my ($hash, $trackname) = @_; + return 'wrong syntax: set playTrackByName [ ]' if(!defined $trackname); + + my @parts = split(" ", $trackname); + my $device_id = Spotify_getTargetDeviceID($hash, $parts[-1], 0) if(@parts > 1); # resolve device id (may be last part of the command) + $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 + + Spotify_findTrackByName($hash, $trackname); + my $result = $hash->{helper}{searchResult}; + return 'could not find track' if(!defined $result); + + my @uris = ($result->{uri}); + Spotify_playTrackByURI($hash, \@uris, $device_id); + return undef; +} + +sub Spotify_findTrackByName($$) { # finds a track by its name and returns the result in the readings + my ($hash, $trackname, $saveTrack) = @_; + return 'wrong syntax: set findTrackByName [ ]' if(!defined $trackname); + + delete $hash->{helper}{searchResult}; + Spotify_apiRequest($hash, 'search?limit=1&type=track&q='. urlEncode($trackname), undef, 'GET', 1); + my $result = $hash->{helper}{dispatch}{json}{tracks}{items}[0]; + return 'could not find track' if(!defined $result); + + $hash->{helper}{searchResult} = $result; + Spotify_saveTrack($hash, $result, 'search_track', 1); + + return undef; +} + +sub Spotify_findArtistByName($$) { # finds an artist by its name and returns the result in the readings + my ($hash, $artistname, $saveTrack) = @_; + return 'wrong syntax: set findArtistByName ' if(!defined $artistname); + + delete $hash->{helper}{searchResult}; + Spotify_apiRequest($hash, 'search?limit=1&type=artist&q='. urlEncode($artistname), undef, 'GET', 1); + my $result = $hash->{helper}{dispatch}{json}{artists}{items}[0]; + return 'could not find artist' if(!defined $result); + + $hash->{helper}{searchResult} = $result; + Spotify_saveArtist($hash, $result, 'search_artist', 1); + + return undef; +} + +sub Spotify_playArtistByName($$) { # play an artist by its name using search + my ($hash, $artistname) = @_; + my $name = $hash->{NAME}; + return 'wrong syntax: set playArtistByName [ ]' if(!defined $artistname); + + my @parts = split(" ", $artistname); + my $device_id = Spotify_getTargetDeviceID($hash, $parts[-1], 0) if(@parts > 1); # resolve device id (may be last part of the command) + $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 + + Spotify_findArtistByName($hash, $artistname); + my $result = $hash->{helper}{searchResult}; + return 'could not find artist' if(!defined $result); + + Spotify_playContextByURI($hash, $result->{uri}, undef, $device_id); + Log3 $name, 3, "$name: artist $result->{uri} ($result->{name})"; + return undef; +} + +sub Spotify_playPlaylistByName($$) { # play a playlist by its name + my ($hash, $playlistname) = @_; + my $name = $hash->{NAME}; + return 'wrong syntax: set playPlaylistByName ' if(!defined $playlistname); + + Spotify_apiRequest($hash, 'search?limit=1&type=playlist&q='. urlEncode($playlistname), undef, 'GET', 1); + my $result = $hash->{helper}{dispatch}{json}{playlists}{items}[0]; + return 'could not find playlist' if(!defined $result); + + Spotify_playContextByURI($hash, $result->{uri}, undef, undef); + Log3 $name, 3, "$name: $result->{uri} ($result->{name})"; + return undef; +} + +sub Spotify_playSavedTracks($$$) { # play users saved tracks + my ($hash, $first, $device_id) = @_; + my $name = $hash->{NAME}; + + $device_id = $first if(!$first !~ /^[0-9]+$/); + $first = 1 if(!defined $first || !$first !~ /^[0-9]+$/); + + Spotify_apiRequest($hash, 'me/tracks?limit=50'. ($first > 50 ? '&offset='. int($first/50)-1 : ''), undef, 'GET', 1); # getting saved tracks + my $result = $hash->{helper}{dispatch}{json}{items}; + return 'could not get saved tracks' if(!defined $result); + + my @uris = map { $_->{track}{uri} } @{$result}; + shift @uris for 1..($first%50-1); # removing first elements users wants to skip + Spotify_playTrackByURI($hash, \@uris, $device_id); # play them + + Log3 $name, 3, "$name: saved tracks"; + + return undef; +} + +sub Spotify_playRandomTrackFromPlaylistByURI($$$$) { # select a random track from a given playlist and play it (use case: e.g. alarm clocks) + my ($hash, $uri, $limit, $device_id) = @_; + my $name = $hash->{NAME}; + return 'wrong syntax: set playRandomTrackFromPlaylistByURI [ ] [ ]' if(!defined $uri); + + my ($user_id, $playlist_id) = $uri =~ m/user:(.*):playlist:(.*)/; + return 'invalid playlist_uri' if(!defined $user_id || !defined $playlist_id); + + $device_id = $limit if(!defined $device_id && $limit !~ /^[0-9]+$/); + $limit = undef if($limit !~ /^[0-9]+$/); + + Spotify_apiRequest($hash, "users/$user_id/playlists/$playlist_id/tracks?fields=items(track(name,uri))". (defined $limit ? "&limit=$limit" : ""), undef, 'GET', 1); + my $result = $hash->{helper}{dispatch}{json}{items}; + return 'could not find playlist' if(!defined $result); + + my @alltracks = map { $_->{track} } @{$result}; + my $selectedTrack = $alltracks[rand @alltracks]; + my @uris = ($selectedTrack->{uri}); + $hash->{helper}{skipTrackLog} = 1; + Spotify_playTrackByURI($hash, \@uris, $device_id); + Log3 $name, 3, "$name: random track $selectedTrack->{uri} ($selectedTrack->{name}) from $uri"; + return undef; +} + +sub Spotify_play($$$$$) { # any play command (colleciton or track) + my ($hash, $uris, $context_uri, $position, $device_id) = @_; + my $name = $hash->{NAME}; + + my $data = undef; + if(defined $uris) { + if(@{$uris} > 1 && @{$uris}[-1] !~ /spotify:/) { + $device_id = pop @{$uris}; + } + + $data = {uris => $uris}; + } else { + $data = {context_uri => $context_uri, {offset => {position => $position-1}}}; + } + + $device_id = Spotify_getTargetDeviceID($hash, $device_id, 1); + + Spotify_apiRequest($hash, 'me/player/play'. (defined $device_id ? '?device_id='. $device_id : ''), $data, 'PUT', 1); + Spotify_updatePlaybackStatus($hash, 1); + return undef; +} + +sub Spotify_volumeFade($$$$$) { # fade the volume of a device + my ($hash, $targetVolume, $duration, $step, $device_id) = @_; + return 'wrong syntax: set volumeFade [ ] [ ]' if(!defined $targetVolume); + + Spotify_updateDevices($hash, 1); # make sure devices are up to date (a valid start volume is required) + $device_id = $duration if($duration !~ /^[0-9]+$/); + my $startVolume = $hash->{helper}{device_active}{volume_percent}; + return 'could not get start volume of active device' if(!defined $startVolume); + $step = 5 if(!defined $step); # fall back to default step if not specified + $duration = 5 if(!defined $duration || $duration !~ /^[0-9]+$/); # fallback to default value if duration is not specified or valid + my $delta = abs($targetVolume - $startVolume); + my $requiredSteps = $delta/$step; + return Spotify_setVolume($hash, 0, $targetVolume, $device_id) if($requiredSteps == 0); # no steps required, set volume and exit + + #Log3 "spotify", 3, "fading volume start $startVolume target $targetVolume duration $duration step $step steps $requiredSteps"; + + $hash->{helper}{fading}{step} = $step; + $hash->{helper}{fading}{startVolume} = $startVolume; + $hash->{helper}{fading}{targetVolume} = $targetVolume; + $hash->{helper}{fading}{requiredSteps} = $requiredSteps; + $hash->{helper}{fading}{iteration} = 0; + $hash->{helper}{fading}{duration} = $duration; + $hash->{helper}{fading}{device_id} = $device_id; + + Spotify_volumeFadeStep($hash); + + return undef; +} + +sub Spotify_togglePlayback($) { # toggle playback (pause if active, resume otherwise) + my ($hash) = @_; + my $name = $hash->{NAME}; + Log3 $name, 3, "$name: togglePlayback"; + + if($hash->{helper}{is_playing}) { + Spotify_pausePlayback($hash); + } else { + Spotify_resumePlayback($hash); + } + + return undef; +} + + +sub Spotify_getTargetDeviceID($$$) { # resolve target device settings + my ($hash, $device_id, $newPlayback) = @_; + my $name = $hash->{NAME}; + + if(defined $device_id) { # use device id given by user + foreach my $device (@{$hash->{helper}{devices}}) { + return $device->{id} if($device->{id} eq $device_id || lc($device->{name}) eq lc($device_id)); # resolve name to / verify device_id + } + + # if not verified, continue to look for target device + } + + # no specific device given by user for this command + return $attr{$name}{defaultPlaybackDeviceID} if(defined $attr{$name}{defaultPlaybackDeviceID} # use default device or active device + && ( + ( + defined $attr{$name}{alwaysStartOnDefaultDevice} + && (!$hash->{helper}{is_playing} || $newPlayback) + && $attr{$name}{alwaysStartOnDefaultDevice} + ) + || !defined $hash->{helper}{device_active}{id} + ) + ); + + # no default or active device available + return $hash->{helper}{devices}[0]{id} if($newPlayback && !defined $hash->{helper}{device_active}{id}); # use first device available device on new playback + # if no new playback, trust the user anyway (maybe the device list is outdated) + return undef; +} + +sub Spotify_getTransferTargetDeviceID($$) { # get target device id for transfer + my ($hash, $targetdevice_id) = @_; + my $device_id = Spotify_getTargetDeviceID($hash, $targetdevice_id, 1); # resolve to user settings + 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 + + # target device not found, no (inactive) default device available + Spotify_updateDevices($hash, 1); # make sure devices are up to date + + # choose any device that is not active + foreach my $device (@{$hash->{helper}{devices}}) { + return $device->{id} if(!$device->{is_active}); + } + + return undef; +} + +sub Spotify_volumeFadeStep($) { # do a single fading stemp + my ($hash) = @_; + return if(!defined $hash->{helper}{fading}); + my $iteration = $hash->{helper}{fading}{iteration}; + my $requiredSteps = $hash->{helper}{fading}{requiredSteps}; + my $startVolume = $hash->{helper}{fading}{startVolume}; + my $targetVolume = $hash->{helper}{fading}{targetVolume}; + my $step = $hash->{helper}{fading}{step}; + my $isLastStep = $iteration+1 == $requiredSteps; + my $nextVolume = int($isLastStep ? $targetVolume : $startVolume + ($iteration+1)*$step*($targetVolume < $startVolume ? -1 : 1)); + my $deltaBetweenSteps = ($hash->{helper}{fading}{duration}/$requiredSteps); # time in s between each step + + #Log3 "spotify", 3, "fading volume step start $startVolume target $targetVolume steps $requiredSteps step $step nextVolume $nextVolume iteration $iteration delta $deltaBetweenSteps"; + + return if($nextVolume < 0 || $nextVolume > 100); + + $hash->{helper}{fading}{iteration}++; + Spotify_setVolume($hash, 0, $nextVolume, $hash->{helper}{fading}{device_id}); + + if(!$isLastStep) { + if($deltaBetweenSteps < 2) { + InternalTimer(gettimeofday()+$deltaBetweenSteps*($iteration+1), 'Spotify_volumeFadeStep', $hash); + } else { + select(undef, undef, undef, $deltaBetweenSteps); + Spotify_volumeFadeStep($hash); + } + } + + delete $hash->{helper}{fading} if($isLastStep); + return undef; +} + +sub Spotify_dispatch($$$) { + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my ($path) = split('\?', $param->{apiPath}, 2); + my ($pathpt0, $pathpt1, $pathpt2) = split('/', $path, 3); + my $method = $param->{method}; + delete $hash->{helper}{dispatch}; + + if(!defined($param->{hash})){ + Log3 "Spotify", 2, 'Spotify: dispatch fail (hash missing)'; + return undef; + } + + my $json = eval { JSON->new->utf8(0)->decode($data) }; + $hash->{helper}{dispatch}{json} = $json; + #Log3 $name, 3, $name . ' : ' . $hash . $data; + + if(defined $json->{error}) { + Log3 $name, 3, "$name: request failed: $json->{error}{message}"; + Spotify_refreshToken($hash) if($json->{error} eq "invalid_grant" || $json->{error} eq "token_expired"); + return "request failed: $json->{error}{message}"; + } + + Log3 $name, 4, "$name: dispatch successful $path"; + + if($path eq 'me/') { + return 'could not get user data' if(!defined $json->{id}); + + $hash->{helper}{user_id} = $json->{id}; + $hash->{helper}{subscription} = $json->{product}; + $hash->{helper}{uri} = $json->{uri}; + + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged($hash, 'user_id', $json->{id}); + readingsBulkUpdateIfChanged($hash, 'user_country', $json->{country}); + readingsBulkUpdateIfChanged($hash, 'user_subscription', $json->{subscription}); + readingsBulkUpdateIfChanged($hash, 'user_display_name', $json->{display_name}); + readingsBulkUpdateIfChanged($hash, 'user_profile_pic_url', $json->{images}[0]{url}) if(defined $json->{images} && $json->{images} > 0); + readingsBulkUpdateIfChanged($hash, 'user_follower_cnt', $json->{followers}{total}); + readingsEndUpdate($hash, 1); + } + + if($path eq 'me/player/devices') { + return 'could not update devices' if(!defined $json->{devices}); + + delete $hash->{helper}{device_active}; + + # delete any devices that are out of bounds + if(defined $hash->{helper}{devices}) { + my $index = 1; + foreach my $device (@{$hash->{helper}{devices}}) { + if($index > @{$json->{devices}}) { + CommandDeleteReading(undef, "$name device_". $index ."_.*"); + } + $index++; + } + } else { + CommandDeleteReading(undef, "$name device_.*"); + } + + $hash->{helper}{devices} = $json->{devices}; + readingsBeginUpdate($hash); + + my $index = 1; + foreach my $device (@{$hash->{helper}{devices}}) { + foreach my $prefix (("device_". $index ."_", 'device_active_')) { + if($prefix ne 'device_active_' || $device->{is_active}) { + readingsBulkUpdateIfChanged($hash, $prefix . 'id', $device->{id}); + readingsBulkUpdateIfChanged($hash, $prefix . 'name', $device->{name}); + readingsBulkUpdateIfChanged($hash, $prefix . 'type', $device->{type}); + readingsBulkUpdateIfChanged($hash, $prefix . 'volume', $device->{volume_percent}); + } + } + + $hash->{helper}{device_active} = $device if($device->{is_active}); # found active device + $hash->{helper}{device_default} = $device if(defined $attr{$name}{defaultPlaybackDeviceID} && $device->{id} eq $attr{$name}{defaultPlaybackDeviceID}); # found users default device + $index++; + } + readingsBulkUpdateIfChanged($hash, 'devices_cnt', $index-1); + readingsEndUpdate($hash, 1); + + CommandDeleteReading(undef, "$name device_acitve_.*") if(!defined $hash->{helper}{device_active}); + } + + if($path eq 'me/player') { + if(!defined $json->{is_playing}) { + $hash->{STATE} = 'connected'; + $hash->{helper}{is_playing} = 0; + readingsSingleUpdate($hash, 'is_playing', 0, 1); + return undef; + } + + $hash->{helper}{is_playing} = $json->{is_playing} ne 'false'; + $hash->{helper}{repeat} = $json->{repeat_state} eq 'track' ? 'one' : ($json->{repeat_state} eq 'context' ? 'all' : 'off'); + $hash->{helper}{shuffle} = $json->{shuffle_state}; + $hash->{helper}{progress_ms} = $json->{progress_ms}; + $hash->{STATE} = $json->{is_playing} ? 'playing' : 'paused'; + + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged($hash, 'is_playing', $json->{is_playing} ne 'false' ? 1 : 0); + readingsBulkUpdateIfChanged($hash, 'shuffle', $json->{shuffle_state} ? 'on' : 'off'); + readingsBulkUpdateIfChanged($hash, 'repeat', $hash->{helper}{repeat}); + readingsBulkUpdateIfChanged($hash, 'progress_ms', $json->{progress_ms}); + + if(defined $json->{item}) { + my $item = $json->{item}; + $hash->{helper}{track} = $item; + Spotify_saveTrack($hash, $item, 'track', 0); + } else { + CommandDeleteReading(undef, "$name track_.*"); + } + + if(defined $json->{device} && $json->{device}{is_active}) { + my $device = $json->{device}; + $hash->{helper}{device_active} = $device; + readingsBulkUpdateIfChanged($hash, 'device_active_id', $device->{id}); + readingsBulkUpdateIfChanged($hash, 'device_active_name', $device->{name}); + readingsBulkUpdateIfChanged($hash, 'device_active_volume', $device->{volume_percent}); + readingsBulkUpdateIfChanged($hash, 'device_active_type', $device->{type}); + } else { + delete $hash->{helper}{device_active}; + CommandDeleteReading(undef, "$name device_active_.*"); + } + + if($json->{is_playing} ne 'false') { + if(!defined $hash->{helper}{updatePlaybackTimer_next} || $hash->{helper}{updatePlaybackTimer_next} <= gettimeofday()) { # start refresh timer if not already started + $hash->{helper}{updatePlaybackTimer_next} = gettimeofday()+15; # refresh playback status every 15 seconds if currently playing + InternalTimer($hash->{helper}{updatePlaybackTimer_next}, 'Spotify_updatePlaybackStatus', $hash); + } + + if(defined $json->{item} && (!defined $hash->{helper}{nextSongTimer} || $hash->{helper}{nextSongTimer} <= gettimeofday())) { # refresh on finish of the song + $hash->{helper}{nextSongTimer} = gettimeofday() + int(($json->{item}{duration_ms} - $json->{progress_ms}) / 1000) + 1; + InternalTimer($hash->{helper}{nextSongTimer}, "Spotify_updatePlaybackStatus", $hash); + } + } + + return undef; + } + + + if($path eq 'me/player/volume') { + Spotify_updateDevices($hash, 0) if(!defined $hash->{helper}{fading}); + return undef; # do not fall through + } + + if(defined $pathpt1 && $pathpt1 eq 'player' && $method ne 'GET') { # on every modification on the player, update playback status + Spotify_updatePlaybackStatus($hash, 1); + InternalTimer(gettimeofday()+2, 'Spotify_updatePlaybackStatus', $hash); # make sure the api is already up to date and lists the changes + } + + return undef; +} + +sub Spotify_poll($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + my $pollInterval = $attr{$name}{updateInterval}; + InternalTimer(gettimeofday()+(defined $pollInterval ? $pollInterval : 10*60), "Spotify_poll", $hash); + Spotify_update($hash, 0); +} + +sub Spotify_update($$) { + my ($hash, $full) = @_; + Spotify_updateMe($hash, 0) if($full); + Spotify_updateDevices($hash, 0); + Spotify_updatePlaybackStatus($hash, 0); +} + +sub Spotify_saveTrack($$$$) { # save a track object to the readings + my ($hash, $track, $prefix, $beginUpdate) = @_; + readingsBeginUpdate($hash) if($beginUpdate); + readingsBulkUpdateIfChanged($hash, $prefix."_name", $track->{name}); + readingsBulkUpdateIfChanged($hash, $prefix."_uri", $track->{uri}); + readingsBulkUpdateIfChanged($hash, $prefix."_popularity", $track->{popularity}); + readingsBulkUpdateIfChanged($hash, $prefix."_duration_ms", $track->{duration_ms}); + readingsBulkUpdateIfChanged($hash, $prefix."_artist_name", $track->{artists}[0]{name}); + readingsBulkUpdateIfChanged($hash, $prefix."_artist_uri", $track->{artists}[0]{uri}); + readingsBulkUpdateIfChanged($hash, $prefix."_album_name", $track->{album}{name}); + readingsBulkUpdateIfChanged($hash, $prefix."_album_uri", $track->{album}{uri}); + readingsEndUpdate($hash, 1) if($beginUpdate); +} + +sub Spotify_saveArtist($$$$) { # save an artist object to the readings + my ($hash, $artist, $prefix, $beginUpdate) = @_; + readingsBeginUpdate($hash) if($beginUpdate); + readingsBulkUpdate($hash, $prefix."_name", $artist->{name}); + readingsBulkUpdate($hash, $prefix."_uri", $artist->{uri}); + readingsBulkUpdate($hash, $prefix."_popularity", $artist->{popularity}); + readingsBulkUpdate($hash, $prefix."_follower_cnt", $artist->{followers}{total}); + readingsBulkUpdate($hash, $prefix."_profile_pic_url", $artist->{images}[0]{url}); + readingsEndUpdate($hash, 1) if($beginUpdate); +} + +1; + +=pod +=item device +=item summary control your Spotify (Connect) playback +=item summary_DE Steuerung von Spotify (Connect) +=begin html + + +

Spotify

+
    + The Spotify module enables you to control your Spotify (Connect) playback.
    + To be able to control your music, you need to authorize with the Spotify WEB API. To do that, a Spotify API application is required.
    + While creating the app, enter any redirect_uri. By default the module will use https://oskar.pw/ as redirect_uri since the site outputs your temporary authentification code.
    + It is safe to rely on this site because the code is useless without your client secret and only valid for a few minutes.
    + If you want to use it, make sure to add it as redirect_uri to your app - however, you are free to use any other url and extract the code after signing in yourself.
    +
    + +

    Define

    +
      + define <name> Spotify <client_id> <client_secret> [ <redirect_url> ]
      +
    +
    +
      + Example: define Spotify Spotify f88e5f5c2911152d914391592e717738 301b6d1a245e4fe01c2f8b4efd250756
      +
    +
    + Once defined, open up your browser and call the URL displayed in AUTHORIZATION_URL, sign in with spotify and extract the code after being redirected.
    + If you get a redirect_uri mismatch make sure to either add https://oskar.pw/ as redirect url or that your url matches exactly with the one defined.
    + As soon as you obtained the code call set <name> code <code> - your state should change to connected and you are ready to go.
    + +
    + +

    set <required> [ <optional> ]

    + Without a target device given, the active device (or default device if alwaysStartOnDefaultDevice is enabled) will be used.
    + You can also use the name of the target device instead of the id if it does not contain spaces - where it states <device_id / device_name> spaces are allowed.
    + If no default device is defined and none is active, it will use the first available device.

    +
      +
    • + findArtistByName
      + finds an artist using its name and returns the result to the readings +
    • +
    • + findTrackByName
      + finds a track using its name and returns the result to the readings +
    • +
    • + pause
      + pause the current playback +
    • +
    • + playArtistByName <artist_name> [ <device_id> ]
      + plays an artist using its name (uses search) +
    • +
    • + playContextByURI <context_uri> [ <device_id / device_name> ]
      + plays a context (playlist, album or artist) using a Spotify URI +
    • +
    • + playPlaylistByName <playlist_name>
      + plays any playlist by providing a name (uses search) +
    • +
    • + playRandomTrackFromPlaylistByURI <track_uri> [ <limit> ] [ <device_id> ]
      + plays a random track from a playlist (only considering the first <limit> songs) +
    • +
    • + playSavedTracks [ <nr_of_start_track> ] [ <device_id> ]
      + plays the saved tracks (beginning with track <nr_of_start_track>) +
    • +
    • + playTrackByName <track_name> [ <device_id> ]
      + finds a song by its name and plays it +
    • +
    • + playTrackByURI <track_uri> [ <device_id / device_name> ]
      + plays a track using a track uri +
    • +
    • + repeat <track,context,off>
      + sets the repeat mode: either one, all (meaning playlist or album) or off +
    • +
    • + resume
      + resumes playback +
    • +
    • + seekToPosition <position>
      + seeks to the position lt;position> (in seconds, supported formats: 01:20, 80, 00:20, 20) +
    • +
    • + shuffle <0,1>
      + sets the shuffle mode: either on or off +
    • +
    • + skipToNext
      + skips to the next track +
    • +
    • + skipToPrevious
      + skips to the previous track +
    • +
    • + togglePlayback
      + toggles the playback (resumes if paused, pauses if playing) +
    • +
    • + transferPlayback [ <device_id> ]
      + transfers the current playback to the specified device (or the next inactive device if not specified) +
    • +
    • + update
      + updates playback and devices +
    • +
    • + volume <volume> [ <device_id> ]
      + sets the volume +
    • +
    • + volumeFade <volume> [ <duration> <step> ] [ <device_id> ]
      + fades the volume +
    • +
    +
    + +

    Get

    +
      + N/A +
    +
    + +

    Attributes

    +
      +
    • + alwaysStartOnDefaultDevice
      + always start new playback on the default device
      + default: 0 +
    • +
    • + defaultPlaybackDeviceID
      + the prefered device by its id
      +
    • +
    • + updateInterval
      + the interval to update your playback status while no music is running (in seconds)
      + default: 600 +
    • +
    +
+ +=end html +=begin html_DE + + +

Spotify

+
    + Das Spotify Modul ermöglicht die Steuerung von Spotify (Connect).
    + Um die Wiedergabe zu steuern, wird die Spotify WEB API verwendet. Dafür wird eine eigene Spotify API application benötigt.
    + Während der Erstellung muss eine redirect_uri angegeben - standardmäßig wird vom Modul https://oskar.pw/ verwendet, da diese Seite nach der Anmeldung den Code in leserlicher Form ausgibt.
    + Die Seite kann bedenkenlos verwendet werden, da der Code ohne client_secret nutzlos und nur wenige Minuten gültig ist.
    + Wenn du diese verwenden willst, stelle sicher, diese bei der Erstellung anzugeben, ansonsten kann jede beliebige andere Seite verwendet werden und der Code aus der URL extrahiert werden.
    +
    + +

    Define

    +
      + define <name> Spotify <client_id> <client_secret> [ <redirect_url> ]
      +
    +
    +
      + Beispiel: define Spotify Spotify f88e5f5c2911152d914391592e717738 301b6d1a245e4fe01c2f8b4efd250756
      +
    +
    + Sobald das Gerät angelegt wurde, muss die AUTHORIZATION_URL im Browser geöffnet werden und die Anmeldung mit Spotify erfolgen.
    + Sollte der Fehler redirect_uri mismatch auftauchen, stelle sicher, dass https://oskar.pw/ als redirect_uri hinzugefügt wurde oder die verwendete URL exakt übereinstimmt.
    + Sobald der Anmeldecode ermittelt wurde, führe folgenden Befehl aus: set <name> code <code> - der Status sollte nun auf connected wechseln und das Gerät ist einsatzbereit.
    + +
    + +

    set <required> [ <optional> ]

    + Wird kein Zielgerät angegeben, wird das aktive (oder das Standard-Gerät, wenn alwaysStartOnDefaultDevice aktiviert ist) verwendet.
    + An den Stellen, wo eine <device_id> verlangt wird, kann auch der Gerätename, sofern dieser keine Leerzeichen enthält, verwendet werden. Dort wo es <device_name> heißt, sind auch Leerzeichen im Namen zugelassen. + Wenn kein aktives oder Standard-Gerät vorhanden ist, wird das erste verfügbare Gerät verwendet.

    +
      +
    • + findArtistByName
      + sucht einen Künstler und liefert das Ergebnis in den Readings +
    • +
    • + findTrackByName
      + sucht einen Track und liefert das Ergebnis in den Readings +
    • +
    • + pause
      + pausiert die aktuelle Wiedergabe +
    • +
    • + playArtistByName <artist_name> [ <device_id> ]
      + sucht einen Künstler und spielt dessen Tracks ab +
    • +
    • + playContextByURI <context_uri> [ <device_id / device_name> ]
      + spielt einen Context (Playlist, Album oder Künstler) durch Angabe der URI ab +
    • +
    • + playPlaylistByName <playlist_name>
      + sucht eine Playlist und spielt diese ab +
    • +
    • + playRandomTrackFromPlaylistByURI <track_uri> [ <limit> ] [ <device_id> ]
      + spielt einen zufälligen Track aus einer Playlist ab (berücksichtigt nur die ersten <limit> Tracks der Playlist) +
    • +
    • + playSavedTracks [ <nr_of_start_track> ] [ <device_id> ]
      + spielt die gespeicherten Tracks ab (beginnend mit Track Nummer <nr_of_start_track>) +
    • +
    • + playTrackByName <track_name> [ <device_id> ]
      + sucht den Song und spielt ihn ab +
    • +
    • + playTrackByURI <track_uri> [ <device_id / device_name> ]
      + spielt einen Song durch Angabe der URI ab +
    • +
    • + repeat <track,context,off>
      + setzt den Wiederholungsmodus: entweder one, all (Playlist, Album, Künstler) oder off +
    • +
    • + resume
      + fährt mit der Wiedergabe fort +
    • +
    • + seekToPosition <position>
      + spult an die Position lt;position> (in Sekunden, erlaubte Formate: 01:20, 80, 00:20, 20) +
    • +
    • + shuffle <0,1>
      + setzt den Shuffle-Modus: entweder on oder off +
    • +
    • + skipToNext
      + weiter zum nächsten Track +
    • +
    • + skipToPrevious
      + zurück zum vorherigen Track +
    • +
    • + togglePlayback
      + toggelt die Wiedergabe (hält an, wenn sie aktiv ist, ansonsten fortsetzen) +
    • +
    • + transferPlayback [ <device_id> ]
      + überträgt die aktuelle Wiedergabe auf ein anderes Gerät (wenn kein Gerät angegeben wird, wird das nächste inaktive verwendet) +
    • +
    • + update
      + lädt den aktuellen Zustand neu +
    • +
    • + volume <volume> [ <device_id> ]
      + setzt die Lautstärke +
    • +
    • + volumeFade <volume> [ <duration> <step> ] [ <device_id> ]
      + setzt die Lautstärke schrittweise +
    • +
    +
    + +

    Get

    +
      + N/A +
    +
    + +

    Attributes

    +
      +
    • + alwaysStartOnDefaultDevice
      + startet neue Wiedergabe immer auf dem Standard-Gerät
      + default: 0 +
    • +
    • + defaultPlaybackDeviceID
      + das Standard-Gerät nach ID
      +
    • +
    • + updateInterval
      + Intervall in Sekunden, in dem der Status aktualisiert wird, wenn keine Musik läuft
      + default: 600 +
    • +
    +
+ +=end html_DE +=cut \ No newline at end of file diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index 1a92212d5..c32fb04e4 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -184,6 +184,7 @@ FHEM/36_Vallox.pm Skjall http://forum.fhem.de/index.php FHEM/36_WMBUS.pm kaihs http://forum.fhem.de Sonstige Systeme FHEM/37_SHC.pm rr2000 http://forum.fhem.de Sonstige Systeme FHEM/37_SHCdev.pm rr2000 http://forum.fhem.de Sonstige Systeme +FHEM/37_Spotify.pm neumann http://forum.fhem.de Multimedia FHEM/37_dash_dhcp.pm justme1968 http://forum.fhem.de Sonstige Systeme FHEM/37_fakeRoku.pm justme1968 http://forum.fhem.de Multimedia FHEM/37_harmony.pm justme1968 http://forum.fhem.de Multimedia