ToobPlayer UI. .mdata file handling.

This commit is contained in:
Robin E. R. Davies
2025-06-19 16:04:32 -04:00
parent bf9846e37f
commit f35ee9330d
29 changed files with 671 additions and 425 deletions
@@ -309,6 +309,10 @@ namespace pipedal
const_iterator begin() const { return values.begin(); }
const_iterator end() const { return values.end(); }
iterator erase(iterator it)
{
return values.erase(it);
}
iterator find(const std::string &key);
const_iterator find(const std::string &key) const;
+2
View File
@@ -119,6 +119,8 @@ namespace pipedal
std::filesystem::path MakeRelativePath(const std::filesystem::path &path, const std::filesystem::path&parentPath);
bool IsSubdirectory(const std::filesystem::path &path, const std::filesystem::path &basePath);
bool HasDotDot(const std::filesystem::path &path);
std::string ToLower(const std::string&value);
+16
View File
@@ -190,6 +190,22 @@ std::filesystem::path pipedal::MakeRelativePath(const std::filesystem::path &pat
}
bool pipedal::HasDotDot(const std::filesystem::path &path)
{
for (auto &part : path)
{
if (part == "..")
{
return true;
}
if (part == ".")
{
return true;
}
}
return false;
}
bool pipedal::IsSubdirectory(const std::filesystem::path &path, const std::filesystem::path &basePath)
{
auto iPath = path.begin();
+30 -54
View File
@@ -145,8 +145,6 @@ namespace
virtual std::vector<AudioFileMetadata> GetFiles() override;
virtual ThumbnailTemporaryFile GetThumbnail(const std::string &fileNameOnly, int32_t width, int32_t height) override;
virtual ThumbnailTemporaryFile DefaultThumbnailTemporaryFile() override;
virtual size_t TestGetNumberOfThumbnails() override; // test use only.
virtual void TestSetIndexPath(const std::filesystem::path &path) override
{
@@ -160,15 +158,6 @@ 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<DbFileInfo> QueryTracks();
@@ -470,7 +459,7 @@ void AudioDirectoryInfoImpl::DbDeleteFile(DbFileInfo *dbFile)
audioFilesDb->DeleteFile(dbFile);
}
}
ThumbnailTemporaryFile AudioDirectoryInfoImpl::DefaultThumbnailTemporaryFile()
ThumbnailTemporaryFile AudioDirectoryInfo::DefaultThumbnailTemporaryFile()
{
ThumbnailTemporaryFile tempFile;
fs::path defaultThumbnail = GetResourceDirectory() / "img/missing_thumbnail.jpg";
@@ -783,13 +772,18 @@ std::string AudioDirectoryInfoImpl::GetNextAudioFile(const std::string &fileName
{
if (files[i].fileName() == fileNameOnly)
{
if (i + 1 < files.size())
size_t next = i;
while (true)
{
return files[i + 1].fileName();
}
else
{
return files[0].fileName(); // wrap around to the first file.
next = (next + 1) % files.size();
if (!isFolderArtwork(files[next].fileName()))
{
return files[next].fileName();
}
if (next == i)
{
break;
}
}
}
}
@@ -806,13 +800,25 @@ std::string AudioDirectoryInfoImpl::GetPreviousAudioFile(const std::string &file
{
if (files[i].fileName() == fileNameOnly)
{
if (i > 0)
size_t next = i;
while (true)
{
return files[i - 1].fileName();
}
else
{
return files[files.size() - 1].fileName(); // wrap around to the last file.
if (next == 0)
{
next = files.size() - 1; // wrap around to the last file.
}
else
{
--next; // go to the previous file.
}
if (!isFolderArtwork(files[next].fileName()))
{
return files[next].fileName();
}
if (next == i)
{
break;
}
}
}
}
@@ -912,33 +918,3 @@ namespace pipedal
}
}
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);
}
+1 -10
View File
@@ -95,20 +95,11 @@ 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,
int32_t toPosition) = 0;
virtual ThumbnailTemporaryFile DefaultThumbnailTemporaryFile() = 0;
static ThumbnailTemporaryFile DefaultThumbnailTemporaryFile();
static void SetTemporaryDirectory(const std::filesystem::path &path);
static void SetResourceDirectory(const std::filesystem::path &path);
-49
View File
@@ -192,13 +192,6 @@ void AudioFilesDb::CreateDb(const std::filesystem::path &dbPathName)
"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)
{
@@ -510,45 +503,3 @@ void AudioFilesDb::UpdateFilePosition(
updateFileQuery->exec();
}
void AudioFilesDb::DeleteFileExtraMetadata(
const std::string &fileNameOnly,
const std::string &key)
{
auto deleteExtraMetadataQuery = std::make_unique<SQLite::Statement>(
*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<SQLite::Statement>(
*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<SQLite::Statement>(
*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 "";
}
-11
View File
@@ -110,17 +110,6 @@ 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);
+170 -47
View File
@@ -22,9 +22,16 @@
#include "AudioFiles.hpp"
#include "Lv2Log.hpp"
#include "ss.hpp"
#include "util.hpp"
#include "json.hpp"
#include "json_variant.hpp"
#include <iostream>
#include <filesystem>
#include "lv2/lv2plug.in/ns/ext/buf-size/buf-size.h"
using namespace pipedal;
namespace fs = std::filesystem;
FileMetadataFeature::FileMetadataFeature()
{
@@ -33,12 +40,37 @@ FileMetadataFeature::FileMetadataFeature()
interface.handle = (void *)this;
interface.setFileMetadata = &FileMetadataFeature::S_setFileMetadata;
interface.getFileMetadata = &FileMetadataFeature::S_getFileMetadata;
interface.deleteFileMetadata = &FileMetadataFeature::S_deleteFileMetadata;
}
FileMetadataFeature::~FileMetadataFeature()
{
}
void FileMetadataFeature::SetPluginStoragePath(const std::filesystem::path &storagePath)
{
this->tracksPath = storagePath / "shared" / "audio" / "Tracks";
}
// Check if the path is within the tracks directory
bool FileMetadataFeature::IsValidPath(const std::filesystem::path &path) const
{
if (tracksPath.empty())
{
return false;
}
if (HasDotDot(path))
{
return false;
}
// is a subdirectory of track path
// check for directory seperator
if (!IsSubdirectory(path, tracksPath))
{
return false; // Valid subdirectory
}
return true;
}
void FileMetadataFeature::Prepare(MapFeature &map)
{
this->mapFeature = &map;
@@ -46,88 +78,179 @@ void FileMetadataFeature::Prepare(MapFeature &map)
PIPEDAL_FileMetadata_Status FileMetadataFeature::setFileMetadata(
const char *absolute_path,
LV2_URID key,
const char *fileMetadata)
const char *key,
const char *value)
{
const char *strKey = mapFeature->UridToString(key);
if (strKey == nullptr)
if (!IsValidPath(absolute_path))
{
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_INVALID_PATH; // Cannot set metadata for files in the tracks directory
}
std::filesystem::path path{SS(absolute_path << ".mdata")};
return PIPEDAL_FILE_METADATA_SUCCESS; // TODO: Implement this
json_variant jsonData;
if (fs::exists(path))
{
try
{
std::ifstream f(path);
json_reader reader(f);
reader.read(&jsonData);
}
catch (const std::exception &e)
{
jsonData = json_variant::make_object();
}
}
else
{
jsonData = json_variant::make_object();
}
(*jsonData.as_object())[key] = json_variant(value);
{
std::ofstream f(path);
if (!f.is_open())
{
Lv2Log::error(SS("Failed to write metadata file " << path));
return PIPEDAL_FILE_METADATA_PERMISSION_DENIED; // Failed to open existing metadata
}
json_writer writer(f);
writer.write(jsonData);
}
Lv2Log::debug(SS("Set file metadata for " << absolute_path << " key: " << key << " value: " << value));
return PIPEDAL_FILE_METADATA_SUCCESS;
}
uint32_t FileMetadataFeature::getFileMetadata(
const char *absolute_path,
LV2_URID key,
const char *key,
char *fileMetadata,
uint32_t fileMetadataSize)
{
const char *strKey = mapFeature->UridToString(key);
if (strKey == nullptr)
if (fileMetadata)
fileMetadata[0] = '\0'; // Ensure the buffer is null-terminated
if (!IsValidPath(absolute_path))
{
return PIPEDAL_FILE_METADATA_INVALID_KEY;
return 0; // Cannot set metadata for files in the tracks directory
}
std::filesystem::path path{absolute_path};
std::filesystem::path path{SS(absolute_path << ".mdata")};
AudioDirectoryInfo::Ptr audioDirectoryInfo = AudioDirectoryInfo::Create(path.parent_path());
json_variant jsonData;
std::string result;
std::string metadata = audioDirectoryInfo->GetFileMetadata(path.filename(), strKey);
if (metadata.empty())
if (fs::exists(path))
{
return 0; // No metadata found
try
{
std::ifstream f(path);
json_reader reader(f);
reader.read(&jsonData);
result = jsonData.as_object()->at(std::string(key)).as_string();
}
catch (const std::exception &e)
{
return 0;
}
}
size_t required = metadata.size() + 1;
if (required >= std::numeric_limits<uint32_t>::max())
else
{
return PIPEDAL_FILE_METADATA_ERR_UNKNOWNM; // Metadata too large
Lv2Log::debug(SS("No metadata file found for " << absolute_path << " key: " << key));
return 0;
}
if (fileMetadata == nullptr || required > fileMetadataSize)
size_t len = result.length() + 1; // +1 for null terminator
if (len > fileMetadataSize || fileMetadata == nullptr)
{
return static_cast<uint32_t>(required); // Return size needed including null terminator
// Not enough space in the buffer, return the size needed.
return len;
}
if (fileMetadataSize < metadata.length() + 1)
memcpy(fileMetadata, result.c_str(), len);
Lv2Log::debug(SS("Get file metadata for " << absolute_path << " key: " << key << " value: " << result));
return len;
}
PIPEDAL_FileMetadata_Status FileMetadataFeature::deleteFileMetadata(
const char *absolute_path,
const char *key)
{
if (!IsValidPath(absolute_path))
{
return static_cast<uint32_t>(metadata.size() + 1); // Return size needed including null terminator
return PIPEDAL_FILE_METADATA_INVALID_PATH; // Cannot set metadata for files in the tracks directory
}
std::strncpy(fileMetadata, metadata.c_str(), fileMetadataSize);
return required;
Lv2Log::debug(SS("Delete file metadata for " << absolute_path << " key: " << key));
std::filesystem::path path{SS(absolute_path << ".mdata")};
if (!fs::exists(path))
{
return PIPEDAL_FILE_METADATA_NOT_FOUND; // Metadata file does not exist
}
json_variant jsonData;
try
{
std::ifstream f(path);
json_reader reader(f);
reader.read(&jsonData);
}
catch (const std::exception &e)
{
return PIPEDAL_FILE_METADATA_NOT_FOUND; // Failed to read existing metadata
}
auto obj = jsonData.as_object();
auto it = obj->find(std::string(key));
if (it == obj->end())
{
return PIPEDAL_FILE_METADATA_NOT_FOUND; // Key not found
}
obj->erase(it);
if (obj->begin() == obj->end())
{
// If the object is empty, delete the metadata file
fs::remove(path);
return PIPEDAL_FILE_METADATA_SUCCESS; // Metadata deleted successfully
}
std::ofstream f(path);
if (!f.is_open())
{
Lv2Log::error(SS("Failed to write metadata file " << path));
return PIPEDAL_FILE_METADATA_PERMISSION_DENIED; // Failed to open existing metadata
}
json_writer writer(f);
writer.write(jsonData);
return PIPEDAL_FILE_METADATA_SUCCESS;
}
PIPEDAL_FileMetadata_Status FileMetadataFeature::S_setFileMetadata(
PIPEDAL_FILE_METADATA_Handle handle,
const char *absolute_path,
LV2_URID key,
const char *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,
PIPEDAL_FILE_METADATA_Handle handle,
const char *absolute_path,
const char *key,
char *buffer,
uint32_t bufferSize)
{
return ((FileMetadataFeature *)handle)->getFileMetadata(absolute_path, key, buffer, bufferSize);
}
PIPEDAL_FileMetadata_Status FileMetadataFeature::S_deleteFileMetadata(
PIPEDAL_FILE_METADATA_Handle handle,
const char *absolute_path,
const char *key)
{
return ((FileMetadataFeature *)handle)->deleteFileMetadata(absolute_path, key);
}
+27 -5
View File
@@ -21,7 +21,7 @@
#include "MapFeature.hpp"
#include "PiPedalUI.hpp"
#include "ext/PiPedal/FileMetadataFeature.h"
#include "lv2ext/pipedal.lv2/ext/FileMetadataFeature.h"
namespace pipedal
{
@@ -32,27 +32,49 @@ namespace pipedal
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 PIPEDAL_FileMetadata_Status S_setFileMetadata(
PIPEDAL_FILE_METADATA_Handle handle,
const char *absolute_path,
const char*key,
const char *fileMetadata);
PIPEDAL_FileMetadata_Status setFileMetadata(
const char *absolute_path,
const char*key,
const char *fileMetadata);
static uint32_t S_getFileMetadata(
PIPEDAL_FILE_METADATA_Handle handle,
LV2_URID key,
const char *filePath,
const char*key,
char *buffer,
uint32_t bufferSize);
uint32_t getFileMetadata(
const char *absolute_path,
LV2_URID key,
const char*key,
char *fileMetadata,
uint32_t fileMetadataSize);
static PIPEDAL_FileMetadata_Status S_deleteFileMetadata(
PIPEDAL_FILE_METADATA_Handle handle,
const char *absolute_path,
const char*key);
PIPEDAL_FileMetadata_Status deleteFileMetadata(
const char *absolute_path,
const char*key);
bool IsValidPath(const std::filesystem::path &path) const;
public:
FileMetadataFeature();
void Prepare(MapFeature &map);
void SetPluginStoragePath(const std::filesystem::path &storagePath);
~FileMetadataFeature();
public:
std::filesystem::path tracksPath;
MapFeature *mapFeature = nullptr;
const LV2_Feature *GetFeature()
{
+1
View File
@@ -95,6 +95,7 @@ namespace pipedal
virtual void OnNetworkChanging(bool hotspotConnected) = 0;
virtual void OnHasWifiChanged(bool hasWifi) = 0;
virtual void Close() = 0;
};
class HotspotManager;
+5
View File
@@ -249,6 +249,7 @@ static float nodeAsFloat(const LilvNode *node, float default_ = 0)
void PluginHost::SetPluginStoragePath(const std::filesystem::path &path)
{
pluginStoragePath = path;
fileMetadataFeature.SetPluginStoragePath(path);
}
std::string PluginHost::GetPluginStoragePath() const
@@ -266,6 +267,9 @@ PluginHost::PluginHost()
optionsFeature.Prepare(mapFeature, 44100, this->GetMaxAudioBufferSize(), this->GetAtomBufferSize());
lv2Features.push_back(optionsFeature.GetFeature());
fileMetadataFeature.Prepare(mapFeature);
lv2Features.push_back(fileMetadataFeature.GetFeature());
lv2Features.push_back(nullptr);
@@ -902,6 +906,7 @@ std::vector<std::string> supportedFeatures = {
LV2_STATE__freePath,
LV2_CORE__inPlaceBroken,
PIPEDAL_HOST_FEATURE,
PIPEDAL__FILE_METADATA_FEATURE,
// UI features that we can ignore, since we won't load their ui.
"http://lv2plug.in/ns/extensions/ui#makeResident",
+5 -1
View File
@@ -25,6 +25,7 @@
#include "PluginType.hpp"
#include <lilv/lilv.h>
#include "MapFeature.hpp"
#include "FileMetadataFeature.hpp"
#include "OptionsFeature.hpp"
#include <filesystem>
#include <cmath>
@@ -794,6 +795,7 @@ namespace pipedal
std::vector<const LV2_Feature *> lv2Features;
MapFeature mapFeature;
FileMetadataFeature fileMetadataFeature;
OptionsFeature optionsFeature;
std::string pluginStoragePath;
@@ -858,9 +860,11 @@ namespace pipedal
virtual std::shared_ptr<HostWorkerThread> GetHostWorkerThread();
public:
virtual MapFeature &GetMapFeature() { return this->mapFeature; }
virtual MapFeature &GetMapFeature() override { return this->mapFeature; }
void CheckForResourceInitialization(const std::string& pluginUri,const std::filesystem::path& pluginUploadDirectory);
std::string MapResourcePath(const std::string&uri, const std::string&relativePath);
void ReloadPlugins();
+66 -1
View File
@@ -140,6 +140,12 @@ void PresetBundleWriterImpl::WriteToFile(const std::filesystem::path &filePath)
{
Lv2Log::warning(SS("Media file not found: " << sourcePath));
}
std::filesystem::path metadataPath = SS(sourcePath.string() << ".mdata");
if (std::filesystem::exists(metadataPath))
{
zipFile->WriteFile(SS(zipName << ".mdata"), metadataPath);
}
}
zipFile->Close();
}
@@ -236,7 +242,28 @@ private:
void ExtractMediaFile(const std::string &zipFileName);
bool IsSameFile(const std::string &zipFileName, const std::filesystem::path &filePath)
{
return zipFile->CompareFiles(zipFileName, filePath);
if (!zipFile->CompareFiles(zipFileName, filePath))
{
return false;
}
std::string metadataZipFilename = SS(zipFileName <<".mdata");
std::filesystem::path metadataFilename = SS(filePath.string() <<".mdata");
if (!std::filesystem::exists(metadataFilename))
{
if (!zipFile->FileExists(metadataZipFilename))
{
return true;
}
return false;
} else {
if (!zipFile->FileExists(metadataZipFilename))
{
return false;
}
return zipFile->CompareFiles(metadataZipFilename, metadataFilename);
}
return true;
}
void RenameMediaFileProperty(const std::string oldName, const std::string &newName);
@@ -268,6 +295,36 @@ void PresetBundleReaderImpl::RenameMediaFileProperty(const std::string oldName,
}
}
}
std::string fullOldPath = (pluginUploadDirectory / oldName).string();
for (auto &property : plugin->pathProperties_)
{
std::stringstream ss(property.second);
json_reader reader(ss);
json_variant vProperty;
reader.read(&vProperty);
if (vProperty.is_object())
{
auto obj = vProperty.as_object();
if (obj->contains("otype_") &&
(*obj)["otype_"].as_string() == "Path")
{
if (obj->contains("value"))
{
std::string value = (*obj)["value"].as_string();
if (value == fullOldPath)
{
(*obj)["value"] = (pluginUploadDirectory / newName).string();
std::ostringstream ss;
json_writer writer(ss);
writer.write(vProperty);
plugin->pathProperties_[property.first] = ss.str();
break;
}
}
}
}
}
}
}
for (auto preset : pluginPresets.presets_)
@@ -363,6 +420,10 @@ void PresetBundleReaderImpl::ExtractMediaFile(const std::string &zipFileName)
namespace fs = std::filesystem;
if (zipFileName.starts_with("media/"))
{
if (zipFileName.ends_with(".mdata"))
{
return;
}
std::string baseName = zipFileName.substr(6);
fs::path targetFileName = this->pluginUploadDirectory / std::filesystem::path(baseName);
bool renamed = false;
@@ -383,6 +444,10 @@ void PresetBundleReaderImpl::ExtractMediaFile(const std::string &zipFileName)
else
{
zipFile->ExtractTo(zipFileName, targetFileName);
if (zipFile->FileExists(SS(zipFileName << ".mdata")))
{
zipFile->ExtractTo(SS(zipFileName << ".mdata"), SS(targetFileName.string() << ".mdata"));
}
break;
}
}
+30 -2
View File
@@ -2023,6 +2023,11 @@ void Storage::DeleteSampleFile(const std::filesystem::path &fileName)
else
{
std::filesystem::remove(fileName);
if (IsInAudioTracksDirectory(fileName))
{
// remove the metadata file as well.
std::filesystem::remove(fileName.string() + ".mdata");
}
}
}
catch (const std::exception &)
@@ -2180,6 +2185,10 @@ std::string Storage::RenameFilePropertyFile(
}
std::filesystem::rename(oldPath, newPath);
if (fs::exists(oldPath.string()+".mdata")) {
// rename the metadata file as well.
std::filesystem::rename(oldPath.string()+".mdata", newPath.string()+".mdata");
}
return newPath;
}
@@ -2209,11 +2218,17 @@ fs::path MakeVersionedPath(const fs::path &path)
}
}
int version = 1;
int version = 0;
while (fs::exists(newPath))
{
// append (n) to the file name.
std::string newFileName = stem + " (" + std::to_string(version) + ")" + newPath.extension().string();
std::string newFileName;
if (version == 0) {
newFileName = SS(stem << newPath.extension().string());
} else {
// append (n) to the file name.
newFileName = SS(stem << " (" << std::to_string(version) << ")" << newPath.extension().string());
}
newPath = newPath.parent_path() / newFileName;
++version;
}
@@ -2260,6 +2275,19 @@ std::string Storage::CopyFilePropertyFile(
}
std::filesystem::create_hard_link(oldPath, newPath);
if (IsInAudioTracksDirectory(oldPath)
&& IsInAudioTracksDirectory(newPath))
{
std::filesystem::path metadataPath = SS(oldPath.string() << ".mdata");
if (fs::exists(metadataPath)) {
// copy the metadata file as well.
std::filesystem::copy_file(
metadataPath,
SS(newPath.string() << ".mdata"),
fs::copy_options::overwrite_existing);
}
}
return newPath;
}
+22 -26
View File
@@ -59,21 +59,6 @@ using namespace pipedal;
using namespace boost::system;
namespace fs = std::filesystem;
static bool HasDotDot(const std::filesystem::path &path)
{
for (auto &part : path)
{
if (part == "..")
{
return true;
}
if (part == ".")
{
return true;
}
}
return false;
}
class UserUploadResponse
{
@@ -535,30 +520,41 @@ public:
try
{
fs::path path = request_uri.query("path");
if (path.empty())
if (path.empty() ||
!fs::exists(path) ||
!this->model->IsInUploadsDirectory(path) ||
HasDotDot(path))
{
// path for folder thumbnails.
path = request_uri.query("ffile");
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
std::shared_ptr<TemporaryFile> thumbnail;
if (!path.empty() && fs::exists(path) &&
this->model->IsInUploadsDirectory(path) &&
!HasDotDot(path))
{
throw PiPedalException("File not found.");
// path is a folder.
thumbnail = std::make_shared<TemporaryFile>();
thumbnail->SetNonDeletedPath(path);
}
else
{
auto t = AudioDirectoryInfo::DefaultThumbnailTemporaryFile();
thumbnail = std::make_shared<TemporaryFile>();
thumbnail->SetNonDeletedPath(t.Path());
}
std::shared_ptr<TemporaryFile> thumbnail = std::make_shared<TemporaryFile>();
thumbnail->SetNonDeletedPath(path);
res.set(HttpField::content_type, MimeTypes::instance().MimeTypeFromExtension(path.extension()));
res.set(HttpField::cache_control, CACHE_CONTROL_INDEFINITELY); // URL is cache-busted, and will change if the file ismodified.
setLastModifiedFromFile(res,path);
res.set(HttpField::content_length, std::to_string(fs::file_size(path)));
setLastModifiedFromFile(res,thumbnail->Path());
res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnail->Path())));
res.setBodyFile(thumbnail);
return;
}
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
{
throw PiPedalException("File not found.");
}
int32_t width = ConvertThumbnailSize(request_uri.query("w"));
int32_t height = ConvertThumbnailSize(request_uri.query("h"));
+20 -14
View File
@@ -52,15 +52,27 @@ public:
{
throw std::runtime_error("Can't open zip file.");
}
zip_int64_t nEntries = zip_get_num_entries(zipFile, 0);
for (zip_int64_t i = 0; i < nEntries; ++i)
{
const char *name = zip_get_name(zipFile, i, ZIP_FL_ENC_STRICT);
if (name)
{
files.push_back(name);
nameMap[name] = i;
}
}
}
virtual ~ZipFileImpl();
virtual std::vector<std::string> GetFiles() override;
virtual const std::vector<std::string>& GetFiles() override;
virtual bool CompareFiles(const std::string &zipName, const std::filesystem::path& path) override;
virtual void ExtractTo(const std::string &zipName, const std::filesystem::path &path) override;
virtual zip_file_input_stream GetFileInputStream(const std::string& filename,size_t bufferSize = 16*1024) override;
virtual size_t GetFileSize(const std::string&filename) override;
virtual bool FileExists(const std::string &zipName) const override;
private:
std::vector<std::string> files;
std::map<std::string, zip_int64_t> nameMap; // avoid o(2) extraction operations.
const std::filesystem::path path;
zip_t *zipFile = nullptr;
@@ -79,20 +91,9 @@ ZipFileImpl::~ZipFileImpl()
}
}
std::vector<std::string> ZipFileImpl::GetFiles()
const std::vector<std::string>& ZipFileImpl::GetFiles()
{
std::vector<std::string> result;
zip_int64_t nEntries = zip_get_num_entries(zipFile, 0);
for (zip_int64_t i = 0; i < nEntries; ++i)
{
const char *name = zip_get_name(zipFile, i, ZIP_FL_ENC_STRICT);
if (name)
{
result.push_back(name);
nameMap[name] = i;
}
}
return result;
return files;
}
@@ -293,6 +294,11 @@ zip_file_input_stream ZipFileImpl::GetFileInputStream(const std::string& filenam
}
return zip_file_input_stream(f,bufferSize);
}
bool ZipFileImpl::FileExists(const std::string&fileName) const
{
return nameMap.find(fileName) != nameMap.end();
}
size_t ZipFileImpl::GetFileSize(const std::string&filename)
{
zip_stat_t stat;
+2 -1
View File
@@ -68,11 +68,12 @@ namespace pipedal {
ZipFileReader&operator=(const ZipFileReader&) = delete;
virtual ~ZipFileReader();
virtual std::vector<std::string> GetFiles() = 0;
virtual const std::vector<std::string>& GetFiles() = 0;
virtual void ExtractTo(const std::string &zipName, const std::filesystem::path& path) = 0;
virtual bool CompareFiles(const std::string &zipName, const std::filesystem::path& path) = 0;
virtual zip_file_input_stream GetFileInputStream(const std::string& filename,size_t bufferSize = 16*1024) = 0;
virtual size_t GetFileSize(const std::string&filename) = 0;
virtual bool FileExists(const std::string&fileName) const = 0;
};
-105
View File
@@ -1,105 +0,0 @@
/*
* 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
@@ -0,0 +1,129 @@
/*
* 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_FILE_METADATA_NOT_FOUND = 5, /**< Metadata not found. */
} 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,
const char *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 used to identify the metdadata for a particular appliction.
@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,
const char *filePath,
const char *key,
char *buffer,
uint32_t bufferSize);
/**
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 used to identify the metdadata for a particular appliction.
@return A status code indicating success or failure.
This function will delete the metadata for the file, if it exists. If the file does not exist,
or is not in the Tracks directory, it will return PIPEDAL_FILE_METADATA_INVALID_PATH.
*/
PIPEDAL_FileMetadata_Status (*deleteFileMetadata)(
PIPEDAL_FILE_METADATA_Handle handle,
const char *filePath,
const char *key);
} PIPEDAL_FileMetadata_Interface;
typedef struct
{
const char *URI;
PIPEDAL_FileMetadata_Interface *interface;
} PIPEDAL_FileMetadata_Feature;
#ifdef __cplusplus
}
#endif
#endif // PIPEDAL_FILE_METADAtA_FEATURE_H
+1 -1
View File
@@ -1,3 +1,4 @@
Numeric up-down buttons blown in light theme.
Tooltips on touch ui?
@@ -28,4 +29,3 @@ pcm.pipedal_aux_in {
}
}
Move to /usr/local because...?q
+35 -1
View File
@@ -21,7 +21,7 @@
* SOFTWARE.
*/
import { pathConcat, pathParentDirectory } from "./FileUtils";
import { pathConcat, pathParentDirectory, pathFileName } from "./FileUtils";
import { PiPedalModel } from "./PiPedalModel";
export class ThumbnailType {
@@ -32,6 +32,40 @@ export class ThumbnailType {
};
const fileVersionRegex = /\((\d+)\)\.[0-9a-zA-Z]*$/;
function getVersionSuffix(filePath: string): string | null {
const match = filePath.match(fileVersionRegex);
return match ? match[1] : null;
}
export function getTrackTitle(pathname: string, metadata: AudioFileMetadata | null | undefined): string {
if (!metadata) {
return pathFileName(pathname);
}
if (metadata.title === "") {
return pathFileName(pathname);
}
let trackDisplay = "";
if (metadata.track > 0) {
if (metadata.track >= 1000) {
trackDisplay = (metadata.track % 1000).toString() + "/" + Math.floor(metadata.track / 1000) + ". ";
} else {
trackDisplay = metadata.track.toString() + ". ";
}
}
let result = trackDisplay + metadata.title;
if (result === "") {
result = pathFileName(pathname);
}
let versionSuffix = getVersionSuffix(pathname);
if (versionSuffix) {
result += " (" + versionSuffix + ")";
}
return result;
}
export default class AudioFileMetadata {
//AudioFileMetadata&operator=(const AudioFileMetadata&) = default;
deserialize(o: any) {
+13 -13
View File
@@ -18,7 +18,7 @@
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import React, { Component } from 'react';
import IconButton from '@mui/material/IconButton';
import IconButtonEx from './IconButtonEx';
import Typography from '@mui/material/Typography';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import { BankIndexEntry, BankIndex } from './Banks';
@@ -422,17 +422,17 @@ const BankDialog = withStyles(
<div style={{ flex: "0 0 auto" }}>
<AppBar className={classes.dialogAppBar} style={{ display: this.isEditMode() ? "none" : "block" }} >
<Toolbar>
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
<IconButtonEx tooltip="Back" edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
disabled={this.isEditMode()}
>
<ArrowBackIcon />
</IconButton>
</IconButtonEx>
<Typography noWrap variant="h6" className={classes.dialogTitle}>
Banks
</Typography>
<IconButton color="inherit" onClick={(e) => this.showActionBar(true)} >
<IconButtonEx tooltip="Edit" color="inherit" onClick={(e) => this.showActionBar(true)} >
<EditIcon />
</IconButton>
</IconButtonEx>
</Toolbar>
</AppBar>
<AppBar className={actionBarClass} style={{ display: this.isEditMode() ? "block" : "none" }}
@@ -440,14 +440,14 @@ const BankDialog = withStyles(
>
<Toolbar>
{(!this.props.isEditDialog) ? (
<IconButton edge="start" color="inherit" onClick={(e) => this.showActionBar(false)} aria-label="close">
<IconButtonEx tooltip="Close" edge="start" color="inherit" onClick={(e) => this.showActionBar(false)} aria-label="close">
<CloseIcon />
</IconButton>
</IconButtonEx>
) : (
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
<IconButtonEx tooltip="Back" edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
>
<ArrowBackIcon />
</IconButton>
</IconButtonEx>
)}
<Typography noWrap variant="h6" className={classes.dialogTitle}>
@@ -477,12 +477,12 @@ const BankDialog = withStyles(
}
}
/>
<IconButton color="inherit" onClick={(e) => this.handleDeleteClick()} >
<IconButtonEx tooltip="Delete" color="inherit" onClick={(e) => this.handleDeleteClick()} >
<img src="/img/old_delete_outline_white_24dp.svg" alt="Delete" style={{ width: 24, height: 24, opacity: 0.6 }} />
</IconButton>
<IconButton color="inherit" onClick={(e) => { this.onMoreClick(e) }} >
</IconButtonEx>
<IconButtonEx tooltip="More..." color="inherit" onClick={(e) => { this.onMoreClick(e) }} >
<MoreVertIcon />
</IconButton>
</IconButtonEx>
<Menu
id="more-menu"
anchorEl={this.state.moreMenuAnchorEl}
+4 -1
View File
@@ -42,7 +42,10 @@ function ButtonEx(props: ButtonExProps) {
placement="top-start" arrow
enterDelay={1500} enterNextDelay={1500}
>
<Button {...extra} style={style} />
<span>
{/* Using span to prevent tooltip from disappearing when button is disabled */}
<Button {...extra} style={style} />
</span>
</Tooltip>
);
}
+8 -26
View File
@@ -62,7 +62,7 @@ import UploadFileDialog from './UploadFileDialog';
import OkCancelDialog from './OkCancelDialog';
import HomeIcon from '@mui/icons-material/Home';
import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDialog';
import { getAlbumArtUri } from './AudioFileMetadata';
import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata';
interface Point {
x: number;
@@ -95,6 +95,7 @@ const audioFileExtensions: { [name: string]: boolean } = {
};
function screenToClient(element: HTMLDivElement, point: Point): Point {
const dpr = (window.devicePixelRatio || 1) as number;;
let rect = element.getBoundingClientRect();
@@ -650,15 +651,18 @@ export default withStyles(
}
getCompactTrackTitle(fileEntry: FileEntry): string {
let metadata = fileEntry.metadata;
let title = getTrackTitle(fileEntry.pathname, metadata);
if (!metadata) {
return "#error";
return title;
}
let title = this.getTrackTitle(fileEntry);
if (metadata.album !== "") {
title += " (" + metadata.album + ")";
}
return title;
}
getAlbumTitle(fileEntry: FileEntry): string {
let artist = fileEntry.metadata?.artist || "";
if (artist == "") {
@@ -668,28 +672,6 @@ export default withStyles(
let joiner = (artist !== "" && album !== "") ? " - " : "";
return album + joiner + artist;
}
getTrackTitle(fileEntry: FileEntry): string {
if (!fileEntry.metadata) {
return pathFileName(fileEntry.pathname);
}
if (fileEntry.metadata.title === "") {
return pathFileName(fileEntry.pathname);
}
let metadata = fileEntry.metadata;
let trackDisplay = "";
if (metadata.track > 0) {
if (metadata.track >= 1000) {
trackDisplay = (metadata.track % 1000).toString() + "/" + Math.floor(metadata.track / 1000) + ". ";
} else {
trackDisplay = metadata.track.toString() + ". ";
}
}
let result = trackDisplay + metadata.title;
if (result === "") {
result = pathFileName(fileEntry.pathname);
}
return result;
}
getTrackThumbnail(fileEntry: FileEntry): string {
return getAlbumArtUri(this.model, fileEntry.metadata, fileEntry.pathname);
}
@@ -1100,7 +1082,7 @@ export default withStyles(
className={classes.secondaryText}
variant="body2"
style={{ flex: "0 0 auto", textOverflow: "ellipsis", textAlign: "left", marginBottom: 4 }}>
{this.getTrackTitle(value)}
{getTrackTitle(value.pathname, value.metadata)}
</Typography>
<Typography noWrap className={classes.secondaryText}
variant="body2"
+11 -4
View File
@@ -42,6 +42,7 @@ import { AndroidHostInterface, FakeAndroidHost } from './AndroidHost';
import { ColorTheme, getColorScheme, setColorScheme } from './DarkMode';
import FilePropertyDirectoryTree from './FilePropertyDirectoryTree';
import AudioFileMetadata from './AudioFileMetadata';
import { pathFileName } from './FileUtils';
export enum State {
@@ -2445,10 +2446,13 @@ export class PiPedalModel //implements PiPedalModel
let downloadUrl = this.varServerUrl + "downloadMediaFile?path=" + encodeURIComponent(filePath);
// download with no flashing temporary tab.
let link = window.document.createElement("A") as HTMLLinkElement;
let link = window.document.createElement("A") as HTMLAnchorElement;
link.href = downloadUrl;
link.setAttribute("download", "");
link.target = "_blank";
link.setAttribute("download", pathFileName(filePath));
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
download(targetType: string, instanceId: number | string): void {
@@ -2457,10 +2461,13 @@ export class PiPedalModel //implements PiPedalModel
// window.open(url, "_blank");
// download with no flashing temporary tab.
let link = window.document.createElement("A") as HTMLLinkElement;
let link = window.document.createElement("A") as HTMLAnchorElement;
link.href = url;
link.setAttribute("download", "");
link.target = "_blank";
link.download = "download.piPreset";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
uploadUserFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise<string> {
+21 -22
View File
@@ -450,13 +450,13 @@ const PluginControlView =
/>
));
}
private ixKey: number = 1;
private ixKey: number = 1;
makeStandardControl(uiControl: UiControl, controlValues: ControlValue[]): ReactNode {
let symbol = uiControl.symbol;
if (!uiControl.is_input) {
return (
<PluginOutputControl key={uiControl.symbol } instanceId={this.props.instanceId} uiControl={uiControl} />
<PluginOutputControl key={uiControl.symbol} instanceId={this.props.instanceId} uiControl={uiControl} />
);
}
@@ -472,7 +472,7 @@ const PluginControlView =
throw new PiPedalStateError("Missing control value.");
}
return ((
<PluginControl key={uiControl.symbol} instanceId={this.props.instanceId} uiControl={uiControl} value={controlValue.value}
<PluginControl key={"ppc" + uiControl.symbol} instanceId={this.props.instanceId} uiControl={uiControl} value={controlValue.value}
onChange={(value: number) => { this.onControlValueChanged(controlValue!.key, value) }}
onPreviewChange={(value: number) => { this.onPreviewChange(controlValue!.key, value) }}
requestIMEEdit={(uiControl: any, value: any) => this.requestImeEdit(uiControl, value)}
@@ -498,7 +498,7 @@ const PluginControlView =
let previousControl = controls[controls.length - 1];
if (!(previousControl instanceof ControlGroup)) {
let pair = (
<div key={"k"+this.ixKey++} className={classes.controlPair}>
<div key={"k" + this.ixKey++} className={classes.controlPair}>
{previousControl as ReactNode}
{newControl}
</div>
@@ -508,7 +508,7 @@ const PluginControlView =
// (e.g.. inserting at position 4 still places the extended control after four previous controls
controls.push((
<div key={"k"+this.ixKey++} className={classes.controlSpacer} />
<div key={"k" + this.ixKey++} className={classes.controlSpacer} />
));
} else {
controls.push(newControl);
@@ -686,7 +686,7 @@ const PluginControlView =
let item = controlGroup.controls[j];
controls.push(
(
<div key={"ctl" + (this.controlKeyIndex++)} className={classes.controlPadding}>
<div key={"ctlx" + (this.controlKeyIndex++)} className={classes.controlPadding}>
{item}
</div>
@@ -695,7 +695,7 @@ const PluginControlView =
}
result.push((
<div key={"ctl" + (this.controlKeyIndex++)} className={!isLandscapeGrid ? classes.portGroup : classes.portGroupLandscape}>
<div key={"ctlx" + (this.controlKeyIndex++)} className={!isLandscapeGrid ? classes.portGroup : classes.portGroupLandscape}>
<div className={classes.portGroupTitle}>
<Tooltip title={controlGroup.name}
placement="top-start" arrow enterDelay={1500} enterNextDelay={1500}
@@ -713,21 +713,20 @@ const PluginControlView =
));
} else {
if (this.fullScreen())
{
result.push(
<div style={{position: "relative", width: "100%", height:"100%"}}
>
{node as ReactNode}
</div>
);
} else {
result.push((
<div key={"ctl" + (this.controlKeyIndex++)} className={hasGroups ? classes.portgroupControlPadding : classes.controlPadding} >
{node as ReactNode}
</div>
));
}
if (this.fullScreen()) {
result.push(
<div key={"fullScreen" + this.props.instanceId} style={{ position: "relative", width: "100%", height: "100%" }}
>
{node as ReactNode}
</div>
);
} else {
result.push((
<div key={"ctl" + (this.controlKeyIndex++)} className={hasGroups ? classes.portgroupControlPadding : classes.controlPadding} >
{node as ReactNode}
</div>
));
}
}
}
+15 -14
View File
@@ -20,10 +20,11 @@
import React, { SyntheticEvent,Component } from 'react';
import { css } from '@emotion/react';
import {isDarkMode} from './DarkMode';
import IconButton from '@mui/material/IconButton';
import IconButtonEx from './IconButtonEx';
import Typography from '@mui/material/Typography';
import { PiPedalModel, PiPedalModelFactory, PresetIndexEntry, PresetIndex } from './PiPedalModel';
import Button from "@mui/material/Button";
import Button from '@mui/material/Button';
import ButtonEx from './ButtonEx';
import ButtonBase from "@mui/material/ButtonBase";
import DialogEx from './DialogEx';
import AppBar from '@mui/material/AppBar';
@@ -351,17 +352,17 @@ const PresetDialog = withStyles(
<div style={{ flex: "0 0 auto" }}>
<AppBar className={classes.dialogAppBar} style={{ display: this.isEditMode() ? "none" : "block" }} >
<Toolbar>
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
<IconButtonEx tooltip="Back" edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
disabled={this.isEditMode()}
>
<ArrowBackIcon />
</IconButton>
</IconButtonEx>
<Typography variant="h6" className={classes.dialogTitle}>
Presets
</Typography>
<IconButton color="inherit" onClick={(e) => this.showActionBar(true)} >
<IconButtonEx tooltip="Edit"color="inherit" onClick={(e) => this.showActionBar(true)} >
<EditIcon />
</IconButton>
</IconButtonEx>
</Toolbar>
</AppBar>
<AppBar className={actionBarClass} style={{ display: this.isEditMode() ? "block" : "none" }}
@@ -369,14 +370,14 @@ const PresetDialog = withStyles(
>
<Toolbar>
{(!this.props.isEditDialog) ? (
<IconButton edge="start" color="inherit" onClick={(e) => this.showActionBar(false)} aria-label="close">
<IconButtonEx tooltip="Close" edge="start" color="inherit" onClick={(e) => this.showActionBar(false)} aria-label="close">
<CloseIcon />
</IconButton>
</IconButtonEx>
) : (
<IconButton edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back"
<IconButtonEx edge="start" tooltip="Back" color="inherit" onClick={this.handleDialogClose} aria-label="back"
>
<ArrowBackIcon />
</IconButton>
</IconButtonEx>
)}
<Typography variant="h6" className={classes.dialogTitle}>
@@ -402,12 +403,12 @@ const PresetDialog = withStyles(
}
}
/>
<IconButton color="inherit" onClick={(e) => this.handleDeleteClick()} >
<IconButtonEx tooltip="Delete" color="inherit" onClick={(e) => this.handleDeleteClick()} >
<img src="/img/old_delete_outline_white_24dp.svg" alt="Delete" style={{ width: 24, height: 24, opacity: 0.6 }} />
</IconButton>
<IconButton color="inherit" onClick={(e) => { this.onMoreClick(e) }} >
</IconButtonEx>
<IconButtonEx tooltip="More..." color="inherit" onClick={(e) => { this.onMoreClick(e) }} >
<MoreVertIcon />
</IconButton>
</IconButtonEx>
<Menu
id="more-menu"
anchorEl={this.state.moreMenuAnchorEl}
+29 -14
View File
@@ -45,6 +45,8 @@ import { getAlbumArtUri } from './AudioFileMetadata';
import RepeatIcon from '@mui/icons-material/Repeat';
import Timebase, { LoopParameters, TimebaseUnits } from './Timebase';
import ControlSlider from './ControlSlider';
import { getTrackTitle } from './AudioFileMetadata';
let Player__seek = "http://two-play.com/plugins/toob-player#seek"
const AUDIO_FILE_PROPERTY_URI = "http://two-play.com/plugins/toob-player#audioFile";
@@ -320,19 +322,9 @@ export default function ToobPlayerControl(
}
model.getAudioFileMetadata(path)
.then((metadata) => {
let strTrack: string = "";
if (metadata.track > 0) {
let disc = (metadata.track / 1000);
if (disc >= 1) {
strTrack = (metadata.track % 1000).toString() + '/' + Math.floor(disc).toString();
} else {
strTrack = metadata.track.toString();
}
strTrack += ". ";
}
let coverArtUri = getAlbumArtUri(model, metadata, path);
setCoverArt(coverArtUri);
setTitle(strTrack + metadata.title);
setTitle(getTrackTitle(path, metadata));
setAlbum(metadata.album);
setArtist(metadata.artist);
setAlbumArtist(metadata.albumArtist);
@@ -419,6 +411,8 @@ export default function ToobPlayerControl(
);
}
function FilePanel() {
let textColor = pluginState == PluginState.Error ? "error" : "textPrimary";
return (
<ButtonBase style={{ display: "block", width: "100%", borderRadius: 10, textAlign: "left" }}
onClick={() => { SelectFile() }}
@@ -442,10 +436,10 @@ export default function ToobPlayerControl(
</Typography>
) : (
<div>
<Typography variant="body1" noWrap>
<Typography variant="body1" color={textColor} noWrap>
{titleLine}
</Typography>
<Typography variant="body2" noWrap sx={{}}>
<Typography variant="body2" color={textColor} noWrap sx={{}}>
{albumLine}
</Typography>
</div>
@@ -483,6 +477,27 @@ export default function ToobPlayerControl(
function onLoopPropertyChanged(loopSettingsJson: string) {
try {
if (loopSettingsJson === "") {
setTimebase(
{
units: TimebaseUnits.Seconds,
tempo: 120.0,
timeSignature: { numerator: 4, denominator: 4 }
})
let loopParameters = {
start: 0.0,
loopEnable: false,
loopStart: 0.0,
loopEnd: 0.0
};
setLoopParameters(loopParameters);
setStart(0.0);
setLoopEnable(false);
setLoopStart(0.0);
setLoopEnd(0.0);
return;
}
let atomObject = JSON.parse(loopSettingsJson);
let loopParameters: LoopParameters = atomObject.loopParameters as LoopParameters;
let newTimebase: Timebase | undefined = atomObject.timebase as (Timebase | undefined);;
@@ -827,7 +842,7 @@ export default function ToobPlayerControl(
playing={!paused}
onPreview={() => { handlePreview(); }}
value={loopParameters}
onCancelPlaying= {()=> { handleCancelPlaying();}}
onCancelPlaying={() => { handleCancelPlaying(); }}
timebase={timebase}
onTimebaseChange={(newTimebase) => {
setTimebase(newTimebase);
+4 -3
View File
@@ -110,14 +110,14 @@ const ToobPlayerView =
for (let mixControl of mixPanel.controls) {
extraControls.push(
(
<div key={"k"+ iKey++} style={{flex: "0 0 auto", position: "relative",height: "100%" }}>
<div key={"kExtra"+ iKey++} style={{flex: "0 0 auto", position: "relative",height: "100%" }}>
{mixControl}
</div>
));
}
let panel = (
<ToobPlayerControl key="MusicPlayer" instanceId={this.props.instanceId} extraControls={extraControls} />
<ToobPlayerControl key={"MusicPlayer" + this.props.instanceId} instanceId={this.props.instanceId} extraControls={extraControls} />
);
let result: (React.ReactNode | ControlGroup)[] = [];
@@ -128,6 +128,7 @@ const ToobPlayerView =
render() {
return (
<PluginControlView
key={"ToobPlayerView" + this.props.instanceId}
instanceId={this.props.instanceId}
item={this.props.item}
customization={this}
@@ -145,7 +146,7 @@ class ToobPlayerViewFactory implements IControlViewFactory {
uri: string = "http://two-play.com/plugins/toob-player";
Create(model: PiPedalModel, pedalboardItem: PedalboardItem): React.ReactNode {
return (<ToobPlayerView instanceId={pedalboardItem.instanceId} item={pedalboardItem} />);
return (<ToobPlayerView key={"ppmv"+pedalboardItem.instanceId} instanceId={pedalboardItem.instanceId} item={pedalboardItem} />);
}