🎸 Owntone (Musik-Player) mit FHEM steuern, HTTPMOD, Coverbild, Thumbnail, JSON-API

Begonnen von Torxgewinde, 06 November 2023, 19:29:38

Vorheriges Thema - Nächstes Thema

Torxgewinde

Hallo,
Ich habe die mir wichtigen Elemente aus der WEBAPI/JSON-API von Owntone herausgesucht und mit HTTPMOD abgerufen. Vielleicht ist es für den ein oder anderen auch nützlich. Die Adresse http://192.168.123.123 ist zu ersetzen mit der Adresse von eurem Owntone.

Warum? Ich nutze eigentlich MPD, hatte aber Probleme mit mehren Outputs.

So sieht es in einem "Room" aus:
Du darfst diesen Dateianhang nicht ansehen.

Hier ein Beispiel für Readings mit mehren Outputs:
Du darfst diesen Dateianhang nicht ansehen.

Unterstütze API-Elemente:
  • Player Status abfragen (play, pause, stop, volume, etc.)
  • Start, pause oder stop der Wiedergabe
  • Lied vorwärts und zurück (skip forward, backwards)
  • Setze Wiedergabemodus (Set shuffle mode)
  • Playlist ababrbeitung (Set consume mode)
  • Wiederholungsmodus (Set repeat mode)
  • Lautstärke vom Mastermixer oder pro Output (Set master volume or volume for a specific output)
  • Springe zu einer Stelle im Lied (Seek to a position in the currently playing track)
  • Liste der gerade aktiven Outputs holen (Get a list of available outputs)
  • Setzen welche Outputs aktiv sein sollen (Set enabled outputs)
  • Widergabeliste holen (Get a list of queue items)
  • Coverbild in FHEM als Reading darstellen (aktualisiert sich mit FHEMs-Javascript-Funktionen)

Besonderheiten:
  • Server ist ein userAttr "OwntoneServer". Dort muss man die Adresse, Port und Protokoll nur an einer Stelle eintragen. In anderen HTTPMOD Snippets taucht die Serveradresse an teilweise vielen Stellen auf.
  • Ähnlich zu Set-Magic oder wie bei DOIF können Readings und Attribute mit [device:reading] in HTTPMOD genutzt werden. Die Beispiele sonst nutzen gerne %% %%. Ich fand die Set-Magic Schreibweise dann einleuchtender.
  • Coverbild wird von Owntone-Server geladen und BASE64 codiert in einem Reading in FHEM gespeichert. Zum Laden wird HttpUtils_NonblockingGet genutzt. Dafür muss man nicht mit der 99_myUtils.pm arbeiten, das ist hier alles bereits im Device mit drin.

Beispiele:
  • set Owntone.device setOutputs Audio-1 setzt jeden Output aktiv der den String "Audio-1" enthält.
  • set Owntone.device setOutputs Audio-1|Computer setzt jeden Output aktiv der den String "Audio-1" oder "Computer" enthält.
  • set Owntone.device setOutputs "type"="dummy" setzt jeden Dummy-Output aktiv. Den Wert bei dem set-Befehl eingeben, oder entsprechend Escapen
  • set Owntone.device setOutputs .* setzt jeden Output aktiv.
  • set Owntone.device setVolume 40 setzt die Gesamtlautstärke auf 40%
  • set Owntone.device setVolume 35|0 setzt die Lautstärke nur vom Output mit der ID 0 auf 35%
  • set Owntone.device setVolume 37|Audio-1 setzt den zuerst gefundenen Output mit dem String "Audio-1" im JSON-Buffer auf 37%

Hier das Device. Einfach Copy & Paste und dann den eigenen Owntone Server im Attribut OwntoneServer eintragen:
defmod Owntone.device HTTPMOD [$name:OwntoneServer]/api/player 10
attr Owntone.device userattr OwntoneServer codeBlock
attr Owntone.device OwntoneServer http://192.168.123.123:3689
attr Owntone.device alias Owntone
attr Owntone.device cmdIcon next:rc_NEXT play:rc_PLAY pause:rc_PAUSE volup:rc_VOLPLUS voldown:rc_VOLMINUS
attr Owntone.device comment Examples:\
\
# to send the play command to Owntone:\
set Owntone.device play\
\
# to send the stop command to Owntone (not using "stop", to avoid issues with HTTPMOD commands):\
set Owntone.device playerStop\
\
# to set the queue to an URI, for example to the playlist with ID 9:\
set Owntone.device setQueueURI library:playlist:68\
\
# to set the queue to multiple URIs, for example to the playlist with ID 9 and to add the track with id 12\
set Owntone.device setQueueURI library:playlist:9,library:track:12\
\
# Assumed there is a playist with the name "FFN", call this to set the Queue and play that playlist:\
set Owntone.device setQueuePlaylist FFN\
\
# I decided that playlists with only digits can be treated as Owntones IDs. So, this will play Playlist with ID 123:\
set Owntone.device setQueuePlaylist 123\
# If your Playlists have numeric values as names, you probably want to change the IExpr accordingly.\
\
# To enable all outputs:\
set Owntone.device setOutputs .*\
\
# To enable just the Output with name "Audio-1":\
set Owntone.device setOutputs Audio-1\
\
# To enable three Outputs named "Audio-1", "Audio-3", "Computer"\
set Owntone.device setOutputs Audio-1|Audio-3|Computer\
\
# To set the Master-Volume to an absolute value of 34%:\
set Owntone.device volumeMasterVolume 34\
\
# To set the Master-Volume 5% louder:\
set Owntone.device volumeMasterStep 5\
\
# To set the Master-Volume 5% lower:\
set Owntone.device volumeMasterStep -5\
\
# Alternative to set the Master volume to 40%:\
set Owntone.device setVolume 40\
\
# To set the volume of output with id 0 to 35%:\
set Owntone.device setVolume 35|0\
\
# To set the volume of output with string "Audio-1" somewhere in its JSON buffer to 35%:\
set Owntone.device setVolume 35|Audio-1\
# Note: Only the first matching output is considered!\
# Note: Only outputs from a previous "get outputs" can be searched
attr Owntone.device disable 0
attr Owntone.device event-on-change-reading .*
attr Owntone.device eventMap /volumeMasterStep 5:volup/volumeMasterStep -5:voldown/
attr Owntone.device get01JSON state
attr Owntone.device get01Name state
attr Owntone.device get01Replacement01Value /api/player
attr Owntone.device get02JSON repeat
attr Owntone.device get02Name repeat
attr Owntone.device get02Replacement01Value /api/player
attr Owntone.device get03JSON consume
attr Owntone.device get03Name consume
attr Owntone.device get03Replacement01Value /api/player
attr Owntone.device get04JSON shuffle
attr Owntone.device get04Name shuffle
attr Owntone.device get04Replacement01Value /api/player
attr Owntone.device get05JSON volume
attr Owntone.device get05Name volume
attr Owntone.device get05Replacement01Value /api/player
attr Owntone.device get06JSON item_id
attr Owntone.device get06Name itemID
attr Owntone.device get06Replacement01Value /api/player
attr Owntone.device get07JSON item_length_ms
attr Owntone.device get07Name itemLength_ms
attr Owntone.device get07Replacement01Value /api/player
attr Owntone.device get08JSON item_progress_ms
attr Owntone.device get08Name itemProgress_ms
attr Owntone.device get08Replacement01Value /api/player
attr Owntone.device get09DeleteIfUnmatched 1
attr Owntone.device get09Name outputs
attr Owntone.device get09Poll 1
attr Owntone.device get09RegOpt g
attr Owntone.device get09Regex (\{[^\{\}]*\})
attr Owntone.device get09Replacement01Value /api/outputs
attr Owntone.device get10AutoNumLen 3
attr Owntone.device get10DeleteIfUnmatched 1
attr Owntone.device get10Name queue
attr Owntone.device get10Poll 1
attr Owntone.device get10RegOpt g
attr Owntone.device get10Regex (\{[^\{\}]*\})
attr Owntone.device get10Replacement01Value /api/queue
attr Owntone.device get40AutoNumLen 3
attr Owntone.device get40DeleteIfUnmatched 1
attr Owntone.device get40Name libraryPlaylists
attr Owntone.device get40Poll 1
attr Owntone.device get40RegOpt g
attr Owntone.device get40Regex (\{[^\{\}]*\})
attr Owntone.device get40Replacement01Value /api/library/playlists
attr Owntone.device getURL [$name:OwntoneServer]%%endpoint%%
attr Owntone.device group Musik
attr Owntone.device icon audio_play
attr Owntone.device reading01JSON state
attr Owntone.device reading01Name state
attr Owntone.device reading02JSON repeat
attr Owntone.device reading02Name repeat
attr Owntone.device reading03JSON consume
attr Owntone.device reading03Name consume
attr Owntone.device reading04JSON shuffle
attr Owntone.device reading04Name shuffle
attr Owntone.device reading05JSON volume
attr Owntone.device reading05Name volume
attr Owntone.device reading06JSON item_id
attr Owntone.device reading06Name itemID
attr Owntone.device reading07JSON item_length_ms
attr Owntone.device reading07Name itemLength_ms
attr Owntone.device reading08JSON item_progress_ms
attr Owntone.device reading08Name itemProgress_ms
attr Owntone.device replacement01Mode text
attr Owntone.device replacement01Regex %%endpoint%%
attr Owntone.device replacement02Mode expression
attr Owntone.device replacement02Regex \[([^:]+):([^\]]+)\]
attr Owntone.device replacement02Value my $device = $name if ($1 eq "\$name") // $1;;\
ReadingsVal($device, $2, undef) or AttrVal($device, $2, "???");;
attr Owntone.device room Musik
attr Owntone.device set01Method PUT
attr Owntone.device set01Name play
attr Owntone.device set01NoArg 1
attr Owntone.device set01Replacement01Value /api/player/play
attr Owntone.device set02Method PUT
attr Owntone.device set02Name pause
attr Owntone.device set02NoArg 1
attr Owntone.device set02Replacement01Value /api/player/pause
attr Owntone.device set03Method PUT
attr Owntone.device set03Name playerStop
attr Owntone.device set03NoArg 1
attr Owntone.device set03Replacement01Value /api/player/stop
attr Owntone.device set04Method PUT
attr Owntone.device set04Name toggle
attr Owntone.device set04NoArg 1
attr Owntone.device set04Replacement01Value /api/player/toggle
attr Owntone.device set05Method PUT
attr Owntone.device set05Name next
attr Owntone.device set05NoArg 1
attr Owntone.device set05Replacement01Value /api/player/next
attr Owntone.device set06Method PUT
attr Owntone.device set06Name previous
attr Owntone.device set06NoArg 1
attr Owntone.device set06Replacement01Value /api/player/previous
attr Owntone.device set07IMap false:off, true:on
attr Owntone.device set07Method PUT
attr Owntone.device set07Name shuffle
attr Owntone.device set07Replacement01Value /api/player/shuffle?state=$val
attr Owntone.device set08IMap false:off, true:on
attr Owntone.device set08Method PUT
attr Owntone.device set08Name consume
attr Owntone.device set08Replacement01Value /api/player/consume?state=$val
attr Owntone.device set09IMap off:off, all:all, single:single
attr Owntone.device set09Method PUT
attr Owntone.device set09Name repeat
attr Owntone.device set09Replacement01Value /api/player/repeat?state=$val
attr Owntone.device set10Max 100
attr Owntone.device set10Method PUT
attr Owntone.device set10Min 0
attr Owntone.device set10Name volumeMasterVolume
attr Owntone.device set10Replacement01Value /api/player/volume?volume=$val
attr Owntone.device set11Max 100
attr Owntone.device set11Method PUT
attr Owntone.device set11Min -100
attr Owntone.device set11Name volumeMasterStep
attr Owntone.device set11Replacement01Value /api/player/volume?step=$val
attr Owntone.device set12Data $val
attr Owntone.device set12IExpr # This is a Regex to select outputs that are to be enabled\
#\
# Example: Audio-1\
#          selects any output that has the string "Audio-1" in the JSON buffer\
# Example: Audio-1|Computer\
#          selects any output that has "Audio-1" or "Computer" in the buffer\
# Example: "type"="dummy"\
#          selects any output that is of type dummy (or contains such a string)\
# Example: .*\
#          matches any known output, all outputs are to be enabled then\
#\
# The array of outputs must be queried beforehand, to retrieve a list of outputs!\
my $outputsToEnableRegex = $val or ".*";;\
\
# return an array of all current output id as JSON\
my $ret = join ',',\
          map { '"'. decode_json(ReadingsVal($name, $_, "{}"))->{id} .'"'}\
          grep { ReadingsVal($name, $_, "???") =~ /$outputsToEnableRegex/ }\
          grep { $_ =~ /^outputs(?:-\d+)*$/ }\
          keys %{$defs{$name}{READINGS}};;\
\
$ret = '{"outputs":[ '.$ret.' ]}';;\
$ret;;
attr Owntone.device set12Method PUT
attr Owntone.device set12Name setOutputs
attr Owntone.device set12NoArg 0
attr Owntone.device set12Replacement01Value /api/outputs/set
attr Owntone.device set12TextArg 1
attr Owntone.device set13IExpr # textArg passed to this setter can be just the volume (0..100) in percent\
# optionally it can be "volume|id" or "volume|outputRegex"\
# to adjust the volume of a single, individual output\
#\
# Examples:\
#\
# To set the Master volume to 40%:\
# set Owntone.device setVolume 40\
#\
# To set the volume of output with id 0 to 35%:\
# set Owntone.device setVolume 35|0\
#\
# To set the volume of output with string "Audio-1" in the JSON buffer to 35%:\
# set Owntone.device setVolume 35|Audio-1\
# Note: Only the first matching output is considered!\
# Note: Only outputs from a previous "get outputs" can be searched\
#\
my ($vol, $id) = split(/\|/, $val);;\
\
# check if id is present and numeric --> just use it\
if (defined $id && $id =~ /^\d+$/) {\
    $val = "$vol&output_id=$id";;\
} elsif ( defined $id ) {\
    # if $id is present but non-numeric search a matching output\
    $id = (map { decode_json(ReadingsVal($name, $_, "{}"))->{id} }\
           grep { ReadingsVal($name, $_, "???") =~ /$id/ }\
           grep { $_ =~ /^outputs(?:-\d+)*$/ }\
           keys %{$defs{$name}{READINGS}})[0];;\
    $val = "$vol&output_id=$id" \
}\
\
$val;;
attr Owntone.device set13Method PUT
attr Owntone.device set13Name setVolume
attr Owntone.device set13Replacement01Value /api/player/volume?volume=$val
attr Owntone.device set13TextArg 1
attr Owntone.device set14Method POST
attr Owntone.device set14Name setQueueURI
attr Owntone.device set14Replacement01Value /api/queue/items/add?clear=true&playback=start&uris=$val
attr Owntone.device set14TextArg 1
attr Owntone.device set15IExpr # textArg passed to this setter sets the queue to a playlist\
#\
# Example: Assumed there is a playist with the name "FFN":\
# set Owntone.device setQueuePlaylist FFN\
#\
# Note: Only already known Playlists can be searched, so get them first,\
#       or alternatively configure them being polled\
# get Owntone.device libraryPlaylists\
\
# check if val numeric --> just use it as ID\
return $val if ($val =~ /^\d+$/);;\
\
#go through all currently known Playlists\
my $IDs = join ",",\
          map { decode_json(ReadingsVal($name, $_, "{}"))->{id} }\
          grep { ReadingsVal($name, $_, "???") =~ /"name"\s*:\s*"$val"/ }\
          grep { $_ =~ /^libraryPlaylists(?:-\d+)*$/ }\
          keys %{$defs{$name}{READINGS}};;\
\
return $IDs;;
attr Owntone.device set15Method POST
attr Owntone.device set15Name setQueuePlaylist
attr Owntone.device set15Replacement01Value /api/queue/items/add?clear=true&playback=start&uris=library:playlist:$val
attr Owntone.device set15TextArg 1
attr Owntone.device set18Method PUT
attr Owntone.device set18Name seekPosition_ms
attr Owntone.device set18Replacement01Value /api/player/seek?position_ms=$val
attr Owntone.device set19Method PUT
attr Owntone.device set19Name seekSeek_ms
attr Owntone.device set19Replacement01Value /api/player/seek?seek_ms=$val
attr Owntone.device set30Method PUT
attr Owntone.device set30Name clearQueue
attr Owntone.device set30NoArg 1
attr Owntone.device set30Replacement01Value /api/queue/clear
attr Owntone.device set40Method PUT
attr Owntone.device set40Name libraryRescan
attr Owntone.device set40NoArg 1
attr Owntone.device set40Replacement01Value /api/update
attr Owntone.device set41Method PUT
attr Owntone.device set41Name metadataRescan
attr Owntone.device set41NoArg 1
attr Owntone.device set41Replacement01Value /api/rescan
attr Owntone.device setURL [$name:OwntoneServer]%%endpoint%%
attr Owntone.device sortby 09
attr Owntone.device stateFormat <div style="width: 256px">\
<div>\
queueNowPlayingArtwork\
</div>\
<div>\
queueNowPlaying\
</div>\
<div>\
(state)\
</div>\
</div>
attr Owntone.device userReadings queueNowPlaying:(queue.*|itemID.*) {\
# return the currently playing track info as HTML snippet\
my $needle = ReadingsVal($name, "itemID", "-1");;\
\
my $readingName = (grep { ReadingsVal($name, $_, "???") =~ $needle }\
                   grep { $_ =~ /^queue(?:-\d+)*$/ }\
                   keys %{$defs{$name}{READINGS}})[-1];;\
\
#error message if not found\
return "$needle not found" unless $readingName;;\
\
my $j = decode_json ReadingsVal($name, $readingName, "???");;\
\
return "not found" if (!$j->{title} or !$j->{artist} or !$j->{album});;\
\
return $j->{title}.  "<br />".\
       $j->{artist}. "<br />".\
       $j->{album};;\
},\
queueNowPlayingArtworkURL:(queue.*|itemID.*) {\
# this returns an URL for the current artwork as shown in Owntone\
# it also triggers a download of the artwork into a reading\
my $needle = ReadingsVal($name, "itemID", "-1");;\
my $server = AttrVal($name, "OwntoneServer", "-1");;\
my $this = ReadingsVal($name, $reading, "-1");;\
\
my $readingName = (grep { ReadingsVal($name, $_, "???") =~ $needle }\
                   grep { $_ =~ /^queue(?:-\d+)*$/ }\
                   keys %{$defs{$name}{READINGS}})[-1];;\
\
#error message if not found\
return "$needle not found" unless $readingName;;\
\
my $j = decode_json ReadingsVal($name, $readingName, "???");;\
\
# error if not the expected json values are present,\
# which is a good sanity check\
return "not found" if (!$j->{title} or\
                       !$j->{artist} or \
                       !$j->{album} or \
                       !$j->{artwork_url});;\
\
# concatenate the Servername from Attr and URL\
my $result = $server.'/'.$j->{artwork_url};;\
# replace /./ with /\
$result =~ s/\/\.\//\//g;;\
\
# defining a callback here captures essential variables\
# the callback is executed when the download returns\
my $callback = sub($$$) {\
my ($hash, $err, $data) = @_;;\
my $hash_defs = $defs{$name};;\
readingsBeginUpdate($hash_defs);;\
\
        my $img_base64;;\
if ($data eq "") {\
#simple error JPEG (tiny, shows an x in the middle) if errors occured\
$img_base64 = "/9j/4AAQSkZJRgABAQEBLAEsAAD//gATQ3JlYXRlZCB3aXRoIEdJTVD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoH".\
"BwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQU".\
"FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wgARCAAKAAoDAREAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAA".\
"AAYH/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAAB3AuQD//EABgQAAIDAAAAAAAAAAAAAAAAAAEEAwUQ/9oACAEB".\
"AAEFAmmJBcb/AP/EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQMBAT8BH//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQIBAT8B".\
"H//EACAQAAEDAgcAAAAAAAAAAAAAAAIBAxIRURMUICEyQoH/2gAIAQEABj8CiLp5GY45S4OdRSyLtXy66P/EABkQAQACAwAAAAAA".\
"AAAAAAAAAAERISBRYf/aAAgBAQABPyEkTFNdBOhlXgv/2gAMAwEAAgADAAAAEBJP/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgB".\
"AwEBPxAf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAgEBPxAf/8QAGBABAQADAAAAAAAAAAAAAAAAAREQMVH/2gAIAQEAAT8Q".\
"S/gOJm9cYEYjmf/Z";;\
} else {\
            $img_base64 = encode_base64($data);;\
}\
\
#make life easier for FHEM by removing newlines\
$img_base64 =~ s/\n//g;;\
\
# need to assume mimetype is JPEG, browsers seem to tolerate false image-mimetypes silently\
# if img-width is not suitable, override the default value with a CSS style\
$img_base64 = "<html><img width=\"128\" src=\"data:image/jpg;;;;base64, $img_base64\"></img></html>";;\
\
#update or create new reading\
readingsBulkUpdate($hash_defs, "queueNowPlayingArtwork", $img_base64);;\
readingsEndUpdate($hash_defs, 1);;\
};;\
\
# only execute the download if the URL changed, we only arrive here if no errors occured\
HttpUtils_NonblockingGet({ url=>$result, callback=>$callback }) if ($this ne $result);;\
\
return $result;;\
},\
playbackTimeMinutes:itemProgress_ms.* {\
# useful for one of my other devices\
return int(ReadingsNum($name, "itemProgress_ms", "-1")/(1000*60));;\
}
attr Owntone.device verbose 1
attr Owntone.device webCmd play:pause:next:volup:voldown