From d39fe3aa9c7e8f07d191f95228bcd3cd200f074d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laszlo=20Heged=C3=BCs?= Date: Sat, 15 Apr 2023 22:29:29 +0200 Subject: [PATCH 1/8] Add Playlist class and implementations --- src/AudioPlayer.cpp | 251 ++++++++---------- src/AudioPlayer.h | 55 ++-- src/Common.h | 10 + src/Playlist.h | 141 +++++++++++ src/SdCard.cpp | 377 +++++++++++----------------- src/SdCard.h | 5 +- src/Web.cpp | 2 +- src/cpp.h | 29 +++ src/playlists/CacheFilePlaylist.hpp | 278 ++++++++++++++++++++ src/playlists/FolderPlaylist.hpp | 246 ++++++++++++++++++ src/playlists/WebstreamPlaylist.hpp | 31 +++ 11 files changed, 1028 insertions(+), 397 deletions(-) create mode 100644 src/Playlist.h create mode 100644 src/cpp.h create mode 100644 src/playlists/CacheFilePlaylist.hpp create mode 100644 src/playlists/FolderPlaylist.hpp create mode 100644 src/playlists/WebstreamPlaylist.hpp diff --git a/src/AudioPlayer.cpp b/src/AudioPlayer.cpp index e4119fd6..9acbaa2b 100644 --- a/src/AudioPlayer.cpp +++ b/src/AudioPlayer.cpp @@ -20,6 +20,7 @@ #include "Web.h" #include "Wlan.h" #include "main.h" +#include "playlists/WebstreamPlaylist.hpp" #include #include @@ -55,10 +56,7 @@ static uint8_t AudioPlayer_MaxVolumeHeadphone = 11u; // Maximum volume that can static void AudioPlayer_Task(void *parameter); static void AudioPlayer_HeadphoneVolumeManager(void); -static char **AudioPlayer_ReturnPlaylistFromWebstream(const char *_webUrl); -static int AudioPlayer_ArrSortHelper(const void *a, const void *b); -static void AudioPlayer_SortPlaylist(char **arr, int n); -static void AudioPlayer_RandomizePlaylist(char **str, const uint32_t count); +static Playlist *AudioPlayer_ReturnPlaylistFromWebstream(const char *_webUrl); static size_t AudioPlayer_NvsRfidWriteWrapper(const char *_rfidCardId, const char *_track, const uint32_t _playPosition, const uint8_t _playMode, const uint16_t _trackLastPlayed, const uint16_t _numberOfTracks); static void AudioPlayer_ClearCover(void); @@ -401,9 +399,30 @@ void AudioPlayer_Task(void *parameter) { } } - trackQStatus = xQueueReceive(gTrackQueue, &gPlayProperties.playlist, 0); + // Update playtime stats every 250 ms + if ((millis() - AudioPlayer_LastPlaytimeStatsTimestamp) > 250) { + AudioPlayer_LastPlaytimeStatsTimestamp = millis(); + // Update current playtime and duration + AudioPlayer_CurrentTime = audio->getAudioCurrentTime(); + AudioPlayer_FileDuration = audio->getAudioFileDuration(); + // Calculate relative position in file (for trackprogress neopixel & web-ui) + if (!gPlayProperties.playlistFinished && !gPlayProperties.isWebstream) { + if (!gPlayProperties.pausePlay && (gPlayProperties.seekmode != SEEK_POS_PERCENT) && (audio->getFileSize() > 0)) { // To progress necessary when paused + gPlayProperties.currentRelPos = ((double) (audio->getFilePos() - audio->inBufferFilled()) / (double) audio->getFileSize()) * 100; + } + } else { + gPlayProperties.currentRelPos = 0; + } + } + + Playlist *tmp; + trackQStatus = xQueueReceive(gTrackQueue, &tmp, 0); if (trackQStatus == pdPASS || gPlayProperties.trackFinished || trackCommand != NO_ACTION) { if (trackQStatus == pdPASS) { + // clean up the old playlist + delete gPlayProperties.playlist; + gPlayProperties.playlist = tmp; + if (gPlayProperties.pausePlay) { gPlayProperties.pausePlay = false; } @@ -431,7 +450,7 @@ void AudioPlayer_Task(void *parameter) { if (gPlayProperties.saveLastPlayPosition) { // Don't save for AUDIOBOOK_LOOP because not necessary if (gPlayProperties.currentTrackNumber + 1 < gPlayProperties.numberOfTracks) { // Only save if there's another track, otherwise it will be saved at end of playlist anyway - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber + 1, gPlayProperties.numberOfTracks); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber + 1, gPlayProperties.numberOfTracks); } } if (gPlayProperties.sleepAfterCurrentTrack) { // Go to sleep if "sleep after track" was requested @@ -480,7 +499,7 @@ void AudioPlayer_Task(void *parameter) { } if (gPlayProperties.saveLastPlayPosition && !gPlayProperties.pausePlay) { Log_Printf(LOGLEVEL_INFO, trackPausedAtPos, audio->getFilePos(), audio->getFilePos() - audio->inBufferFilled()); - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), audio->getFilePos() - audio->inBufferFilled(), gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), audio->getFilePos() - audio->inBufferFilled(), gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); } gPlayProperties.pausePlay = !gPlayProperties.pausePlay; Web_SendWebsocketData(0, 30); @@ -507,7 +526,7 @@ void AudioPlayer_Task(void *parameter) { gPlayProperties.currentTrackNumber++; } if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); Log_Println(trackStartAudiobook, LOGLEVEL_INFO); } Log_Println(cmndNextTrack, LOGLEVEL_INFO); @@ -555,31 +574,35 @@ void AudioPlayer_Task(void *parameter) { } } - if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); - Log_Println(trackStartAudiobook, LOGLEVEL_INFO); - } + if (gPlayProperties.saveLastPlayPosition) { + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); + Log_Println(trackStartAudiobook, LOGLEVEL_INFO); + } - Log_Println(cmndPrevTrack, LOGLEVEL_INFO); - if (!gPlayProperties.playlistFinished) { - audio->stopSong(); - } - } else { - if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); - } + Log_Println(cmndPrevTrack, LOGLEVEL_INFO); + if (!gPlayProperties.playlistFinished) { audio->stopSong(); - Led_Indicate(LedIndicatorType::Rewind); - audioReturnCode = audio->connecttoFS(gFSystem, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber)); - // consider track as finished, when audio lib call was not successful - if (!audioReturnCode) { - System_IndicateError(); - gPlayProperties.trackFinished = true; - continue; - } - Log_Println(trackStart, LOGLEVEL_INFO); + } + } else { + if (gPlayProperties.playMode == WEBSTREAM) { + Log_Println(trackChangeWebstream, LOGLEVEL_INFO); + System_IndicateError(); continue; } + if (gPlayProperties.saveLastPlayPosition) { + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); + } + audio->stopSong(); + Led_Indicate(LedIndicatorType::Rewind); + audioReturnCode = audio->connecttoFS(gFSystem, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str()); + // consider track as finished, when audio lib call was not successful + if (!audioReturnCode) { + System_IndicateError(); + gPlayProperties.trackFinished = true; + continue; + } + Log_Println(trackStart, LOGLEVEL_INFO); + continue; } break; case FIRSTTRACK: @@ -590,7 +613,7 @@ void AudioPlayer_Task(void *parameter) { } gPlayProperties.currentTrackNumber = 0; if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); Log_Println(trackStartAudiobook, LOGLEVEL_INFO); } Log_Println(cmndFirstTrack, LOGLEVEL_INFO); @@ -608,7 +631,7 @@ void AudioPlayer_Task(void *parameter) { if (gPlayProperties.currentTrackNumber + 1 < gPlayProperties.numberOfTracks) { gPlayProperties.currentTrackNumber = gPlayProperties.numberOfTracks - 1; if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); Log_Println(trackStartAudiobook, LOGLEVEL_INFO); } Log_Println(cmndLastTrack, LOGLEVEL_INFO); @@ -634,7 +657,7 @@ void AudioPlayer_Task(void *parameter) { if (gPlayProperties.playUntilTrackNumber == gPlayProperties.currentTrackNumber && gPlayProperties.playUntilTrackNumber > 0) { if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), 0, gPlayProperties.playMode, 0, gPlayProperties.numberOfTracks); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); } gPlayProperties.playlistFinished = true; gPlayProperties.playMode = NO_PLAYLIST; @@ -647,7 +670,7 @@ void AudioPlayer_Task(void *parameter) { if (!gPlayProperties.repeatPlaylist) { if (gPlayProperties.saveLastPlayPosition) { // Set back to first track - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + 0), 0, gPlayProperties.playMode, 0, gPlayProperties.numberOfTracks); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(0).c_str(), 0, gPlayProperties.playMode, 0, gPlayProperties.numberOfTracks); } gPlayProperties.playlistFinished = true; gPlayProperties.playMode = NO_PLAYLIST; @@ -672,12 +695,12 @@ void AudioPlayer_Task(void *parameter) { Log_Println(repeatPlaylistDueToPlaymode, LOGLEVEL_NOTICE); gPlayProperties.currentTrackNumber = 0; if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, *(gPlayProperties.playlist + 0), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(0).c_str(), 0, gPlayProperties.playMode, 0, gPlayProperties.numberOfTracks); } } } - if (!strncmp("http", *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), 4)) { + if (!strncmp("http", gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 4)) { gPlayProperties.isWebstream = true; } else { gPlayProperties.isWebstream = false; @@ -686,17 +709,17 @@ void AudioPlayer_Task(void *parameter) { audioReturnCode = false; if (gPlayProperties.playMode == WEBSTREAM || (gPlayProperties.playMode == LOCAL_M3U && gPlayProperties.isWebstream)) { // Webstream - audioReturnCode = audio->connecttohost(*(gPlayProperties.playlist + gPlayProperties.currentTrackNumber)); + audioReturnCode = audio->connecttohost(gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str()); gPlayProperties.playlistFinished = false; gTriedToConnectToHost = true; } else if (gPlayProperties.playMode != WEBSTREAM && !gPlayProperties.isWebstream) { // Files from SD - if (!gFSystem.exists(*(gPlayProperties.playlist + gPlayProperties.currentTrackNumber))) { // Check first if file/folder exists - Log_Printf(LOGLEVEL_ERROR, dirOrFileDoesNotExist, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber)); + if (!gFSystem.exists(gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber))) { // Check first if file/folder exists + Log_Printf(LOGLEVEL_ERROR, dirOrFileDoesNotExist, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str()); gPlayProperties.trackFinished = true; continue; } else { - audioReturnCode = audio->connecttoFS(gFSystem, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber)); + audioReturnCode = audio->connecttoFS(gFSystem, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str()); // consider track as finished, when audio lib call was not successful } } @@ -722,13 +745,13 @@ void AudioPlayer_Task(void *parameter) { } } else { if (gPlayProperties.numberOfTracks > 1) { - Audio_setTitle("(%u/%u): %s", gPlayProperties.currentTrackNumber + 1, gPlayProperties.numberOfTracks, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber)); + Audio_setTitle("(%u/%u): %s", gPlayProperties.currentTrackNumber+1, gPlayProperties.numberOfTracks, gPlayProperties.playlist->getFilename(gPlayProperties.currentTrackNumber).c_str()); } else { - Audio_setTitle("%s", *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber)); + Audio_setTitle("%s", gPlayProperties.playlist->getFilename(gPlayProperties.currentTrackNumber).c_str()); } } AudioPlayer_ClearCover(); - Log_Printf(LOGLEVEL_NOTICE, currentlyPlaying, *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber), (gPlayProperties.currentTrackNumber + 1), gPlayProperties.numberOfTracks); + Log_Printf(LOGLEVEL_NOTICE, currentlyPlaying, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), (gPlayProperties.currentTrackNumber + 1), gPlayProperties.numberOfTracks); gPlayProperties.playlistFinished = false; } } @@ -922,52 +945,36 @@ void AudioPlayer_PauseOnMinVolume(const uint8_t oldVolume, const uint8_t newVolu // Receives de-serialized RFID-data (from NVS) and dispatches playlists for the given // playmode to the track-queue. void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _lastPlayPos, const uint32_t _playMode, const uint16_t _trackLastPlayed) { -// Make sure last playposition for audiobook is saved when new RFID-tag is applied -#ifdef SAVE_PLAYPOS_WHEN_RFID_CHANGE - if (!gPlayProperties.pausePlay && (gPlayProperties.playMode == AUDIOBOOK || gPlayProperties.playMode == AUDIOBOOK_LOOP)) { - AudioPlayer_TrackControlToQueueSender(PAUSEPLAY); - while (!gPlayProperties.pausePlay) { // Make sure to wait until playback is paused in order to be sure that playposition saved in NVS - vTaskDelay(portTICK_PERIOD_MS * 100u); + // Make sure last playposition for audiobook is saved when new RFID-tag is applied + #ifdef SAVE_PLAYPOS_WHEN_RFID_CHANGE + if (!gPlayProperties.pausePlay && (gPlayProperties.playMode == AUDIOBOOK || gPlayProperties.playMode == AUDIOBOOK_LOOP)) { + AudioPlayer_TrackControlToQueueSender(PAUSEPLAY); + while (!gPlayProperties.pausePlay) { // Make sure to wait until playback is paused in order to be sure that playposition saved in NVS + vTaskDelay(portTICK_PERIOD_MS * 100u); + } } - } -#endif - char filename[255]; - - size_t sizeCpy = strnlen(_itemToPlay, sizeof(filename) - 1); // get the len of the play item (to a max of 254 chars) - memcpy(filename, _itemToPlay, sizeCpy); - filename[sizeCpy] = '\0'; // terminate the string + #endif gPlayProperties.startAtFilePos = _lastPlayPos; gPlayProperties.currentTrackNumber = _trackLastPlayed; - char **musicFiles; + std::optional playlist = std::nullopt; if (_playMode != WEBSTREAM) { - if (_playMode == RANDOM_SUBDIRECTORY_OF_DIRECTORY || _playMode == RANDOM_SUBDIRECTORY_OF_DIRECTORY_ALL_TRACKS_OF_DIR_RANDOM) { - const char *tmp = SdCard_pickRandomSubdirectory(filename); // *filename (input): target-directory // *filename (output): random subdirectory - if (tmp == NULL) { // If error occured while extracting random subdirectory - musicFiles = NULL; - } else { - musicFiles = SdCard_ReturnPlaylist(filename, _playMode); // Provide random subdirectory in order to enter regular playlist-generation + if (_playMode == RANDOM_SUBDIRECTORY_OF_DIRECTORY) { + auto tmp = SdCard_pickRandomSubdirectory(_itemToPlay); // *filename (input): target-directory // *filename (output): random subdirectory + if (tmp) { // If error occured while extracting random subdirectory + playlist = SdCard_ReturnPlaylist(tmp.value().c_str(), _playMode); // Provide random subdirectory in order to enter regular playlist-generation } } else { - musicFiles = SdCard_ReturnPlaylist(filename, _playMode); + playlist = SdCard_ReturnPlaylist(_itemToPlay, _playMode); } } else { - musicFiles = AudioPlayer_ReturnPlaylistFromWebstream(filename); + playlist = AudioPlayer_ReturnPlaylistFromWebstream(_itemToPlay); } // Catch if error occured (e.g. file not found) - if (musicFiles == NULL) { - Log_Println(errorOccured, LOGLEVEL_ERROR); - System_IndicateError(); - if (gPlayProperties.playMode != NO_PLAYLIST) { - AudioPlayer_TrackControlToQueueSender(STOP); - } - return; - } - gPlayProperties.playMode = BUSY; // Show @Neopixel, if uC is busy with creating playlist - if (!strcmp(*(musicFiles - 1), "0")) { + if (!playlist || !playlist.value()->isValid()) { Log_Println(noMp3FilesInDir, LOGLEVEL_NOTICE); System_IndicateError(); if (!gPlayProperties.pausePlay) { @@ -982,7 +989,7 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l } gPlayProperties.playMode = _playMode; - gPlayProperties.numberOfTracks = strtoul(*(musicFiles - 1), NULL, 10); + gPlayProperties.numberOfTracks = playlist.value()->size(); // Set some default-values gPlayProperties.repeatCurrentTrack = false; gPlayProperties.repeatPlaylist = false; @@ -999,7 +1006,6 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l switch (gPlayProperties.playMode) { case SINGLE_TRACK: { Log_Println(modeSingleTrack, LOGLEVEL_NOTICE); - xQueueSend(gTrackQueue, &(musicFiles), 0); break; } @@ -1007,7 +1013,6 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l gPlayProperties.repeatCurrentTrack = true; gPlayProperties.repeatPlaylist = true; Log_Println(modeSingleTrackLoop, LOGLEVEL_NOTICE); - xQueueSend(gTrackQueue, &(musicFiles), 0); break; } @@ -1017,16 +1022,14 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l gPlayProperties.numberOfTracks = 1; // Limit number to 1 even there are more entries in the playlist Led_ResetToNightBrightness(); Log_Println(modeSingleTrackRandom, LOGLEVEL_NOTICE); - AudioPlayer_RandomizePlaylist(musicFiles, gPlayProperties.numberOfTracks); - xQueueSend(gTrackQueue, &(musicFiles), 0); + playlist.value()->randomize(); break; } case AUDIOBOOK: { // Tracks need to be alph. sorted! gPlayProperties.saveLastPlayPosition = true; Log_Println(modeSingleAudiobook, LOGLEVEL_NOTICE); - AudioPlayer_SortPlaylist(musicFiles, gPlayProperties.numberOfTracks); - xQueueSend(gTrackQueue, &(musicFiles), 0); + playlist.value()->sort(); break; } @@ -1034,66 +1037,65 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l gPlayProperties.repeatPlaylist = true; gPlayProperties.saveLastPlayPosition = true; Log_Println(modeSingleAudiobookLoop, LOGLEVEL_NOTICE); - AudioPlayer_SortPlaylist(musicFiles, gPlayProperties.numberOfTracks); - xQueueSend(gTrackQueue, &(musicFiles), 0); + playlist.value()->sort(); break; } case ALL_TRACKS_OF_DIR_SORTED: case RANDOM_SUBDIRECTORY_OF_DIRECTORY: { - Log_Printf(LOGLEVEL_NOTICE, modeAllTrackAlphSorted, filename); - AudioPlayer_SortPlaylist(musicFiles, gPlayProperties.numberOfTracks); - xQueueSend(gTrackQueue, &(musicFiles), 0); + Log_Printf(LOGLEVEL_NOTICE, modeAllTrackAlphSorted, _itemToPlay); + playlist.value()->sort(); break; } - case ALL_TRACKS_OF_DIR_RANDOM: - case RANDOM_SUBDIRECTORY_OF_DIRECTORY_ALL_TRACKS_OF_DIR_RANDOM: { - Log_Printf(LOGLEVEL_NOTICE, modeAllTrackRandom, filename); - AudioPlayer_RandomizePlaylist(musicFiles, gPlayProperties.numberOfTracks); - xQueueSend(gTrackQueue, &(musicFiles), 0); + case ALL_TRACKS_OF_DIR_RANDOM: { + Log_Println(modeAllTrackRandom, LOGLEVEL_NOTICE); + playlist.value()->randomize(); break; } case ALL_TRACKS_OF_DIR_SORTED_LOOP: { gPlayProperties.repeatPlaylist = true; Log_Println(modeAllTrackAlphSortedLoop, LOGLEVEL_NOTICE); - AudioPlayer_SortPlaylist(musicFiles, gPlayProperties.numberOfTracks); - xQueueSend(gTrackQueue, &(musicFiles), 0); + playlist.value()->sort(); break; } case ALL_TRACKS_OF_DIR_RANDOM_LOOP: { gPlayProperties.repeatPlaylist = true; Log_Println(modeAllTrackRandomLoop, LOGLEVEL_NOTICE); - AudioPlayer_RandomizePlaylist(musicFiles, gPlayProperties.numberOfTracks); - xQueueSend(gTrackQueue, &(musicFiles), 0); + playlist.value()->randomize(); break; } case WEBSTREAM: { // This is always just one "track" Log_Println(modeWebstream, LOGLEVEL_NOTICE); - if (Wlan_IsConnected()) { - xQueueSend(gTrackQueue, &(musicFiles), 0); - } else { + if (!Wlan_IsConnected()) { Log_Println(webstreamNotAvailable, LOGLEVEL_ERROR); - System_IndicateError(); - gPlayProperties.playMode = NO_PLAYLIST; + goto error; } break; } case LOCAL_M3U: { // Can be one or multiple SD-files or webradio-stations; or a mix of both Log_Println(modeWebstreamM3u, LOGLEVEL_NOTICE); - xQueueSend(gTrackQueue, &(musicFiles), 0); break; } default: - Log_Printf(LOGLEVEL_ERROR, modeInvalid, gPlayProperties.playMode); - gPlayProperties.playMode = NO_PLAYLIST; - System_IndicateError(); + Log_Println(modeDoesNotExist, LOGLEVEL_ERROR); + goto error; + } + + // send the playlist to the queue + xQueueSend(gTrackQueue, &(playlist), 0); + return; + +error: + System_IndicateError(); + gPlayProperties.playMode = NO_PLAYLIST; + delete playlist.value(); } /* Wraps putString for writing settings into NVS for RFID-cards. @@ -1139,16 +1141,8 @@ size_t AudioPlayer_NvsRfidWriteWrapper(const char *_rfidCardId, const char *_tra } // Adds webstream to playlist; same like SdCard_ReturnPlaylist() but always only one entry -char **AudioPlayer_ReturnPlaylistFromWebstream(const char *_webUrl) { - static char number[] = "1"; - static char *url[2] = {number, nullptr}; - - free(url[1]); - - number[0] = '1'; - url[1] = x_strdup(_webUrl); - - return &(url[1]); +Playlist *AudioPlayer_ReturnPlaylistFromWebstream(const char *_webUrl) { + return new WebstreamPlaylist(_webUrl); } // Adds new control-command to control-queue @@ -1156,35 +1150,6 @@ void AudioPlayer_TrackControlToQueueSender(const uint8_t trackCommand) { xQueueSend(gTrackControlQueue, &trackCommand, 0); } -// Knuth-Fisher-Yates-algorithm to randomize playlist -void AudioPlayer_RandomizePlaylist(char **str, const uint32_t count) { - if (!count) { - return; - } - - uint32_t i, r; - char *swap = NULL; - uint32_t max = count - 1; - - for (i = 0; i < count; i++) { - r = (max > 0) ? rand() % max : 0; - swap = *(str + max); - *(str + max) = *(str + r); - *(str + r) = swap; - max--; - } -} - -// Helper to sort playlist alphabetically -static int AudioPlayer_ArrSortHelper(const void *a, const void *b) { - return strcmp(*(const char **) a, *(const char **) b); -} - -// Sort playlist alphabetically -void AudioPlayer_SortPlaylist(char **arr, int n) { - qsort(arr, n, sizeof(const char *), AudioPlayer_ArrSortHelper); -} - // Clear cover send notification void AudioPlayer_ClearCover(void) { gPlayProperties.coverFilePos = 0; diff --git a/src/AudioPlayer.h b/src/AudioPlayer.h index 88bf2f71..b091e178 100644 --- a/src/AudioPlayer.h +++ b/src/AudioPlayer.h @@ -1,33 +1,34 @@ #pragma once +#include "Playlist.h" + typedef struct { // Bit field - uint8_t playMode : 4; // playMode - char **playlist; // playlist - char title[255]; // current title - bool repeatCurrentTrack : 1; // If current track should be looped - bool repeatPlaylist : 1; // If whole playlist should be looped - uint16_t currentTrackNumber : 9; // Current tracknumber - uint16_t numberOfTracks : 9; // Number of tracks in playlist - unsigned long startAtFilePos; // Offset to start play (in bytes) - double currentRelPos; // Current relative playPosition (in %) - bool sleepAfterCurrentTrack : 1; // If uC should go to sleep after current track - bool sleepAfterPlaylist : 1; // If uC should go to sleep after whole playlist - bool sleepAfter5Tracks : 1; // If uC should go to sleep after 5 tracks - bool saveLastPlayPosition : 1; // If playposition/current track should be saved (for AUDIOBOOK) - char playRfidTag[13]; // ID of RFID-tag that started playlist - bool pausePlay : 1; // If pause is active - bool trackFinished : 1; // If current track is finished - bool playlistFinished : 1; // If whole playlist is finished - uint8_t playUntilTrackNumber : 6; // Number of tracks to play after which uC goes to sleep - uint8_t seekmode : 2; // If seekmode is active and if yes: forward or backwards? - bool newPlayMono : 1; // true if mono; false if stereo (helper) - bool currentPlayMono : 1; // true if mono; false if stereo - bool isWebstream : 1; // Indicates if track currenty played is a webstream - uint8_t tellMode : 2; // Tell mode for text to speech announcments - bool currentSpeechActive : 1; // If speech-play is active - bool lastSpeechActive : 1; // If speech-play was active - size_t coverFilePos; // current cover file position - size_t coverFileSize; // current cover file size + uint8_t playMode: 4; // playMode + char title[255]; // current title + bool repeatCurrentTrack: 1; // If current track should be looped + bool repeatPlaylist: 1; // If whole playlist should be looped + uint16_t currentTrackNumber: 9; // Current tracknumber + uint16_t numberOfTracks: 9; // Number of tracks in playlist + unsigned long startAtFilePos; // Offset to start play (in bytes) + double currentRelPos; // Current relative playPosition (in %) + bool sleepAfterCurrentTrack: 1; // If uC should go to sleep after current track + bool sleepAfterPlaylist: 1; // If uC should go to sleep after whole playlist + bool sleepAfter5Tracks: 1; // If uC should go to sleep after 5 tracks + bool saveLastPlayPosition: 1; // If playposition/current track should be saved (for AUDIOBOOK) + char playRfidTag[13]; // ID of RFID-tag that started playlist + bool pausePlay: 1; // If pause is active + bool trackFinished: 1; // If current track is finished + bool playlistFinished: 1; // If whole playlist is finished + uint8_t playUntilTrackNumber: 6; // Number of tracks to play after which uC goes to sleep + uint8_t seekmode: 2; // If seekmode is active and if yes: forward or backwards? + bool newPlayMono: 1; // true if mono; false if stereo (helper) + bool currentPlayMono: 1; // true if mono; false if stereo + bool isWebstream: 1; // Indicates if track currenty played is a webstream + uint8_t tellMode: 2; // Tell mode for text to speech announcments + bool currentSpeechActive: 1; // If speech-play is active + bool lastSpeechActive: 1; // If speech-play was active + size_t coverFilePos; // current cover file position + size_t coverFileSize; // current cover file size } playProps; extern playProps gPlayProperties; diff --git a/src/Common.h b/src/Common.h index 15881097..3d67e10b 100644 --- a/src/Common.h +++ b/src/Common.h @@ -1,5 +1,7 @@ #pragma once +#include + // FilePathLength #define MAX_FILEPATH_LENTGH 256 @@ -22,6 +24,14 @@ inline bool isNumber(const char *str) { } } +inline const char *getPath(File &f) { + #if ESP_ARDUINO_VERSION_MAJOR >= 2 + return f.path(); + #else + return f.name(); + #endif +} + // Checks if string starts with prefix // Returns true if so inline bool startsWith(const char *str, const char *pre) { diff --git a/src/Playlist.h b/src/Playlist.h new file mode 100644 index 00000000..7df32360 --- /dev/null +++ b/src/Playlist.h @@ -0,0 +1,141 @@ +#pragma once + +#include +#include +#include "cpp.h" + +#if MEM_DEBUG == 1 + #warning Memory access guards are enabled. Disable MEM_DEBUG for production builds +#endif + +using sortFunc = int(*)(const void*,const void*); + +class Playlist { +public: + Playlist() { } + virtual ~Playlist() { } + + virtual size_t size() const = 0; + + virtual bool isValid() const = 0; + + virtual const String getAbsolutePath(size_t idx) const = 0; + + virtual const String getFilename(size_t idx) const = 0; + + static int alphabeticSort(const void *x, const void *y) { + const char *a = static_cast(x); + const char *b = static_cast(y); + + return strcmp(a, b); + } + + virtual void sort(sortFunc func = alphabeticSort) { } + + virtual void randomize() { } + + +protected: + + template + class PsramAllocator { + public: + typedef T value_type; + typedef value_type* pointer; + typedef const value_type* const_pointer; + typedef value_type& reference; + typedef const value_type& const_reference; + typedef std::size_t size_type; + typedef std::ptrdiff_t difference_type; + + public: + template + struct rebind { + typedef PsramAllocator other; + }; + + public: + inline explicit PsramAllocator() {} + inline ~PsramAllocator() {} + inline PsramAllocator(PsramAllocator const&) {} + template + inline explicit PsramAllocator(PsramAllocator const&) {} + + // address + inline pointer address(reference r) { return &r; } + + inline const_pointer address(const_reference r) { return &r; } + + // memory allocation + inline pointer allocate(size_type cnt, typename std::allocator::const_pointer = 0) { + T *ptr = nullptr; + if(psramFound()) { + ptr = (T*)ps_malloc(cnt * sizeof(T)); + } else { + ptr = (T*)malloc(cnt * sizeof(T)); + } + return ptr; + } + + inline void deallocate(pointer p, size_type cnt) { + free(p); + } + + // size + inline size_type max_size() const { + return std::numeric_limits::max() / sizeof(T); + } + + // construction/destruction + inline void construct(pointer p, const T& t) { + new(p) T(t); + } + + inline void destroy(pointer p) { + p->~T(); + } + + inline bool operator==(PsramAllocator const& a) { return this == &a; } + inline bool operator!=(PsramAllocator const& a) { return !operator==(a); } + }; + + using pstring = std::basic_string, PsramAllocator>; + + virtual void destroy() { } + + static constexpr auto audioFileSufix = std::to_array({ + ".mp3", + ".aac", + ".m3u", + ".m4a", + ".wav", + ".flac", + ".aac" + }); + + // Check if file-type is correct + bool fileValid(const String _fileItem) { + if(!_fileItem) + return false; + + // check for http address + if(_fileItem.startsWith("http://") || _fileItem.startsWith("https://")) { + return true; + } + + // Ignore hidden files starting with a '.' + // lastIndex is -1 if '/' is not found --> first index will be 0 + int fileNameIndex = _fileItem.lastIndexOf('/') + 1; + if(_fileItem[fileNameIndex] == '.') { + return false; + } + + for(const auto e:audioFileSufix) { + if(_fileItem.endsWith(e)) { + return true; + } + } + return false; + } + +}; diff --git a/src/SdCard.cpp b/src/SdCard.cpp index e29746c0..9f3c6c9f 100644 --- a/src/SdCard.cpp +++ b/src/SdCard.cpp @@ -9,6 +9,10 @@ #include "MemX.h" #include "System.h" +#include "playlists/CacheFilePlaylist.hpp" +#include "playlists/FolderPlaylist.hpp" +#include "playlists/WebstreamPlaylist.hpp" + #ifdef SD_MMC_1BIT_MODE fs::FS gFSystem = (fs::FS) SD_MMC; #else @@ -143,270 +147,195 @@ bool fileValid(const char *_fileItem) { } // Takes a directory as input and returns a random subdirectory from it -char *SdCard_pickRandomSubdirectory(char *_directory) { +std::optional SdCard_pickRandomSubdirectory(const char *_directory) { uint32_t listStartTimestamp = millis(); // Look if file/folder requested really exists. If not => break. File directory = gFSystem.open(_directory); if (!directory) { - Log_Printf(LOGLEVEL_ERROR, dirOrFileDoesNotExist, _directory); - return NULL; + // does not exists + Log_Println(dirOrFileDoesNotExist, LOGLEVEL_ERROR); + return std::nullopt; } Log_Printf(LOGLEVEL_NOTICE, tryToPickRandomDir, _directory); - static uint8_t allocCount = 1; - uint16_t allocSize = psramInit() ? 65535 : 1024; // There's enough PSRAM. So we don't have to care... - uint16_t directoryCount = 0; - char *buffer = _directory; // input char* is reused as it's content no longer needed - char *subdirectoryList = (char *) x_calloc(allocSize, sizeof(char)); - - if (subdirectoryList == NULL) { - Log_Println(unableToAllocateMemForLinearPlaylist, LOGLEVEL_ERROR); - System_IndicateError(); - return NULL; - } - - // Create linear list of subdirectories with #-delimiters - while (true) { - bool isDir = false; - String MyfileName = directory.getNextFileName(&isDir); - if (MyfileName == "") { - break; - } - if (!isDir) { - continue; - } else { - strncpy(buffer, MyfileName.c_str(), 255); - // Log_Printf(LOGLEVEL_INFO, nameOfFileFound, buffer); - if ((strlen(subdirectoryList) + strlen(buffer) + 2) >= allocCount * allocSize) { - char *tmp = (char *) realloc(subdirectoryList, ++allocCount * allocSize); - Log_Println(reallocCalled, LOGLEVEL_DEBUG); - if (tmp == NULL) { - Log_Println(unableToAllocateMemForLinearPlaylist, LOGLEVEL_ERROR); - System_IndicateError(); - free(subdirectoryList); - return NULL; - } - subdirectoryList = tmp; + size_t dirCount = 0; + while(true) { + bool isDir; + #if defined(HAS_FILEEXPLORER_SPEEDUP) || (ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(2, 0, 8)) + const String path = directory.getNextFileName(&isDir); + if(path.isEmpty()) { + break; + } + #else + File fileItem = directory.openNextFile(); + if(!fileItem) { + break; } - strcat(subdirectoryList, stringDelimiter); - strcat(subdirectoryList, buffer); - directoryCount++; + isDir = fileItem.isDirectory(); + #endif + if(isDir) { + dirCount++; } } - strcat(subdirectoryList, stringDelimiter); - - if (!directoryCount) { - free(subdirectoryList); - return NULL; + if(!dirCount) { + // no paths in folder + return std::nullopt; } - uint16_t randomNumber = random(directoryCount) + 1; // Create random-number with max = subdirectory-count - uint16_t delimiterFoundCount = 0; - uint32_t a = 0; - uint8_t b = 0; - - // Walk through subdirectory-array and extract randomized subdirectory - while (subdirectoryList[a] != '\0') { - if (subdirectoryList[a] == '#') { - delimiterFoundCount++; - } else { - if (delimiterFoundCount == randomNumber) { // Pick subdirectory of linear char* according to random number - buffer[b++] = subdirectoryList[a]; + const uint32_t randomNumber = esp_random() % dirCount; + String path; + for(size_t i=0;i= ESP_ARDUINO_VERSION_VAL(2, 0, 8)) + path = directory.getNextFileName(&isDir); + #else + File fileItem = directory.openNextFile(); + if(!fileItem) { + path = ""; + } else { + path = getPath(fileItem); + isDir = fileItem.isDirectory(); } + #endif + if(path.isEmpty()) { + // we reached the end before finding the correct dir! + return std::nullopt; } - if (delimiterFoundCount > randomNumber || (b == 254)) { // It's over when next delimiter is found or buffer is full - buffer[b] = '\0'; - free(subdirectoryList); - Log_Printf(LOGLEVEL_NOTICE, pickedRandomDir, _directory); - return buffer; // Full path of random subdirectory + if(isDir) { + i++; } - a++; } - - free(subdirectoryList); + Log_Printf(LOGLEVEL_NOTICE, pickedRandomDir, path.c_str()); Log_Printf(LOGLEVEL_DEBUG, "pick random directory from SD-card finished: %lu ms", (millis() - listStartTimestamp)); - return NULL; + return path; +} + +static std::optional SdCard_ParseM3UPlaylist(File f, bool forceExtended = false) { + const String line = f.readStringUntil('\n'); + bool extended = line.startsWith("#EXTM3U") || forceExtended; + FolderPlaylist *playlist = new FolderPlaylist(); + + if(extended) { + // extended m3u file format + // ignore all lines starting with '#' + + while(f.available()) { + String line = f.readStringUntil('\n'); + if(!line.startsWith("#")){ + // this something we have to save + line.trim(); + if(!playlist->push_back(line)) { + delete playlist; + return std::nullopt; + } + } + } + // resize memory to fit our count + playlist->compress(); + return playlist; + } + + // normal m3u is just a bunch of filenames, 1 / line + f.seek(0); + while(f.available()) { + String line = f.readStringUntil('\n'); + line.trim(); + if(!playlist->push_back(line)) { + delete playlist; + return std::nullopt; + } + } + // resize memory to fit our count + playlist->compress(); + return playlist; } /* Puts SD-file(s) or directory into a playlist First element of array always contains the number of payload-items. */ -char **SdCard_ReturnPlaylist(const char *fileName, const uint32_t _playMode) { - static char **files; - char *serializedPlaylist = NULL; - bool enablePlaylistFromM3u = false; - // uint32_t listStartTimestamp = millis(); - - if (files != NULL) { // If **ptr already exists, de-allocate its memory - Log_Printf(LOGLEVEL_DEBUG, releaseMemoryOfOldPlaylist, ESP.getFreeHeap()); - freeMultiCharArray(files, strtoul(files[0], NULL, 10) + 1); - Log_Printf(LOGLEVEL_DEBUG, freeMemoryAfterFree, ESP.getFreeHeap()); - } +std::optional SdCard_ReturnPlaylist(const char *fileName, const uint32_t _playMode) { + bool rebuildCacheFile = false; // Look if file/folder requested really exists. If not => break. File fileOrDirectory = gFSystem.open(fileName); if (!fileOrDirectory) { - Log_Printf(LOGLEVEL_ERROR, dirOrFileDoesNotExist, fileName); - return nullptr; + Log_Println(dirOrFileDoesNotExist, LOGLEVEL_ERROR); + return std::nullopt; } - Log_Printf(LOGLEVEL_DEBUG, freeMemory, ESP.getFreeHeap()); - - // Parse m3u-playlist and create linear-playlist out of it - if (_playMode == LOCAL_M3U) { - if (fileOrDirectory && !fileOrDirectory.isDirectory() && fileOrDirectory.size() > 0) { - enablePlaylistFromM3u = true; - uint16_t allocCount = 1; - uint16_t allocSize = psramInit() ? 65535 : 1024; // There's enough PSRAM. So we don't have to care... - - serializedPlaylist = (char *) x_calloc(allocSize, sizeof(char)); - if (serializedPlaylist == NULL) { - Log_Println(unableToAllocateMemForLinearPlaylist, LOGLEVEL_ERROR); - System_IndicateError(); - return nullptr; - } - char buf; - char lastBuf = '#'; - uint32_t fPos = 1; - - serializedPlaylist[0] = '#'; - while (fileOrDirectory.available() > 0) { - buf = fileOrDirectory.read(); - if (buf == '#') { - // skip M3U comment lines starting with # - fileOrDirectory.readStringUntil('\n'); - continue; - } - if (fPos + 1 >= allocCount * allocSize) { - char *tmp = (char *) realloc(serializedPlaylist, ++allocCount * allocSize); - Log_Println(reallocCalled, LOGLEVEL_DEBUG); - if (tmp == NULL) { - Log_Println(unableToAllocateMemForLinearPlaylist, LOGLEVEL_ERROR); - System_IndicateError(); - free(serializedPlaylist); - return nullptr; - } - serializedPlaylist = tmp; - } - - if (buf != '\n' && buf != '\r') { - serializedPlaylist[fPos++] = buf; - lastBuf = buf; - } else { - if (lastBuf != '#') { // Strip empty lines from m3u - serializedPlaylist[fPos++] = '#'; - lastBuf = '#'; - } + // Create linear playlist of caching-file + #ifdef CACHED_PLAYLIST_ENABLE + // Build absolute path of cacheFile + auto cacheFilePath = CacheFilePlaylist::getCachefilePath(fileOrDirectory); + + // Decide if to use cacheFile. It needs to exist first check if cacheFile (already) exists + // ...and playmode has to be != random/single (as random along with caching doesn't make sense at all) + if (cacheFilePath && gFSystem.exists(cacheFilePath.value()) && _playMode != SINGLE_TRACK && _playMode != SINGLE_TRACK_LOOP) { + // Read linear playlist (csv with #-delimiter) from cachefile (faster!) + + File cacheFile = gFSystem.open(cacheFilePath.value()); + if (cacheFile && cacheFile.size()) { + CacheFilePlaylist *cachePlaylist = new CacheFilePlaylist(); + + bool success = cachePlaylist->deserialize(cacheFile); + if(success) { + // always first assume a current playlist format + return cachePlaylist; } + // we had some error reading the cache file, wait for the other to rebuild it + // we do not need the class anymore, so destroy it + delete cachePlaylist; } - if (serializedPlaylist[fPos - 1] == '#') { // Remove trailing delimiter if set - serializedPlaylist[fPos - 1] = '\0'; - } - } else { - return nullptr; - } - } - - // Don't read from m3u-file. Means: read filenames from SD and make playlist of it - if (!enablePlaylistFromM3u) { - Log_Println(playlistGen, LOGLEVEL_NOTICE); - char fileNameBuf[255]; - // File-mode - if (!fileOrDirectory.isDirectory()) { - files = (char **) x_malloc(sizeof(char *) * 2); - if (files == nullptr) { - Log_Println(unableToAllocateMemForPlaylist, LOGLEVEL_ERROR); - System_IndicateError(); - return nullptr; - } - Log_Println(fileModeDetected, LOGLEVEL_INFO); - strncpy(fileNameBuf, fileOrDirectory.path(), sizeof(fileNameBuf) / sizeof(fileNameBuf[0])); - if (fileValid(fileNameBuf)) { - files[1] = x_strdup(fileNameBuf); - } - files[0] = x_strdup("1"); // Number of files is always 1 in file-mode - - return &(files[1]); + // we failed to read the cache file... set the flag to rebuild it + rebuildCacheFile = true; } + #endif - // Directory-mode (linear-playlist) - uint16_t allocCount = 1; - uint16_t allocSize = 4096; - if (psramInit()) { - allocSize = 65535; // There's enough PSRAM. So we don't have to care... - } + Log_Printf(LOGLEVEL_DEBUG, freeMemory, ESP.getFreeHeap()); - serializedPlaylist = (char *) x_calloc(allocSize, sizeof(char)); - while (true) { - bool isDir = false; - String MyfileName = fileOrDirectory.getNextFileName(&isDir); - if (MyfileName == "") { - break; - } - if (isDir) { - continue; - } else { - strncpy(fileNameBuf, MyfileName.c_str(), sizeof(fileNameBuf) / sizeof(fileNameBuf[0])); - // Don't support filenames that start with "." and only allow .mp3 and other supported audio file formats - if (fileValid(fileNameBuf)) { - // Log_Printf(LOGLEVEL_INFO, "%s: %s", nameOfFileFound), fileNameBuf); - if ((strlen(serializedPlaylist) + strlen(fileNameBuf) + 2) >= allocCount * allocSize) { - char *tmp = (char *) realloc(serializedPlaylist, ++allocCount * allocSize); - Log_Println(reallocCalled, LOGLEVEL_DEBUG); - if (tmp == nullptr) { - Log_Println(unableToAllocateMemForLinearPlaylist, LOGLEVEL_ERROR); - System_IndicateError(); - free(serializedPlaylist); - return nullptr; - } - serializedPlaylist = tmp; - } - strcat(serializedPlaylist, stringDelimiter); - strcat(serializedPlaylist, fileNameBuf); - } - } + // Parse m3u-playlist and create linear-playlist out of it + if (_playMode == LOCAL_M3U) { + if (fileOrDirectory && !fileOrDirectory.isDirectory() && fileOrDirectory.size()) { + // create a m3u playlist and parse the file + return SdCard_ParseM3UPlaylist(fileOrDirectory); } + // if we reach here, we failed + return std::nullopt; } - // Get number of elements out of serialized playlist - uint32_t cnt = 0; - for (uint32_t k = 0; k < (strlen(serializedPlaylist)); k++) { - if (serializedPlaylist[k] == '#') { - cnt++; - } - } + // If we reached here, we did not read a cache file nor an m3u file. Means: read filenames from SD and make playlist of it + Log_Println(playlistGenModeUncached, LOGLEVEL_NOTICE); - // Alloc only necessary number of playlist-pointers - files = (char **) x_malloc(sizeof(char *) * (cnt + 1)); - if (files == nullptr) { - Log_Println(unableToAllocateMemForPlaylist, LOGLEVEL_ERROR); - System_IndicateError(); - free(serializedPlaylist); - return nullptr; + // File-mode + if (!fileOrDirectory.isDirectory()) { + Log_Println(fileModeDetected, LOGLEVEL_INFO); + const char *path = getPath(fileOrDirectory); + if (fileValid(path)) { + return new WebstreamPlaylist(path); + } } - // Extract elements out of serialized playlist and copy to playlist - char *token; - token = strtok(serializedPlaylist, stringDelimiter); - uint32_t pos = 1; - while (token != NULL) { - files[pos++] = x_strdup(token); - token = strtok(NULL, stringDelimiter); + // Folder mode + FolderPlaylist *playlist = new FolderPlaylist(); + playlist->createFromFolder(fileOrDirectory); + if (!playlist->isValid()) { + // something went wrong + Log_Println(unableToAllocateMemForLinearPlaylist, LOGLEVEL_ERROR); + delete playlist; + return std::nullopt; } - free(serializedPlaylist); - - files[0] = (char *) x_malloc(sizeof(char) * 5); - - if (files[0] == nullptr) { - Log_Println(unableToAllocateMemForPlaylist, LOGLEVEL_ERROR); - System_IndicateError(); - freeMultiCharArray(files, cnt + 1); - return nullptr; - } - snprintf(files[0], 5, "%u", cnt); - Log_Printf(LOGLEVEL_NOTICE, numberOfValidFiles, cnt); - // Log_Printf(LOGLEVEL_DEBUG, "build playlist from SD-card finished: %lu ms", (millis() - listStartTimestamp)); + #ifdef CACHED_PLAYLIST_ENABLE + if(cacheFilePath && rebuildCacheFile) { + File cacheFile = gFSystem.open(cacheFilePath.value(), FILE_WRITE); + if(cacheFile) { + CacheFilePlaylist::serialize(cacheFile, *playlist); + } + cacheFile.close(); + } + #endif - return &(files[1]); // return ptr+1 (starting at 1st payload-item); ptr+0 contains number of items + // we are finished + Log_Printf(LOGLEVEL_NOTICE, numberOfValidFiles, playlist->size()); + return playlist; } diff --git a/src/SdCard.h b/src/SdCard.h index 50803556..b7821af2 100644 --- a/src/SdCard.h +++ b/src/SdCard.h @@ -1,5 +1,6 @@ #pragma once #include "settings.h" +#include "Playlist.h" #ifdef SD_MMC_1BIT_MODE #include "SD_MMC.h" #else @@ -14,5 +15,5 @@ sdcard_type_t SdCard_GetType(void); uint64_t SdCard_GetSize(); uint64_t SdCard_GetFreeSize(); void SdCard_PrintInfo(); -char **SdCard_ReturnPlaylist(const char *fileName, const uint32_t _playMode); -char *SdCard_pickRandomSubdirectory(char *_directory); +std::optional SdCard_ReturnPlaylist(const char *fileName, const uint32_t _playMode); +std::optional SdCard_pickRandomSubdirectory(const char *_directory); diff --git a/src/Web.cpp b/src/Web.cpp index 76b3dc3b..d56e61be 100644 --- a/src/Web.cpp +++ b/src/Web.cpp @@ -1879,7 +1879,7 @@ static void handleCoverImageRequest(AsyncWebServerRequest *request) { } return; } - char *coverFileName = *(gPlayProperties.playlist + gPlayProperties.currentTrackNumber); + const char *coverFileName = gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(); Log_Println(coverFileName, LOGLEVEL_DEBUG); File coverFile = gFSystem.open(coverFileName, FILE_READ); diff --git a/src/cpp.h b/src/cpp.h new file mode 100644 index 00000000..52bec646 --- /dev/null +++ b/src/cpp.h @@ -0,0 +1,29 @@ +#pragma once + + +// check for c++20 features +#if __cplusplus < 202002L + +#include +#include + +namespace std { + +namespace detail { + +template +constexpr std::array, N> + to_array_impl(T (&&a)[N], std::index_sequence) { + return {{std::move(a[I])...}}; +} + +} + +template +constexpr std::array, N> to_array(T (&&a)[N]) { + return detail::to_array_impl(std::move(a), std::make_index_sequence{}); +} + +} // namespace std + +#endif diff --git a/src/playlists/CacheFilePlaylist.hpp b/src/playlists/CacheFilePlaylist.hpp new file mode 100644 index 00000000..ca21d0d9 --- /dev/null +++ b/src/playlists/CacheFilePlaylist.hpp @@ -0,0 +1,278 @@ +#pragma once + +#include +#include +#include +#include + +#include "../Playlist.h" +#include "FolderPlaylist.hpp" + +class CacheFilePlaylist : public FolderPlaylist { +public: + CacheFilePlaylist(char divider = '/') : FolderPlaylist(divider), headerFlags(Flags()), headerValid(false) { } + CacheFilePlaylist(File &cacheFile, char divider = '/') : FolderPlaylist(divider), headerFlags(Flags()), headerValid(false) { + deserialize(cacheFile); + } + virtual ~CacheFilePlaylist() { + this->destroy(); + } + + static bool serialize(File &target, const FolderPlaylist &list) { + // write the header into the file + // header is big endian + BinaryCacheHeader header; + size_t ret; + + // first is the magic marker + header.magic = magic; + ret = write(target, magic); + + // the header version & flags + header.version = version; + ret += write(target, version); + + // update flags + Flags flags; + flags.relative = list.isRelative(); + + header.flags = flags; + ret += write(target, flags); + + // write the number of entries and the crc (not implemented yet) + header.count = list.size(); + ret += write(target, header.count); + header.crc = crcBase; + header.sep = separator; + ret += write(target, calcCRC(header)); + + ret += target.write(header.sep); + + if(ret != headerSize) { + #ifdef MEM_DEBUG + assert(ret != headerSize); + #endif + return false; + } + + return writeEntries(target, list); + } + + static std::optional getCachefilePath(File &dir) { + if(!dir.isDirectory()) + return {}; + + const char *path = getPath(dir); + return String(path) + "/" + playlistCacheFile; + } + + bool deserialize(File &cache) { + + if(!checkHeader(cache)) { + return false; + } + + // destroy old data, if present + if(isValid()) { + clear(); + } + + deserializeHeader(cache); + + // everything was ok read the files + return readEntries(cache); + } + + virtual bool isValid() const override { + return headerValid && FolderPlaylist::isValid(); + } + + void clear() { + destroy(); + headerValid = false; + headerFlags = Flags(); + headerCount = 0; + } + +protected: + // bitwise flags for future use + struct Flags { + bool relative; + + Flags(): relative(false) {} + + operator uint16_t() const { + // this function casts the flags into a 16bit wide bit array + // f.e. + // uint16_t flags = 0x00; + // flags |= (flag << 0); + // flags |= (otherFlag << 1); + // return flags; + uint16_t bitfield = 0x00; + + bitfield |= (relative << 0); + return bitfield; + } + + Flags &operator=(const uint16_t binary) { + // here we get a bitfield and break it down into our internal variables + // f.e. + // flag = binary & _BV(0); + // otherFlag = binary & _BV(1); + + relative = binary & _BV(0); + return *this; + } + }; + + struct BinaryCacheHeader + { + uint16_t magic; + uint16_t version; + uint16_t flags; + uint32_t count; + uint32_t crc; + uint8_t sep; + }; + static constexpr uint16_t magic = 0x4346; //< Magic header "CF" + static constexpr uint16_t version = 1; //< Current cache file version, if header or enconding changes, this has to be incremented + static constexpr uint32_t crcBase = 0x00; //< starting value of the crc calculation + static constexpr size_t headerSize = 15; //< the expected size of the header: magic(2) + version(2) + flags(2) + count(4) + crc(4) + separator(1) + + Flags headerFlags; //< A 16bit bitfield of flags + bool headerValid; + size_t headerCount; + + static constexpr char separator = '#'; //< separator for all entries + + static int checkHeader(File &cache) { + int ret = 1; + // read the header from the file + BinaryCacheHeader header; + + header.magic = read16(cache); + header.version = read16(cache); + + // first checkpoint + if(header.magic != magic || header.version != version) { + // header did not match, bail out + ret = -1; + } + + // read the flags and the count + header.flags = read16(cache); + header.count = read32(cache); + + // second checkpoint, crc and separator + header.crc = crcBase; + uint32_t crc = read32(cache); + header.sep = cache.read(); + if((ret > 0) && (calcCRC(header) != crc || header.sep != separator)) { + // crc missmatch, bail out + ret = -2; + } + + cache.seek(0); + return ret; + } + + bool deserializeHeader(File &cache) { + headerValid = false; + if(checkHeader(cache) < 1) { + return false; + } + // header is valid, read on + headerValid = true; + + // ignore magic & version + cache.seek(sizeof(BinaryCacheHeader::magic) + sizeof(BinaryCacheHeader::version)); + + // read the flags and the count + headerFlags = read16(cache); + headerCount = read32(cache); + + // reserve the memory + files.reserve(headerCount); + + cache.seek(sizeof(BinaryCacheHeader::crc) + sizeof(BinaryCacheHeader::sep), SeekMode::SeekCur); + return true; + } + + // helper function to write 16 bit in big endian + static size_t write(File &f, uint16_t value) { + size_t ret; + + ret = f.write(value >> 8); + ret += f.write(value); + return ret; + } + + // helper function to write 32 bit in big endian + static size_t write(File &f, uint32_t value) { + size_t ret; + + ret = write(f, uint16_t(value >> 16)); + ret += write(f, uint16_t(value)); + return ret; + } + + // helper fuction to read 16 bit in big endian + static uint16_t read16(File &f) { + return (f.read() << 8) | f.read(); + } + + // helper fuction to read 32 bit in big endian + static uint32_t read32(File &f) { + return (read16(f) << 16) | read16(f); + } + + static uint32_t calcCRC(const BinaryCacheHeader &header) { + // add all header fields individually since BinaryCacheHeader is not packed + uint32_t ret = crc32_le(0, reinterpret_cast(&header.magic), sizeof(header.magic)); + ret = crc32_le(ret, reinterpret_cast(&header.version), sizeof(header.version)); + ret = crc32_le(ret, reinterpret_cast(&header.flags), sizeof(header.flags)); + ret = crc32_le(ret, reinterpret_cast(&header.count), sizeof(header.count)); + ret = crc32_le(ret, reinterpret_cast(&header.crc), sizeof(header.crc)); + ret = crc32_le(ret, reinterpret_cast(&header.sep), sizeof(header.sep)); + return ret; + } + + bool writeEntries(File &f) const { + return writeEntries(f, *this); + } + + static bool writeEntries(File &f, const FolderPlaylist &list) { + // if flag is set, use relative path + if(list.isRelative()) { + f.write(reinterpret_cast(list.getBase()), strlen(list.getBase())); + f.write(separator); + } + + // write all entries with the separator to the file + for(size_t i=0;i(path.c_str()), path.length()) != path.length()) { + return false; + } + f.write(separator); + } + return true; + } + + bool readEntries(File &f) { + // if flag is set, use relative path + if(headerFlags.relative) { + const String basePath = f.readStringUntil(separator); + this->setBase(basePath); + } + + while(f.available()) { + const String path = f.readStringUntil(separator); + if(!this->push_back(path)){ + return false; + } + } + + return true; + } +}; diff --git a/src/playlists/FolderPlaylist.hpp b/src/playlists/FolderPlaylist.hpp new file mode 100644 index 00000000..47225542 --- /dev/null +++ b/src/playlists/FolderPlaylist.hpp @@ -0,0 +1,246 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "../Playlist.h" + +class FolderPlaylist : public Playlist { +protected: + pstring base; + std::vector> files; + char divider; + +public: + FolderPlaylist(size_t _capacity = 64, char _divider = '/') + : base(pstring()), files(std::vector>(_capacity)), divider(_divider) { } + FolderPlaylist(File &folder, size_t _capacity = 64, char _divider = '/') : FolderPlaylist(_capacity, divider) { + createFromFolder(folder); + } + + virtual ~FolderPlaylist() { + destroy(); + } + + bool createFromFolder(File &folder) { + // This is not a folder, so bail out + if(!folder || !folder.isDirectory()){ + return false; + } + + // clean up any previously used memory + clear(); + + // since we are enumerating, we don't have to think about absolute files with different bases + base = getPath(folder); + + // reserve a sane amout of memory + files.reserve(64); + + // enumerate all files in the folder + while(true) { + bool isDir; + String path; + #if defined(HAS_FILEEXPLORER_SPEEDUP) || (ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(2, 0, 8)) + path = folder.getNextFileName(&isDir); + #else + File f = folder.openNextFile(); + if(!f){ + path=""; + }else{ + path = getPath(f); + isDir = f.isDirectory(); + } + #endif + if(path.isEmpty()) { + break; + } + if(isDir) { + continue; + } + + if(fileValid(path)) { + // push this file into the array + bool success = push_back(path); + if(!success) { + return false; + } + } + } + // resize memory to fit our count + files.shrink_to_fit(); + + return true; + } + + void setBase(const char *_base) { + base = _base; + } + + void setBase(const String _base) { + base = _base.c_str(); + } + + const char *getBase() const { + return base.c_str(); + } + + bool isRelative() const { + return base.length(); + } + + bool push_back(const char *path) { + log_n("path: %s", path); + if(!fileValid(path)) { + return false; + } + + // here we check if we have to cut up the path (currently it's only a crude check for absolute paths) + if(isRelative() && path[0] == '/') { + // we are in relative mode and got an absolute path, check if the path begins with our base + // Also check if the path is so short, that there is no space for a filename in it + if( (strncmp(path, base.c_str(), base.length()) != 0) || (strlen(path) < (base.length() + strlen("/.abc")))) { + // we refuse files other than our base + return false; + } + path = path + base.length(); // modify pointer to the end of the path + } + + files.push_back(path); + return true; + } + + bool push_back(const String path) { + return push_back(path.c_str()); + } + + void compress() { + files.shrink_to_fit(); + } + + void clear() { + destroy(); + init(); + } + + void setDivider(char _divider) { divider = _divider; } + bool getDivider() const { return divider; } + + virtual size_t size() const override { return files.size(); }; + + virtual bool isValid() const override { return files.size(); } + + virtual const String getAbsolutePath(size_t idx) const override { + #if MEM_DEBUG == 1 + assert(idx < files.size()); + #endif + if(isRelative()) { + // we are in relative mode + return String(base.c_str()) + divider + files[idx].c_str(); + } + return String(files[idx].c_str()); + }; + + virtual const String getFilename(size_t idx) const override { + #if MEM_DEBUG == 1 + assert(idx < files.size()); + #endif + if(isRelative()) { + return String(files[idx].c_str()); + } + pstring path = files[idx]; + return String(path.substr(path.find_last_of("/") + 1).c_str()); + }; + + virtual void sort(sortFunc func = alphabeticSort) override { + std::sort(files.begin(), files.end()); + } + + virtual void randomize() override { + if(files.size() < 2) { + // we can not randomize less than 2 entries + return; + } + + // randomize using the "normal" random engine and shuffle + std::default_random_engine rnd(millis()); + std::shuffle(files.begin(), files.end(), rnd); + } + + class Iterator { + public: + using iterator_category = std::forward_iterator_tag; // could be increased to random_access_iterator_tag + using difference_type = std::ptrdiff_t; + using value_type = pstring; + using pointer = value_type*; + using reference = value_type&; + + class ArrowHelper { + value_type value; + public: + ArrowHelper(value_type _str) : value(_str) {} + pointer operator->() { + return &value; + } + }; + + __attribute__((always_inline)) inline const ArrowHelper operator->() const { + return ArrowHelper(operator*()); + } + + __attribute__((always_inline)) inline const value_type operator*() const { + return m_folder->base + m_folder->divider + *m_ptr; + } + + // Constructor + __attribute__((always_inline)) inline Iterator(FolderPlaylist *folder, pointer ptr) : m_folder(folder), m_ptr(ptr) {} + + // Copy Constructor & assignment + __attribute__((always_inline)) inline Iterator(const Iterator &rhs) : m_folder(rhs.m_folder), m_ptr(rhs.m_ptr) {} + __attribute__((always_inline)) inline Iterator &operator=(const Iterator &rhs) = default; + + // Pointer increment + __attribute__((always_inline)) inline Iterator &operator++() { + m_ptr++; + return *this; + } + __attribute__((always_inline)) inline Iterator &operator++(int) { + Iterator tmp(*this); + m_ptr++; + return tmp; + } + + // boolean operators + __attribute__((always_inline)) inline operator bool() const { + return (m_ptr); + } + __attribute__((always_inline)) inline bool operator==(const Iterator &rhs) { + return (m_ptr == rhs.m_ptr) && (m_folder == rhs.m_folder); + } + __attribute__((always_inline)) inline bool operator!=(const Iterator &rhs) { + return !operator==(rhs); + } + + protected: + const FolderPlaylist *m_folder; + pointer m_ptr; + }; + + Iterator cbegin() { return Iterator(this, files.data()); } + Iterator cend() { return Iterator(this, (files.data() + files.size())); } + +protected: + + virtual void destroy() override { + files.clear(); + base.clear(); + } + + void init() { + divider = '/'; + } +}; diff --git a/src/playlists/WebstreamPlaylist.hpp b/src/playlists/WebstreamPlaylist.hpp new file mode 100644 index 00000000..4a24b2f4 --- /dev/null +++ b/src/playlists/WebstreamPlaylist.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include "../Playlist.h" + +class WebstreamPlaylist : public Playlist { +protected: + pstring url; + +public: + WebstreamPlaylist(const char *_url) : url(_url) { } + WebstreamPlaylist() : url(nullptr) { } + virtual ~WebstreamPlaylist() override { + }; + + bool setUrl(const char *_url) { + if(fileValid(_url)) { + url = _url; + return true; + } + return false; + } + + virtual size_t size() const override { return (url.length()) ? 1 : 0; } + virtual bool isValid() const override { return url.length(); } + virtual const String getAbsolutePath(size_t idx = 0) const override { return String(url.c_str()); }; + virtual const String getFilename(size_t idx = 0) const override { return String(url.c_str()); }; + +}; From 43f6296648540b4258ba5011d68f2b1b6f6cfb02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laszlo=20Heged=C3=BCs?= Date: Sun, 30 Apr 2023 12:50:36 +0200 Subject: [PATCH 2/8] Rework fileValid to check for lower case --- src/Playlist.h | 73 +++++++++++++++++++++++++++++++------------------- src/SdCard.cpp | 23 +--------------- 2 files changed, 46 insertions(+), 50 deletions(-) diff --git a/src/Playlist.h b/src/Playlist.h index 7df32360..5358804f 100644 --- a/src/Playlist.h +++ b/src/Playlist.h @@ -2,6 +2,7 @@ #include #include +#include #include "cpp.h" #if MEM_DEBUG == 1 @@ -34,6 +35,44 @@ class Playlist { virtual void randomize() { } + // Check if file-type is correct + static bool fileValid(const String _fileItem) { + constexpr size_t maxExtLen = strlen(*std::max_element(audioFileSufix.begin(), audioFileSufix.end(), [](const char *a, const char *b){ + return strlen(a) < strlen(b); + })); + + if(!_fileItem) + return false; + + // check for http address + if(_fileItem.startsWith("http://") || _fileItem.startsWith("https://")) { + return true; + } + + // Ignore hidden files starting with a '.' + // lastIndex is -1 if '/' is not found --> first index will be 0 + int fileNameIndex = _fileItem.lastIndexOf('/') + 1; + if(_fileItem[fileNameIndex] == '.') { + return false; + } + + String extBuf; + const size_t extStart = _fileItem.lastIndexOf('.'); + const size_t extLen = _fileItem.length() - extStart; + if(extLen > maxExtLen) { + // we either did not find a . or extension was too long + return false; + } + extBuf = _fileItem.substring(extStart); + extBuf.toLowerCase(); + + for(const auto e:audioFileSufix) { + if(extBuf.equals(e)) { + return true; + } + } + return false; + } protected: @@ -106,36 +145,14 @@ class Playlist { static constexpr auto audioFileSufix = std::to_array({ ".mp3", ".aac", - ".m3u", ".m4a", ".wav", ".flac", - ".aac" + ".aac", + // playlists + ".m3u", + ".m3u8", + ".pls", + ".asx" }); - - // Check if file-type is correct - bool fileValid(const String _fileItem) { - if(!_fileItem) - return false; - - // check for http address - if(_fileItem.startsWith("http://") || _fileItem.startsWith("https://")) { - return true; - } - - // Ignore hidden files starting with a '.' - // lastIndex is -1 if '/' is not found --> first index will be 0 - int fileNameIndex = _fileItem.lastIndexOf('/') + 1; - if(_fileItem[fileNameIndex] == '.') { - return false; - } - - for(const auto e:audioFileSufix) { - if(_fileItem.endsWith(e)) { - return true; - } - } - return false; - } - }; diff --git a/src/SdCard.cpp b/src/SdCard.cpp index 9f3c6c9f..ff99e5ac 100644 --- a/src/SdCard.cpp +++ b/src/SdCard.cpp @@ -125,27 +125,6 @@ void SdCard_PrintInfo() { Log_Printf(LOGLEVEL_NOTICE, sdInfo, cardSize, freeSize); } -// Check if file-type is correct -bool fileValid(const char *_fileItem) { - // make file extension to lowercase (compare case insenstive) - char *lFileItem; - lFileItem = x_strdup(_fileItem); - if (lFileItem == NULL) { - return false; - } - lFileItem = strlwr(lFileItem); - const char ch = '/'; - char *subst; - subst = strrchr(lFileItem, ch); // Don't use files that start with . - bool isValid = (!startsWith(subst, (char *) "/.")) && ( - // audio file formats - endsWith(lFileItem, ".mp3") || endsWith(lFileItem, ".aac") || endsWith(lFileItem, ".m4a") || endsWith(lFileItem, ".wav") || endsWith(lFileItem, ".flac") || endsWith(lFileItem, ".ogg") || endsWith(lFileItem, ".oga") || endsWith(lFileItem, ".opus") || - // playlist file formats - endsWith(lFileItem, ".m3u") || endsWith(lFileItem, ".m3u8") || endsWith(lFileItem, ".pls") || endsWith(lFileItem, ".asx")); - free(lFileItem); - return isValid; -} - // Takes a directory as input and returns a random subdirectory from it std::optional SdCard_pickRandomSubdirectory(const char *_directory) { uint32_t listStartTimestamp = millis(); @@ -310,7 +289,7 @@ std::optional SdCard_ReturnPlaylist(const char *fileName, const uint3 if (!fileOrDirectory.isDirectory()) { Log_Println(fileModeDetected, LOGLEVEL_INFO); const char *path = getPath(fileOrDirectory); - if (fileValid(path)) { + if (Playlist::fileValid(path)) { return new WebstreamPlaylist(path); } } From 6806501c83d70d4d09df53b32f673059595d9117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laszlo=20Heged=C3=BCs?= Date: Tue, 2 May 2023 00:04:40 +0200 Subject: [PATCH 3/8] Work on the queue side of the playlist Add function to request a file path from the playlist. Needed since we do not make the playlist itself directly accessible. --- src/AudioPlayer.cpp | 177 +++++++++++++++++++++----------------------- src/AudioPlayer.h | 2 + src/Queues.cpp | 10 +++ src/Queues.h | 30 +++++++- src/SdCard.cpp | 17 ++--- src/SdCard.h | 2 +- src/Web.cpp | 2 +- 7 files changed, 136 insertions(+), 104 deletions(-) diff --git a/src/AudioPlayer.cpp b/src/AudioPlayer.cpp index 9acbaa2b..5de51ca6 100644 --- a/src/AudioPlayer.cpp +++ b/src/AudioPlayer.cpp @@ -31,7 +31,12 @@ playProps gPlayProperties; TaskHandle_t AudioTaskHandle; -// uint32_t cnt123 = 0; +//uint32_t cnt123 = 0; + +// Playlist +static std::unique_ptr playlist; +static bool playlistChanged = false; +static std::mutex playlist_mutex; // Volume static uint8_t AudioPlayer_CurrentVolume = AUDIOPLAYER_VOLUME_INIT; @@ -56,7 +61,7 @@ static uint8_t AudioPlayer_MaxVolumeHeadphone = 11u; // Maximum volume that can static void AudioPlayer_Task(void *parameter); static void AudioPlayer_HeadphoneVolumeManager(void); -static Playlist *AudioPlayer_ReturnPlaylistFromWebstream(const char *_webUrl); +static std::unique_ptr AudioPlayer_ReturnPlaylistFromWebstream(const char *_webUrl); static size_t AudioPlayer_NvsRfidWriteWrapper(const char *_rfidCardId, const char *_track, const uint32_t _playPosition, const uint8_t _playMode, const uint16_t _trackLastPlayed, const uint16_t _numberOfTracks); static void AudioPlayer_ClearCover(void); @@ -242,6 +247,16 @@ void Audio_setTitle(const char *format, ...) { #endif } +const String AudioPlayer_getCurrentTrackPath(size_t track) { + std::lock_guard guard(playlist_mutex); + if(track >= playlist->size()) { + // requested track is larger than the array + return String(); + } + // return the absolute path + return playlist->getAbsolutePath(track); +} + // Set maxVolume depending on headphone-adjustment is enabled and headphone is/is not connected // Enable/disable PA/HP-amps initially void AudioPlayer_SetupVolumeAndAmps(void) { @@ -357,8 +372,7 @@ void AudioPlayer_Task(void *parameter) { } uint8_t currentVolume; - static BaseType_t trackQStatus; - static uint8_t trackCommand = NO_ACTION; + uint8_t trackCommand = NO_ACTION; bool audioReturnCode; AudioPlayer_CurrentTime = 0; AudioPlayer_FileDuration = 0; @@ -399,34 +413,15 @@ void AudioPlayer_Task(void *parameter) { } } - // Update playtime stats every 250 ms - if ((millis() - AudioPlayer_LastPlaytimeStatsTimestamp) > 250) { - AudioPlayer_LastPlaytimeStatsTimestamp = millis(); - // Update current playtime and duration - AudioPlayer_CurrentTime = audio->getAudioCurrentTime(); - AudioPlayer_FileDuration = audio->getAudioFileDuration(); - // Calculate relative position in file (for trackprogress neopixel & web-ui) - if (!gPlayProperties.playlistFinished && !gPlayProperties.isWebstream) { - if (!gPlayProperties.pausePlay && (gPlayProperties.seekmode != SEEK_POS_PERCENT) && (audio->getFileSize() > 0)) { // To progress necessary when paused - gPlayProperties.currentRelPos = ((double) (audio->getFilePos() - audio->inBufferFilled()) / (double) audio->getFileSize()) * 100; - } - } else { - gPlayProperties.currentRelPos = 0; - } - } - - Playlist *tmp; - trackQStatus = xQueueReceive(gTrackQueue, &tmp, 0); - if (trackQStatus == pdPASS || gPlayProperties.trackFinished || trackCommand != NO_ACTION) { - if (trackQStatus == pdPASS) { - // clean up the old playlist - delete gPlayProperties.playlist; - gPlayProperties.playlist = tmp; + if (playlistChanged || gPlayProperties.trackFinished || trackCommand != NO_ACTION) { + std::lock_guard guard(playlist_mutex); + if (playlistChanged) { if (gPlayProperties.pausePlay) { gPlayProperties.pausePlay = false; } audio->stopSong(); + playlistChanged = false; Log_Printf(LOGLEVEL_NOTICE, newPlaylistReceived, gPlayProperties.numberOfTracks); Log_Printf(LOGLEVEL_DEBUG, "Free heap: %u", ESP.getFreeHeap()); @@ -450,7 +445,7 @@ void AudioPlayer_Task(void *parameter) { if (gPlayProperties.saveLastPlayPosition) { // Don't save for AUDIOBOOK_LOOP because not necessary if (gPlayProperties.currentTrackNumber + 1 < gPlayProperties.numberOfTracks) { // Only save if there's another track, otherwise it will be saved at end of playlist anyway - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber + 1, gPlayProperties.numberOfTracks); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber + 1, gPlayProperties.numberOfTracks); } } if (gPlayProperties.sleepAfterCurrentTrack) { // Go to sleep if "sleep after track" was requested @@ -499,7 +494,7 @@ void AudioPlayer_Task(void *parameter) { } if (gPlayProperties.saveLastPlayPosition && !gPlayProperties.pausePlay) { Log_Printf(LOGLEVEL_INFO, trackPausedAtPos, audio->getFilePos(), audio->getFilePos() - audio->inBufferFilled()); - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), audio->getFilePos() - audio->inBufferFilled(), gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), audio->getFilePos() - audio->inBufferFilled(), gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); } gPlayProperties.pausePlay = !gPlayProperties.pausePlay; Web_SendWebsocketData(0, 30); @@ -526,7 +521,7 @@ void AudioPlayer_Task(void *parameter) { gPlayProperties.currentTrackNumber++; } if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); Log_Println(trackStartAudiobook, LOGLEVEL_INFO); } Log_Println(cmndNextTrack, LOGLEVEL_INFO); @@ -574,35 +569,31 @@ void AudioPlayer_Task(void *parameter) { } } - if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); - Log_Println(trackStartAudiobook, LOGLEVEL_INFO); - } + if (gPlayProperties.saveLastPlayPosition) { + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); + Log_Println(trackStartAudiobook, LOGLEVEL_INFO); + } - Log_Println(cmndPrevTrack, LOGLEVEL_INFO); - if (!gPlayProperties.playlistFinished) { + Log_Println(cmndPrevTrack, LOGLEVEL_INFO); + if (!gPlayProperties.playlistFinished) { + audio->stopSong(); + } + } else { + if (gPlayProperties.saveLastPlayPosition) { + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); + } audio->stopSong(); - } - } else { - if (gPlayProperties.playMode == WEBSTREAM) { - Log_Println(trackChangeWebstream, LOGLEVEL_INFO); - System_IndicateError(); - continue; - } - if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); - } - audio->stopSong(); - Led_Indicate(LedIndicatorType::Rewind); - audioReturnCode = audio->connecttoFS(gFSystem, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str()); - // consider track as finished, when audio lib call was not successful - if (!audioReturnCode) { - System_IndicateError(); - gPlayProperties.trackFinished = true; + Led_Indicate(LedIndicatorType::Rewind); + audioReturnCode = audio->connecttoFS(gFSystem, playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str()); + // consider track as finished, when audio lib call was not successful + if (!audioReturnCode) { + System_IndicateError(); + gPlayProperties.trackFinished = true; + continue; + } + Log_Println(trackStart, LOGLEVEL_INFO); continue; } - Log_Println(trackStart, LOGLEVEL_INFO); - continue; } break; case FIRSTTRACK: @@ -613,7 +604,7 @@ void AudioPlayer_Task(void *parameter) { } gPlayProperties.currentTrackNumber = 0; if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); Log_Println(trackStartAudiobook, LOGLEVEL_INFO); } Log_Println(cmndFirstTrack, LOGLEVEL_INFO); @@ -631,7 +622,7 @@ void AudioPlayer_Task(void *parameter) { if (gPlayProperties.currentTrackNumber + 1 < gPlayProperties.numberOfTracks) { gPlayProperties.currentTrackNumber = gPlayProperties.numberOfTracks - 1; if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); Log_Println(trackStartAudiobook, LOGLEVEL_INFO); } Log_Println(cmndLastTrack, LOGLEVEL_INFO); @@ -657,7 +648,7 @@ void AudioPlayer_Task(void *parameter) { if (gPlayProperties.playUntilTrackNumber == gPlayProperties.currentTrackNumber && gPlayProperties.playUntilTrackNumber > 0) { if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 0, gPlayProperties.playMode, 0, gPlayProperties.numberOfTracks); } gPlayProperties.playlistFinished = true; gPlayProperties.playMode = NO_PLAYLIST; @@ -670,7 +661,7 @@ void AudioPlayer_Task(void *parameter) { if (!gPlayProperties.repeatPlaylist) { if (gPlayProperties.saveLastPlayPosition) { // Set back to first track - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(0).c_str(), 0, gPlayProperties.playMode, 0, gPlayProperties.numberOfTracks); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, playlist->getAbsolutePath(0).c_str(), 0, gPlayProperties.playMode, 0, gPlayProperties.numberOfTracks); } gPlayProperties.playlistFinished = true; gPlayProperties.playMode = NO_PLAYLIST; @@ -695,12 +686,12 @@ void AudioPlayer_Task(void *parameter) { Log_Println(repeatPlaylistDueToPlaymode, LOGLEVEL_NOTICE); gPlayProperties.currentTrackNumber = 0; if (gPlayProperties.saveLastPlayPosition) { - AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, gPlayProperties.playlist->getAbsolutePath(0).c_str(), 0, gPlayProperties.playMode, 0, gPlayProperties.numberOfTracks); + AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, playlist->getAbsolutePath(0).c_str(), 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber, gPlayProperties.numberOfTracks); } } } - if (!strncmp("http", gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 4)) { + if (!strncmp("http", playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), 4)) { gPlayProperties.isWebstream = true; } else { gPlayProperties.isWebstream = false; @@ -709,17 +700,17 @@ void AudioPlayer_Task(void *parameter) { audioReturnCode = false; if (gPlayProperties.playMode == WEBSTREAM || (gPlayProperties.playMode == LOCAL_M3U && gPlayProperties.isWebstream)) { // Webstream - audioReturnCode = audio->connecttohost(gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str()); + audioReturnCode = audio->connecttohost(playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str()); gPlayProperties.playlistFinished = false; gTriedToConnectToHost = true; } else if (gPlayProperties.playMode != WEBSTREAM && !gPlayProperties.isWebstream) { // Files from SD - if (!gFSystem.exists(gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber))) { // Check first if file/folder exists - Log_Printf(LOGLEVEL_ERROR, dirOrFileDoesNotExist, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str()); + if (!gFSystem.exists(playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str())) { // Check first if file/folder exists + Log_Printf(LOGLEVEL_ERROR, dirOrFileDoesNotExist, playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str()); gPlayProperties.trackFinished = true; continue; } else { - audioReturnCode = audio->connecttoFS(gFSystem, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str()); + audioReturnCode = audio->connecttoFS(gFSystem, playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str()); // consider track as finished, when audio lib call was not successful } } @@ -745,13 +736,13 @@ void AudioPlayer_Task(void *parameter) { } } else { if (gPlayProperties.numberOfTracks > 1) { - Audio_setTitle("(%u/%u): %s", gPlayProperties.currentTrackNumber+1, gPlayProperties.numberOfTracks, gPlayProperties.playlist->getFilename(gPlayProperties.currentTrackNumber).c_str()); + Audio_setTitle("(%u/%u): %s", gPlayProperties.currentTrackNumber + 1, gPlayProperties.numberOfTracks, playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str()); } else { - Audio_setTitle("%s", gPlayProperties.playlist->getFilename(gPlayProperties.currentTrackNumber).c_str()); + Audio_setTitle("%s", playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str()); } } AudioPlayer_ClearCover(); - Log_Printf(LOGLEVEL_NOTICE, currentlyPlaying, gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), (gPlayProperties.currentTrackNumber + 1), gPlayProperties.numberOfTracks); + Log_Printf(LOGLEVEL_NOTICE, currentlyPlaying, playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(), (gPlayProperties.currentTrackNumber + 1), gPlayProperties.numberOfTracks); gPlayProperties.playlistFinished = false; } } @@ -957,24 +948,25 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l gPlayProperties.startAtFilePos = _lastPlayPos; gPlayProperties.currentTrackNumber = _trackLastPlayed; - std::optional playlist = std::nullopt; + std::optional> newPlaylist = std::nullopt; + bool error = false; if (_playMode != WEBSTREAM) { if (_playMode == RANDOM_SUBDIRECTORY_OF_DIRECTORY) { auto tmp = SdCard_pickRandomSubdirectory(_itemToPlay); // *filename (input): target-directory // *filename (output): random subdirectory if (tmp) { // If error occured while extracting random subdirectory - playlist = SdCard_ReturnPlaylist(tmp.value().c_str(), _playMode); // Provide random subdirectory in order to enter regular playlist-generation + newPlaylist = SdCard_ReturnPlaylist(tmp.value().c_str(), _playMode); // Provide random subdirectory in order to enter regular playlist-generation } } else { - playlist = SdCard_ReturnPlaylist(_itemToPlay, _playMode); + newPlaylist = SdCard_ReturnPlaylist(_itemToPlay, _playMode); } } else { - playlist = AudioPlayer_ReturnPlaylistFromWebstream(_itemToPlay); + newPlaylist = AudioPlayer_ReturnPlaylistFromWebstream(_itemToPlay); } // Catch if error occured (e.g. file not found) gPlayProperties.playMode = BUSY; // Show @Neopixel, if uC is busy with creating playlist - if (!playlist || !playlist.value()->isValid()) { + if (!newPlaylist || !newPlaylist.value()->isValid()) { Log_Println(noMp3FilesInDir, LOGLEVEL_NOTICE); System_IndicateError(); if (!gPlayProperties.pausePlay) { @@ -989,7 +981,7 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l } gPlayProperties.playMode = _playMode; - gPlayProperties.numberOfTracks = playlist.value()->size(); + gPlayProperties.numberOfTracks = newPlaylist.value()->size(); // Set some default-values gPlayProperties.repeatCurrentTrack = false; gPlayProperties.repeatPlaylist = false; @@ -1022,14 +1014,14 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l gPlayProperties.numberOfTracks = 1; // Limit number to 1 even there are more entries in the playlist Led_ResetToNightBrightness(); Log_Println(modeSingleTrackRandom, LOGLEVEL_NOTICE); - playlist.value()->randomize(); + newPlaylist.value()->randomize(); break; } case AUDIOBOOK: { // Tracks need to be alph. sorted! gPlayProperties.saveLastPlayPosition = true; Log_Println(modeSingleAudiobook, LOGLEVEL_NOTICE); - playlist.value()->sort(); + newPlaylist.value()->sort(); break; } @@ -1037,34 +1029,34 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l gPlayProperties.repeatPlaylist = true; gPlayProperties.saveLastPlayPosition = true; Log_Println(modeSingleAudiobookLoop, LOGLEVEL_NOTICE); - playlist.value()->sort(); + newPlaylist.value()->sort(); break; } case ALL_TRACKS_OF_DIR_SORTED: case RANDOM_SUBDIRECTORY_OF_DIRECTORY: { Log_Printf(LOGLEVEL_NOTICE, modeAllTrackAlphSorted, _itemToPlay); - playlist.value()->sort(); + newPlaylist.value()->sort(); break; } case ALL_TRACKS_OF_DIR_RANDOM: { Log_Println(modeAllTrackRandom, LOGLEVEL_NOTICE); - playlist.value()->randomize(); + newPlaylist.value()->randomize(); break; } case ALL_TRACKS_OF_DIR_SORTED_LOOP: { gPlayProperties.repeatPlaylist = true; Log_Println(modeAllTrackAlphSortedLoop, LOGLEVEL_NOTICE); - playlist.value()->sort(); + newPlaylist.value()->sort(); break; } case ALL_TRACKS_OF_DIR_RANDOM_LOOP: { gPlayProperties.repeatPlaylist = true; Log_Println(modeAllTrackRandomLoop, LOGLEVEL_NOTICE); - playlist.value()->randomize(); + newPlaylist.value()->randomize(); break; } @@ -1072,7 +1064,7 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l Log_Println(modeWebstream, LOGLEVEL_NOTICE); if (!Wlan_IsConnected()) { Log_Println(webstreamNotAvailable, LOGLEVEL_ERROR); - goto error; + error = true; } break; } @@ -1083,19 +1075,22 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l } default: - Log_Println(modeDoesNotExist, LOGLEVEL_ERROR); - goto error; + Log_Println(modeInvalid, LOGLEVEL_ERROR); + error = true; } - // send the playlist to the queue - xQueueSend(gTrackQueue, &(playlist), 0); - return; + if(!error) { + // transfer ownership of the new playlist to the audio thread + std::lock_guard guard(playlist_mutex); + playlist = std::move(newPlaylist.value()); + playlistChanged = true; + return; + } -error: + // if we reach here, we had an error System_IndicateError(); gPlayProperties.playMode = NO_PLAYLIST; - delete playlist.value(); } /* Wraps putString for writing settings into NVS for RFID-cards. @@ -1141,8 +1136,8 @@ size_t AudioPlayer_NvsRfidWriteWrapper(const char *_rfidCardId, const char *_tra } // Adds webstream to playlist; same like SdCard_ReturnPlaylist() but always only one entry -Playlist *AudioPlayer_ReturnPlaylistFromWebstream(const char *_webUrl) { - return new WebstreamPlaylist(_webUrl); +std::unique_ptr AudioPlayer_ReturnPlaylistFromWebstream(const char *_webUrl) { + return std::make_unique(_webUrl); } // Adds new control-command to control-queue diff --git a/src/AudioPlayer.h b/src/AudioPlayer.h index b091e178..4f665b6d 100644 --- a/src/AudioPlayer.h +++ b/src/AudioPlayer.h @@ -59,3 +59,5 @@ time_t AudioPlayer_GetPlayTimeSinceStart(void); time_t AudioPlayer_GetPlayTimeAllTime(void); uint32_t AudioPlayer_GetCurrentTime(void); uint32_t AudioPlayer_GetFileDuration(void); + +const String AudioPlayer_getCurrentTrackPath(size_t track); diff --git a/src/Queues.cpp b/src/Queues.cpp index 85991199..d91d03ba 100644 --- a/src/Queues.cpp +++ b/src/Queues.cpp @@ -4,6 +4,16 @@ #include "Log.h" #include "Rfid.h" +#include "Queues.h" +#include "Playlist.h" + +SharedObject gVolume; +SharedObject gTrackControl; +SharedObject> gTrack; +SharedObject gRfidCard; + + + QueueHandle_t gVolumeQueue; QueueHandle_t gTrackQueue; QueueHandle_t gTrackControlQueue; diff --git a/src/Queues.h b/src/Queues.h index 5f30a8b6..831e58e0 100644 --- a/src/Queues.h +++ b/src/Queues.h @@ -1,8 +1,36 @@ #pragma once extern QueueHandle_t gVolumeQueue; -extern QueueHandle_t gTrackQueue; extern QueueHandle_t gTrackControlQueue; extern QueueHandle_t gRfidCardQueue; void Queues_Init(void); + +#include +#include +#include "Rfid.h" + +template +class SharedObject { +public: + SharedObject() = default; + ~SharedObject() = default; + + const T &get() { + std::lock_guard guard(mutex); + return obj; + } + + void put(T newObj) { + std::lock_guard guard(mutex); + obj = newObj; + } + +private: + T obj{}; + std::mutex mutex{}; +}; + +extern SharedObject gVolume; +extern SharedObject gTrackControl; +extern SharedObject gRfidCard; diff --git a/src/SdCard.cpp b/src/SdCard.cpp index ff99e5ac..11634dcb 100644 --- a/src/SdCard.cpp +++ b/src/SdCard.cpp @@ -190,10 +190,10 @@ std::optional SdCard_pickRandomSubdirectory(const char *_directory return path; } -static std::optional SdCard_ParseM3UPlaylist(File f, bool forceExtended = false) { +static std::optional> SdCard_ParseM3UPlaylist(File f, bool forceExtended = false) { const String line = f.readStringUntil('\n'); bool extended = line.startsWith("#EXTM3U") || forceExtended; - FolderPlaylist *playlist = new FolderPlaylist(); + auto playlist = std::make_unique(); if(extended) { // extended m3u file format @@ -205,7 +205,6 @@ static std::optional SdCard_ParseM3UPlaylist(File f, bool forceExtend // this something we have to save line.trim(); if(!playlist->push_back(line)) { - delete playlist; return std::nullopt; } } @@ -221,7 +220,6 @@ static std::optional SdCard_ParseM3UPlaylist(File f, bool forceExtend String line = f.readStringUntil('\n'); line.trim(); if(!playlist->push_back(line)) { - delete playlist; return std::nullopt; } } @@ -232,7 +230,7 @@ static std::optional SdCard_ParseM3UPlaylist(File f, bool forceExtend /* Puts SD-file(s) or directory into a playlist First element of array always contains the number of payload-items. */ -std::optional SdCard_ReturnPlaylist(const char *fileName, const uint32_t _playMode) { +std::optional> SdCard_ReturnPlaylist(const char *fileName, const uint32_t _playMode) { bool rebuildCacheFile = false; // Look if file/folder requested really exists. If not => break. @@ -254,7 +252,7 @@ std::optional SdCard_ReturnPlaylist(const char *fileName, const uint3 File cacheFile = gFSystem.open(cacheFilePath.value()); if (cacheFile && cacheFile.size()) { - CacheFilePlaylist *cachePlaylist = new CacheFilePlaylist(); + auto cachePlaylist = std::make_unique(); bool success = cachePlaylist->deserialize(cacheFile); if(success) { @@ -263,7 +261,7 @@ std::optional SdCard_ReturnPlaylist(const char *fileName, const uint3 } // we had some error reading the cache file, wait for the other to rebuild it // we do not need the class anymore, so destroy it - delete cachePlaylist; + cachePlaylist.release(); } // we failed to read the cache file... set the flag to rebuild it rebuildCacheFile = true; @@ -290,17 +288,16 @@ std::optional SdCard_ReturnPlaylist(const char *fileName, const uint3 Log_Println(fileModeDetected, LOGLEVEL_INFO); const char *path = getPath(fileOrDirectory); if (Playlist::fileValid(path)) { - return new WebstreamPlaylist(path); + return std::make_unique(path); } } // Folder mode - FolderPlaylist *playlist = new FolderPlaylist(); + auto playlist = std::make_unique(); playlist->createFromFolder(fileOrDirectory); if (!playlist->isValid()) { // something went wrong Log_Println(unableToAllocateMemForLinearPlaylist, LOGLEVEL_ERROR); - delete playlist; return std::nullopt; } diff --git a/src/SdCard.h b/src/SdCard.h index b7821af2..59534ee3 100644 --- a/src/SdCard.h +++ b/src/SdCard.h @@ -15,5 +15,5 @@ sdcard_type_t SdCard_GetType(void); uint64_t SdCard_GetSize(); uint64_t SdCard_GetFreeSize(); void SdCard_PrintInfo(); -std::optional SdCard_ReturnPlaylist(const char *fileName, const uint32_t _playMode); +std::optional> SdCard_ReturnPlaylist(const char *fileName, const uint32_t _playMode); std::optional SdCard_pickRandomSubdirectory(const char *_directory); diff --git a/src/Web.cpp b/src/Web.cpp index d56e61be..30e64594 100644 --- a/src/Web.cpp +++ b/src/Web.cpp @@ -1879,7 +1879,7 @@ static void handleCoverImageRequest(AsyncWebServerRequest *request) { } return; } - const char *coverFileName = gPlayProperties.playlist->getAbsolutePath(gPlayProperties.currentTrackNumber).c_str(); + const char *coverFileName = AudioPlayer_getCurrentTrackPath(gPlayProperties.currentTrackNumber).c_str(); Log_Println(coverFileName, LOGLEVEL_DEBUG); File coverFile = gFSystem.open(coverFileName, FILE_READ); From 57d031c47194cf46d964cc45356ab5d402aa2fba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laszlo=20Heged=C3=BCs?= Date: Mon, 30 Oct 2023 11:31:33 +0100 Subject: [PATCH 4/8] remove playlist cache support --- src/SdCard.cpp | 44 ----- src/playlists/CacheFilePlaylist.hpp | 278 ---------------------------- 2 files changed, 322 deletions(-) delete mode 100644 src/playlists/CacheFilePlaylist.hpp diff --git a/src/SdCard.cpp b/src/SdCard.cpp index 11634dcb..4182a125 100644 --- a/src/SdCard.cpp +++ b/src/SdCard.cpp @@ -9,7 +9,6 @@ #include "MemX.h" #include "System.h" -#include "playlists/CacheFilePlaylist.hpp" #include "playlists/FolderPlaylist.hpp" #include "playlists/WebstreamPlaylist.hpp" @@ -231,8 +230,6 @@ static std::optional> SdCard_ParseM3UPlaylist(File f, /* Puts SD-file(s) or directory into a playlist First element of array always contains the number of payload-items. */ std::optional> SdCard_ReturnPlaylist(const char *fileName, const uint32_t _playMode) { - bool rebuildCacheFile = false; - // Look if file/folder requested really exists. If not => break. File fileOrDirectory = gFSystem.open(fileName); if (!fileOrDirectory) { @@ -240,34 +237,6 @@ std::optional> SdCard_ReturnPlaylist(const char *fileN return std::nullopt; } - // Create linear playlist of caching-file - #ifdef CACHED_PLAYLIST_ENABLE - // Build absolute path of cacheFile - auto cacheFilePath = CacheFilePlaylist::getCachefilePath(fileOrDirectory); - - // Decide if to use cacheFile. It needs to exist first check if cacheFile (already) exists - // ...and playmode has to be != random/single (as random along with caching doesn't make sense at all) - if (cacheFilePath && gFSystem.exists(cacheFilePath.value()) && _playMode != SINGLE_TRACK && _playMode != SINGLE_TRACK_LOOP) { - // Read linear playlist (csv with #-delimiter) from cachefile (faster!) - - File cacheFile = gFSystem.open(cacheFilePath.value()); - if (cacheFile && cacheFile.size()) { - auto cachePlaylist = std::make_unique(); - - bool success = cachePlaylist->deserialize(cacheFile); - if(success) { - // always first assume a current playlist format - return cachePlaylist; - } - // we had some error reading the cache file, wait for the other to rebuild it - // we do not need the class anymore, so destroy it - cachePlaylist.release(); - } - // we failed to read the cache file... set the flag to rebuild it - rebuildCacheFile = true; - } - #endif - Log_Printf(LOGLEVEL_DEBUG, freeMemory, ESP.getFreeHeap()); // Parse m3u-playlist and create linear-playlist out of it @@ -280,9 +249,6 @@ std::optional> SdCard_ReturnPlaylist(const char *fileN return std::nullopt; } - // If we reached here, we did not read a cache file nor an m3u file. Means: read filenames from SD and make playlist of it - Log_Println(playlistGenModeUncached, LOGLEVEL_NOTICE); - // File-mode if (!fileOrDirectory.isDirectory()) { Log_Println(fileModeDetected, LOGLEVEL_INFO); @@ -301,16 +267,6 @@ std::optional> SdCard_ReturnPlaylist(const char *fileN return std::nullopt; } - #ifdef CACHED_PLAYLIST_ENABLE - if(cacheFilePath && rebuildCacheFile) { - File cacheFile = gFSystem.open(cacheFilePath.value(), FILE_WRITE); - if(cacheFile) { - CacheFilePlaylist::serialize(cacheFile, *playlist); - } - cacheFile.close(); - } - #endif - // we are finished Log_Printf(LOGLEVEL_NOTICE, numberOfValidFiles, playlist->size()); return playlist; diff --git a/src/playlists/CacheFilePlaylist.hpp b/src/playlists/CacheFilePlaylist.hpp deleted file mode 100644 index ca21d0d9..00000000 --- a/src/playlists/CacheFilePlaylist.hpp +++ /dev/null @@ -1,278 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "../Playlist.h" -#include "FolderPlaylist.hpp" - -class CacheFilePlaylist : public FolderPlaylist { -public: - CacheFilePlaylist(char divider = '/') : FolderPlaylist(divider), headerFlags(Flags()), headerValid(false) { } - CacheFilePlaylist(File &cacheFile, char divider = '/') : FolderPlaylist(divider), headerFlags(Flags()), headerValid(false) { - deserialize(cacheFile); - } - virtual ~CacheFilePlaylist() { - this->destroy(); - } - - static bool serialize(File &target, const FolderPlaylist &list) { - // write the header into the file - // header is big endian - BinaryCacheHeader header; - size_t ret; - - // first is the magic marker - header.magic = magic; - ret = write(target, magic); - - // the header version & flags - header.version = version; - ret += write(target, version); - - // update flags - Flags flags; - flags.relative = list.isRelative(); - - header.flags = flags; - ret += write(target, flags); - - // write the number of entries and the crc (not implemented yet) - header.count = list.size(); - ret += write(target, header.count); - header.crc = crcBase; - header.sep = separator; - ret += write(target, calcCRC(header)); - - ret += target.write(header.sep); - - if(ret != headerSize) { - #ifdef MEM_DEBUG - assert(ret != headerSize); - #endif - return false; - } - - return writeEntries(target, list); - } - - static std::optional getCachefilePath(File &dir) { - if(!dir.isDirectory()) - return {}; - - const char *path = getPath(dir); - return String(path) + "/" + playlistCacheFile; - } - - bool deserialize(File &cache) { - - if(!checkHeader(cache)) { - return false; - } - - // destroy old data, if present - if(isValid()) { - clear(); - } - - deserializeHeader(cache); - - // everything was ok read the files - return readEntries(cache); - } - - virtual bool isValid() const override { - return headerValid && FolderPlaylist::isValid(); - } - - void clear() { - destroy(); - headerValid = false; - headerFlags = Flags(); - headerCount = 0; - } - -protected: - // bitwise flags for future use - struct Flags { - bool relative; - - Flags(): relative(false) {} - - operator uint16_t() const { - // this function casts the flags into a 16bit wide bit array - // f.e. - // uint16_t flags = 0x00; - // flags |= (flag << 0); - // flags |= (otherFlag << 1); - // return flags; - uint16_t bitfield = 0x00; - - bitfield |= (relative << 0); - return bitfield; - } - - Flags &operator=(const uint16_t binary) { - // here we get a bitfield and break it down into our internal variables - // f.e. - // flag = binary & _BV(0); - // otherFlag = binary & _BV(1); - - relative = binary & _BV(0); - return *this; - } - }; - - struct BinaryCacheHeader - { - uint16_t magic; - uint16_t version; - uint16_t flags; - uint32_t count; - uint32_t crc; - uint8_t sep; - }; - static constexpr uint16_t magic = 0x4346; //< Magic header "CF" - static constexpr uint16_t version = 1; //< Current cache file version, if header or enconding changes, this has to be incremented - static constexpr uint32_t crcBase = 0x00; //< starting value of the crc calculation - static constexpr size_t headerSize = 15; //< the expected size of the header: magic(2) + version(2) + flags(2) + count(4) + crc(4) + separator(1) - - Flags headerFlags; //< A 16bit bitfield of flags - bool headerValid; - size_t headerCount; - - static constexpr char separator = '#'; //< separator for all entries - - static int checkHeader(File &cache) { - int ret = 1; - // read the header from the file - BinaryCacheHeader header; - - header.magic = read16(cache); - header.version = read16(cache); - - // first checkpoint - if(header.magic != magic || header.version != version) { - // header did not match, bail out - ret = -1; - } - - // read the flags and the count - header.flags = read16(cache); - header.count = read32(cache); - - // second checkpoint, crc and separator - header.crc = crcBase; - uint32_t crc = read32(cache); - header.sep = cache.read(); - if((ret > 0) && (calcCRC(header) != crc || header.sep != separator)) { - // crc missmatch, bail out - ret = -2; - } - - cache.seek(0); - return ret; - } - - bool deserializeHeader(File &cache) { - headerValid = false; - if(checkHeader(cache) < 1) { - return false; - } - // header is valid, read on - headerValid = true; - - // ignore magic & version - cache.seek(sizeof(BinaryCacheHeader::magic) + sizeof(BinaryCacheHeader::version)); - - // read the flags and the count - headerFlags = read16(cache); - headerCount = read32(cache); - - // reserve the memory - files.reserve(headerCount); - - cache.seek(sizeof(BinaryCacheHeader::crc) + sizeof(BinaryCacheHeader::sep), SeekMode::SeekCur); - return true; - } - - // helper function to write 16 bit in big endian - static size_t write(File &f, uint16_t value) { - size_t ret; - - ret = f.write(value >> 8); - ret += f.write(value); - return ret; - } - - // helper function to write 32 bit in big endian - static size_t write(File &f, uint32_t value) { - size_t ret; - - ret = write(f, uint16_t(value >> 16)); - ret += write(f, uint16_t(value)); - return ret; - } - - // helper fuction to read 16 bit in big endian - static uint16_t read16(File &f) { - return (f.read() << 8) | f.read(); - } - - // helper fuction to read 32 bit in big endian - static uint32_t read32(File &f) { - return (read16(f) << 16) | read16(f); - } - - static uint32_t calcCRC(const BinaryCacheHeader &header) { - // add all header fields individually since BinaryCacheHeader is not packed - uint32_t ret = crc32_le(0, reinterpret_cast(&header.magic), sizeof(header.magic)); - ret = crc32_le(ret, reinterpret_cast(&header.version), sizeof(header.version)); - ret = crc32_le(ret, reinterpret_cast(&header.flags), sizeof(header.flags)); - ret = crc32_le(ret, reinterpret_cast(&header.count), sizeof(header.count)); - ret = crc32_le(ret, reinterpret_cast(&header.crc), sizeof(header.crc)); - ret = crc32_le(ret, reinterpret_cast(&header.sep), sizeof(header.sep)); - return ret; - } - - bool writeEntries(File &f) const { - return writeEntries(f, *this); - } - - static bool writeEntries(File &f, const FolderPlaylist &list) { - // if flag is set, use relative path - if(list.isRelative()) { - f.write(reinterpret_cast(list.getBase()), strlen(list.getBase())); - f.write(separator); - } - - // write all entries with the separator to the file - for(size_t i=0;i(path.c_str()), path.length()) != path.length()) { - return false; - } - f.write(separator); - } - return true; - } - - bool readEntries(File &f) { - // if flag is set, use relative path - if(headerFlags.relative) { - const String basePath = f.readStringUntil(separator); - this->setBase(basePath); - } - - while(f.available()) { - const String path = f.readStringUntil(separator); - if(!this->push_back(path)){ - return false; - } - } - - return true; - } -}; From b8796e2ec352cdcb0d7ed274d03674f1f07b99f0 Mon Sep 17 00:00:00 2001 From: laszloh Date: Thu, 2 Nov 2023 09:15:53 +0100 Subject: [PATCH 5/8] Remove mock server test files --- test/webpage/package-lock.json | 1019 -------------------------------- test/webpage/package.json | 15 - test/webpage/server.js | 15 - 3 files changed, 1049 deletions(-) delete mode 100644 test/webpage/package-lock.json delete mode 100644 test/webpage/package.json delete mode 100644 test/webpage/server.js diff --git a/test/webpage/package-lock.json b/test/webpage/package-lock.json deleted file mode 100644 index 88786348..00000000 --- a/test/webpage/package-lock.json +++ /dev/null @@ -1,1019 +0,0 @@ -{ - "name": "express-test", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "express-test", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "body-parser": "^1.20.1", - "express": "^4.18.2" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - } - }, - "dependencies": { - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - } - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - } - }, - "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "requires": { - "safe-buffer": "5.2.1" - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" - }, - "express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "requires": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - } - }, - "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - } - }, - "forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" - }, - "object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" - }, - "on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "requires": { - "ee-first": "1.1.1" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "requires": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - } - }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "requires": { - "side-channel": "^1.0.4" - } - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "requires": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "dependencies": { - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } - } - }, - "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - } - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - }, - "toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" - } - } -} diff --git a/test/webpage/package.json b/test/webpage/package.json deleted file mode 100644 index 064ddcfd..00000000 --- a/test/webpage/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "express-test", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC", - "dependencies": { - "body-parser": "^1.20.1", - "express": "^4.18.2" - } -} diff --git a/test/webpage/server.js b/test/webpage/server.js deleted file mode 100644 index 21d29765..00000000 --- a/test/webpage/server.js +++ /dev/null @@ -1,15 +0,0 @@ -const express = require("express"); -const app = express(); - -app.use(express.static("../../html")); - -app.get("/", (req, res)=>{ - res.redirect("/management.html"); -}); - -const server = app.listen(8081, ()=>{ - const host = server.address().address; - const port = server.address().port; - - console.log("Test server listening at http://%s:%s", host, port); -}) \ No newline at end of file From f43e0b09b6633646929ce9c6825cf050a1d6e823 Mon Sep 17 00:00:00 2001 From: laszloh Date: Thu, 2 Nov 2023 09:17:03 +0100 Subject: [PATCH 6/8] Add test files for webstream and file playlist These test files check malloc, realloc and dealloc calls from the new playlist system with a mock file system. --- test/mock_fs.hpp | 172 ++++++++++++++++ test/test_filelist/test_main.cpp | 328 ++++++++++++++++++++++++++++++ test/test_webstream/test_main.cpp | 126 ++++++++++++ 3 files changed, 626 insertions(+) create mode 100644 test/mock_fs.hpp create mode 100644 test/test_filelist/test_main.cpp create mode 100644 test/test_webstream/test_main.cpp diff --git a/test/mock_fs.hpp b/test/mock_fs.hpp new file mode 100644 index 00000000..47a2a15f --- /dev/null +++ b/test/mock_fs.hpp @@ -0,0 +1,172 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace mockfs +{ + +struct Node { + ~Node() { } + + // create a new data node with string data + static Node fromStr(const char* path, const char *str = nullptr) { + Node n; + n.fullPath = path; + if(str) + n.content.insert(n.content.begin(), str, str + strlen(str)); + return n; + } + + static Node fromBuffer(const char* path, const uint8_t *buf, size_t size) { + Node n; + n.fullPath = path; + n.content.insert(n.content.begin(), buf, buf+size); + return n; + } + + // create a new data node with Strings + static Node fromStr(const String path, const String str) { + Node n; + n.fullPath = path; + n.content.insert(n.content.begin(), str.begin(), str.end()); + return n; + } + + // create a new data node with Strings + static Node empty(const String path, size_t initBuffer = 4096) { + Node n; + n.fullPath = path; + n.content.reserve(initBuffer); + return n; + } + + String fullPath{String()}; + bool isDir{false}; + std::vector content{std::vector()}; + std::vector files{std::vector()}; +}; + +class MockFileImp : public fs::FileImpl { +protected: + Node *node; + std::vector::iterator cit; + bool readOnly; + bool fileOpen; + time_t lastWrite; + std::vector::iterator it; + +public: + static fs::FileImplPtr open(Node *n, bool ro) { + return std::make_shared(n, ro); + } + + MockFileImp(Node *n, bool ro) : node(n), cit(node->content.begin()), readOnly(ro), fileOpen(true), lastWrite(0), it(node->files.begin()) { } + + virtual ~MockFileImp() override { + close(); + } + + virtual size_t write(const uint8_t *buf, size_t size) override { + if(readOnly) + return 0; + + lastWrite = time(NULL); + node->content.insert(node->content.end(), &buf[0], &buf[size]); + return size; + } + + virtual size_t read(uint8_t* buf, size_t size) override { + // basic check if we reach the end of the file + const size_t cap = std::distance(cit, node->content.end()); + const size_t rsize = std::min(size, cap); + + std::copy(cit, cit + rsize, buf); + cit += rsize; + return rsize; + } + + virtual void flush() override { } + + virtual bool seek(uint32_t pos, SeekMode mode) override { + switch(mode) { + case SeekCur: + // test if we go over the end + if((cit + pos) >= node->content.end()) { + return false; + } + cit += pos; + break; + + case SeekSet: + if(pos > node->content.size()) { + return false; + } + cit = node->content.begin() + pos; + break; + + case SeekEnd: + if(pos > node->content.size()) { + return false; + } + cit = node->content.end() - pos; + break; + } + return true; + } + + virtual size_t position() const override { + return std::distance::const_iterator>(node->content.begin(), cit); + } + + virtual size_t size() const override { return node->content.size(); }; + + virtual bool setBufferSize(size_t size) { return false; }; + + virtual void close() override { + cit = node->content.begin(); + fileOpen = false; + node->content.shrink_to_fit(); + } + + virtual time_t getLastWrite() override { return lastWrite; }; + + virtual const char* name() const override { return node->fullPath.c_str(); }; + + virtual boolean isDirectory(void) override { return node->isDir; }; + + virtual fs::FileImplPtr openNextFile(const char* mode) override { + if(!node->isDir || it >= node->files.end()) + return nullptr; + auto newFilePtr = std::make_shared(&(*it), strcmp(mode, "R") == 0); + it++; + return newFilePtr; + }; + + virtual boolean seekDir(long position) { + uint32_t offset = static_cast(position); + if(!node->isDir || (it + offset) >= node->files.end()) + return false; + it += offset; + return true; + } + + virtual String getNextFileName(void) { + auto next = it++; + return next->fullPath; + } + + virtual void rewindDirectory(void) override { + it = node->files.begin(); + } + + virtual operator bool() override { + return fileOpen; + } +}; + +} // namespace mockfs diff --git a/test/test_filelist/test_main.cpp b/test/test_filelist/test_main.cpp new file mode 100644 index 00000000..ac9ba54b --- /dev/null +++ b/test/test_filelist/test_main.cpp @@ -0,0 +1,328 @@ +#include +#include +#include +#include "../mock_fs.hpp" + +#include "playlists/FolderPlaylist.hpp" + +size_t allocCount = 0; +size_t deAllocCount = 0; +size_t reAllocCount = 0; + +size_t heap; +size_t psram; + +// mock allocator +struct UnitTestAllocator { + void* allocate(size_t size) { + void *ret; + if(psramInit()) { + ret = ps_malloc(size); + } else { + ret = malloc(size); + } + if(ret) { + allocCount++; + } + return ret; + } + + void deallocate(void* ptr) { + free(ptr); + deAllocCount ++; + } + + void* reallocate(void* ptr, size_t new_size) { + void *ret; + if(psramInit()) { + ret = ps_realloc(ptr, new_size); + } else { + ret = realloc(ptr, new_size); + } + if(ret) + reAllocCount++; + return ret; + } +}; + +FolderPlaylistAlloc *folderPlaylist; + +void get_free_memory(void) { + heap = ESP.getFreeHeap(); + psram = ESP.getFreePsram(); +} + +void test_free_memory(void) { + size_t cHeap = ESP.getFreeHeap(); + size_t cPsram = ESP.getFreePsram(); + + TEST_ASSERT_INT_WITHIN_MESSAGE(4, heap, cHeap, "Free heap after test (delta = 4 byte)"); + TEST_ASSERT_INT_WITHIN_MESSAGE(4, psram, cPsram, "Free psram after test (delta = 4 byte)"); +} + +// set stuff up here, this function is before a test function +void setUp(void) { + allocCount = deAllocCount = reAllocCount = 0; + get_free_memory(); +} + +void tearDown(void) { + test_free_memory(); +} + +void setup_static(void) { + folderPlaylist = new FolderPlaylistAlloc(); +} + +void test_folder_alloc(void) { + TEST_ASSERT_TRUE(folderPlaylist->reserve(10)); + + folderPlaylist->clear(); + + test_free_memory(); + + // test memory actions + TEST_ASSERT_EQUAL_MESSAGE(0, allocCount, "Calls to malloc"); + TEST_ASSERT_EQUAL_MESSAGE(1, deAllocCount, "Calls to free"); + TEST_ASSERT_EQUAL_MESSAGE(1, reAllocCount, "Calls to realloc"); +} + +void test_folder_content_absolute(void) { + constexpr std::array contentAbsolute PROGMEM = {{ + "/sdcard/music/folderA/song1.mp3", + "/sdcard/music/folderA/song2.mp3", + "/sdcard/music/folderB/song3.mp3", + "/sdcard/music/folderC/song4.mp3", + "/sdcard/music/folderD/song5.mp3", + "/sdcard/music/folderA/song6.mp3", + }}; + + folderPlaylist->clear(); + TEST_ASSERT_TRUE(folderPlaylist->reserve(contentAbsolute.size())); + for(auto e : contentAbsolute) { + TEST_ASSERT_TRUE(folderPlaylist->push_back(e)); + } + TEST_ASSERT_EQUAL(contentAbsolute.size(), folderPlaylist->size()); + + for(size_t i=0;igetAbsolutePath(i).c_str()); + } + + folderPlaylist->clear(); + + // test memory actions + TEST_ASSERT_EQUAL_MESSAGE(contentAbsolute.size(), allocCount, "Calls to malloc"); + TEST_ASSERT_EQUAL_MESSAGE(1 + contentAbsolute.size(), deAllocCount, "Calls to free"); + TEST_ASSERT_EQUAL_MESSAGE(1, reAllocCount, "Calls to realloc"); +} + +void test_folder_content_relative(void) { + constexpr const char *basePath = "/sdcard/music/folderX"; + constexpr std::array contentRelative PROGMEM = {{ + "/sdcard/music/folderX/song1.mp3", + "/sdcard/music/folderX/song2.mp3", + "/sdcard/music/folderX/song3.mp3", + "/sdcard/music/folderX/song4.mp3", + }}; + + folderPlaylist->clear(); // <-- nop operation + TEST_ASSERT_TRUE(folderPlaylist->setBase(basePath)); + TEST_ASSERT_TRUE(folderPlaylist->reserve(contentRelative.size())); + + for(auto e : contentRelative) { + TEST_ASSERT_TRUE(folderPlaylist->push_back(e)); + } + TEST_ASSERT_EQUAL(contentRelative.size(), folderPlaylist->size()); + + for(size_t i=0;igetAbsolutePath(i).c_str()); + } + + // this tests should fail + constexpr const char *wrongBasePath PROGMEM = "/sdcard/music/folderZ/song1.mp3"; + constexpr const char *noMusicFile PROGMEM = "/sdcard/music/folderX/song4.doc"; + + TEST_ASSERT_FALSE(folderPlaylist->push_back(wrongBasePath)); + TEST_ASSERT_FALSE(folderPlaylist->push_back(noMusicFile)); + + // cleanup + folderPlaylist->clear(); + + // test memory actions + TEST_ASSERT_EQUAL_MESSAGE(1 + contentRelative.size(), allocCount, "Calls to malloc"); + TEST_ASSERT_EQUAL_MESSAGE(contentRelative.size() + 1 + 1, deAllocCount, "Calls to free"); + TEST_ASSERT_EQUAL_MESSAGE(1, reAllocCount, "Calls to realloc"); +} + +void test_folder_content_automatic(void) { + // this test will access a mock file system implementation + mockfs::Node musicFolder = { + .fullPath = "/sdcard/music/folderE", + .isDir = true, + .content = std::vector(), + .files = { + { + .fullPath = "/sdcard/music/folderE/song1.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector() + }, + { + .fullPath = "/sdcard/music/folderE/song2.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector() + }, + { + .fullPath = "/sdcard/music/folderE/song3.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector() + }, + { + .fullPath = "/sdcard/music/folderE/song4.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector() + }, + { + .fullPath = "/sdcard/music/folderE/A Folder", + .isDir = true, + .content = std::vector(), + .files = std::vector() + }, + { + .fullPath = "/sdcard/music/folderE/song5.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector() + }, + { + .fullPath = "/sdcard/music/folderE/song6.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector() + }, + } + }; + constexpr size_t numFiles = 6; + File root(mockfs::MockFileImp::open(&musicFolder, true)); + + folderPlaylist->createFromFolder(root); + TEST_ASSERT_EQUAL_MESSAGE(numFiles, folderPlaylist->size(), "Number of elements in Playlist"); + + size_t i=0; + for(auto it=musicFolder.files.begin();it!=musicFolder.files.end();it++){ + if(!it->isDir) { + TEST_ASSERT_EQUAL_STRING(it->fullPath.c_str(), folderPlaylist->getAbsolutePath(i).c_str()); + i++; + } + } + + // cleanup + folderPlaylist->clear(); + + // test memory actions + TEST_ASSERT_EQUAL_MESSAGE(1 + numFiles, allocCount, "Calls to malloc"); + TEST_ASSERT_EQUAL_MESSAGE(8, deAllocCount, "Calls to free"); + TEST_ASSERT_EQUAL_MESSAGE(2, reAllocCount, "Calls to realloc"); +} + +void test_folder_content_special_char(void) { + // this test will access a mock file system implementation + mockfs::Node musicFolder = { + .fullPath = "/sdcard/music/131072 ๐Ÿ˜€", + .isDir = true, + .content = std::vector(), + .files = { + { + .fullPath = "/sdcard/music/131072 ๐Ÿ˜€/Zongz.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector() + }, + { + .fullPath = "/sdcard/music/131072 ๐Ÿ˜€/Mรผsรคk.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector() + }, + { + .fullPath = "/sdcard/music/131072 ๐Ÿ˜€/็‹—.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector() + }, + { + .fullPath = "/sdcard/music/131072 ๐Ÿ˜€/trรจs รฉlรฉgant.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector() + }, + { + .fullPath = "/sdcard/music/131072 ๐Ÿ˜€/A Folder", + .isDir = true, + .content = std::vector(), + .files = std::vector() + }, + { + .fullPath = "/sdcard/music/131072 ๐Ÿ˜€/ั€ะพััะธัะฝะธะฝ.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector() + }, + { + .fullPath = "/sdcard/music/131072 ๐Ÿ˜€/" "\xD9\x85\xD9\x88\xD8\xB3\xD9\x8a\xD9\x82" ".mp3", + .fullPath = "/sdcard/music/131072 ๐Ÿ˜€/" "\xD9\x85\xD9\x88\xD8\xB3\xD9\x8a\xD9\x82" ".mp3", + .valid = true, + .isDir = false, + .content = std::vector(), + .files = std::vector() + }, + } + }; + constexpr size_t numFiles = 6; + File root(mockfs::MockFileImp::open(&musicFolder, true)); + + folderPlaylist->createFromFolder(root); + TEST_ASSERT_EQUAL_MESSAGE(numFiles, folderPlaylist->size(), "Number of elements in Playlist"); + + size_t i=0; + for(auto it=musicFolder.files.begin();it!=musicFolder.files.end();it++){ + log_n("Path: %s", it->fullPath.c_str()); + if(!it->isDir) { + TEST_ASSERT_EQUAL_STRING(it->fullPath.c_str(), folderPlaylist->getAbsolutePath(i).c_str()); + i++; + } + } + + // cleanup + folderPlaylist->clear(); + + // test memory actions + TEST_ASSERT_EQUAL_MESSAGE(1 + numFiles, allocCount, "Calls to malloc"); + TEST_ASSERT_EQUAL_MESSAGE(8, deAllocCount, "Calls to free"); + TEST_ASSERT_EQUAL_MESSAGE(2, reAllocCount, "Calls to realloc"); +} + +void setup() +{ + Serial.begin(115200); + delay(2000); // service delay + UNITY_BEGIN(); + + setup_static(); + + RUN_TEST(test_folder_alloc); + RUN_TEST(test_folder_content_absolute); + RUN_TEST(test_folder_content_relative); + RUN_TEST(test_folder_content_automatic); + RUN_TEST(test_folder_content_special_char); + + + UNITY_END(); // stop unit testing +} + +void loop() +{ +} diff --git a/test/test_webstream/test_main.cpp b/test/test_webstream/test_main.cpp new file mode 100644 index 00000000..cb17d677 --- /dev/null +++ b/test/test_webstream/test_main.cpp @@ -0,0 +1,126 @@ +#include +#include +#include + +#include "playlists/WebstreamPlaylist.hpp" + +size_t allocCount = 0; +size_t deAllocCount = 0; +size_t reAllocCount = 0; + +size_t heap; +size_t psram; + +// mock allocator +struct UnitTestAllocator { + void* allocate(size_t size) { + void *ret; + if(psramInit()) { + ret = ps_malloc(size); + } else { + ret = malloc(size); + } + if(ret) { + allocCount++; + } + return ret; + } + + void deallocate(void* ptr) { + free(ptr); + deAllocCount ++; + } + + void* reallocate(void* ptr, size_t new_size) { + void *ret; + if(psramInit()) { + ret = ps_realloc(ptr, new_size); + } else { + ret = realloc(ptr, new_size); + } + if(ret) + reAllocCount++; + return ret; + } +}; + +constexpr const char* webStream = "http://test.com/stream.mp3"; +WebstreamPlaylistAlloc *webPlaylist; + + +void setUp(void) { + // set stuff up here, this function is before a test function +} + +void tearDown(void) { + +} + +void setup_static(void) { + webPlaylist = new WebstreamPlaylistAlloc(); +} + +void get_free_memory(void) { + heap = ESP.getFreeHeap(); + psram = ESP.getFreePsram(); +} + +void test_webstream_alloc(void) { + webPlaylist->setUrl(webStream); + + TEST_ASSERT_EQUAL_MESSAGE(1, allocCount, "Calls to malloc"); + TEST_ASSERT_EQUAL_MESSAGE(0, deAllocCount, "Calls to free"); + TEST_ASSERT_EQUAL_MESSAGE(0, reAllocCount, "Calls to realloc"); +} + +void test_webstream_content(void) { + TEST_ASSERT_EQUAL_STRING_MESSAGE(webStream, webPlaylist->getAbsolutePath().c_str(), "Test if stored value equal constructor value"); +} + +void test_webstream_change(void) { + const char *newStream = "http://test2.com/stream.mp3"; + webPlaylist->setUrl(newStream); + TEST_ASSERT_EQUAL_STRING_MESSAGE(newStream, webPlaylist->getAbsolutePath().c_str(), "Test if stored value equal constructor value"); + + // test memory actions + TEST_ASSERT_EQUAL_MESSAGE(2, allocCount, "Calls to malloc"); + TEST_ASSERT_EQUAL_MESSAGE(1, deAllocCount, "Calls to free"); + TEST_ASSERT_EQUAL_MESSAGE(0, reAllocCount, "Calls to realloc"); +} + +void test_webstream_dealloc(void) { + delete webPlaylist; + + TEST_ASSERT_EQUAL_MESSAGE(2, allocCount, "Calls to malloc"); + TEST_ASSERT_EQUAL_MESSAGE(2, deAllocCount, "Calls to free"); + TEST_ASSERT_EQUAL_MESSAGE(0, reAllocCount, "Calls to realloc"); +} + +void test_free_memory(void) { + size_t cHeap = ESP.getFreeHeap(); + size_t cPsram = ESP.getFreePsram(); + + TEST_ASSERT_INT_WITHIN_MESSAGE(4, heap, cHeap, "Free heap after test (delta = 4 byte)"); + TEST_ASSERT_INT_WITHIN_MESSAGE(4, psram, cPsram, "Free psram after test (delta = 4 byte)"); +} + +void setup() +{ + delay(2000); // service delay + UNITY_BEGIN(); + + get_free_memory(); + setup_static(); + + RUN_TEST(test_webstream_alloc); + RUN_TEST(test_webstream_content); + RUN_TEST(test_webstream_change); + RUN_TEST(test_webstream_dealloc); + RUN_TEST(test_free_memory); + + UNITY_END(); // stop unit testing +} + +void loop() +{ +} \ No newline at end of file From 67f90a7254b4ef53676c16b22e71539ae764f9ae Mon Sep 17 00:00:00 2001 From: laszloh Date: Thu, 2 Nov 2023 15:25:49 +0100 Subject: [PATCH 7/8] Apply automatic code formatting after rebase Rebase on dev finished. Applied automatic code formatting and removed duplicated test code, fix warning and reomve prototype code and unused queues. --- src/AudioPlayer.cpp | 30 +- src/AudioPlayer.h | 52 +-- src/Common.h | 6 +- src/Playlist.h | 71 ++-- src/Queues.cpp | 17 - src/Queues.h | 29 -- src/SdCard.cpp | 70 ++-- src/SdCard.h | 1 + src/cpp.h | 26 +- src/playlists/FolderPlaylist.hpp | 93 +++--- src/playlists/WebstreamPlaylist.hpp | 18 +- test/mock_allocator.hpp | 37 +++ test/mock_fs.hpp | 320 +++++++++--------- test/test_filelist/test_main.cpp | 494 +++++++++++++--------------- test/test_webstream/test_main.cpp | 121 +++---- 15 files changed, 649 insertions(+), 736 deletions(-) create mode 100644 test/mock_allocator.hpp diff --git a/src/AudioPlayer.cpp b/src/AudioPlayer.cpp index 5de51ca6..2da2ac0b 100644 --- a/src/AudioPlayer.cpp +++ b/src/AudioPlayer.cpp @@ -24,6 +24,7 @@ #include #include +#include #define AUDIOPLAYER_VOLUME_MAX 21u #define AUDIOPLAYER_VOLUME_MIN 0u @@ -31,7 +32,7 @@ playProps gPlayProperties; TaskHandle_t AudioTaskHandle; -//uint32_t cnt123 = 0; +// uint32_t cnt123 = 0; // Playlist static std::unique_ptr playlist; @@ -249,7 +250,7 @@ void Audio_setTitle(const char *format, ...) { const String AudioPlayer_getCurrentTrackPath(size_t track) { std::lock_guard guard(playlist_mutex); - if(track >= playlist->size()) { + if (track >= playlist->size()) { // requested track is larger than the array return String(); } @@ -936,15 +937,15 @@ void AudioPlayer_PauseOnMinVolume(const uint8_t oldVolume, const uint8_t newVolu // Receives de-serialized RFID-data (from NVS) and dispatches playlists for the given // playmode to the track-queue. void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _lastPlayPos, const uint32_t _playMode, const uint16_t _trackLastPlayed) { - // Make sure last playposition for audiobook is saved when new RFID-tag is applied - #ifdef SAVE_PLAYPOS_WHEN_RFID_CHANGE - if (!gPlayProperties.pausePlay && (gPlayProperties.playMode == AUDIOBOOK || gPlayProperties.playMode == AUDIOBOOK_LOOP)) { - AudioPlayer_TrackControlToQueueSender(PAUSEPLAY); - while (!gPlayProperties.pausePlay) { // Make sure to wait until playback is paused in order to be sure that playposition saved in NVS - vTaskDelay(portTICK_PERIOD_MS * 100u); - } +// Make sure last playposition for audiobook is saved when new RFID-tag is applied +#ifdef SAVE_PLAYPOS_WHEN_RFID_CHANGE + if (!gPlayProperties.pausePlay && (gPlayProperties.playMode == AUDIOBOOK || gPlayProperties.playMode == AUDIOBOOK_LOOP)) { + AudioPlayer_TrackControlToQueueSender(PAUSEPLAY); + while (!gPlayProperties.pausePlay) { // Make sure to wait until playback is paused in order to be sure that playposition saved in NVS + vTaskDelay(portTICK_PERIOD_MS * 100u); } - #endif + } +#endif gPlayProperties.startAtFilePos = _lastPlayPos; gPlayProperties.currentTrackNumber = _trackLastPlayed; @@ -953,9 +954,9 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l if (_playMode != WEBSTREAM) { if (_playMode == RANDOM_SUBDIRECTORY_OF_DIRECTORY) { - auto tmp = SdCard_pickRandomSubdirectory(_itemToPlay); // *filename (input): target-directory // *filename (output): random subdirectory - if (tmp) { // If error occured while extracting random subdirectory - newPlaylist = SdCard_ReturnPlaylist(tmp.value().c_str(), _playMode); // Provide random subdirectory in order to enter regular playlist-generation + auto tmp = SdCard_pickRandomSubdirectory(_itemToPlay); // get a random subdirectory + if (tmp) { // If error occured while extracting random subdirectory + newPlaylist = SdCard_ReturnPlaylist(tmp.value().c_str(), _playMode); // Provide random subdirectory in order to enter regular playlist-generation } } else { newPlaylist = SdCard_ReturnPlaylist(_itemToPlay, _playMode); @@ -1077,10 +1078,9 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l default: Log_Println(modeInvalid, LOGLEVEL_ERROR); error = true; - } - if(!error) { + if (!error) { // transfer ownership of the new playlist to the audio thread std::lock_guard guard(playlist_mutex); playlist = std::move(newPlaylist.value()); diff --git a/src/AudioPlayer.h b/src/AudioPlayer.h index 4f665b6d..d2d23705 100644 --- a/src/AudioPlayer.h +++ b/src/AudioPlayer.h @@ -3,32 +3,32 @@ #include "Playlist.h" typedef struct { // Bit field - uint8_t playMode: 4; // playMode - char title[255]; // current title - bool repeatCurrentTrack: 1; // If current track should be looped - bool repeatPlaylist: 1; // If whole playlist should be looped - uint16_t currentTrackNumber: 9; // Current tracknumber - uint16_t numberOfTracks: 9; // Number of tracks in playlist - unsigned long startAtFilePos; // Offset to start play (in bytes) - double currentRelPos; // Current relative playPosition (in %) - bool sleepAfterCurrentTrack: 1; // If uC should go to sleep after current track - bool sleepAfterPlaylist: 1; // If uC should go to sleep after whole playlist - bool sleepAfter5Tracks: 1; // If uC should go to sleep after 5 tracks - bool saveLastPlayPosition: 1; // If playposition/current track should be saved (for AUDIOBOOK) - char playRfidTag[13]; // ID of RFID-tag that started playlist - bool pausePlay: 1; // If pause is active - bool trackFinished: 1; // If current track is finished - bool playlistFinished: 1; // If whole playlist is finished - uint8_t playUntilTrackNumber: 6; // Number of tracks to play after which uC goes to sleep - uint8_t seekmode: 2; // If seekmode is active and if yes: forward or backwards? - bool newPlayMono: 1; // true if mono; false if stereo (helper) - bool currentPlayMono: 1; // true if mono; false if stereo - bool isWebstream: 1; // Indicates if track currenty played is a webstream - uint8_t tellMode: 2; // Tell mode for text to speech announcments - bool currentSpeechActive: 1; // If speech-play is active - bool lastSpeechActive: 1; // If speech-play was active - size_t coverFilePos; // current cover file position - size_t coverFileSize; // current cover file size + uint8_t playMode : 4; // playMode + char title[255]; // current title + bool repeatCurrentTrack : 1; // If current track should be looped + bool repeatPlaylist : 1; // If whole playlist should be looped + uint16_t currentTrackNumber : 9; // Current tracknumber + uint16_t numberOfTracks : 9; // Number of tracks in playlist + unsigned long startAtFilePos; // Offset to start play (in bytes) + double currentRelPos; // Current relative playPosition (in %) + bool sleepAfterCurrentTrack : 1; // If uC should go to sleep after current track + bool sleepAfterPlaylist : 1; // If uC should go to sleep after whole playlist + bool sleepAfter5Tracks : 1; // If uC should go to sleep after 5 tracks + bool saveLastPlayPosition : 1; // If playposition/current track should be saved (for AUDIOBOOK) + char playRfidTag[13]; // ID of RFID-tag that started playlist + bool pausePlay : 1; // If pause is active + bool trackFinished : 1; // If current track is finished + bool playlistFinished : 1; // If whole playlist is finished + uint8_t playUntilTrackNumber : 6; // Number of tracks to play after which uC goes to sleep + uint8_t seekmode : 2; // If seekmode is active and if yes: forward or backwards? + bool newPlayMono : 1; // true if mono; false if stereo (helper) + bool currentPlayMono : 1; // true if mono; false if stereo + bool isWebstream : 1; // Indicates if track currenty played is a webstream + uint8_t tellMode : 2; // Tell mode for text to speech announcments + bool currentSpeechActive : 1; // If speech-play is active + bool lastSpeechActive : 1; // If speech-play was active + size_t coverFilePos; // current cover file position + size_t coverFileSize; // current cover file size } playProps; extern playProps gPlayProperties; diff --git a/src/Common.h b/src/Common.h index 3d67e10b..d277690d 100644 --- a/src/Common.h +++ b/src/Common.h @@ -25,11 +25,11 @@ inline bool isNumber(const char *str) { } inline const char *getPath(File &f) { - #if ESP_ARDUINO_VERSION_MAJOR >= 2 + if constexpr (ESP_ARDUINO_VERSION_MAJOR >= 2) { return f.path(); - #else + } else { return f.name(); - #endif + } } // Checks if string starts with prefix diff --git a/src/Playlist.h b/src/Playlist.h index 5358804f..2eae60bf 100644 --- a/src/Playlist.h +++ b/src/Playlist.h @@ -1,15 +1,12 @@ #pragma once -#include -#include -#include #include "cpp.h" -#if MEM_DEBUG == 1 - #warning Memory access guards are enabled. Disable MEM_DEBUG for production builds -#endif +#include +#include +#include -using sortFunc = int(*)(const void*,const void*); +using sortFunc = int (*)(const void *, const void *); class Playlist { public: @@ -25,8 +22,8 @@ class Playlist { virtual const String getFilename(size_t idx) const = 0; static int alphabeticSort(const void *x, const void *y) { - const char *a = static_cast(x); - const char *b = static_cast(y); + const char *a = static_cast(x); + const char *b = static_cast(y); return strcmp(a, b); } @@ -37,37 +34,38 @@ class Playlist { // Check if file-type is correct static bool fileValid(const String _fileItem) { - constexpr size_t maxExtLen = strlen(*std::max_element(audioFileSufix.begin(), audioFileSufix.end(), [](const char *a, const char *b){ + constexpr size_t maxExtLen = strlen(*std::max_element(audioFileSufix.begin(), audioFileSufix.end(), [](const char *a, const char *b) { return strlen(a) < strlen(b); })); - if(!_fileItem) + if (!_fileItem) { return false; + } - // check for http address - if(_fileItem.startsWith("http://") || _fileItem.startsWith("https://")) { + // check for http address + if (_fileItem.startsWith("http://") || _fileItem.startsWith("https://")) { return true; } // Ignore hidden files starting with a '.' // lastIndex is -1 if '/' is not found --> first index will be 0 - int fileNameIndex = _fileItem.lastIndexOf('/') + 1; - if(_fileItem[fileNameIndex] == '.') { + int fileNameIndex = _fileItem.lastIndexOf('/') + 1; + if (_fileItem[fileNameIndex] == '.') { return false; } String extBuf; const size_t extStart = _fileItem.lastIndexOf('.'); const size_t extLen = _fileItem.length() - extStart; - if(extLen > maxExtLen) { + if (extLen > maxExtLen) { // we either did not find a . or extension was too long return false; } extBuf = _fileItem.substring(extStart); extBuf.toLowerCase(); - for(const auto e:audioFileSufix) { - if(extBuf.equals(e)) { + for (const auto e : audioFileSufix) { + if (extBuf.equals(e)) { return true; } } @@ -75,30 +73,29 @@ class Playlist { } protected: - template class PsramAllocator { public: typedef T value_type; - typedef value_type* pointer; - typedef const value_type* const_pointer; - typedef value_type& reference; - typedef const value_type& const_reference; + typedef value_type *pointer; + typedef const value_type *const_pointer; + typedef value_type &reference; + typedef const value_type &const_reference; typedef std::size_t size_type; typedef std::ptrdiff_t difference_type; public: - template + template struct rebind { typedef PsramAllocator other; }; public: - inline explicit PsramAllocator() {} - inline ~PsramAllocator() {} - inline PsramAllocator(PsramAllocator const&) {} - template - inline explicit PsramAllocator(PsramAllocator const&) {} + inline explicit PsramAllocator() { } + inline ~PsramAllocator() { } + inline PsramAllocator(PsramAllocator const &) { } + template + inline explicit PsramAllocator(PsramAllocator const &) { } // address inline pointer address(reference r) { return &r; } @@ -108,10 +105,10 @@ class Playlist { // memory allocation inline pointer allocate(size_type cnt, typename std::allocator::const_pointer = 0) { T *ptr = nullptr; - if(psramFound()) { - ptr = (T*)ps_malloc(cnt * sizeof(T)); + if (psramFound()) { + ptr = (T *) ps_malloc(cnt * sizeof(T)); } else { - ptr = (T*)malloc(cnt * sizeof(T)); + ptr = (T *) malloc(cnt * sizeof(T)); } return ptr; } @@ -126,22 +123,23 @@ class Playlist { } // construction/destruction - inline void construct(pointer p, const T& t) { - new(p) T(t); + inline void construct(pointer p, const T &t) { + new (p) T(t); } inline void destroy(pointer p) { p->~T(); } - inline bool operator==(PsramAllocator const& a) { return this == &a; } - inline bool operator!=(PsramAllocator const& a) { return !operator==(a); } + inline bool operator==(PsramAllocator const &a) { return this == &a; } + inline bool operator!=(PsramAllocator const &a) { return !operator==(a); } }; using pstring = std::basic_string, PsramAllocator>; virtual void destroy() { } + // clang-format off static constexpr auto audioFileSufix = std::to_array({ ".mp3", ".aac", @@ -155,4 +153,5 @@ class Playlist { ".pls", ".asx" }); + // clang-format on }; diff --git a/src/Queues.cpp b/src/Queues.cpp index d91d03ba..e0eb0f49 100644 --- a/src/Queues.cpp +++ b/src/Queues.cpp @@ -4,18 +4,7 @@ #include "Log.h" #include "Rfid.h" -#include "Queues.h" -#include "Playlist.h" - -SharedObject gVolume; -SharedObject gTrackControl; -SharedObject> gTrack; -SharedObject gRfidCard; - - - QueueHandle_t gVolumeQueue; -QueueHandle_t gTrackQueue; QueueHandle_t gTrackControlQueue; QueueHandle_t gRfidCardQueue; @@ -35,10 +24,4 @@ void Queues_Init(void) { if (gTrackControlQueue == NULL) { Log_Println(unableToCreateMgmtQ, LOGLEVEL_ERROR); } - - char **playlistArray; - gTrackQueue = xQueueCreate(1, sizeof(playlistArray)); - if (gTrackQueue == NULL) { - Log_Println(unableToCreatePlayQ, LOGLEVEL_ERROR); - } } diff --git a/src/Queues.h b/src/Queues.h index 831e58e0..b15592a8 100644 --- a/src/Queues.h +++ b/src/Queues.h @@ -5,32 +5,3 @@ extern QueueHandle_t gTrackControlQueue; extern QueueHandle_t gRfidCardQueue; void Queues_Init(void); - -#include -#include -#include "Rfid.h" - -template -class SharedObject { -public: - SharedObject() = default; - ~SharedObject() = default; - - const T &get() { - std::lock_guard guard(mutex); - return obj; - } - - void put(T newObj) { - std::lock_guard guard(mutex); - obj = newObj; - } - -private: - T obj{}; - std::mutex mutex{}; -}; - -extern SharedObject gVolume; -extern SharedObject gTrackControl; -extern SharedObject gRfidCard; diff --git a/src/SdCard.cpp b/src/SdCard.cpp index 4182a125..1064804b 100644 --- a/src/SdCard.cpp +++ b/src/SdCard.cpp @@ -8,7 +8,6 @@ #include "Log.h" #include "MemX.h" #include "System.h" - #include "playlists/FolderPlaylist.hpp" #include "playlists/WebstreamPlaylist.hpp" @@ -126,7 +125,8 @@ void SdCard_PrintInfo() { // Takes a directory as input and returns a random subdirectory from it std::optional SdCard_pickRandomSubdirectory(const char *_directory) { - uint32_t listStartTimestamp = millis(); + constexpr bool fileNameSupport = (ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(2, 0, 8)); + const uint32_t listStartTimestamp = millis(); // Look if file/folder requested really exists. If not => break. File directory = gFSystem.open(_directory); @@ -138,53 +138,53 @@ std::optional SdCard_pickRandomSubdirectory(const char *_directory Log_Printf(LOGLEVEL_NOTICE, tryToPickRandomDir, _directory); size_t dirCount = 0; - while(true) { + while (true) { bool isDir; - #if defined(HAS_FILEEXPLORER_SPEEDUP) || (ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(2, 0, 8)) + if constexpr (fileNameSupport) { const String path = directory.getNextFileName(&isDir); - if(path.isEmpty()) { + if (path.isEmpty()) { break; } - #else + } else { File fileItem = directory.openNextFile(); - if(!fileItem) { + if (!fileItem) { break; } isDir = fileItem.isDirectory(); - #endif - if(isDir) { + } + if (isDir) { dirCount++; } } - if(!dirCount) { + if (!dirCount) { // no paths in folder return std::nullopt; } const uint32_t randomNumber = esp_random() % dirCount; String path; - for(size_t i=0;i= ESP_ARDUINO_VERSION_VAL(2, 0, 8)) + if constexpr (fileNameSupport) { path = directory.getNextFileName(&isDir); - #else + } else { File fileItem = directory.openNextFile(); - if(!fileItem) { + if (!fileItem) { path = ""; } else { path = getPath(fileItem); isDir = fileItem.isDirectory(); } - #endif - if(path.isEmpty()) { + } + if (path.isEmpty()) { // we reached the end before finding the correct dir! return std::nullopt; } - if(isDir) { + if (isDir) { i++; } } - Log_Printf(LOGLEVEL_NOTICE, pickedRandomDir, path.c_str()); + Log_Printf(LOGLEVEL_NOTICE, pickedRandomDir, path.c_str()); Log_Printf(LOGLEVEL_DEBUG, "pick random directory from SD-card finished: %lu ms", (millis() - listStartTimestamp)); return path; } @@ -194,31 +194,31 @@ static std::optional> SdCard_ParseM3UPlaylist(File f, bool extended = line.startsWith("#EXTM3U") || forceExtended; auto playlist = std::make_unique(); - if(extended) { - // extended m3u file format - // ignore all lines starting with '#' + if (extended) { + // extended m3u file format + // ignore all lines starting with '#' - while(f.available()) { - String line = f.readStringUntil('\n'); - if(!line.startsWith("#")){ - // this something we have to save - line.trim(); - if(!playlist->push_back(line)) { - return std::nullopt; - } - } - } - // resize memory to fit our count + while (f.available()) { + String line = f.readStringUntil('\n'); + if (!line.startsWith("#")) { + // this something we have to save + line.trim(); + if (!playlist->push_back(line)) { + return std::nullopt; + } + } + } + // resize memory to fit our count playlist->compress(); - return playlist; + return playlist; } // normal m3u is just a bunch of filenames, 1 / line f.seek(0); - while(f.available()) { + while (f.available()) { String line = f.readStringUntil('\n'); line.trim(); - if(!playlist->push_back(line)) { + if (!playlist->push_back(line)) { return std::nullopt; } } diff --git a/src/SdCard.h b/src/SdCard.h index 59534ee3..87db523b 100644 --- a/src/SdCard.h +++ b/src/SdCard.h @@ -1,5 +1,6 @@ #pragma once #include "settings.h" + #include "Playlist.h" #ifdef SD_MMC_1BIT_MODE #include "SD_MMC.h" diff --git a/src/cpp.h b/src/cpp.h index 52bec646..49e1f99b 100644 --- a/src/cpp.h +++ b/src/cpp.h @@ -1,27 +1,25 @@ #pragma once - -// check for c++20 features -#if __cplusplus < 202002L - -#include #include +#include -namespace std { +#ifndef __cpp_lib_to_array + #define __cpp_lib_to_array 201907L +namespace std { namespace detail { - + template constexpr std::array, N> - to_array_impl(T (&&a)[N], std::index_sequence) { - return {{std::move(a[I])...}}; -} - -} - +to_array_impl(T (&&a)[N], std::index_sequence) { + return {{std::move(a[I])...}}; +} // namespace detail + +} // namespace detail + template constexpr std::array, N> to_array(T (&&a)[N]) { - return detail::to_array_impl(std::move(a), std::make_index_sequence{}); + return detail::to_array_impl(std::move(a), std::make_index_sequence {}); } } // namespace std diff --git a/src/playlists/FolderPlaylist.hpp b/src/playlists/FolderPlaylist.hpp index 47225542..ede9b34e 100644 --- a/src/playlists/FolderPlaylist.hpp +++ b/src/playlists/FolderPlaylist.hpp @@ -1,13 +1,14 @@ #pragma once -#include -#include +#include "../Playlist.h" +#include "Common.h" + #include +#include +#include +#include #include #include -#include - -#include "../Playlist.h" class FolderPlaylist : public Playlist { protected: @@ -16,9 +17,12 @@ class FolderPlaylist : public Playlist { char divider; public: - FolderPlaylist(size_t _capacity = 64, char _divider = '/') - : base(pstring()), files(std::vector>(_capacity)), divider(_divider) { } - FolderPlaylist(File &folder, size_t _capacity = 64, char _divider = '/') : FolderPlaylist(_capacity, divider) { + FolderPlaylist(size_t _capacity = 64, char _divider = '/') + : base(pstring()) + , files(std::vector>(_capacity)) + , divider(_divider) { } + FolderPlaylist(File &folder, size_t _capacity = 64, char _divider = '/') + : FolderPlaylist(_capacity, divider) { createFromFolder(folder); } @@ -28,45 +32,45 @@ class FolderPlaylist : public Playlist { bool createFromFolder(File &folder) { // This is not a folder, so bail out - if(!folder || !folder.isDirectory()){ + if (!folder || !folder.isDirectory()) { return false; } // clean up any previously used memory clear(); - // since we are enumerating, we don't have to think about absolute files with different bases + // since we are enumerating, we don't have to think about absolute files with different bases base = getPath(folder); // reserve a sane amout of memory files.reserve(64); // enumerate all files in the folder - while(true) { + while (true) { bool isDir; String path; - #if defined(HAS_FILEEXPLORER_SPEEDUP) || (ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(2, 0, 8)) + if constexpr (fileNameSupport) { path = folder.getNextFileName(&isDir); - #else + } else { File f = folder.openNextFile(); - if(!f){ - path=""; - }else{ + if (!f) { + path = ""; + } else { path = getPath(f); isDir = f.isDirectory(); } - #endif - if(path.isEmpty()) { + } + if (path.isEmpty()) { break; } - if(isDir) { + if (isDir) { continue; } - if(fileValid(path)) { + if (fileValid(path)) { // push this file into the array bool success = push_back(path); - if(!success) { + if (!success) { return false; } } @@ -95,19 +99,19 @@ class FolderPlaylist : public Playlist { bool push_back(const char *path) { log_n("path: %s", path); - if(!fileValid(path)) { + if (!fileValid(path)) { return false; } // here we check if we have to cut up the path (currently it's only a crude check for absolute paths) - if(isRelative() && path[0] == '/') { + if (isRelative() && path[0] == '/') { // we are in relative mode and got an absolute path, check if the path begins with our base // Also check if the path is so short, that there is no space for a filename in it - if( (strncmp(path, base.c_str(), base.length()) != 0) || (strlen(path) < (base.length() + strlen("/.abc")))) { + if ((strncmp(path, base.c_str(), base.length()) != 0) || (strlen(path) < (base.length() + strlen("/.abc")))) { // we refuse files other than our base return false; } - path = path + base.length(); // modify pointer to the end of the path + path = path + base.length(); // modify pointer to the end of the path } files.push_back(path); @@ -135,10 +139,7 @@ class FolderPlaylist : public Playlist { virtual bool isValid() const override { return files.size(); } virtual const String getAbsolutePath(size_t idx) const override { - #if MEM_DEBUG == 1 - assert(idx < files.size()); - #endif - if(isRelative()) { + if (isRelative()) { // we are in relative mode return String(base.c_str()) + divider + files[idx].c_str(); } @@ -146,10 +147,7 @@ class FolderPlaylist : public Playlist { }; virtual const String getFilename(size_t idx) const override { - #if MEM_DEBUG == 1 - assert(idx < files.size()); - #endif - if(isRelative()) { + if (isRelative()) { return String(files[idx].c_str()); } pstring path = files[idx]; @@ -161,7 +159,7 @@ class FolderPlaylist : public Playlist { } virtual void randomize() override { - if(files.size() < 2) { + if (files.size() < 2) { // we can not randomize less than 2 entries return; } @@ -172,17 +170,19 @@ class FolderPlaylist : public Playlist { } class Iterator { - public: - using iterator_category = std::forward_iterator_tag; // could be increased to random_access_iterator_tag - using difference_type = std::ptrdiff_t; - using value_type = pstring; - using pointer = value_type*; - using reference = value_type&; + public: + using iterator_category = std::forward_iterator_tag; // could be increased to random_access_iterator_tag + using difference_type = std::ptrdiff_t; + using value_type = pstring; + using pointer = value_type *; + using reference = value_type &; class ArrowHelper { value_type value; + public: - ArrowHelper(value_type _str) : value(_str) {} + ArrowHelper(value_type _str) + : value(_str) { } pointer operator->() { return &value; } @@ -197,10 +197,14 @@ class FolderPlaylist : public Playlist { } // Constructor - __attribute__((always_inline)) inline Iterator(FolderPlaylist *folder, pointer ptr) : m_folder(folder), m_ptr(ptr) {} + __attribute__((always_inline)) inline Iterator(FolderPlaylist *folder, pointer ptr) + : m_folder(folder) + , m_ptr(ptr) { } // Copy Constructor & assignment - __attribute__((always_inline)) inline Iterator(const Iterator &rhs) : m_folder(rhs.m_folder), m_ptr(rhs.m_ptr) {} + __attribute__((always_inline)) inline Iterator(const Iterator &rhs) + : m_folder(rhs.m_folder) + , m_ptr(rhs.m_ptr) { } __attribute__((always_inline)) inline Iterator &operator=(const Iterator &rhs) = default; // Pointer increment @@ -208,7 +212,7 @@ class FolderPlaylist : public Playlist { m_ptr++; return *this; } - __attribute__((always_inline)) inline Iterator &operator++(int) { + __attribute__((always_inline)) inline Iterator operator++(int) { Iterator tmp(*this); m_ptr++; return tmp; @@ -234,6 +238,7 @@ class FolderPlaylist : public Playlist { Iterator cend() { return Iterator(this, (files.data() + files.size())); } protected: + static constexpr bool fileNameSupport = (ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(2, 0, 8)); virtual void destroy() override { files.clear(); diff --git a/src/playlists/WebstreamPlaylist.hpp b/src/playlists/WebstreamPlaylist.hpp index 4a24b2f4..9a989277 100644 --- a/src/playlists/WebstreamPlaylist.hpp +++ b/src/playlists/WebstreamPlaylist.hpp @@ -1,22 +1,23 @@ #pragma once -#include -#include - #include "../Playlist.h" +#include +#include + class WebstreamPlaylist : public Playlist { protected: pstring url; public: - WebstreamPlaylist(const char *_url) : url(_url) { } - WebstreamPlaylist() : url(nullptr) { } - virtual ~WebstreamPlaylist() override { - }; + WebstreamPlaylist(const char *_url) + : url(_url) { } + WebstreamPlaylist() + : url(nullptr) { } + virtual ~WebstreamPlaylist() override {}; bool setUrl(const char *_url) { - if(fileValid(_url)) { + if (fileValid(_url)) { url = _url; return true; } @@ -27,5 +28,4 @@ class WebstreamPlaylist : public Playlist { virtual bool isValid() const override { return url.length(); } virtual const String getAbsolutePath(size_t idx = 0) const override { return String(url.c_str()); }; virtual const String getFilename(size_t idx = 0) const override { return String(url.c_str()); }; - }; diff --git a/test/mock_allocator.hpp b/test/mock_allocator.hpp new file mode 100644 index 00000000..d73e207b --- /dev/null +++ b/test/mock_allocator.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include + +// mock allocator +struct UnitTestAllocator { + void *allocate(size_t size) { + void *ret; + if (psramInit()) { + ret = ps_malloc(size); + } else { + ret = malloc(size); + } + if (ret) { + allocCount++; + } + return ret; + } + + void deallocate(void *ptr) { + free(ptr); + deAllocCount++; + } + + void *reallocate(void *ptr, size_t new_size) { + void *ret; + if (psramInit()) { + ret = ps_realloc(ptr, new_size); + } else { + ret = realloc(ptr, new_size); + } + if (ret) { + reAllocCount++; + } + return ret; + } +}; \ No newline at end of file diff --git a/test/mock_fs.hpp b/test/mock_fs.hpp index 47a2a15f..a24c57ff 100644 --- a/test/mock_fs.hpp +++ b/test/mock_fs.hpp @@ -1,172 +1,180 @@ #pragma once -#include -#include - -#include #include #include +#include +#include +#include -namespace mockfs -{ +namespace mockfs { struct Node { - ~Node() { } - - // create a new data node with string data - static Node fromStr(const char* path, const char *str = nullptr) { - Node n; - n.fullPath = path; - if(str) - n.content.insert(n.content.begin(), str, str + strlen(str)); - return n; - } - - static Node fromBuffer(const char* path, const uint8_t *buf, size_t size) { - Node n; - n.fullPath = path; - n.content.insert(n.content.begin(), buf, buf+size); - return n; - } - - // create a new data node with Strings - static Node fromStr(const String path, const String str) { - Node n; - n.fullPath = path; - n.content.insert(n.content.begin(), str.begin(), str.end()); - return n; - } - - // create a new data node with Strings - static Node empty(const String path, size_t initBuffer = 4096) { - Node n; - n.fullPath = path; - n.content.reserve(initBuffer); - return n; - } - - String fullPath{String()}; - bool isDir{false}; - std::vector content{std::vector()}; - std::vector files{std::vector()}; + ~Node() { } + + // create a new data node with string data + static Node fromStr(const char *path, const char *str = nullptr) { + Node n; + n.fullPath = path; + if (str) { + n.content.insert(n.content.begin(), str, str + strlen(str)); + } + return n; + } + + static Node fromBuffer(const char *path, const uint8_t *buf, size_t size) { + Node n; + n.fullPath = path; + n.content.insert(n.content.begin(), buf, buf + size); + return n; + } + + // create a new data node with Strings + static Node fromStr(const String path, const String str) { + Node n; + n.fullPath = path; + n.content.insert(n.content.begin(), str.begin(), str.end()); + return n; + } + + // create a new data node with Strings + static Node empty(const String path, size_t initBuffer = 4096) { + Node n; + n.fullPath = path; + n.content.reserve(initBuffer); + return n; + } + + String fullPath {String()}; + bool isDir {false}; + std::vector content {std::vector()}; + std::vector files {std::vector()}; }; class MockFileImp : public fs::FileImpl { protected: - Node *node; - std::vector::iterator cit; - bool readOnly; - bool fileOpen; - time_t lastWrite; - std::vector::iterator it; + Node *node; + std::vector::iterator cit; + bool readOnly; + bool fileOpen; + time_t lastWrite; + std::vector::iterator it; public: - static fs::FileImplPtr open(Node *n, bool ro) { - return std::make_shared(n, ro); - } - - MockFileImp(Node *n, bool ro) : node(n), cit(node->content.begin()), readOnly(ro), fileOpen(true), lastWrite(0), it(node->files.begin()) { } - - virtual ~MockFileImp() override { - close(); - } - - virtual size_t write(const uint8_t *buf, size_t size) override { - if(readOnly) - return 0; - - lastWrite = time(NULL); - node->content.insert(node->content.end(), &buf[0], &buf[size]); - return size; - } - - virtual size_t read(uint8_t* buf, size_t size) override { - // basic check if we reach the end of the file - const size_t cap = std::distance(cit, node->content.end()); - const size_t rsize = std::min(size, cap); - - std::copy(cit, cit + rsize, buf); - cit += rsize; - return rsize; - } - - virtual void flush() override { } - - virtual bool seek(uint32_t pos, SeekMode mode) override { - switch(mode) { - case SeekCur: - // test if we go over the end - if((cit + pos) >= node->content.end()) { - return false; - } - cit += pos; - break; - - case SeekSet: - if(pos > node->content.size()) { - return false; - } - cit = node->content.begin() + pos; - break; - - case SeekEnd: - if(pos > node->content.size()) { - return false; - } - cit = node->content.end() - pos; - break; - } - return true; - } - - virtual size_t position() const override { - return std::distance::const_iterator>(node->content.begin(), cit); - } - - virtual size_t size() const override { return node->content.size(); }; - - virtual bool setBufferSize(size_t size) { return false; }; - - virtual void close() override { - cit = node->content.begin(); - fileOpen = false; - node->content.shrink_to_fit(); - } - - virtual time_t getLastWrite() override { return lastWrite; }; - - virtual const char* name() const override { return node->fullPath.c_str(); }; - - virtual boolean isDirectory(void) override { return node->isDir; }; - - virtual fs::FileImplPtr openNextFile(const char* mode) override { - if(!node->isDir || it >= node->files.end()) - return nullptr; - auto newFilePtr = std::make_shared(&(*it), strcmp(mode, "R") == 0); - it++; - return newFilePtr; - }; - - virtual boolean seekDir(long position) { - uint32_t offset = static_cast(position); - if(!node->isDir || (it + offset) >= node->files.end()) - return false; - it += offset; - return true; - } - - virtual String getNextFileName(void) { - auto next = it++; - return next->fullPath; - } - - virtual void rewindDirectory(void) override { - it = node->files.begin(); - } - - virtual operator bool() override { - return fileOpen; - } + static fs::FileImplPtr open(Node *n, bool ro) { + return std::make_shared(n, ro); + } + + MockFileImp(Node *n, bool ro) + : node(n) + , cit(node->content.begin()) + , readOnly(ro) + , fileOpen(true) + , lastWrite(0) + , it(node->files.begin()) { } + + virtual ~MockFileImp() override { + close(); + } + + virtual size_t write(const uint8_t *buf, size_t size) override { + if (readOnly) { + return 0; + } + + lastWrite = time(NULL); + node->content.insert(node->content.end(), &buf[0], &buf[size]); + return size; + } + + virtual size_t read(uint8_t *buf, size_t size) override { + // basic check if we reach the end of the file + const size_t cap = std::distance(cit, node->content.end()); + const size_t rsize = std::min(size, cap); + + std::copy(cit, cit + rsize, buf); + cit += rsize; + return rsize; + } + + virtual void flush() override { } + + virtual bool seek(uint32_t pos, SeekMode mode) override { + switch (mode) { + case SeekCur: + // test if we go over the end + if ((cit + pos) >= node->content.end()) { + return false; + } + cit += pos; + break; + + case SeekSet: + if (pos > node->content.size()) { + return false; + } + cit = node->content.begin() + pos; + break; + + case SeekEnd: + if (pos > node->content.size()) { + return false; + } + cit = node->content.end() - pos; + break; + } + return true; + } + + virtual size_t position() const override { + return std::distance::const_iterator>(node->content.begin(), cit); + } + + virtual size_t size() const override { return node->content.size(); }; + + virtual bool setBufferSize(size_t size) { return false; }; + + virtual void close() override { + cit = node->content.begin(); + fileOpen = false; + node->content.shrink_to_fit(); + } + + virtual time_t getLastWrite() override { return lastWrite; }; + + virtual const char *name() const override { return node->fullPath.c_str(); }; + + virtual boolean isDirectory(void) override { return node->isDir; }; + + virtual fs::FileImplPtr openNextFile(const char *mode) override { + if (!node->isDir || it >= node->files.end()) { + return nullptr; + } + auto newFilePtr = std::make_shared(&(*it), strcmp(mode, "R") == 0); + it++; + return newFilePtr; + }; + + virtual boolean seekDir(long position) { + uint32_t offset = static_cast(position); + if (!node->isDir || (it + offset) >= node->files.end()) { + return false; + } + it += offset; + return true; + } + + virtual String getNextFileName(void) { + auto next = it++; + return next->fullPath; + } + + virtual void rewindDirectory(void) override { + it = node->files.begin(); + } + + virtual operator bool() override { + return fileOpen; + } }; } // namespace mockfs diff --git a/test/test_filelist/test_main.cpp b/test/test_filelist/test_main.cpp index ac9ba54b..e1db3c70 100644 --- a/test/test_filelist/test_main.cpp +++ b/test/test_filelist/test_main.cpp @@ -1,10 +1,12 @@ #include -#include -#include -#include "../mock_fs.hpp" +#include "../mock_allocator.hpp" +#include "../mock_fs.hpp" #include "playlists/FolderPlaylist.hpp" +#include +#include + size_t allocCount = 0; size_t deAllocCount = 0; size_t reAllocCount = 0; @@ -12,317 +14,261 @@ size_t reAllocCount = 0; size_t heap; size_t psram; -// mock allocator -struct UnitTestAllocator { - void* allocate(size_t size) { - void *ret; - if(psramInit()) { - ret = ps_malloc(size); - } else { - ret = malloc(size); - } - if(ret) { - allocCount++; - } - return ret; - } - - void deallocate(void* ptr) { - free(ptr); - deAllocCount ++; - } - - void* reallocate(void* ptr, size_t new_size) { - void *ret; - if(psramInit()) { - ret = ps_realloc(ptr, new_size); - } else { - ret = realloc(ptr, new_size); - } - if(ret) - reAllocCount++; - return ret; - } -}; - FolderPlaylistAlloc *folderPlaylist; void get_free_memory(void) { - heap = ESP.getFreeHeap(); - psram = ESP.getFreePsram(); + heap = ESP.getFreeHeap(); + psram = ESP.getFreePsram(); } void test_free_memory(void) { - size_t cHeap = ESP.getFreeHeap(); - size_t cPsram = ESP.getFreePsram(); + size_t cHeap = ESP.getFreeHeap(); + size_t cPsram = ESP.getFreePsram(); - TEST_ASSERT_INT_WITHIN_MESSAGE(4, heap, cHeap, "Free heap after test (delta = 4 byte)"); - TEST_ASSERT_INT_WITHIN_MESSAGE(4, psram, cPsram, "Free psram after test (delta = 4 byte)"); + TEST_ASSERT_INT_WITHIN_MESSAGE(4, heap, cHeap, "Free heap after test (delta = 4 byte)"); + TEST_ASSERT_INT_WITHIN_MESSAGE(4, psram, cPsram, "Free psram after test (delta = 4 byte)"); } // set stuff up here, this function is before a test function void setUp(void) { - allocCount = deAllocCount = reAllocCount = 0; - get_free_memory(); + allocCount = deAllocCount = reAllocCount = 0; + get_free_memory(); } void tearDown(void) { - test_free_memory(); + test_free_memory(); } void setup_static(void) { - folderPlaylist = new FolderPlaylistAlloc(); + folderPlaylist = new FolderPlaylistAlloc(); } void test_folder_alloc(void) { - TEST_ASSERT_TRUE(folderPlaylist->reserve(10)); + TEST_ASSERT_TRUE(folderPlaylist->reserve(10)); - folderPlaylist->clear(); + folderPlaylist->clear(); - test_free_memory(); + test_free_memory(); - // test memory actions - TEST_ASSERT_EQUAL_MESSAGE(0, allocCount, "Calls to malloc"); - TEST_ASSERT_EQUAL_MESSAGE(1, deAllocCount, "Calls to free"); - TEST_ASSERT_EQUAL_MESSAGE(1, reAllocCount, "Calls to realloc"); + // test memory actions + TEST_ASSERT_EQUAL_MESSAGE(0, allocCount, "Calls to malloc"); + TEST_ASSERT_EQUAL_MESSAGE(1, deAllocCount, "Calls to free"); + TEST_ASSERT_EQUAL_MESSAGE(1, reAllocCount, "Calls to realloc"); } void test_folder_content_absolute(void) { - constexpr std::array contentAbsolute PROGMEM = {{ - "/sdcard/music/folderA/song1.mp3", - "/sdcard/music/folderA/song2.mp3", - "/sdcard/music/folderB/song3.mp3", - "/sdcard/music/folderC/song4.mp3", - "/sdcard/music/folderD/song5.mp3", - "/sdcard/music/folderA/song6.mp3", - }}; - - folderPlaylist->clear(); - TEST_ASSERT_TRUE(folderPlaylist->reserve(contentAbsolute.size())); - for(auto e : contentAbsolute) { - TEST_ASSERT_TRUE(folderPlaylist->push_back(e)); - } - TEST_ASSERT_EQUAL(contentAbsolute.size(), folderPlaylist->size()); - - for(size_t i=0;igetAbsolutePath(i).c_str()); - } - - folderPlaylist->clear(); - - // test memory actions - TEST_ASSERT_EQUAL_MESSAGE(contentAbsolute.size(), allocCount, "Calls to malloc"); - TEST_ASSERT_EQUAL_MESSAGE(1 + contentAbsolute.size(), deAllocCount, "Calls to free"); - TEST_ASSERT_EQUAL_MESSAGE(1, reAllocCount, "Calls to realloc"); + constexpr std::array contentAbsolute PROGMEM = { + { + "/sdcard/music/folderA/song1.mp3", + "/sdcard/music/folderA/song2.mp3", + "/sdcard/music/folderB/song3.mp3", + "/sdcard/music/folderC/song4.mp3", + "/sdcard/music/folderD/song5.mp3", + "/sdcard/music/folderA/song6.mp3", + } + }; + + folderPlaylist->clear(); + TEST_ASSERT_TRUE(folderPlaylist->reserve(contentAbsolute.size())); + for (auto e : contentAbsolute) { + TEST_ASSERT_TRUE(folderPlaylist->push_back(e)); + } + TEST_ASSERT_EQUAL(contentAbsolute.size(), folderPlaylist->size()); + + for (size_t i = 0; i < contentAbsolute.size(); i++) { + TEST_ASSERT_EQUAL_STRING(contentAbsolute[i], folderPlaylist->getAbsolutePath(i).c_str()); + } + + folderPlaylist->clear(); + + // test memory actions + TEST_ASSERT_EQUAL_MESSAGE(contentAbsolute.size(), allocCount, "Calls to malloc"); + TEST_ASSERT_EQUAL_MESSAGE(1 + contentAbsolute.size(), deAllocCount, "Calls to free"); + TEST_ASSERT_EQUAL_MESSAGE(1, reAllocCount, "Calls to realloc"); } void test_folder_content_relative(void) { - constexpr const char *basePath = "/sdcard/music/folderX"; - constexpr std::array contentRelative PROGMEM = {{ - "/sdcard/music/folderX/song1.mp3", - "/sdcard/music/folderX/song2.mp3", - "/sdcard/music/folderX/song3.mp3", - "/sdcard/music/folderX/song4.mp3", - }}; - - folderPlaylist->clear(); // <-- nop operation - TEST_ASSERT_TRUE(folderPlaylist->setBase(basePath)); - TEST_ASSERT_TRUE(folderPlaylist->reserve(contentRelative.size())); - - for(auto e : contentRelative) { - TEST_ASSERT_TRUE(folderPlaylist->push_back(e)); - } - TEST_ASSERT_EQUAL(contentRelative.size(), folderPlaylist->size()); - - for(size_t i=0;igetAbsolutePath(i).c_str()); - } - - // this tests should fail - constexpr const char *wrongBasePath PROGMEM = "/sdcard/music/folderZ/song1.mp3"; - constexpr const char *noMusicFile PROGMEM = "/sdcard/music/folderX/song4.doc"; - - TEST_ASSERT_FALSE(folderPlaylist->push_back(wrongBasePath)); - TEST_ASSERT_FALSE(folderPlaylist->push_back(noMusicFile)); - - // cleanup - folderPlaylist->clear(); - - // test memory actions - TEST_ASSERT_EQUAL_MESSAGE(1 + contentRelative.size(), allocCount, "Calls to malloc"); - TEST_ASSERT_EQUAL_MESSAGE(contentRelative.size() + 1 + 1, deAllocCount, "Calls to free"); - TEST_ASSERT_EQUAL_MESSAGE(1, reAllocCount, "Calls to realloc"); + constexpr const char *basePath = "/sdcard/music/folderX"; + constexpr std::array contentRelative PROGMEM = { + { + "/sdcard/music/folderX/song1.mp3", + "/sdcard/music/folderX/song2.mp3", + "/sdcard/music/folderX/song3.mp3", + "/sdcard/music/folderX/song4.mp3", + } + }; + + folderPlaylist->clear(); // <-- nop operation + TEST_ASSERT_TRUE(folderPlaylist->setBase(basePath)); + TEST_ASSERT_TRUE(folderPlaylist->reserve(contentRelative.size())); + + for (auto e : contentRelative) { + TEST_ASSERT_TRUE(folderPlaylist->push_back(e)); + } + TEST_ASSERT_EQUAL(contentRelative.size(), folderPlaylist->size()); + + for (size_t i = 0; i < contentRelative.size(); i++) { + TEST_ASSERT_EQUAL_STRING(contentRelative[i], folderPlaylist->getAbsolutePath(i).c_str()); + } + + // this tests should fail + constexpr const char *wrongBasePath PROGMEM = "/sdcard/music/folderZ/song1.mp3"; + constexpr const char *noMusicFile PROGMEM = "/sdcard/music/folderX/song4.doc"; + + TEST_ASSERT_FALSE(folderPlaylist->push_back(wrongBasePath)); + TEST_ASSERT_FALSE(folderPlaylist->push_back(noMusicFile)); + + // cleanup + folderPlaylist->clear(); + + // test memory actions + TEST_ASSERT_EQUAL_MESSAGE(1 + contentRelative.size(), allocCount, "Calls to malloc"); + TEST_ASSERT_EQUAL_MESSAGE(contentRelative.size() + 1 + 1, deAllocCount, "Calls to free"); + TEST_ASSERT_EQUAL_MESSAGE(1, reAllocCount, "Calls to realloc"); } void test_folder_content_automatic(void) { - // this test will access a mock file system implementation - mockfs::Node musicFolder = { - .fullPath = "/sdcard/music/folderE", - .isDir = true, - .content = std::vector(), - .files = { - { - .fullPath = "/sdcard/music/folderE/song1.mp3", - .isDir = false, - .content = std::vector(), - .files = std::vector() - }, - { - .fullPath = "/sdcard/music/folderE/song2.mp3", - .isDir = false, - .content = std::vector(), - .files = std::vector() - }, - { - .fullPath = "/sdcard/music/folderE/song3.mp3", - .isDir = false, - .content = std::vector(), - .files = std::vector() - }, - { - .fullPath = "/sdcard/music/folderE/song4.mp3", - .isDir = false, - .content = std::vector(), - .files = std::vector() - }, - { - .fullPath = "/sdcard/music/folderE/A Folder", - .isDir = true, - .content = std::vector(), - .files = std::vector() - }, - { - .fullPath = "/sdcard/music/folderE/song5.mp3", - .isDir = false, - .content = std::vector(), - .files = std::vector() - }, - { - .fullPath = "/sdcard/music/folderE/song6.mp3", - .isDir = false, - .content = std::vector(), - .files = std::vector() - }, - } - }; - constexpr size_t numFiles = 6; - File root(mockfs::MockFileImp::open(&musicFolder, true)); - - folderPlaylist->createFromFolder(root); - TEST_ASSERT_EQUAL_MESSAGE(numFiles, folderPlaylist->size(), "Number of elements in Playlist"); - - size_t i=0; - for(auto it=musicFolder.files.begin();it!=musicFolder.files.end();it++){ - if(!it->isDir) { - TEST_ASSERT_EQUAL_STRING(it->fullPath.c_str(), folderPlaylist->getAbsolutePath(i).c_str()); - i++; - } - } - - // cleanup - folderPlaylist->clear(); - - // test memory actions - TEST_ASSERT_EQUAL_MESSAGE(1 + numFiles, allocCount, "Calls to malloc"); - TEST_ASSERT_EQUAL_MESSAGE(8, deAllocCount, "Calls to free"); - TEST_ASSERT_EQUAL_MESSAGE(2, reAllocCount, "Calls to realloc"); + // this test will access a mock file system implementation + mockfs::Node musicFolder = { + .fullPath = "/sdcard/music/folderE", + .isDir = true, + .content = std::vector(), + .files = { + {.fullPath = "/sdcard/music/folderE/song1.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector()}, + {.fullPath = "/sdcard/music/folderE/song2.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector()}, + {.fullPath = "/sdcard/music/folderE/song3.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector()}, + {.fullPath = "/sdcard/music/folderE/song4.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector()}, + {.fullPath = "/sdcard/music/folderE/A Folder", + .isDir = true, + .content = std::vector(), + .files = std::vector()}, + {.fullPath = "/sdcard/music/folderE/song5.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector()}, + {.fullPath = "/sdcard/music/folderE/song6.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector()}, + } + }; + constexpr size_t numFiles = 6; + File root(mockfs::MockFileImp::open(&musicFolder, true)); + + folderPlaylist->createFromFolder(root); + TEST_ASSERT_EQUAL_MESSAGE(numFiles, folderPlaylist->size(), "Number of elements in Playlist"); + + size_t i = 0; + for (auto it = musicFolder.files.begin(); it != musicFolder.files.end(); it++) { + if (!it->isDir) { + TEST_ASSERT_EQUAL_STRING(it->fullPath.c_str(), folderPlaylist->getAbsolutePath(i).c_str()); + i++; + } + } + + // cleanup + folderPlaylist->clear(); + + // test memory actions + TEST_ASSERT_EQUAL_MESSAGE(1 + numFiles, allocCount, "Calls to malloc"); + TEST_ASSERT_EQUAL_MESSAGE(8, deAllocCount, "Calls to free"); + TEST_ASSERT_EQUAL_MESSAGE(2, reAllocCount, "Calls to realloc"); } void test_folder_content_special_char(void) { - // this test will access a mock file system implementation - mockfs::Node musicFolder = { - .fullPath = "/sdcard/music/131072 ๐Ÿ˜€", - .isDir = true, - .content = std::vector(), - .files = { - { - .fullPath = "/sdcard/music/131072 ๐Ÿ˜€/Zongz.mp3", - .isDir = false, - .content = std::vector(), - .files = std::vector() - }, - { - .fullPath = "/sdcard/music/131072 ๐Ÿ˜€/Mรผsรคk.mp3", - .isDir = false, - .content = std::vector(), - .files = std::vector() - }, - { - .fullPath = "/sdcard/music/131072 ๐Ÿ˜€/็‹—.mp3", - .isDir = false, - .content = std::vector(), - .files = std::vector() - }, - { - .fullPath = "/sdcard/music/131072 ๐Ÿ˜€/trรจs รฉlรฉgant.mp3", - .isDir = false, - .content = std::vector(), - .files = std::vector() - }, - { - .fullPath = "/sdcard/music/131072 ๐Ÿ˜€/A Folder", - .isDir = true, - .content = std::vector(), - .files = std::vector() - }, - { - .fullPath = "/sdcard/music/131072 ๐Ÿ˜€/ั€ะพััะธัะฝะธะฝ.mp3", - .isDir = false, - .content = std::vector(), - .files = std::vector() - }, - { - .fullPath = "/sdcard/music/131072 ๐Ÿ˜€/" "\xD9\x85\xD9\x88\xD8\xB3\xD9\x8a\xD9\x82" ".mp3", - .fullPath = "/sdcard/music/131072 ๐Ÿ˜€/" "\xD9\x85\xD9\x88\xD8\xB3\xD9\x8a\xD9\x82" ".mp3", - .valid = true, - .isDir = false, - .content = std::vector(), - .files = std::vector() - }, - } - }; - constexpr size_t numFiles = 6; - File root(mockfs::MockFileImp::open(&musicFolder, true)); - - folderPlaylist->createFromFolder(root); - TEST_ASSERT_EQUAL_MESSAGE(numFiles, folderPlaylist->size(), "Number of elements in Playlist"); - - size_t i=0; - for(auto it=musicFolder.files.begin();it!=musicFolder.files.end();it++){ - log_n("Path: %s", it->fullPath.c_str()); - if(!it->isDir) { - TEST_ASSERT_EQUAL_STRING(it->fullPath.c_str(), folderPlaylist->getAbsolutePath(i).c_str()); - i++; - } - } - - // cleanup - folderPlaylist->clear(); - - // test memory actions - TEST_ASSERT_EQUAL_MESSAGE(1 + numFiles, allocCount, "Calls to malloc"); - TEST_ASSERT_EQUAL_MESSAGE(8, deAllocCount, "Calls to free"); - TEST_ASSERT_EQUAL_MESSAGE(2, reAllocCount, "Calls to realloc"); -} + // this test will access a mock file system implementation + mockfs::Node musicFolder = { + .fullPath = "/sdcard/music/131072 ๐Ÿ˜€", + .isDir = true, + .content = std::vector(), + .files = { + {.fullPath = "/sdcard/music/131072 ๐Ÿ˜€/Zongz.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector()}, + {.fullPath = "/sdcard/music/131072 ๐Ÿ˜€/Mรผsรคk.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector()}, + {.fullPath = "/sdcard/music/131072 ๐Ÿ˜€/็‹—.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector()}, + {.fullPath = "/sdcard/music/131072 ๐Ÿ˜€/trรจs รฉlรฉgant.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector()}, + {.fullPath = "/sdcard/music/131072 ๐Ÿ˜€/A Folder", + .isDir = true, + .content = std::vector(), + .files = std::vector()}, + {.fullPath = "/sdcard/music/131072 ๐Ÿ˜€/ั€ะพััะธัะฝะธะฝ.mp3", + .isDir = false, + .content = std::vector(), + .files = std::vector()}, + {.fullPath = "/sdcard/music/131072 ๐Ÿ˜€/" + "\xD9\x85\xD9\x88\xD8\xB3\xD9\x8a\xD9\x82" + ".mp3", + .fullPath = "/sdcard/music/131072 ๐Ÿ˜€/" + "\xD9\x85\xD9\x88\xD8\xB3\xD9\x8a\xD9\x82" + ".mp3", + .valid = true, + .isDir = false, + .content = std::vector(), + .files = std::vector()}, + } + }; + constexpr size_t numFiles = 6; + File root(mockfs::MockFileImp::open(&musicFolder, true)); + + folderPlaylist->createFromFolder(root); + TEST_ASSERT_EQUAL_MESSAGE(numFiles, folderPlaylist->size(), "Number of elements in Playlist"); + + size_t i = 0; + for (auto it = musicFolder.files.begin(); it != musicFolder.files.end(); it++) { + log_n("Path: %s", it->fullPath.c_str()); + if (!it->isDir) { + TEST_ASSERT_EQUAL_STRING(it->fullPath.c_str(), folderPlaylist->getAbsolutePath(i).c_str()); + i++; + } + } + + // cleanup + folderPlaylist->clear(); -void setup() -{ - Serial.begin(115200); - delay(2000); // service delay - UNITY_BEGIN(); + // test memory actions + TEST_ASSERT_EQUAL_MESSAGE(1 + numFiles, allocCount, "Calls to malloc"); + TEST_ASSERT_EQUAL_MESSAGE(8, deAllocCount, "Calls to free"); + TEST_ASSERT_EQUAL_MESSAGE(2, reAllocCount, "Calls to realloc"); +} - setup_static(); +void setup() { + Serial.begin(115200); + delay(2000); // service delay + UNITY_BEGIN(); - RUN_TEST(test_folder_alloc); - RUN_TEST(test_folder_content_absolute); - RUN_TEST(test_folder_content_relative); - RUN_TEST(test_folder_content_automatic); - RUN_TEST(test_folder_content_special_char); + setup_static(); + RUN_TEST(test_folder_alloc); + RUN_TEST(test_folder_content_absolute); + RUN_TEST(test_folder_content_relative); + RUN_TEST(test_folder_content_automatic); + RUN_TEST(test_folder_content_special_char); - UNITY_END(); // stop unit testing + UNITY_END(); // stop unit testing } -void loop() -{ +void loop() { } diff --git a/test/test_webstream/test_main.cpp b/test/test_webstream/test_main.cpp index cb17d677..0a33fe73 100644 --- a/test/test_webstream/test_main.cpp +++ b/test/test_webstream/test_main.cpp @@ -1,9 +1,11 @@ #include -#include -#include +#include "../mock_allocator.hpp" #include "playlists/WebstreamPlaylist.hpp" +#include +#include + size_t allocCount = 0; size_t deAllocCount = 0; size_t reAllocCount = 0; @@ -11,116 +13,79 @@ size_t reAllocCount = 0; size_t heap; size_t psram; -// mock allocator -struct UnitTestAllocator { - void* allocate(size_t size) { - void *ret; - if(psramInit()) { - ret = ps_malloc(size); - } else { - ret = malloc(size); - } - if(ret) { - allocCount++; - } - return ret; - } - - void deallocate(void* ptr) { - free(ptr); - deAllocCount ++; - } - - void* reallocate(void* ptr, size_t new_size) { - void *ret; - if(psramInit()) { - ret = ps_realloc(ptr, new_size); - } else { - ret = realloc(ptr, new_size); - } - if(ret) - reAllocCount++; - return ret; - } -}; - -constexpr const char* webStream = "http://test.com/stream.mp3"; +constexpr const char *webStream = "http://test.com/stream.mp3"; WebstreamPlaylistAlloc *webPlaylist; - void setUp(void) { - // set stuff up here, this function is before a test function + // set stuff up here, this function is before a test function } void tearDown(void) { - } void setup_static(void) { - webPlaylist = new WebstreamPlaylistAlloc(); + webPlaylist = new WebstreamPlaylistAlloc(); } void get_free_memory(void) { - heap = ESP.getFreeHeap(); - psram = ESP.getFreePsram(); + heap = ESP.getFreeHeap(); + psram = ESP.getFreePsram(); } void test_webstream_alloc(void) { - webPlaylist->setUrl(webStream); + webPlaylist->setUrl(webStream); - TEST_ASSERT_EQUAL_MESSAGE(1, allocCount, "Calls to malloc"); - TEST_ASSERT_EQUAL_MESSAGE(0, deAllocCount, "Calls to free"); - TEST_ASSERT_EQUAL_MESSAGE(0, reAllocCount, "Calls to realloc"); + TEST_ASSERT_EQUAL_MESSAGE(1, allocCount, "Calls to malloc"); + TEST_ASSERT_EQUAL_MESSAGE(0, deAllocCount, "Calls to free"); + TEST_ASSERT_EQUAL_MESSAGE(0, reAllocCount, "Calls to realloc"); } void test_webstream_content(void) { - TEST_ASSERT_EQUAL_STRING_MESSAGE(webStream, webPlaylist->getAbsolutePath().c_str(), "Test if stored value equal constructor value"); + TEST_ASSERT_EQUAL_STRING_MESSAGE(webStream, webPlaylist->getAbsolutePath().c_str(), "Test if stored value equal constructor value"); } void test_webstream_change(void) { - const char *newStream = "http://test2.com/stream.mp3"; - webPlaylist->setUrl(newStream); - TEST_ASSERT_EQUAL_STRING_MESSAGE(newStream, webPlaylist->getAbsolutePath().c_str(), "Test if stored value equal constructor value"); - - // test memory actions - TEST_ASSERT_EQUAL_MESSAGE(2, allocCount, "Calls to malloc"); - TEST_ASSERT_EQUAL_MESSAGE(1, deAllocCount, "Calls to free"); - TEST_ASSERT_EQUAL_MESSAGE(0, reAllocCount, "Calls to realloc"); + const char *newStream = "http://test2.com/stream.mp3"; + webPlaylist->setUrl(newStream); + TEST_ASSERT_EQUAL_STRING_MESSAGE(newStream, webPlaylist->getAbsolutePath().c_str(), "Test if stored value equal constructor value"); + + // test memory actions + TEST_ASSERT_EQUAL_MESSAGE(2, allocCount, "Calls to malloc"); + TEST_ASSERT_EQUAL_MESSAGE(1, deAllocCount, "Calls to free"); + TEST_ASSERT_EQUAL_MESSAGE(0, reAllocCount, "Calls to realloc"); } void test_webstream_dealloc(void) { - delete webPlaylist; + delete webPlaylist; - TEST_ASSERT_EQUAL_MESSAGE(2, allocCount, "Calls to malloc"); - TEST_ASSERT_EQUAL_MESSAGE(2, deAllocCount, "Calls to free"); - TEST_ASSERT_EQUAL_MESSAGE(0, reAllocCount, "Calls to realloc"); + TEST_ASSERT_EQUAL_MESSAGE(2, allocCount, "Calls to malloc"); + TEST_ASSERT_EQUAL_MESSAGE(2, deAllocCount, "Calls to free"); + TEST_ASSERT_EQUAL_MESSAGE(0, reAllocCount, "Calls to realloc"); } void test_free_memory(void) { - size_t cHeap = ESP.getFreeHeap(); - size_t cPsram = ESP.getFreePsram(); + size_t cHeap = ESP.getFreeHeap(); + size_t cPsram = ESP.getFreePsram(); - TEST_ASSERT_INT_WITHIN_MESSAGE(4, heap, cHeap, "Free heap after test (delta = 4 byte)"); - TEST_ASSERT_INT_WITHIN_MESSAGE(4, psram, cPsram, "Free psram after test (delta = 4 byte)"); + TEST_ASSERT_INT_WITHIN_MESSAGE(4, heap, cHeap, "Free heap after test (delta = 4 byte)"); + TEST_ASSERT_INT_WITHIN_MESSAGE(4, psram, cPsram, "Free psram after test (delta = 4 byte)"); } -void setup() -{ - delay(2000); // service delay - UNITY_BEGIN(); +void setup() { + delay(2000); // service delay + UNITY_BEGIN(); - get_free_memory(); - setup_static(); + get_free_memory(); + setup_static(); - RUN_TEST(test_webstream_alloc); - RUN_TEST(test_webstream_content); - RUN_TEST(test_webstream_change); - RUN_TEST(test_webstream_dealloc); - RUN_TEST(test_free_memory); + RUN_TEST(test_webstream_alloc); + RUN_TEST(test_webstream_content); + RUN_TEST(test_webstream_change); + RUN_TEST(test_webstream_dealloc); + RUN_TEST(test_free_memory); - UNITY_END(); // stop unit testing + UNITY_END(); // stop unit testing } -void loop() -{ -} \ No newline at end of file +void loop() { +} From 2e810d228a24a56e7ea46875e3c5a974fb2bc413 Mon Sep 17 00:00:00 2001 From: laszloh Date: Wed, 29 Nov 2023 17:01:33 +0100 Subject: [PATCH 8/8] Secure gPlayProperties more with the lock_guard Aquire the lock before making write access to the gPlayProperties. --- src/AudioPlayer.cpp | 13 ++++++++----- src/cpp.h | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/AudioPlayer.cpp b/src/AudioPlayer.cpp index 2da2ac0b..3e71b31c 100644 --- a/src/AudioPlayer.cpp +++ b/src/AudioPlayer.cpp @@ -947,15 +947,14 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l } #endif - gPlayProperties.startAtFilePos = _lastPlayPos; - gPlayProperties.currentTrackNumber = _trackLastPlayed; std::optional> newPlaylist = std::nullopt; bool error = false; if (_playMode != WEBSTREAM) { if (_playMode == RANDOM_SUBDIRECTORY_OF_DIRECTORY) { auto tmp = SdCard_pickRandomSubdirectory(_itemToPlay); // get a random subdirectory - if (tmp) { // If error occured while extracting random subdirectory + if (tmp) { + // If no error occured while extracting random subdirectory newPlaylist = SdCard_ReturnPlaylist(tmp.value().c_str(), _playMode); // Provide random subdirectory in order to enter regular playlist-generation } } else { @@ -965,6 +964,9 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l newPlaylist = AudioPlayer_ReturnPlaylistFromWebstream(_itemToPlay); } + // get the lock, since from here on we are modifying a shared variable between two Threads (IDLE & mp3play) + std::lock_guard guard(playlist_mutex); + // Catch if error occured (e.g. file not found) gPlayProperties.playMode = BUSY; // Show @Neopixel, if uC is busy with creating playlist if (!newPlaylist || !newPlaylist.value()->isValid()) { @@ -981,6 +983,8 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l return; } + gPlayProperties.startAtFilePos = _lastPlayPos; + gPlayProperties.currentTrackNumber = _trackLastPlayed; gPlayProperties.playMode = _playMode; gPlayProperties.numberOfTracks = newPlaylist.value()->size(); // Set some default-values @@ -996,7 +1000,7 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l gPrefsSettings.putString("lastRfid", gCurrentRfidTagId); #endif - switch (gPlayProperties.playMode) { + switch (_playMode) { case SINGLE_TRACK: { Log_Println(modeSingleTrack, LOGLEVEL_NOTICE); break; @@ -1082,7 +1086,6 @@ void AudioPlayer_TrackQueueDispatcher(const char *_itemToPlay, const uint32_t _l if (!error) { // transfer ownership of the new playlist to the audio thread - std::lock_guard guard(playlist_mutex); playlist = std::move(newPlaylist.value()); playlistChanged = true; return; diff --git a/src/cpp.h b/src/cpp.h index 49e1f99b..a0c2ddec 100644 --- a/src/cpp.h +++ b/src/cpp.h @@ -13,7 +13,7 @@ template constexpr std::array, N> to_array_impl(T (&&a)[N], std::index_sequence) { return {{std::move(a[I])...}}; -} // namespace detail +} } // namespace detail