23 Commits

Author SHA1 Message Date
d06f2da73d Added multiple stream types to play from playlist 2023-12-10 16:45:17 +01:00
cd62f273c1 Added file handler for id3 data 2023-12-10 16:16:41 +01:00
90bd0f68c3 Updated Credits section 2023-12-10 14:38:38 +00:00
af662aa327 Improved Mirrors section in README.md 2023-12-10 14:37:25 +00:00
f53ef3c5dc Update README.md 2023-12-10 14:34:11 +00:00
425e579211 Improved the Raodmap in README.md 2023-12-10 12:39:37 +00:00
0fe0668659 Fixed typo in README.md 2023-12-10 12:05:31 +00:00
3b12974ce2 Update README.md 2023-12-10 12:05:16 +00:00
5ee15af74d Added Wiki hint in README.md 2023-12-09 22:53:09 +00:00
f2988c2eb5 Removed sending unnecessary data on the .../playback/next and .../previous endpoint 2023-12-09 23:36:00 +01:00
87042e72e0 Changed example structure 2023-12-09 22:25:43 +01:00
98a91e4974 Actually added now. Whoops :) 2023-12-09 21:54:05 +01:00
e3c5d4b6c9 Added more images in the diy project example 2023-12-09 21:53:32 +01:00
8904840428 Don't unmute when volume is set 2023-12-09 21:39:34 +01:00
c4362f998c Improved mute/unmute api endpoint functionality 2023-12-09 16:15:11 +01:00
ddb9a1e49e Updated Mirror section in README.md 2023-12-09 09:52:01 +00:00
61e167d036 Update README.md 2023-12-09 09:24:31 +00:00
da55732450 Fixed wrong rule in .gitignore 2023-12-09 08:52:43 +00:00
d68d773090 Added some badges to README.md for better readability 2023-12-09 08:34:22 +00:00
2d41b65ca8 Added more features to Roadmap 2023-12-09 08:24:14 +00:00
b1a677ab35 Added playlist play and create methods to API_DOC.txt 2023-12-08 22:05:28 +00:00
3b6811073a Added playlist play endpoint (POST) 2023-12-08 23:01:59 +01:00
72e60c34e0 Added playlist create endpoint 2023-12-08 22:54:17 +01:00
16 changed files with 223 additions and 78 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,2 @@
# ignore IDE specific files and folders # ignore IDE specific files and folders
/.theia/ .theia/

View File

@@ -1,33 +1,36 @@
API Location Method Returns Redirects to Description API Location Method Returns Redirects to Description
--------------------------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------------------------
/api/ API root /api/ API root
---------------- ----------------
/api/v1/playback/ Playback functions /api/v1/playback/ Playback functions
---------------- ----------------
/api/v1/playback/toggle GET JSON object Toggle Playback /api/v1/playback/toggle GET JSON object Toggle Playback
/api/v1/playback/play GET JSON object Start playback /api/v1/playback/play GET JSON object Start playback
/api/v1/playback/pause GET JSON object Pause playback /api/v1/playback/pause GET JSON object Pause playback
/api/v1/playback/next GET JSON object Play next audio /api/v1/playback/next GET JSON object Play next audio
/api/v1/playback/previous GET JSON object Play previous audio /api/v1/playback/previous GET JSON object Play previous audio
/api/v1/playback/info GET JSON object Get the title, artist, album, path, type, ... of the current played resource /api/v1/playback/info GET JSON object Get the title, artist, album, path, type, ... of the current played resource
/api/v1/playlist/ /api/v1/playlist/
---------------- ----------------
/api/v1/playlist/get GET JSON object Returns the whole current playlist /api/v1/playlist/get GET JSON object Returns the whole current playlist
/api/v1/playlist/create POST JSON object Creates a playlist of the given directory
/api/v1/volume/ /api/v1/playlist/play POST JSON object Plays a playlist from the SD card
----------------
/api/v1/volume/up GET JSON object Higher the volume (max. 20) /api/v1/volume/
/api/v1/volume/down GET JSON object Lower the volume (min. 0) ----------------
/api/v1/volume/mute GET JSON object Set volume to 0 /api/v1/volume/up GET JSON object Higher the volume (max. 20)
/api/v1/volume/get GET JSON object Do nothing, just get the volume /api/v1/volume/down GET JSON object Lower the volume (min. 0)
/api/v1/volume/<0-20> GET JSON object Set volume to a specific value between 0 and 20 /api/v1/volume/mute GET JSON object Mute (does not affect volume)
/api/v1/volume/unmute GET JSON object Unmute (set volume to current volume)
/api/v1/settings/ /api/v1/volume/get GET JSON object Do nothing, just get the volume
---------------- /api/v1/volume/<0-20> GET JSON object Set volume to a specific value between 0 and 20
/api/v1/settings/restart GET JSON object Performs a reboot of the microcontroller (after waiting 5000ms)
/api/v1/settings/
----------------
/api/v1/settings/restart GET JSON object Performs a reboot of the microcontroller (after waiting 5000ms)

View File

@@ -1,5 +1,11 @@
# NetSpeaker # NetSpeaker
[![License: Unlicense](https://img.shields.io/badge/License-Unlicense-blue.svg?style=for-the-badge)](https://unlicense.org/)
[![Open Source](https://img.shields.io/badge/Open_Source-gray?style=for-the-badge&logo=opensourceinitiative&logoColor=3DA639)](https://opensource.org/)
[![ESP32](https://img.shields.io/badge/ESP32-FCC624?style=for-the-badge&logo=esphome&logoColor=black)](https://www.espressif.com/en/products/socs/esp32)
[![State: Unstable](https://img.shields.io/badge/State-Unstable-red.svg?style=for-the-badge)]()
NetSpeaker is a project that aims to make it easier to build your own sound system, just like sqeezebox, for example. NetSpeaker is a project that aims to make it easier to build your own sound system, just like sqeezebox, for example.
There are two operating modes, to adapt to the specific use case - default mode is 0. The mode can be set at compile time There are two operating modes, to adapt to the specific use case - default mode is 0. The mode can be set at compile time
@@ -8,6 +14,13 @@ via the constant operation_mode. Valid choices are:
- 0: interconnected (HTTP API and WiFi, no buttons); useful for bigger projects - 0: interconnected (HTTP API and WiFi, no buttons); useful for bigger projects
- 1: standalone (Buttons, no WiFi and HTTP API); useful if you want to build a small player - 1: standalone (Buttons, no WiFi and HTTP API); useful if you want to build a small player
Any questions? Hopefully a look into the **Wiki** will help.
## Audio compatibility
For audio compatibilty, please have a look at [schreibfaul1's git repository's wiki](https://github.com/schreibfaul1/ESP32-audioI2S/wiki#which-external-dacs-can-be-used), where he goes into that in detail.
## Roadmap ## Roadmap
@@ -22,6 +35,7 @@ Features, already implemented, or still in progress for the v1.0.0 release!
- [X] /api/v1/playback/pause - [X] /api/v1/playback/pause
- [X] /api/v1/playback/next - [X] /api/v1/playback/next
- [X] /api/v1/playback/previous - [X] /api/v1/playback/previous
- [ ] /api/v1/playback/&lt;index&gt;
- [X] /api/v1/playback/info - [X] /api/v1/playback/info
- [ ] /api/v1/playback/id3_image - [ ] /api/v1/playback/id3_image
- [X] /api/v1/playlist/get - [X] /api/v1/playlist/get
@@ -31,16 +45,25 @@ Features, already implemented, or still in progress for the v1.0.0 release!
- [X] /api/v1/volume/up - [X] /api/v1/volume/up
- [X] /api/v1/volume/down - [X] /api/v1/volume/down
- [X] /api/v1/volume/mute - [X] /api/v1/volume/mute
- [X] /api/v1/volume/<0-20> - [X] /api/v1/volume/&lt;0-20&gt;
- [X] /api/v1/settings/restart/ - [X] /api/v1/settings/restart/
- [ ] /api/v1/system/name
- [ ] /api/v1/system/info
- [ ] /api/v1/system/name
- [ ] /api/v1/system/wifi/change
- [ ] /api/v1/system/wifi/get_ssid
- [ ] /api/v1/files/get
- [ ] /api/v1/files/upload
- [X] Automatic WiFi connection - [X] Automatic WiFi connection
- [X] Access Point opened when no WiFi connection could be established - [X] Access Point opened when no WiFi connection could be established
- [ ] Implement a configuration server running on port 8080 - [ ] Implement a configuration server running on port 8080
- [ ] Edit wifi connection - [ ] Edit wifi connection
- [ ] Edit friendly name of the NetSpeaker - [ ] Edit friendly name of the NetSpeaker
- [X] Improve the volume endpoint handler (currently pretty undynamic - not anymore :)
- [ ] Add better encoding as umlauts are not shown **sometimes**
## Credits ## Credits & Acknowledgements
Thanks to... Thanks to...
@@ -48,6 +71,11 @@ Thanks to...
- schreibfaul1 (github) for creating the audio library used in this project - schreibfaul1 (github) for creating the audio library used in this project
- espressif for making the best microprocessor I've ever seen - espressif for making the best microprocessor I've ever seen
External librarys used:
- ESP32-audioI2S library (https://github.com/schreibfaul1/ESP32-audioI2S as of 2023/12)
- Arduino ESP32-specific librarys (https://github.com/espressif/arduino-esp32/ as of 2023/12)
## Contributing ## Contributing
@@ -55,11 +83,10 @@ Pull requests are welcome. For major changes, please open an issue first
to discuss what you would like to change. to discuss what you would like to change.
Please also make sure to test things carefully before contributing them. Please also make sure to test things carefully before contributing them.
## Mirrors
## External librarys used: - [Privacynerd's Gitea (main)](https://git.privacynerd.de/NetSpeaker/NetSpeaker/)
- [Codeberg](https://codeberg.org/NetSpeaker/NetSpeaker)
- ESP32-audioI2S library (https://github.com/schreibfaul1/ESP32-audioI2S as of 2023/12)
- Arduino ESP32-specific librarys (https://github.com/espressif/arduino-esp32/ as of 2023/12)
## License ## License

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -49,8 +49,9 @@ const String apPSK = "aA16161Aa"; // pre-shared-key of access
// create all needed variables // create all needed variables
int currentVolume = 20; // variable where current volume (0...20) is stored int currentVolume = 20; // variable where current volume (0...20) is stored
bool muted = false; // currently muted (does not affect currentVolume)
String currentSongPath; // path to currently playing song String currentSongPath; // path to currently playing song
bool audioPlaying = true; // play song or not? bool audioPlaying = false; // play song or not?
Audio audio; // Audio object (for playing audio, decoding mp3, ...) Audio audio; // Audio object (for playing audio, decoding mp3, ...)
int currentPlaylistPosition = -1; // the current position in the current playlist int currentPlaylistPosition = -1; // the current position in the current playlist
String currentPlaylist = "/audio/" + directoryPlaylistName + playlistExtension; // path to current playlist String currentPlaylist = "/audio/" + directoryPlaylistName + playlistExtension; // path to current playlist
@@ -65,12 +66,12 @@ struct playbackInfo {
String resourcePath; // e.g. /audio/XXX.mp3 String resourcePath; // e.g. /audio/XXX.mp3
String artist; // e.g. Blender Foundation String artist; // e.g. Blender Foundation
String track; // e.g. 01/02 or 01 String track; // e.g. 01/02 or 01
String album; String album; // album
String year; // id3 tag year String year; // id3 tag year
String genre; // id3 tag content type String genre; // id3 tag content type
String copyright; // id3 tag (special) String copyright; // id3 tag (special)
}; };
struct playbackInfo pbInfo = { "", "", "", "", "", "" }; struct playbackInfo pbInfo = { "", "", "", "", "", "", "", "", "" };
void setup() { void setup() {
Serial.begin(115200); // setup Serial console (over USB) with baud 115200 Serial.begin(115200); // setup Serial console (over USB) with baud 115200

View File

@@ -18,19 +18,28 @@ void setupAudio() {
} }
String nextAudio() { String nextAudio() {
String songPath; String url;
currentPlaylistPosition++; // increment once currentPlaylistPosition++; // increment once
songPath = getSongFromPlaylist(currentPlaylist, currentPlaylistPosition); url = getURLFromPlaylist(currentPlaylist, currentPlaylistPosition);
if (songPath == "") { // if the end of the playlist is reached go to start
if (url == "") { // if the end of the playlist is reached go to start
currentPlaylistPosition = -1; // the next time "nextAudio()" is called it will increment to 0 currentPlaylistPosition = -1; // the next time "nextAudio()" is called it will increment to 0
nextAudio(); // recursive call nextAudio(); // recursive call
return ""; // exit return ""; // exit
} }
Serial.printf("[INFO] Starting next audio from playlist (ID: %d, Path: %s)\n", currentPlaylistPosition, songPath.c_str());
audio.connecttoFS(SD, songPath.c_str()); // play the next song
pbInfo.resourcePath = songPath; if (url.startsWith("http")) {
Serial.printf("[INFO] Starting stream from playlist (ID: %d, URL: %s)\n", currentPlaylistPosition, url.c_str());
audio.connecttohost(url.c_str());
} else if (url.startsWith("/")) {
Serial.printf("[INFO] Starting audio from playlist (ID: %d, Path: %s)\n", currentPlaylistPosition, url.c_str());
audio.connecttoFS(SD, url.c_str()); // play the next song
} else if (url.startsWith("speech://")) { // format: speech://CC@TEXT where CC is the country code, e.g. en or de
Serial.printf("[INFO] Starting speech from playlist (ID: %d, Language: %s, Text: %s)\n", currentPlaylistPosition, url.substring(9, 11).c_str(), url.substring(12).c_str());
audio.connecttospeech(url.substring(12).c_str(), url.substring(9, 11).c_str());
}
pbInfo.resourcePath = url;
pbInfo.type = "local"; pbInfo.type = "local";
// clear everything (if a file hadn't got id3 tags so that there won't keep the old names) // clear everything (if a file hadn't got id3 tags so that there won't keep the old names)
@@ -43,20 +52,28 @@ String nextAudio() {
pbInfo.genre = ""; pbInfo.genre = "";
pbInfo.copyright = ""; pbInfo.copyright = "";
return songPath; return url;
} }
String previousAudio() { String previousAudio() {
String songPath; String url;
currentPlaylistPosition--; currentPlaylistPosition--;
if (currentPlaylistPosition < 0) currentPlaylistPosition = 0; if (currentPlaylistPosition < 0) currentPlaylistPosition = 0;
songPath = getSongFromPlaylist(currentPlaylist, currentPlaylistPosition); url = getURLFromPlaylist(currentPlaylist, currentPlaylistPosition);
Serial.printf("[INFO] Starting previous audio from playlist (ID: %d, Path: %s)\n", currentPlaylistPosition, songPath.c_str()); if (url.startsWith("http")) {
audio.connecttoFS(SD, songPath.c_str()); // play the previous song Serial.printf("[INFO] Starting stream from playlist (ID: %d, URL: %s)\n", currentPlaylistPosition, url.c_str());
audio.connecttohost(url.c_str());
} else if (url.startsWith("/")) {
Serial.printf("[INFO] Starting audio from playlist (ID: %d, Path: %s)\n", currentPlaylistPosition, url.c_str());
audio.connecttoFS(SD, url.c_str()); // play the next song
} else if (url.startsWith("speech://")) { // format: speech://CC@TEXT where CC is the country code, e.g. en or de
Serial.printf("[INFO] Starting speech from playlist (ID: %d, Language: %s, Text: %s)\n", currentPlaylistPosition, url.substring(9, 11).c_str(), url.substring(12).c_str());
audio.connecttospeech(url.substring(12).c_str(), url.substring(9, 11).c_str());
}
pbInfo.resourcePath = songPath; pbInfo.resourcePath = url;
pbInfo.type = "local"; pbInfo.type = "local";
// clear everything (if a file hadn't got id3 tags so that there won't keep the old names) // clear everything (if a file hadn't got id3 tags so that there won't keep the old names)
@@ -69,7 +86,7 @@ String previousAudio() {
pbInfo.genre = ""; pbInfo.genre = "";
pbInfo.copyright = ""; pbInfo.copyright = "";
return songPath; return url;
} }
void switchPlaybackIfButtonClicked() { void switchPlaybackIfButtonClicked() {
@@ -121,9 +138,14 @@ void audio_eof_mp3(const char *info) { // MUST HAVE THIS NAME (audio_eof_mp3) b
} }
void audio_eof_speech(const char *info) { void audio_eof_speech(const char *info) {
Serial.printf("[Audio.h] EOF_Speech %s\n", info); Serial.printf("[Audio.h] EOF_Speech %s\n", info);
nextAudio();
} }
void audio_info(const char *info) { void audio_info(const char *info) {
Serial.printf("[Audio.h] Info %s\n", info); Serial.printf("[Audio.h] Info %s\n", info);
if(String(info).startsWith("End of webstream")) {
nextAudio();
}
} }
void audio_id3data(const char *info) { // id3 metadata void audio_id3data(const char *info) { // id3 metadata
Serial.printf("[Audio.h] ID3Data %s\n", info); Serial.printf("[Audio.h] ID3Data %s\n", info);
@@ -163,4 +185,48 @@ void audio_icyurl(const char *info) { // Name of radio station
} }
void audio_lasthost(const char *info) { // stream URL played void audio_lasthost(const char *info) { // stream URL played
Serial.printf("[Audio.h] Lasthost %s\n", info); Serial.printf("[Audio.h] Lasthost %s\n", info);
} }
// *********************************************************************************** //
// *************** Code heavily inspired by schreibfaul1's wiki entry ************** //
// * https://github.com/schreibfaul1/ESP32-audioI2S/wiki#what-audio-events-are-there * //
// *********************************************************************************** //
void audio_id3image(File &file, const size_t pos, const size_t size) { // cover image
Serial.printf("[Audio.h] ID3Image Found at position: %u | Length: %u\n", pos, size);
uint8_t buf[1024];
file.seek(pos + 1); // skip 1 byte encoding
char mimeType[255]; // mime-type (null terminated)
for (uint8_t i = 0u; i < 255; i++) {
mimeType[i] = file.read();
if (uint8_t(mimeType[i]) == 0) break;
}
Serial.printf("[Audio.h] ID3Image MimeType: %s\n", mimeType);
uint8_t imageType = file.read(); // image type (1 Byte)
Serial.printf("[Audio.h] ID3Image ImageType: %d\n", imageType);
for (uint8_t i = 0u; i < 255; i++) { // description (null terminated)
buf[i] = file.read();
if (uint8_t(buf[i]) == 0) break;
}
// raw image data
String imgPath = "/.current";
if (String(mimeType) == "image/jpeg") imgPath += ".jpg";
else if (String(mimeType) == "image/png") imgPath += ".png";
else if (String(mimeType) == "image/svg") imgPath += ".svg";
else imgPath += ".img";
File coverFile = SD.open(imgPath, FILE_WRITE);
size_t len = size;
while (len) {
uint16_t bytesRead = file.read(buf, sizeof(buf));
if (len >= bytesRead) len -= bytesRead;
else {
bytesRead = len;
len = 0;
}
coverFile.write(buf, bytesRead);
}
// write changes to SD card
coverFile.flush();
coverFile.close();
Serial.printf("[Audio.h] ID3Image Cover file written to '%s'\n", imgPath.c_str());
}

View File

@@ -95,7 +95,7 @@ bool createPlaylistFromDirectory(String folderpath) { // create a .m3u playlist
return true; return true;
} }
String getSongFromPlaylist(String path, int position) { String getURLFromPlaylist(String path, int position) {
String song; String song;
File playlist; File playlist;
char currentChar; char currentChar;

View File

@@ -84,10 +84,8 @@ void api_v1_playback_pause() {
void api_v1_playback_next() { void api_v1_playback_next() {
Serial.println("[HTTP] [API] 200 - GET '/api/v1/playback/next'"); Serial.println("[HTTP] [API] 200 - GET '/api/v1/playback/next'");
String songPath = nextAudio(); nextAudio();
String content = "\"resource_path\": \""; String content = "\"resource_playlist_index\": ";
content += songPath;
content += "\", \"resource_playlist_index\": ";
content += String(currentPlaylistPosition); content += String(currentPlaylistPosition);
@@ -97,10 +95,8 @@ void api_v1_playback_next() {
void api_v1_playback_previous() { void api_v1_playback_previous() {
Serial.println("[HTTP] [API] 200 - GET '/api/v1/playback/previous'"); Serial.println("[HTTP] [API] 200 - GET '/api/v1/playback/previous'");
String songPath = previousAudio(); previousAudio();
String content = "\"resource_path\": \""; String content = "\"resource_playlist_index\": ";
content += songPath;
content += "\", \"resource_playlist_index\": ";
content += String(currentPlaylistPosition); content += String(currentPlaylistPosition);
api_server.send(200, "application/json", generate_api_json(true, content)); api_server.send(200, "application/json", generate_api_json(true, content));
@@ -118,7 +114,9 @@ void api_v1_playback_volume() {
currentVolume--; currentVolume--;
if (currentVolume < 0) currentVolume = 0; if (currentVolume < 0) currentVolume = 0;
} else if (option == "mute") { } else if (option == "mute") {
currentVolume = 0; // TODO: remember the previous volume and just mute it; unmute has to be implemented then muted = true;
} else if (option == "unmute") {
muted = false;
} else if (option == "get") { } else if (option == "get") {
// just here that no 'success: false' is sent // just here that no 'success: false' is sent
} else if (option == "0") { } else if (option == "0") {
@@ -167,9 +165,13 @@ void api_v1_playback_volume() {
success = false; success = false;
} }
audio.setVolume(currentVolume); // set volume if (!muted) audio.setVolume(currentVolume); // set volume if not muted
else audio.setVolume(0); // if muted, set volume to 0
String content = "\"volume\": "; // prepare the http response String content = "\"volume\": "; // prepare the http response
content += String(currentVolume); content += String(currentVolume);
content += ", \"muted\": ";
content += muted ? "true" : "false";
api_server.send(200, "application/json", generate_api_json(success, content)); // generate json and send it api_server.send(200, "application/json", generate_api_json(success, content)); // generate json and send it
} }
@@ -199,7 +201,7 @@ void api_v1_playlist_get() {
bool firstRun = true; bool firstRun = true;
while (true) { while (true) {
currentSong = getSongFromPlaylist(currentPlaylist, index); currentSong = getURLFromPlaylist(currentPlaylist, index);
if (currentSong == "") { if (currentSong == "") {
content += "\""; content += "\"";
break; break;
@@ -218,6 +220,50 @@ void api_v1_playlist_get() {
api_server.send(200, "application/json", generate_api_json(true, content)); api_server.send(200, "application/json", generate_api_json(true, content));
} }
void api_v1_playlist_create() {
Serial.println("[HTTP] [API] 200 - POST '/api/v1/playlist/create'");
// get the right POST param
String folderPath;
for (int i = 0; i < api_server.args(); i++) {
if (api_server.argName(i) == "folderPath") {
folderPath = api_server.arg(i);
break;
}
}
bool code = createPlaylistFromDirectory(folderPath);
if (code) { // if creating the playlist was successful
String playlistPath = folderPath + "/" + directoryPlaylistName + playlistExtension;
String content = "\"playlist_path\": \"" + playlistPath + "\"";
api_server.send(200, "application/json", generate_api_json(true, content));
} else {
api_server.send(200, "application/json", generate_api_json(false));
}
}
void api_v1_playlist_play() {
Serial.println("[HTTP] [API] 200 - POST '/api/v1/playlist/play'");
// get the right POST param
String playlistPath;
for (int i = 0; i < api_server.args(); i++) {
if (api_server.argName(i) == "playlistPath") {
playlistPath = api_server.arg(i);
break;
}
}
if (getURLFromPlaylist(playlistPath, 0) == "") { // if playlist is empty or nonexistent
api_server.send(200, "application/json", generate_api_json(false)); // send no success info
} else {
currentPlaylist = playlistPath;
currentPlaylistPosition = -1;
nextAudio();
api_server.send(200, "application/json", generate_api_json(true)); // just return the success info
}
}
void api_v1_settings_restart() { void api_v1_settings_restart() {
SD.end(); // delete SD object and sync its cache to flash SD.end(); // delete SD object and sync its cache to flash
WiFi.disconnect(); // disconnect wifi WiFi.disconnect(); // disconnect wifi
@@ -247,15 +293,17 @@ void setupApiWeb() {
api_server.send(404, "application/json", "{\"code\": 404, \"message\": \"Resource not found.\"}"); api_server.send(404, "application/json", "{\"code\": 404, \"message\": \"Resource not found.\"}");
}); });
api_server.on("/", apiRoot); api_server.on("/", apiRoot);
api_server.on("/api/v1/playback/toggle", api_v1_playback_toggle); api_server.on("/api/v1/playback/toggle", HTTP_GET, api_v1_playback_toggle);
api_server.on("/api/v1/playback/play", api_v1_playback_play); api_server.on("/api/v1/playback/play", HTTP_GET, api_v1_playback_play);
api_server.on("/api/v1/playback/pause", api_v1_playback_pause); api_server.on("/api/v1/playback/pause", HTTP_GET, api_v1_playback_pause);
api_server.on("/api/v1/playback/next", api_v1_playback_next); api_server.on("/api/v1/playback/next", HTTP_GET, api_v1_playback_next);
api_server.on("/api/v1/playback/previous", api_v1_playback_previous); api_server.on("/api/v1/playback/previous", HTTP_GET, api_v1_playback_previous);
api_server.on("/api/v1/playback/info", api_v1_playback_info); api_server.on("/api/v1/playback/info", HTTP_GET, api_v1_playback_info);
api_server.on("/api/v1/playlist/get", api_v1_playlist_get); api_server.on("/api/v1/playlist/get", HTTP_GET, api_v1_playlist_get);
api_server.on(UriBraces("/api/v1/volume/{}"), api_v1_playback_volume); api_server.on("/api/v1/playlist/create", HTTP_POST, api_v1_playlist_create);
api_server.on("/api/v1/settings/restart", api_v1_settings_restart); api_server.on("/api/v1/playlist/play", HTTP_POST, api_v1_playlist_play);
api_server.on(UriBraces("/api/v1/volume/{}"), HTTP_GET, api_v1_playback_volume);
api_server.on("/api/v1/settings/restart", HTTP_GET, api_v1_settings_restart);
Serial.println("[HTTP] [API] Starting API server (http) on port " + String(webport_api)); Serial.println("[HTTP] [API] Starting API server (http) on port " + String(webport_api));
api_server.begin(); api_server.begin();