diff --git a/src/AudioFiles.cpp b/src/AudioFiles.cpp index b9cc1ad..a144241 100644 --- a/src/AudioFiles.cpp +++ b/src/AudioFiles.cpp @@ -160,6 +160,16 @@ namespace int32_t fromPosition, int32_t toPosition) override; + virtual void SetFileMetadata( + const std::string &fileNameOnly, + const std::string &key, + const std::string &value) override; + + virtual std::string GetFileMetadata( + const std::string &fileNameOnly, + const std::string &key) override; + + private: std::vector QueryTracks(); @@ -242,12 +252,22 @@ static int DefaultedSortOrder(int track) } return track; } + +static bool isFolderArtwork(const std::string &name) +{ + return name.ends_with(".jpg") || name.ends_with(".png"); +} + static void SortDbFiles(std::vector &dbFiles, Collator::ptr &collator) { std::sort( dbFiles.begin(), dbFiles.end(), [collator](const DbFileInfo &a, const DbFileInfo &b) { + if (isFolderArtwork(a.fileName()) != isFolderArtwork(b.fileName())) + { + return isFolderArtwork(a.fileName()) < isFolderArtwork(b.fileName()); + } if (a.position() != b.position()) { return DefaultedSortOrder(a.position()) < DefaultedSortOrder(b.position()); @@ -411,9 +431,8 @@ std::vector AudioDirectoryInfoImpl::UpdateDbFiles() else { // We have position info, so we need to update the position. - SortDbFiles(dbFiles, collator); - SortDbFiles(newFiles, collator); dbFiles.insert(dbFiles.end(), newFiles.begin(), newFiles.end()); + SortDbFiles(dbFiles, collator); for (size_t i = 0; i < dbFiles.size(); ++i) { @@ -892,3 +911,34 @@ namespace pipedal return IndexPathToShadowIndexPath(audioRootDirectory, path); } } + +void AudioDirectoryInfoImpl::SetFileMetadata( + const std::string &fileNameOnly, + const std::string &key, + const std::string &value) +{ + OpenAudioDb(); + if (!audioFilesDb) + { + throw std::runtime_error("Directory is not writable."); + } + this->UpdateDbFiles(); + + audioFilesDb->SetFileExtraMetadata(fileNameOnly, key, value); +} + +std::string AudioDirectoryInfoImpl::GetFileMetadata( + const std::string &fileNameOnly, + const std::string &key) +{ + OpenAudioDb(); + if (!audioFilesDb) + { + throw std::runtime_error("Directory is not writable."); + } + this->UpdateDbFiles(); // check for deletions. + + return audioFilesDb->GetFileExtraMetadata(fileNameOnly, key); +} + + diff --git a/src/AudioFiles.hpp b/src/AudioFiles.hpp index 2d9fabe..168e1fc 100644 --- a/src/AudioFiles.hpp +++ b/src/AudioFiles.hpp @@ -95,6 +95,15 @@ namespace pipedal virtual std::string GetNextAudioFile(const std::string &fileNameOnly) = 0; virtual std::string GetPreviousAudioFile(const std::string &fileNameOnly) = 0; + virtual void SetFileMetadata( + const std::string &fileNameOnly, + const std::string &key, + const std::string &value) = 0; + + virtual std::string GetFileMetadata( + const std::string &fileNameOnly, + const std::string &key) = 0; + virtual void MoveAudioFile( const std::string &directory, int32_t fromPosition, diff --git a/src/AudioFilesDb.cpp b/src/AudioFilesDb.cpp index bdc263c..b162f39 100644 --- a/src/AudioFilesDb.cpp +++ b/src/AudioFilesDb.cpp @@ -191,6 +191,14 @@ void AudioFilesDb::CreateDb(const std::filesystem::path &dbPathName) "thumbnail BLOB, " "width INTEGER, " "height INTEGER)"); + + db->exec("CREATE TABLE IF NOT EXISTS extraMetadata (" + "idExtra INTEGER PRIMARY KEY AUTOINCREMENT, " + "idFile INT64 NOT NULL, " + "key TEXT NOT NULL, " + "value TEXT NOT NULL, " + "FOREIGN KEY (idFile) REFERENCES files(idFile) ON DELETE CASCADE, " + "UNIQUE(idFile, key))"); } catch (const SQLite::Exception &e) { @@ -501,3 +509,46 @@ void AudioFilesDb::UpdateFilePosition( updateFileQuery->bind(2, idFile); updateFileQuery->exec(); } + +void AudioFilesDb::DeleteFileExtraMetadata( + const std::string &fileNameOnly, + const std::string &key) +{ + auto deleteExtraMetadataQuery = std::make_unique( + *db, + "DELETE FROM extraMetadata WHERE idFile = (SELECT idFile FROM files WHERE fileName = ?) AND key = ?"); + deleteExtraMetadataQuery->bind(1, fileNameOnly); + deleteExtraMetadataQuery->bind(2, key); + deleteExtraMetadataQuery->exec(); +} +void AudioFilesDb::SetFileExtraMetadata( + const std::string &fileNameOnly, + const std::string &key, + const std::string &value) +{ + + auto setExtraMetadataQuery = std::make_unique( + *db, + "INSERT OR REPLACE INTO extraMetadata (idFile, key, value) " + "VALUES ((SELECT idFile FROM files WHERE fileName = ?), ?, ?)"); + setExtraMetadataQuery->bind(1, fileNameOnly); + setExtraMetadataQuery->bind(2, key); + setExtraMetadataQuery->bind(3, value); + setExtraMetadataQuery->exec(); +} +std::string AudioFilesDb::GetFileExtraMetadata( + const std::string &fileNameOnly, + const std::string &key) +{ + auto getExtraMetadataQuery = std::make_unique( + *db, + "SELECT value FROM extraMetadata WHERE idFile = (SELECT idFile FROM files WHERE fileName = ?) AND key = ?"); + getExtraMetadataQuery->bind(1, fileNameOnly); + getExtraMetadataQuery->bind(2, key); + + if (getExtraMetadataQuery->executeStep()) + { + return getExtraMetadataQuery->getColumn(0).getText(); + } + return ""; +} diff --git a/src/AudioFilesDb.hpp b/src/AudioFilesDb.hpp index 1f44ecb..92d1286 100644 --- a/src/AudioFilesDb.hpp +++ b/src/AudioFilesDb.hpp @@ -110,6 +110,17 @@ namespace pipedal::impl { int64_t idFile, int32_t position); + void DeleteFileExtraMetadata( + const std::string &fileNameOnly, + const std::string &key); + void SetFileExtraMetadata( + const std::string &fileNameOnly, + const std::string &key, + const std::string &value); + std::string GetFileExtraMetadata( + const std::string &fileNameOnly, + const std::string &key); + private: void CreateDb(const std::filesystem::path &dbPathName); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6b744db..2f3b1d7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -275,6 +275,7 @@ set (PIPEDAL_SOURCES LogFeature.hpp LogFeature.cpp Worker.hpp Worker.cpp OptionsFeature.hpp OptionsFeature.cpp + FileMetadataFeature.hpp FileMetadataFeature.cpp VuUpdate.hpp VuUpdate.cpp Units.hpp Units.cpp RingBuffer.hpp diff --git a/src/FileMetadataFeature.cpp b/src/FileMetadataFeature.cpp new file mode 100644 index 0000000..4253215 --- /dev/null +++ b/src/FileMetadataFeature.cpp @@ -0,0 +1,135 @@ +// Copyright (c) 2022 Robin Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#include "pch.h" +#include "FileMetadataFeature.hpp" +#include "AudioFiles.hpp" +#include "Lv2Log.hpp" +#include "ss.hpp" + +#include "lv2/lv2plug.in/ns/ext/buf-size/buf-size.h" +using namespace pipedal; + +FileMetadataFeature::FileMetadataFeature() +{ + feature.URI = PIPEDAL__FILE_METADATA_FEATURE; + feature.data = &interface; + interface.handle = (void *)this; + interface.setFileMetadata = &FileMetadataFeature::S_setFileMetadata; + interface.getFileMetadata = &FileMetadataFeature::S_getFileMetadata; + + xxx publish the feature. + xxx restrict to tracks directory. +} + +FileMetadataFeature::~FileMetadataFeature() +{ +} +void FileMetadataFeature::Prepare(MapFeature &map) +{ + this->mapFeature = ↦ +} + +PIPEDAL_FileMetadata_Status FileMetadataFeature::setFileMetadata( + const char *absolute_path, + LV2_URID key, + const char *fileMetadata) +{ + const char *strKey = mapFeature->UridToString(key); + if (strKey == nullptr) + { + return PIPEDAL_FILE_METADATA_INVALID_KEY; + } + std::filesystem::path path{absolute_path}; + + if (!std::filesystem::exists(path)) + { + return PIPEDAL_FILE_METADATA_INVALID_PATH; // File does not exist + } + if (!std::filesystem::is_regular_file(path)) + { + return PIPEDAL_FILE_METADATA_INVALID_PATH; // Not a regular file + } + try { + AudioDirectoryInfo::Ptr audioDirectoryInfo = AudioDirectoryInfo::Create(path.parent_path()); + audioDirectoryInfo->SetFileMetadata(path.filename(), strKey, fileMetadata); + } catch (const std::exception&e) { + Lv2Log::error(SS("Failed to create AudioDirectoryInfo for " << path << ": " << e.what())); + return PIPEDAL_FILE_METADATA_INVALID_PATH; // Failed to create AudioDirectoryInfo + } + + + return PIPEDAL_FILE_METADATA_SUCCESS; // TODO: Implement this +} + +uint32_t FileMetadataFeature::getFileMetadata( + const char *absolute_path, + LV2_URID key, + char *fileMetadata, + uint32_t fileMetadataSize) +{ + const char *strKey = mapFeature->UridToString(key); + if (strKey == nullptr) + { + return PIPEDAL_FILE_METADATA_INVALID_KEY; + } + + std::filesystem::path path{absolute_path}; + + AudioDirectoryInfo::Ptr audioDirectoryInfo = AudioDirectoryInfo::Create(path.parent_path()); + + std::string metadata = audioDirectoryInfo->GetFileMetadata(path.filename(), strKey); + if (metadata.empty()) + { + return 0; // No metadata found + } + size_t required = metadata.size() + 1; + if (required >= std::numeric_limits::max()) + { + return PIPEDAL_FILE_METADATA_ERR_UNKNOWNM; // Metadata too large + } + if (fileMetadata == nullptr || required > fileMetadataSize) + { + return static_cast(required); // Return size needed including null terminator + } + if (fileMetadataSize < metadata.length() + 1) + { + return static_cast(metadata.size() + 1); // Return size needed including null terminator + } + std::strncpy(fileMetadata, metadata.c_str(), fileMetadataSize); + return required; +} + +PIPEDAL_FileMetadata_Status FileMetadataFeature::S_setFileMetadata( + PIPEDAL_FILE_METADATA_Handle handle, + const char *absolute_path, + LV2_URID key, + const char *fileMetadata) +{ + return ((FileMetadataFeature *)handle)->setFileMetadata(absolute_path, key, fileMetadata); +} +uint32_t FileMetadataFeature::S_getFileMetadata( + PIPEDAL_FILE_METADATA_Handle handle, + LV2_URID key, + const char *absolute_path, + char *buffer, + uint32_t bufferSize) +{ + return ((FileMetadataFeature *)handle)->getFileMetadata(absolute_path, key, buffer, bufferSize); +} diff --git a/src/FileMetadataFeature.hpp b/src/FileMetadataFeature.hpp new file mode 100644 index 0000000..c25b78f --- /dev/null +++ b/src/FileMetadataFeature.hpp @@ -0,0 +1,63 @@ +// Copyright (c) 2022 Robin Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#pragma once + +#include "MapFeature.hpp" +#include "PiPedalUI.hpp" +#include "ext/PiPedal/FileMetadataFeature.h" + +namespace pipedal +{ + + class FileMetadataFeature + { + + private: + LV2_Feature feature; + PIPEDAL_FileMetadata_Interface interface; + static PIPEDAL_FileMetadata_Status S_setFileMetadata(PIPEDAL_FILE_METADATA_Handle handle, const char *absolute_path, LV2_URID key, const char *fileMetadata); + PIPEDAL_FileMetadata_Status setFileMetadata(const char *absolute_path, LV2_URID key, const char *fileMetadata); + + static uint32_t S_getFileMetadata( + PIPEDAL_FILE_METADATA_Handle handle, + LV2_URID key, + const char *filePath, + char *buffer, + uint32_t bufferSize); + uint32_t getFileMetadata( + const char *absolute_path, + LV2_URID key, + char *fileMetadata, + uint32_t fileMetadataSize); + + public: + FileMetadataFeature(); + void Prepare(MapFeature &map); + ~FileMetadataFeature(); + + public: + MapFeature *mapFeature = nullptr; + const LV2_Feature *GetFeature() + { + return &feature; + } + }; + +} \ No newline at end of file diff --git a/src/PiPedalUI.hpp b/src/PiPedalUI.hpp index 52a9ff5..9a2ef3f 100644 --- a/src/PiPedalUI.hpp +++ b/src/PiPedalUI.hpp @@ -1,18 +1,18 @@ /* * MIT License - * + * * Copyright (c) 2023 Robin E. R. Davies - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -31,9 +31,9 @@ #include #include #include "ModFileTypes.hpp" +#include "stdint.h" - -#define PIPEDAL_HOST_FEATURE "http://github.com/rerdavies/pipedal#host" // Plugin can only be hosted by PiPedal +#define PIPEDAL_HOST_FEATURE "http://github.com/rerdavies/pipedal#host" // Plugin can only be hosted by PiPedal #define PIPEDAL_PATCH "http://github.com/rerdavies/pipedal/patch" #define PIPEDAL_PATCH_PREFIX PIPEDAL_PATCH "#" @@ -47,17 +47,17 @@ #define PIPEDAL_UI__fileProperties PIPEDAL_UI_PREFIX "fileProperties" #define PIPEDAL_UI__fileProperty PIPEDAL_UI_PREFIX "fileProperty" -#define PIPEDAL_UI__patchProperty PIPEDAL_UI_PREFIX "patchProperty" -#define PIPEDAL_UI__directory PIPEDAL_UI_PREFIX "directory" -#define PIPEDAL_UI__fileTypes PIPEDAL_UI_PREFIX "fileTypes" +#define PIPEDAL_UI__patchProperty PIPEDAL_UI_PREFIX "patchProperty" +#define PIPEDAL_UI__directory PIPEDAL_UI_PREFIX "directory" +#define PIPEDAL_UI__fileTypes PIPEDAL_UI_PREFIX "fileTypes" #define PIPEDAL_UI__resourceDirectory PIPEDAL_UI_PREFIX "resourceDirectory" -#define PIPEDAL_UI__fileType PIPEDAL_UI_PREFIX "fileType" -#define PIPEDAL_UI__fileExtension PIPEDAL_UI_PREFIX "fileExtension" -#define PIPEDAL_UI__mimeType PIPEDAL_UI_PREFIX "mimeType" +#define PIPEDAL_UI__fileType PIPEDAL_UI_PREFIX "fileType" +#define PIPEDAL_UI__fileExtension PIPEDAL_UI_PREFIX "fileExtension" +#define PIPEDAL_UI__mimeType PIPEDAL_UI_PREFIX "mimeType" -#define PIPEDAL_UI__outputPorts PIPEDAL_UI_PREFIX "outputPorts" -#define PIPEDAL_UI__text PIPEDAL_UI_PREFIX "text" +#define PIPEDAL_UI__outputPorts PIPEDAL_UI_PREFIX "outputPorts" +#define PIPEDAL_UI__text PIPEDAL_UI_PREFIX "text" #define PIPEDAL_UI__frequencyPlot PIPEDAL_UI_PREFIX "frequencyPlot" #define PIPEDAL_UI__xLeft PIPEDAL_UI_PREFIX "xLeft" @@ -67,57 +67,57 @@ #define PIPEDAL_UI__yBottom PIPEDAL_UI_PREFIX "yBottom" #define PIPEDAL_UI__width PIPEDAL_UI_PREFIX "width" -#define PIPEDAL_UI__ledColor PIPEDAL_UI_PREFIX "ledColor" +#define PIPEDAL_UI__ledColor PIPEDAL_UI_PREFIX "ledColor" #define PIPEDAL_UI__graphicEq PIPEDAL_UI_PREFIX "graphicEq" - -namespace pipedal { +namespace pipedal +{ class PluginHost; - - class UiFileType { + class UiFileType + { private: std::string label_; std::string mimeType_; std::string fileExtension_; + public: - UiFileType() { } - UiFileType(PluginHost*pHost, const LilvNode*node); - UiFileType(const std::string&label, const std::string &fileType); + UiFileType() {} + UiFileType(PluginHost *pHost, const LilvNode *node); + UiFileType(const std::string &label, const std::string &fileType); + static std::vector GetArray(PluginHost *pHost, const LilvNode *node, const LilvNode *uri); - static std::vector GetArray(PluginHost*pHost, const LilvNode*node,const LilvNode*uri); - - const std::string& label() const { return label_;} + const std::string &label() const { return label_; } const std::string &fileExtension() const { return fileExtension_; } const std::string &mimeType() const { return mimeType_; } - bool IsValidExtension(const std::string&extension) const; + bool IsValidExtension(const std::string &extension) const; + public: DECLARE_JSON_MAP(UiFileType); - }; - - - class UiPortNotification { + class UiPortNotification + { private: int32_t portIndex_; std::string symbol_; std::string plugin_; std::string protocol_; + public: using ptr = std::shared_ptr; - UiPortNotification() { } - UiPortNotification(PluginHost*pHost, const LilvNode*node); - + UiPortNotification() {} + UiPortNotification(PluginHost *pHost, const LilvNode *node); + public: DECLARE_JSON_MAP(UiPortNotification); - }; - class UiFileProperty { + class UiFileProperty + { private: std::string label_; std::int32_t index_ = -1; @@ -127,57 +127,59 @@ namespace pipedal { std::string portGroup_; std::string resourceDirectory_; std::vector modDirectories_; - bool useLegacyModDirectory_= false; - std::map> fileExtensionsByModDirectory; // non-serialized. + bool useLegacyModDirectory_ = false; + std::map> fileExtensionsByModDirectory; // non-serialized. void PrecalculateFileExtensions(); + public: using ptr = std::shared_ptr; - UiFileProperty() { } - UiFileProperty(PluginHost*pHost, const LilvNode*node, const std::filesystem::path&resourcePath); - UiFileProperty(const std::string&label, const std::string&patchProperty,const std::string &directory); + UiFileProperty() {} + UiFileProperty(PluginHost *pHost, const LilvNode *node, const std::filesystem::path &resourcePath); + UiFileProperty(const std::string &label, const std::string &patchProperty, const std::string &directory); UiFileProperty( - const std::string&label, - const std::string&patchProperty, - const ModFileTypes&modFileType); + const std::string &label, + const std::string &patchProperty, + const ModFileTypes &modFileType); - void setModFileTypes(const ModFileTypes&modFileType); + void setModFileTypes(const ModFileTypes &modFileType); + + std::vector &modDirectories() { return modDirectories_; } + const std::vector &modDirectories() const { return modDirectories_; } - std::vector& modDirectories() { return modDirectories_; } - const std::vector& modDirectories() const { return modDirectories_; } - bool useLegacyModDirectory() const { return useLegacyModDirectory_; } void useLegacyModDirectory(bool value) { useLegacyModDirectory_ = value; } const std::string &label() const { return label_; } int32_t index() const { return index_; } void index(int32_t value) { index_ = value; } - + const std::string &directory() const { return directory_; } void directory(const std::string &path) { directory_ = path; } - const std::string&portGroup() const { return portGroup_; } + const std::string &portGroup() const { return portGroup_; } const std::vector &fileTypes() const { return fileTypes_; } std::vector &fileTypes() { return fileTypes_; } const std::string &patchProperty() const { return patchProperty_; } - bool IsValidExtension(const std::filesystem::path&relativePath) const; + bool IsValidExtension(const std::filesystem::path &relativePath) const; - static bool IsDirectoryNameValid(const std::string&value); + static bool IsDirectoryNameValid(const std::string &value); - static std::string GetFileExtension(const std::filesystem::path&path); - - const std::string&resourceDirectory() const { return resourceDirectory_; } + static std::string GetFileExtension(const std::filesystem::path &path); + const std::string &resourceDirectory() const { return resourceDirectory_; } - const std::set& GetPermittedFileExtensions(const std::string &modDirectory) const; + const std::set &GetPermittedFileExtensions(const std::string &modDirectory) const; + + std::string getParentModDirectory(const std::filesystem::path &path) const; - std::string getParentModDirectory(const std::filesystem::path&path) const; public: DECLARE_JSON_MAP(UiFileProperty); }; - class UiFrequencyPlot { + class UiFrequencyPlot + { private: std::string patchProperty_; std::int32_t index_ = -1; @@ -188,15 +190,16 @@ namespace pipedal { float yBottom_ = -30; bool xLog_ = true; float width_ = 60; + public: using ptr = std::shared_ptr; - UiFrequencyPlot() { } - UiFrequencyPlot(PluginHost*pHost, const LilvNode*node, - const std::filesystem::path&resourcePath); + UiFrequencyPlot() {} + UiFrequencyPlot(PluginHost *pHost, const LilvNode *node, + const std::filesystem::path &resourcePath); const std::string &patchProperty() const { return patchProperty_; } int32_t index() const { return index_; } - const std::string&portGroup() const { return portGroup_; } + const std::string &portGroup() const { return portGroup_; } float xLeft() const { return xLeft_; } float xRight() const { return xRight_; } bool xLog() const { return xLog_; } @@ -208,32 +211,31 @@ namespace pipedal { DECLARE_JSON_MAP(UiFrequencyPlot); }; - class PiPedalUI { + class PiPedalUI + { public: using ptr = std::shared_ptr; - PiPedalUI(PluginHost*pHost, const LilvNode*uiNode, const std::filesystem::path&resourcePath); + PiPedalUI(PluginHost *pHost, const LilvNode *uiNode, const std::filesystem::path &resourcePath); PiPedalUI( std::vector &&fileProperties, std::vector &&frequencyPlots); PiPedalUI( std::vector &&fileProperties); - const std::vector& fileProperties() const + const std::vector &fileProperties() const { return fileProperties_; } - const std::vector& frequencyPlots() const + const std::vector &frequencyPlots() const { return frequencyPlots_; } - - const std::vector &portNotifications() const { return portNotifications_; } - const UiFileProperty*GetFileProperty(const std::string &propertyUri) const + const UiFileProperty *GetFileProperty(const std::string &propertyUri) const { - for (const auto&fileProperty : fileProperties()) + for (const auto &fileProperty : fileProperties()) { if (fileProperty->patchProperty() == propertyUri) { @@ -243,7 +245,8 @@ namespace pipedal { return nullptr; } bool unsupportedPatchProperty() const { return unsupportedPatchProperty_; } - void unsupportedPatchProperty(bool value) { unsupportedPatchProperty_ = value; } + void unsupportedPatchProperty(bool value) { unsupportedPatchProperty_ = value; } + private: bool unsupportedPatchProperty_ = false; // not serialized. std::vector fileProperties_; @@ -252,7 +255,6 @@ namespace pipedal { }; // utilities for validating file paths received via PiPedalFileProperty-related APIs. - bool IsAlphaNumeric(const std::string&value); - + bool IsAlphaNumeric(const std::string &value); }; \ No newline at end of file diff --git a/src/Storage.cpp b/src/Storage.cpp index 217c782..4ae1b77 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -2182,6 +2182,44 @@ std::string Storage::RenameFilePropertyFile( std::filesystem::rename(oldPath, newPath); return newPath; } + +fs::path MakeVersionedPath(const fs::path &path) +{ + if (!fs::exists(path)) + { + return path; // no need to version a non-existing file. + } + fs::path newPath = path; + auto stem = newPath.stem().string();; + if (stem.ends_with(")")) + { + // remove the trailing (n) from the file name. + size_t pos = stem.find_last_of('('); + std::string stemVersion = stem.substr(pos+1, stem.length()-1-(pos+1)); + // check if it is a number. + if (stemVersion.find_first_not_of("0123456789") == std::string::npos) + { + // it is a number, remove the trailing (n). + stem = stem.substr(0, pos); + while (stem.ends_with(" ")) + { + // remove trailing space. + stem = stem.substr(0, stem.length() - 1); + } + } + } + + int version = 1; + while (fs::exists(newPath)) + { + // append (n) to the file name. + std::string newFileName = stem + " (" + std::to_string(version) + ")" + newPath.extension().string(); + newPath = newPath.parent_path() / newFileName; + ++version; + } + return newPath; +} + std::string Storage::CopyFilePropertyFile( const std::string &oldRelativePath, const std::string &newRelativePath, @@ -2217,11 +2255,7 @@ std::string Storage::CopyFilePropertyFile( } else { - if (!overwrite) { - return ""; // signal a portential overwrite. - } else { - fs::remove(newPath); - } + newPath = MakeVersionedPath(newPath); } } diff --git a/src/ext/PiPedal/FileMetadataFeature.h b/src/ext/PiPedal/FileMetadataFeature.h new file mode 100644 index 0000000..c1facf3 --- /dev/null +++ b/src/ext/PiPedal/FileMetadataFeature.h @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2025 Robin E. R. Davies + * All rights reserved. + + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef PIPEDAL_FILE_METADAtA_FEATURE_H +#define PIPEDAL_FILE_METADAtA_FEATURE_H +#include "lv2/core/lv2.h" +#include "lv2/urid/urid.h" + +#define PIPEDAL__FILE_METADATA_FEATURE "http://github.com/rerdavies/pipedal/ext/#fileMetadata" + +#ifdef __cplusplus +extern "C" +{ +#endif + typedef void *PIPEDAL_FILE_METADATA_Handle; + + typedef enum + { + PIPEDAL_FILE_METADATA_SUCCESS = 0, /**< Completed successfully. */ + PIPEDAL_FILE_METADATA_INVALID_PATH = 1, /**< Path is not in Tracks directory, or does not exist. */ + PIPEDAL_FILE_METADATA_PERMISSION_DENIED = 2, /**< Permission denied. */ + PIPEDAL_FILE_METADATA_ERR_UNKNOWNM = 3, /**< Unknown error. */ + PIPEDAL_FILE_METADATA_INVALID_KEY = 4 /**< Key is not a valid URID. */ + + } PIPEDAL_FileMetadata_Status; + + typedef struct + { + /** + Opaque host data. + */ + PIPEDAL_FILE_METADATA_Handle handle; + + /** + Save a piece of plugin-defined metadata for a file. + @param handle MUST be the `handle` member of this struct. + @param absolute_path The absolute path of a file. + @param key A plugin-defined key (LV2_URID) used to identify the metdadata. + @param metdata A string containing the metadata to be saved for the file. + @return A status code indicating success or failure. + + The path must be within the host-defined "Tracks" directory. The file must exist. The plugin does not + need write access to the directory containing the file in order to save metadata. + + Plugins MUST NOT make any assumptions about abstract paths except that + they can be mapped back to the absolute path of the "same" file (though + not necessarily the same original path) using absolute_path(). + + Metatadata will be automatically deleted if the file is deleted, of the file is modified, or moved + to another directory. + + This function should not be called from the plugin's realtime thread, as it may block for a long time. + */ + + PIPEDAL_FileMetadata_Status (*setFileMetadata)(PIPEDAL_FILE_METADATA_Handle handle, const char *absolute_path, LV2_URID key, const char *fileMetadata); + /** + Restore previously saved metdata for the file. + @param handle MUST be the `handle` member of this struct. + @param absolute_path The absolute path of a file. + @param key A plugin-defined key (LV2_URID) used to identify the metdadata. + @param buffer A buffer in which to store the metadata. + @param buffersize The size of the buffer in bytes, including space for the null terminator. + @return A value indicating the number of bytes written to the buffer, or 0 if the metdata was not found. + + If buffersize is insufficient to holed the metatdata, the function will return the number of bytes that would have been written, + including the space for the null terminator, but will not write any data to the buffer. The best way to find out + how much space is needed is to call this function with a buffer size of 0, which will return the size needed to contain the + metadata result. + + This function should not be called from the plugin's realtime thread, as it may block for a long time. + + */ + uint32_t (*getFileMetadata)(PIPEDAL_FILE_METADATA_Handle handle, LV2_URID key, const char *filePath, char *buffer, uint32_t bufferSize); + } PIPEDAL_FileMetadata_Interface; + + typedef struct { + const char* URI; + PIPEDAL_FileMetadata_Interface*interface; + } PIPEDAL_FileMetadata_Feature; + +#ifdef __cplusplus +} +#endif + +#endif // PIPEDAL_FILE_METADAtA_FEATURE_H \ No newline at end of file diff --git a/vite/src/pipedal/FilePropertyDialog.tsx b/vite/src/pipedal/FilePropertyDialog.tsx index 04f83a9..2e9ccb0 100644 --- a/vite/src/pipedal/FilePropertyDialog.tsx +++ b/vite/src/pipedal/FilePropertyDialog.tsx @@ -789,6 +789,8 @@ export default withStyles( } return result; } + + private getIcon(fileEntry: FileEntry, largeIcon: boolean) { let style = largeIcon ? { @@ -1002,6 +1004,9 @@ export default withStyles( if (this.state.reordering && !value.metadata) { return null; // don't render non-track files when reordering. } + if (this.state.reordering && this.isFolderArtwork(value.pathname)) { + return null; + } let displayValue = value.displayName; if (displayValue === "") { displayValue = ""; @@ -1400,6 +1405,14 @@ export default withStyles( } }); + } else { + this.requestFiles(this.state.navDirectory); + this.setState({ + selectedFile: filename, + selectedFileIsDirectory: this.isDirectory(filename), + selectedFileProtected: false + }); + this.requestScroll = true; } }) .catch((e) => {