Audio file metadata and thumbnails
This commit is contained in:
+8
-207
@@ -22,220 +22,21 @@
|
||||
*/
|
||||
|
||||
#include "AudioFileMetadata.hpp"
|
||||
#include <json.hpp>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <json_variant.hpp>
|
||||
#include "LRUCache.hpp"
|
||||
#include <sstream>
|
||||
|
||||
using namespace pipedal;
|
||||
namespace fs = std::filesystem;
|
||||
namespace chrono = std::chrono;
|
||||
|
||||
|
||||
// Safely encode a filename for use in a shell command
|
||||
static std::string shell_escape_filename(const std::string& filename) {
|
||||
std::string escaped;
|
||||
escaped.reserve(filename.size() + 2); // Reserve space for quotes and content
|
||||
|
||||
// Wrap the filename in single quotes
|
||||
escaped += '\'';
|
||||
|
||||
for (char c : filename) {
|
||||
// Escape single quotes by closing the quote, adding escaped quote, and reopening
|
||||
if (c == '\'') {
|
||||
escaped += "'\\''";
|
||||
} else {
|
||||
escaped += c;
|
||||
}
|
||||
}
|
||||
|
||||
escaped += '\'';
|
||||
return escaped;
|
||||
}
|
||||
|
||||
|
||||
static std::string readPipeToString(FILE* file) {
|
||||
std::string content;
|
||||
char buffer[4096];
|
||||
size_t bytesRead;
|
||||
while ((bytesRead = fread(buffer, 1, sizeof(buffer), file)) > 0) {
|
||||
content.append(buffer, bytesRead);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
|
||||
static std::string GetJsonMetadata(const std::filesystem::path &path)
|
||||
{
|
||||
/* ffprobe -loglevel error -show_streams -show_format -print_format stream_tags -of json
|
||||
~/filename.flac */
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "/usr/bin/ffprobe -loglevel error -show_streams -show_format -print_format stream_tags -of json "
|
||||
<< shell_escape_filename(path.string())
|
||||
<< " 2>/dev/null";
|
||||
|
||||
FILE *output = popen(ss.str().c_str(),"r");
|
||||
if(output == nullptr)
|
||||
{
|
||||
throw std::runtime_error("Failed to process file.");
|
||||
}
|
||||
std::string result;
|
||||
try {
|
||||
result = readPipeToString(output);
|
||||
} catch (const std::exception&e)
|
||||
{
|
||||
pclose(output);
|
||||
throw std::runtime_error(e.what());
|
||||
}
|
||||
int retcode = pclose(output);
|
||||
if (retcode != 0)
|
||||
{
|
||||
throw std::runtime_error(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static float MetadataFloat(const json_variant&vt, float defaultValue = 0)
|
||||
{
|
||||
if (vt.is_string())
|
||||
{
|
||||
std::string s = vt.as_string();
|
||||
std::stringstream ss(s);
|
||||
float result = defaultValue;
|
||||
ss >> result;
|
||||
return result;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
static std::string MetadataString(const json_variant&o,const std::vector<std::string>& names) {
|
||||
for (const auto&name: names)
|
||||
{
|
||||
auto result = (*o.as_object())[name];
|
||||
if (result.is_string())
|
||||
{
|
||||
return result.as_string();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
AudioFileMetadata::AudioFileMetadata(const std::filesystem::path &file)
|
||||
{
|
||||
try {
|
||||
const std::string json = GetJsonMetadata(file);
|
||||
|
||||
json_variant vt;
|
||||
|
||||
std::stringstream ss(json);
|
||||
json_reader reader(ss);
|
||||
|
||||
reader.read(&vt);
|
||||
|
||||
auto top = vt.as_object();
|
||||
auto format = top->at("format").as_object();
|
||||
|
||||
this->duration_ = MetadataFloat(format->at("duration"),0);
|
||||
|
||||
auto tags = (*format)["tags"];
|
||||
if (tags.is_object())
|
||||
{
|
||||
this->album_ = MetadataString(tags,{"ALBUM","album"});
|
||||
this->artist_ = MetadataString(tags,{"ARTIST","artist"});
|
||||
this->albumArtist_ = MetadataString(tags,{"ALBUM ARTIST","album_artist","album artist"});
|
||||
this->title_ = MetadataString(tags,{"TITLE","title"});
|
||||
this->date_ = MetadataString(tags,{"DATE","date"});
|
||||
this->year_ = MetadataString(tags,{"YEAR","year"});
|
||||
this->track_ = MetadataString(tags,{"track","TRACK",});
|
||||
this->disc_ = MetadataString(tags,{"disc","DISC"});
|
||||
|
||||
if (title_ == "") {
|
||||
this->title_ = file.stem();
|
||||
}
|
||||
this->totalTracks_ = MetadataString(tags,{"TOTALTRACKS"});
|
||||
}
|
||||
} catch (const std::exception &e)
|
||||
{
|
||||
std::string title = file.stem();
|
||||
this->title_ = title;
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
class AudioCacheKey {
|
||||
public:
|
||||
AudioCacheKey(const std::filesystem::path &path) {
|
||||
this->path = path.string();
|
||||
this->lastWrite = std::filesystem::last_write_time(path);
|
||||
|
||||
}
|
||||
// Equality operator
|
||||
bool operator==(const AudioCacheKey& other) const {
|
||||
return path == other.path && lastWrite == other.lastWrite;
|
||||
}
|
||||
|
||||
public:
|
||||
std::string path;
|
||||
fs::file_time_type lastWrite;
|
||||
};
|
||||
}
|
||||
|
||||
namespace std {
|
||||
template <>
|
||||
struct hash<AudioCacheKey> {
|
||||
|
||||
|
||||
size_t operator()(const AudioCacheKey& key) const {
|
||||
size_t path_hash = hash<std::string>{}(key.path);
|
||||
size_t time_hash = hash<uintmax_t>{}(
|
||||
duration_cast<std::chrono::nanoseconds>(key.lastWrite.time_since_epoch()).count()
|
||||
);
|
||||
// Combine hashes (boost::hash_combine approach)
|
||||
return path_hash ^ (time_hash + 0x9e3779b9 + (path_hash << 6) + (path_hash >> 2));
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
static LRUCache<AudioCacheKey,std::string> metadataCache {500};
|
||||
static std::mutex metadataCacheMutex;
|
||||
|
||||
std::string pipedal::GetAudioFileMetadataString(const std::filesystem::path &path)
|
||||
{
|
||||
std::lock_guard lock { metadataCacheMutex};
|
||||
AudioCacheKey key { path};
|
||||
std::string result;
|
||||
if (metadataCache.get(key,result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
auto metadata = AudioFileMetadata(path);
|
||||
|
||||
std::stringstream ss;
|
||||
json_writer writer(ss);
|
||||
writer.write(metadata);
|
||||
result = ss.str();
|
||||
|
||||
metadataCache.put(key,result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
JSON_MAP_BEGIN(AudioFileMetadata)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, duration)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, fileName)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, lastModified)
|
||||
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, title)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, track)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, album)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, disc)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, artist)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, albumArtist)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, totalTracks)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, date)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, year)
|
||||
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, thumbnailType)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, thumbnailFile)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, thumbnailLastModified)
|
||||
JSON_MAP_REFERENCE(AudioFileMetadata, position)
|
||||
|
||||
JSON_MAP_END()
|
||||
|
||||
+43
-28
@@ -8,10 +8,10 @@
|
||||
* 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
|
||||
@@ -21,16 +21,16 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "json.hpp"
|
||||
|
||||
|
||||
namespace pipedal
|
||||
{
|
||||
|
||||
namespace pipedal {
|
||||
#define GETTER_SETTER_REF(name) \
|
||||
const decltype(name##_) &name() const { return name##_; } \
|
||||
void name(const decltype(name##_) &value) { name##_ = value; }
|
||||
@@ -46,41 +46,56 @@ namespace pipedal
|
||||
decltype(name##_) name() const { return name##_; } \
|
||||
void name(decltype(name##_) value) { name##_ = value; }
|
||||
|
||||
|
||||
enum ThumbnailType
|
||||
{
|
||||
Unknown = 0,
|
||||
Embedded = 1, // embedded in the file
|
||||
Folder = 2, // from a albumArt.jpg or similar
|
||||
None = 3 // Use default thumbnail.
|
||||
};
|
||||
|
||||
class AudioFileMetadata
|
||||
{
|
||||
private:
|
||||
float duration_ = 0;
|
||||
std::string fileName_;
|
||||
int64_t lastModified_ = 0;
|
||||
std::string title_;
|
||||
std::string track_;
|
||||
int32_t track_ = -1; // track number, 0-based, -1 if not set
|
||||
std::string album_;
|
||||
std::string disc_;
|
||||
std::string artist_;
|
||||
std::string albumArtist_;
|
||||
std::string totalTracks_;
|
||||
std::string date_;
|
||||
std::string year_;
|
||||
float duration_ = 0;
|
||||
int32_t thumbnailType_ = 0; // 0 = unknown, 1 = embedded, 3 = folder, 4 = none
|
||||
std::string thumbnailFile_; // only when thumbnailType is 3= folder.
|
||||
int64_t thumbnailLastModified_ = 0;
|
||||
int32_t position_ = -1;
|
||||
|
||||
|
||||
public:
|
||||
|
||||
AudioFileMetadata() = default;
|
||||
AudioFileMetadata(const std::filesystem::path &file);
|
||||
|
||||
GETTER_SETTER(duration);
|
||||
GETTER_SETTER_REF(title);
|
||||
GETTER_SETTER_REF(track);
|
||||
GETTER_SETTER_REF(album);
|
||||
GETTER_SETTER_REF(disc);
|
||||
GETTER_SETTER_REF(artist);
|
||||
GETTER_SETTER_REF(albumArtist);
|
||||
GETTER_SETTER(fileName)
|
||||
GETTER_SETTER(lastModified)
|
||||
GETTER_SETTER_REF(title)
|
||||
GETTER_SETTER_REF(track)
|
||||
GETTER_SETTER_REF(album)
|
||||
GETTER_SETTER(duration)
|
||||
ThumbnailType thumbnailType() const { return (ThumbnailType)thumbnailType_;}
|
||||
void thumbnailType(ThumbnailType value) { thumbnailType_ = (int32_t)value; }
|
||||
GETTER_SETTER(thumbnailFile)
|
||||
GETTER_SETTER(thumbnailLastModified)
|
||||
GETTER_SETTER(position)
|
||||
|
||||
|
||||
public:
|
||||
DECLARE_JSON_MAP(AudioFileMetadata);
|
||||
|
||||
};
|
||||
|
||||
std::string GetAudioFileMetadataString(const std::filesystem::path &path);
|
||||
|
||||
#undef GETTER_SETTER
|
||||
#undef GETTER_SETTER_VEC
|
||||
#undef GETTER_SETTER_REF
|
||||
#undef GETTER_SETTER
|
||||
#undef GETTER_SETTER_VEC
|
||||
#undef GETTER_SETTER_REF
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "AudioFileMetadataReader.hpp"
|
||||
#include <json.hpp>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <json_variant.hpp>
|
||||
#include "LRUCache.hpp"
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include "TemporaryFile.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
namespace fs = std::filesystem;
|
||||
namespace chrono = std::chrono;
|
||||
|
||||
// Safely encode a filename for use in a shell command
|
||||
std::string shell_escape_filename(const std::string &filename)
|
||||
{
|
||||
std::string escaped;
|
||||
escaped.reserve(filename.size() + 2); // Reserve space for quotes and content
|
||||
|
||||
// Wrap the filename in single quotes
|
||||
escaped += '\'';
|
||||
|
||||
for (char c : filename)
|
||||
{
|
||||
// Escape single quotes by closing the quote, adding escaped quote, and reopening
|
||||
if (c == '\'')
|
||||
{
|
||||
escaped += "'\\''";
|
||||
}
|
||||
else
|
||||
{
|
||||
escaped += c;
|
||||
}
|
||||
}
|
||||
|
||||
escaped += '\'';
|
||||
return escaped;
|
||||
}
|
||||
|
||||
static std::string readPipeToString(FILE *file)
|
||||
{
|
||||
std::string content;
|
||||
char buffer[4096];
|
||||
size_t bytesRead;
|
||||
while ((bytesRead = fread(buffer, 1, sizeof(buffer), file)) > 0)
|
||||
{
|
||||
content.append(buffer, bytesRead);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
static std::string GetJsonMetadata(const std::filesystem::path &path)
|
||||
{
|
||||
/* ffprobe -loglevel error -show_streams -show_format -print_format stream_tags -of json
|
||||
~/filename.flac */
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "/usr/bin/ffprobe -loglevel error -show_streams -show_format -print_format stream_tags -of json "
|
||||
<< shell_escape_filename(path.string())
|
||||
<< " 2>/dev/null";
|
||||
|
||||
std::string command = ss.str();
|
||||
FILE *output = popen(command.c_str(), "r");
|
||||
if (output == nullptr)
|
||||
{
|
||||
throw std::runtime_error("Failed to process file.");
|
||||
}
|
||||
std::string result;
|
||||
try
|
||||
{
|
||||
result = readPipeToString(output);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
pclose(output);
|
||||
throw std::runtime_error(e.what());
|
||||
}
|
||||
int retcode = pclose(output);
|
||||
if (retcode != 0)
|
||||
{
|
||||
throw std::runtime_error(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static float MetadataFloat(const json_variant &vt, float defaultValue = 0)
|
||||
{
|
||||
if (vt.is_string())
|
||||
{
|
||||
std::string s = vt.as_string();
|
||||
std::stringstream ss(s);
|
||||
float result = defaultValue;
|
||||
ss >> result;
|
||||
return result;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
static std::string MetadataString(const json_variant &o, const std::vector<std::string> &names)
|
||||
{
|
||||
for (const auto &name : names)
|
||||
{
|
||||
auto result = (*o.as_object())[name];
|
||||
if (result.is_string())
|
||||
{
|
||||
return result.as_string();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static int32_t MetadataTrackToInt(const std::string &track, const std::string &disc)
|
||||
{
|
||||
if (track.empty())
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
try
|
||||
{
|
||||
int trackNumber = std::stoi(track);
|
||||
if (!disc.empty())
|
||||
{
|
||||
int discNumber = std::stoi(disc);
|
||||
trackNumber += discNumber + trackNumber; // e.g. disc 1 track 2 becomes 1002
|
||||
}
|
||||
return trackNumber;
|
||||
}
|
||||
catch (const std::exception &)
|
||||
{
|
||||
return -1; // Invalid track or disc number
|
||||
}
|
||||
}
|
||||
|
||||
AudioFileMetadata::AudioFileMetadata(const std::filesystem::path &file)
|
||||
{
|
||||
try
|
||||
{
|
||||
const std::string json = GetJsonMetadata(file);
|
||||
|
||||
json_variant vt;
|
||||
|
||||
std::stringstream ss(json);
|
||||
json_reader reader(ss);
|
||||
|
||||
reader.read(&vt);
|
||||
|
||||
auto top = vt.as_object();
|
||||
auto format = top->at("format").as_object();
|
||||
|
||||
this->duration_ = MetadataFloat(format->at("duration"), 0);
|
||||
|
||||
auto tags = (*format)["tags"];
|
||||
if (tags.is_object())
|
||||
{
|
||||
// not all of this data gets used (e.g. DATE, YEAR, ALBUM_ARTIST,TOTALTRACKS). But ... write once.
|
||||
this->album_ = MetadataString(tags, {"ALBUM", "album"});
|
||||
//this->artist_ = MetadataString(tags, {"ARTIST", "artist"});
|
||||
//this->albumArtist_ = MetadataString(tags, {"ALBUM ARTIST", "album_artist", "album artist"});
|
||||
this->title_ = MetadataString(tags, {"TITLE", "title"});
|
||||
//this->date_ = MetadataString(tags, {"DATE", "date"});
|
||||
//this->year_ = MetadataString(tags, {"YEAR", "year"});
|
||||
this->track_ =
|
||||
MetadataTrackToInt(
|
||||
MetadataString(tags, {"track","TRACK"}),
|
||||
MetadataString(tags, {"disc", "DISC"})
|
||||
);
|
||||
//this->totalTracks_ = MetadataString(tags, {"TOTALTRACKS"});
|
||||
|
||||
if (title_ == "")
|
||||
{
|
||||
this->title_ = file.stem();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
std::string title = file.stem();
|
||||
this->title_ = title;
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
class AudioCacheKey
|
||||
{
|
||||
public:
|
||||
AudioCacheKey(const std::filesystem::path &path)
|
||||
{
|
||||
this->path = path.string();
|
||||
this->lastWrite = std::filesystem::last_write_time(path);
|
||||
}
|
||||
// Equality operator
|
||||
bool operator==(const AudioCacheKey &other) const
|
||||
{
|
||||
return path == other.path && lastWrite == other.lastWrite;
|
||||
}
|
||||
|
||||
public:
|
||||
std::string path;
|
||||
fs::file_time_type lastWrite;
|
||||
};
|
||||
}
|
||||
|
||||
namespace std
|
||||
{
|
||||
template <>
|
||||
struct hash<AudioCacheKey>
|
||||
{
|
||||
|
||||
size_t operator()(const AudioCacheKey &key) const
|
||||
{
|
||||
size_t path_hash = hash<std::string>{}(key.path);
|
||||
size_t time_hash = hash<uintmax_t>{}(
|
||||
duration_cast<std::chrono::nanoseconds>(key.lastWrite.time_since_epoch()).count());
|
||||
// Combine hashes (boost::hash_combine approach)
|
||||
return path_hash ^ (time_hash + 0x9e3779b9 + (path_hash << 6) + (path_hash >> 2));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static LRUCache<AudioCacheKey, std::string> metadataCache{500};
|
||||
static std::mutex metadataCacheMutex;
|
||||
|
||||
std::string pipedal::GetAudioFileMetadataString(const std::filesystem::path &path)
|
||||
{
|
||||
std::lock_guard lock{metadataCacheMutex};
|
||||
AudioCacheKey key{path};
|
||||
std::string result;
|
||||
if (metadataCache.get(key, result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
auto metadata = AudioFileMetadata(path);
|
||||
|
||||
std::stringstream ss;
|
||||
json_writer writer(ss);
|
||||
writer.write(metadata);
|
||||
result = ss.str();
|
||||
|
||||
metadataCache.put(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
TemporaryFile pipedal::GetAudioFileThumbnail(const std::filesystem::path &path, const std::filesystem::path &outputPath)
|
||||
{
|
||||
return GetAudioFileThumbnail(path, 0, 0, outputPath);
|
||||
}
|
||||
|
||||
std::mutex thumbnailMutex;
|
||||
|
||||
TemporaryFile pipedal::GetAudioFileThumbnail(const std::filesystem::path &path, int32_t width, int32_t height, const std::filesystem::path &tempDirectory)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(thumbnailMutex);
|
||||
fs::path thumbnailDirectory = tempDirectory / "thumbnails";
|
||||
if (!fs::exists(thumbnailDirectory))
|
||||
{
|
||||
fs::create_directories(thumbnailDirectory);
|
||||
} else {
|
||||
// Clean up old thumbnails
|
||||
for (const auto &entry : fs::directory_iterator(thumbnailDirectory))
|
||||
{
|
||||
if (entry.is_regular_file())
|
||||
{
|
||||
fs::remove(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
fs::create_directories(tempDirectory);
|
||||
|
||||
TemporaryFile tempFile(tempDirectory);
|
||||
std::filesystem::path outputPath = thumbnailDirectory / "thumbnail-%03d.jpg";
|
||||
// ffmpeg -loglevel error -i "test2.mp3" -vf scale=200:200 -frames:v 1 thumb-%03.jpg -y
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "/usr/bin/ffmpeg -loglevel error -i " << shell_escape_filename(path.string());
|
||||
if (width > 0 && height > 0)
|
||||
{
|
||||
ss << " -vf \"scale=" << width << ":" << height <<
|
||||
":force_original_aspect_ratio=increase,crop=" << width << ":" << height << "\"";
|
||||
} else {
|
||||
// -vf "crop=min(iw\,ih):min(iw\,ih)"
|
||||
ss << " -vf \"crop=min(iw\\,ih):min(iw\\,ih)\"";
|
||||
}
|
||||
ss << " " << shell_escape_filename(outputPath.string());
|
||||
ss << " -y 2>/dev/null 1>/dev/null";
|
||||
std::string command = ss.str();
|
||||
int rc = system(command.c_str());
|
||||
if (rc < 0) {
|
||||
throw std::runtime_error("Failed to execute ffmpeg command: " + command);
|
||||
}
|
||||
int exitCode = WEXITSTATUS(rc);
|
||||
if (exitCode != EXIT_SUCCESS)
|
||||
{ throw std::runtime_error("Thumbnail not foud.");
|
||||
}
|
||||
fs::remove(tempFile.Path());
|
||||
if (fs::exists(thumbnailDirectory / "thumbnail-001.jpg"))
|
||||
{
|
||||
// Move the thumbnail to the temporary file.
|
||||
fs::rename(thumbnailDirectory / "thumbnail-001.jpg", tempFile.Path());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Failed to create thumbnail.");
|
||||
}
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include "TemporaryFile.hpp"
|
||||
#include "AudioFileMetadata.hpp"
|
||||
|
||||
namespace pipedal
|
||||
{
|
||||
|
||||
|
||||
TemporaryFile GetAudioFileThumbnail(const std::filesystem::path &path, int32_t width, int32_t height, const std::filesystem::path &tempDirectory);
|
||||
TemporaryFile GetAudioFileThumbnail(const std::filesystem::path &path, const std::filesystem::path &tempDirectory);
|
||||
|
||||
std::string GetAudioFileMetadataString(const std::filesystem::path &path);
|
||||
|
||||
|
||||
|
||||
}
|
||||
+667
-98
@@ -8,10 +8,10 @@
|
||||
* 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
|
||||
@@ -21,128 +21,697 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "AudioFiles.hpp"
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include "AudioFileMetadataReader.hpp"
|
||||
#include "Locale.hpp"
|
||||
#include "MimeTypes.hpp"
|
||||
#include <stdexcept>
|
||||
#include "AudioFilesDb.hpp"
|
||||
|
||||
#include "AudioFiles.hpp"
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
|
||||
|
||||
|
||||
using namespace pipedal;
|
||||
#undef _GLIBCXX_DEBUG // Ensure we are not in debug mode, as this file is not compatible with it.
|
||||
#include "SQLiteCpp/SQLiteCpp.h"
|
||||
|
||||
using namespace pipedal;
|
||||
using namespace pipedal::impl;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static constexpr size_t INVALID_INDEX = std::numeric_limits<size_t>::max();
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
class DirectoryList {
|
||||
public:
|
||||
DirectoryList(const fs::path&path);
|
||||
|
||||
const std::vector<std::filesystem::path> &files() {
|
||||
return files_;
|
||||
}
|
||||
private:
|
||||
|
||||
std::vector<std::filesystem::path> files_;
|
||||
};
|
||||
|
||||
class AudioFileInfoImpl: public AudioFileInfo {
|
||||
public:
|
||||
virtual ~AudioFileInfoImpl() { }
|
||||
protected:
|
||||
virtual std::string GetNextAudioFile(const std::string&path) override;
|
||||
virtual std::string GetPreviousAudioFile(const std::string&path) override;
|
||||
};
|
||||
/////////////////////
|
||||
|
||||
DirectoryList::DirectoryList(const fs::path&path)
|
||||
void AudioDirectoryInfo::SetTemporaryDirectory(const std::filesystem::path &path)
|
||||
{
|
||||
temporaryDirectory = path;
|
||||
}
|
||||
void AudioDirectoryInfo::SetResourceDirectory(const std::filesystem::path &path)
|
||||
{
|
||||
resourceDirectory = path;
|
||||
}
|
||||
std::filesystem::path AudioDirectoryInfo::GetTemporaryDirectory() const
|
||||
{
|
||||
if (temporaryDirectory.empty())
|
||||
{
|
||||
for (const auto&dirEntry: fs::directory_iterator(path))
|
||||
return "/tmp/pipedal/audiofiles"; // used during testing.
|
||||
}
|
||||
return temporaryDirectory;
|
||||
}
|
||||
std::filesystem::path AudioDirectoryInfo::GetResourceDirectory() const
|
||||
{
|
||||
if (resourceDirectory.empty())
|
||||
{
|
||||
fs::path path = fs::current_path() / "vite/public/img"; // take from the source directory during testing.
|
||||
return "/usr/share/pipedal/audiofiles"; // used during testing.
|
||||
}
|
||||
return resourceDirectory;
|
||||
}
|
||||
|
||||
std::filesystem::path AudioDirectoryInfo::temporaryDirectory;
|
||||
std::filesystem::path AudioDirectoryInfo::resourceDirectory;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
static int64_t fileTimeToInt64(const fs::file_time_type &fileTime)
|
||||
{
|
||||
return fileTime.time_since_epoch().count();
|
||||
}
|
||||
static int64_t GetLastWriteTime(const fs::path &file)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (dirEntry.is_regular_file())
|
||||
return fileTimeToInt64(fs::last_write_time(file));
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
return 0; // Return 0 if we cannot get the last write time.
|
||||
}
|
||||
}
|
||||
|
||||
class AudioDirectoryInfoImpl : public AudioDirectoryInfo
|
||||
{
|
||||
public:
|
||||
AudioDirectoryInfoImpl(const std::string &path)
|
||||
: path(path)
|
||||
{
|
||||
}
|
||||
virtual ~AudioDirectoryInfoImpl() {}
|
||||
|
||||
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
|
||||
{
|
||||
indexPath = path;
|
||||
}
|
||||
virtual std::string GetNextAudioFile(const std::string &fileNameOnly) override;
|
||||
virtual std::string GetPreviousAudioFile(const std::string &fileNameOnly) override;
|
||||
|
||||
private:
|
||||
std::filesystem::path indexPath;
|
||||
void OpenAudioDb();
|
||||
ThumbnailTemporaryFile GetUnindexedThunbnail(const std::string &fileNameOnly, int32_t width, int32_t height);
|
||||
|
||||
std::shared_ptr<AudioFilesDb> audioFilesDb;
|
||||
fs::path path;
|
||||
using id_t = int64_t;
|
||||
|
||||
std::vector<DbFileInfo> QueryTracks();
|
||||
|
||||
std::vector<DbFileInfo> UpdateDbFiles();
|
||||
void DbSetThumbnailType(
|
||||
int64_t idFile,
|
||||
ThumbnailType thumbnailType,
|
||||
const std::string &thumbnailFile = "",
|
||||
int64_t thumbnailLastModified = 0);
|
||||
void DbSetThumbnailType(
|
||||
const std::string &fileNameOnly,
|
||||
ThumbnailType thumbnailType,
|
||||
const std::string &thumbnailFile = "",
|
||||
int64_t thumbnailLastModified = 0);
|
||||
|
||||
void DbDeleteFile(DbFileInfo *dbFile);
|
||||
void UpdateMetadata(DbFileInfo *dbFile);
|
||||
static constexpr int DB_VERSION = 1;
|
||||
std::filesystem::path GetFolderFile() const;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
AudioDirectoryInfo::Ptr AudioDirectoryInfo::Create(const std::string &path)
|
||||
{
|
||||
return std::shared_ptr<AudioDirectoryInfo>(
|
||||
new AudioDirectoryInfoImpl(path));
|
||||
}
|
||||
|
||||
std::vector<AudioFileMetadata> AudioDirectoryInfoImpl::GetFiles()
|
||||
{
|
||||
OpenAudioDb();
|
||||
std::vector<DbFileInfo> dbFiles = UpdateDbFiles();
|
||||
std::vector<AudioFileMetadata> metadataResults;
|
||||
for (const auto &dbFile : dbFiles)
|
||||
{
|
||||
AudioFileMetadata metadata{dbFile}; // slice copy of just the metadata.
|
||||
metadataResults.push_back(std::move(metadata));
|
||||
}
|
||||
return metadataResults;
|
||||
}
|
||||
|
||||
std::vector<DbFileInfo> AudioDirectoryInfoImpl::QueryTracks()
|
||||
{
|
||||
if (audioFilesDb)
|
||||
{
|
||||
return audioFilesDb->QueryTracks();
|
||||
}
|
||||
return std::vector<DbFileInfo>();
|
||||
}
|
||||
static bool isAudioExtension(const std::string &extension)
|
||||
{
|
||||
return MimeTypes::instance().AudioExtensions().contains(extension);
|
||||
}
|
||||
|
||||
static bool HasPositionInfo(const std::vector<DbFileInfo> &dbFiles)
|
||||
{
|
||||
for (const auto &file : dbFiles)
|
||||
{
|
||||
if (file.position() != -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static int DefaultedSortOrder(int track)
|
||||
{
|
||||
if (track < 0)
|
||||
{
|
||||
return std::numeric_limits<int>::max();
|
||||
}
|
||||
return track;
|
||||
}
|
||||
static void SortDbFiles(std::vector<DbFileInfo> &dbFiles, Collator::ptr &collator)
|
||||
{
|
||||
std::sort(
|
||||
dbFiles.begin(), dbFiles.end(),
|
||||
[collator](const DbFileInfo &a, const DbFileInfo &b)
|
||||
{
|
||||
if (a.position() != b.position())
|
||||
{
|
||||
fs::path t = dirEntry.path();
|
||||
if (!t.filename().string().starts_with('.'))
|
||||
return DefaultedSortOrder(a.position()) < DefaultedSortOrder(b.position());
|
||||
}
|
||||
if (a.track() != b.track())
|
||||
{
|
||||
return DefaultedSortOrder(a.track()) < DefaultedSortOrder(b.track());
|
||||
}
|
||||
if (a.title() != b.title())
|
||||
{
|
||||
return collator->Compare(a.title(), b.title()) < 0;
|
||||
}
|
||||
if (a.album() != b.album())
|
||||
{
|
||||
return collator->Compare(a.title(), b.title()) < 0;
|
||||
}
|
||||
return false;
|
||||
;
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<DbFileInfo> AudioDirectoryInfoImpl::UpdateDbFiles()
|
||||
{
|
||||
std::shared_ptr<SQLite::Transaction> transaction;
|
||||
if (audioFilesDb)
|
||||
{
|
||||
transaction = audioFilesDb->transaction();
|
||||
}
|
||||
std::vector<DbFileInfo> dbFiles = QueryTracks();
|
||||
std::vector<DbFileInfo> newFiles;
|
||||
|
||||
bool updateRequired = false;
|
||||
std::map<std::string, DbFileInfo *> nameToDbRecord;
|
||||
for (size_t i = 0; i < dbFiles.size(); ++i)
|
||||
{
|
||||
auto *pFile = &(dbFiles[i]);
|
||||
nameToDbRecord[pFile->fileName()] = pFile;
|
||||
}
|
||||
|
||||
for (auto dirEntry : fs::directory_iterator(path))
|
||||
{
|
||||
if (!dirEntry.is_directory())
|
||||
{
|
||||
auto path = dirEntry.path();
|
||||
std::string name = path.filename();
|
||||
std::string extension = path.extension();
|
||||
if (isAudioExtension(extension))
|
||||
{
|
||||
int64_t lastModified = fileTimeToInt64(dirEntry.last_write_time());
|
||||
|
||||
auto f = nameToDbRecord.find(name);
|
||||
if (f != nameToDbRecord.end())
|
||||
{
|
||||
files_.push_back(std::move(t));
|
||||
DbFileInfo *dbFile = f->second;
|
||||
dbFile->present(true);
|
||||
if (dbFile->lastModified() != lastModified)
|
||||
{
|
||||
if (audioFilesDb)
|
||||
{
|
||||
audioFilesDb->DeleteThumbnails(dbFile->idFile());
|
||||
}
|
||||
dbFile->thumbnailType(ThumbnailType::Unknown);
|
||||
dbFile->thumbnailFile("");
|
||||
dbFile->thumbnailLastModified(0);
|
||||
UpdateMetadata(dbFile);
|
||||
dbFile->dirty(true);
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DbFileInfo newFile;
|
||||
newFile.fileName(name);
|
||||
newFile.idFile(-1);
|
||||
newFile.dirty(true);
|
||||
newFile.present(true);
|
||||
UpdateMetadata(&newFile);
|
||||
newFiles.push_back(std::move(newFile));
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string AudioFileInfoImpl::GetNextAudioFile(const std::string&path_) {
|
||||
try
|
||||
for (auto i = dbFiles.begin(); i != dbFiles.end(); ++i)
|
||||
{
|
||||
if (!i->present())
|
||||
{
|
||||
fs::path path {path_};
|
||||
if (fs::exists(path) && fs::is_regular_file(path))
|
||||
{
|
||||
DirectoryList directoryList(path.parent_path());
|
||||
size_t index = INVALID_INDEX;
|
||||
auto &files = directoryList.files();
|
||||
for (size_t i= 0; i < files.size(); ++i)
|
||||
{
|
||||
if (files[i] == path)
|
||||
{
|
||||
size_t next = i;
|
||||
if (next == files.size()-1)
|
||||
{
|
||||
next = 0;
|
||||
} else {
|
||||
++next;
|
||||
}
|
||||
return files[next];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} catch (const std::exception&e)
|
||||
{
|
||||
|
||||
DbDeleteFile(&*i);
|
||||
dbFiles.erase(i);
|
||||
updateRequired = true;
|
||||
--i;
|
||||
}
|
||||
return "";
|
||||
|
||||
}
|
||||
std::string AudioFileInfoImpl::GetPreviousAudioFile(const std::string&path_)
|
||||
// refresh folder file names.
|
||||
auto folderFile = GetFolderFile();
|
||||
int64_t folderLastModified = 0;
|
||||
std::string folderFileName;
|
||||
if (!folderFile.empty() && fs::exists(folderFile))
|
||||
{
|
||||
folderLastModified = GetLastWriteTime(folderFile);
|
||||
folderFileName = folderFile.filename().string();
|
||||
}
|
||||
for (auto &dbFile : dbFiles)
|
||||
{
|
||||
if (dbFile.thumbnailType() == ThumbnailType::Unknown)
|
||||
{
|
||||
if (!folderFileName.empty())
|
||||
{
|
||||
dbFile.thumbnailType(ThumbnailType::Folder);
|
||||
dbFile.thumbnailFile(folderFileName);
|
||||
dbFile.thumbnailLastModified(folderLastModified);
|
||||
dbFile.dirty(true);
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
else if (dbFile.thumbnailType() == ThumbnailType::Folder)
|
||||
{
|
||||
// Check if the folder file has changed.
|
||||
if (folderFileName.empty())
|
||||
{
|
||||
// no more folder file. update accordingly.
|
||||
dbFile.thumbnailType(ThumbnailType::None);
|
||||
dbFile.thumbnailFile("");
|
||||
dbFile.thumbnailLastModified(0);
|
||||
dbFile.dirty(true);
|
||||
updateRequired = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// folder file has changed. update accordingly.
|
||||
if (dbFile.thumbnailFile() != folderFileName ||
|
||||
dbFile.thumbnailLastModified() != folderLastModified)
|
||||
{
|
||||
dbFile.thumbnailFile(folderFileName);
|
||||
dbFile.thumbnailLastModified(folderLastModified);
|
||||
dbFile.dirty(true);
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!updateRequired && newFiles.empty())
|
||||
{
|
||||
// Nothing to do.
|
||||
return dbFiles;
|
||||
}
|
||||
|
||||
Locale::ptr locale = Locale::GetInstance();
|
||||
Collator::ptr collator = locale->GetCollator();
|
||||
if (!HasPositionInfo(dbFiles))
|
||||
{
|
||||
dbFiles.insert(dbFiles.end(), newFiles.begin(), newFiles.end());
|
||||
SortDbFiles(dbFiles, collator);
|
||||
}
|
||||
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());
|
||||
|
||||
for (size_t i = 0; i < dbFiles.size(); ++i)
|
||||
{
|
||||
auto newPosition = static_cast<int32_t>(i);
|
||||
if (newPosition != dbFiles[i].position())
|
||||
{
|
||||
dbFiles[i].dirty(true);
|
||||
}
|
||||
dbFiles[i].position(newPosition);
|
||||
}
|
||||
}
|
||||
for (auto &dbFile : dbFiles)
|
||||
{
|
||||
if (dbFile.dirty())
|
||||
{
|
||||
if (audioFilesDb)
|
||||
{
|
||||
audioFilesDb->WriteFile(&dbFile);
|
||||
}
|
||||
dbFile.dirty(false);
|
||||
}
|
||||
}
|
||||
if (transaction)
|
||||
{
|
||||
transaction->commit();
|
||||
transaction = nullptr;
|
||||
}
|
||||
return dbFiles;
|
||||
}
|
||||
|
||||
void AudioDirectoryInfoImpl::DbDeleteFile(DbFileInfo *dbFile)
|
||||
{
|
||||
if (this->audioFilesDb)
|
||||
{
|
||||
audioFilesDb->DeleteFile(dbFile);
|
||||
}
|
||||
}
|
||||
|
||||
// All the varous cover art files I found on my personal music collection.
|
||||
// These are used to find the cover art for a directory. Based on
|
||||
// scanning a large music collection that have been tagged by various tools
|
||||
// including iTunes, Windows Media Playe, and a variety of taggers.
|
||||
//
|
||||
// Files are listed in order of preference.
|
||||
|
||||
static std::vector<std::filesystem::path> COVER_ART_FILES = {
|
||||
"Folder.jpg", // window media player/explorer.
|
||||
"Cover.jpg", // itunes.
|
||||
"folder.jpg", // Linux audio players plex, kodi, Rythmbox, Amorok, Clementine.
|
||||
"cover.jpg", // Linux audio players plex, kodi, Rythmbox, Amorok, Clementine.
|
||||
"Artwork.jpg", // itunes.
|
||||
"Front.jpg",
|
||||
"front.jpg", // linux.
|
||||
"AlbumArt.jpg",
|
||||
"albumArt.jpg",
|
||||
"Frontcover.jpg",
|
||||
"AlbumArtSmall.jpg"};
|
||||
|
||||
fs::path AudioDirectoryInfoImpl::GetFolderFile() const
|
||||
{
|
||||
for (const auto &coverArtFile : COVER_ART_FILES)
|
||||
{
|
||||
fs::path thumbnailPath = path / coverArtFile;
|
||||
if (fs::exists(thumbnailPath))
|
||||
{
|
||||
return thumbnailPath;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void AudioDirectoryInfoImpl::DbSetThumbnailType(
|
||||
int64_t idFile,
|
||||
ThumbnailType thumbnailType,
|
||||
const std::string &thumbnailFile,
|
||||
int64_t thumbnailLastModified)
|
||||
{
|
||||
if (audioFilesDb)
|
||||
{
|
||||
audioFilesDb->UpdateThumbnailInfo(
|
||||
idFile,
|
||||
thumbnailType,
|
||||
thumbnailFile,
|
||||
thumbnailLastModified);
|
||||
}
|
||||
}
|
||||
void AudioDirectoryInfoImpl::DbSetThumbnailType(
|
||||
const std::string&fileNameOnly,
|
||||
ThumbnailType thumbnailType,
|
||||
const std::string &thumbnailFile,
|
||||
int64_t thumbnailLastModified)
|
||||
{
|
||||
if (audioFilesDb)
|
||||
{
|
||||
audioFilesDb->UpdateThumbnailInfo(
|
||||
fileNameOnly,
|
||||
thumbnailType,
|
||||
thumbnailFile,
|
||||
thumbnailLastModified);
|
||||
}
|
||||
}
|
||||
|
||||
static ThumbnailTemporaryFile GetFolderTemporaryFile(const fs::path &folderFile)
|
||||
{
|
||||
ThumbnailTemporaryFile result;
|
||||
result.SetNonDeletedPath(folderFile, MimeTypes::instance().MimeTypeFromExtension(folderFile.extension().string()));
|
||||
return result;
|
||||
}
|
||||
ThumbnailTemporaryFile AudioDirectoryInfoImpl::GetThumbnail(const std::string &fileNameOnly, int32_t width, int32_t height)
|
||||
{
|
||||
fs::path file = this->path / fileNameOnly;
|
||||
OpenAudioDb();
|
||||
if (audioFilesDb)
|
||||
{
|
||||
try
|
||||
{
|
||||
fs::path path {path_};
|
||||
if (fs::exists(path) && fs::is_regular_file(path))
|
||||
|
||||
ThumbnailInfo thumbnailInfo = audioFilesDb->GetThumbnailInfo(file.filename().string());
|
||||
|
||||
if (thumbnailInfo.thumbnailType() == ThumbnailType::Folder)
|
||||
{
|
||||
DirectoryList directoryList(path.parent_path());
|
||||
size_t index = INVALID_INDEX;
|
||||
auto &files = directoryList.files();
|
||||
for (size_t i= 0; i < files.size(); ++i)
|
||||
fs::path thumbnailFile = path / thumbnailInfo.thumbnailFile();
|
||||
if (!fs::exists(thumbnailFile) ||
|
||||
fileTimeToInt64(fs::last_write_time(thumbnailFile)) !=
|
||||
thumbnailInfo.thumbnailLastModified())
|
||||
{
|
||||
if (files[i] == path)
|
||||
{
|
||||
size_t previous = i;
|
||||
if (previous == 0)
|
||||
{
|
||||
previous = files.size()-1;
|
||||
} else {
|
||||
--previous;
|
||||
}
|
||||
return files[previous];
|
||||
}
|
||||
// reset the thumbnail type and try again.
|
||||
audioFilesDb->UpdateThumbnailInfo(
|
||||
fileNameOnly,
|
||||
ThumbnailType::Unknown);
|
||||
return GetThumbnail(fileNameOnly, width, height);
|
||||
}
|
||||
fs::path fullFolderPath = this->path / thumbnailInfo.thumbnailFile();
|
||||
return GetFolderTemporaryFile(fullFolderPath);
|
||||
}
|
||||
if (thumbnailInfo.thumbnailType() == ThumbnailType::Embedded)
|
||||
{
|
||||
std::vector<uint8_t> blob = audioFilesDb->GetEmbeddedThumbnail(fileNameOnly, width, height);
|
||||
|
||||
}
|
||||
} catch (const std::exception&e)
|
||||
{
|
||||
if (!blob.empty())
|
||||
{
|
||||
ThumbnailTemporaryFile tempFile = ThumbnailTemporaryFile::CreateTemporaryFile(GetTemporaryDirectory(), "image/jpeg");
|
||||
|
||||
tempFile.SetMimeType("image/jpeg");
|
||||
// write buffer to tempFile.GetPath()
|
||||
std::ofstream outFile(tempFile.Path(), std::ios::binary);
|
||||
if (!outFile)
|
||||
{
|
||||
throw std::runtime_error("Failed to open temporary file for writing: " + tempFile.Path().string());
|
||||
}
|
||||
outFile.write((const char *)blob.data(), blob.size());
|
||||
return tempFile;
|
||||
}
|
||||
else
|
||||
{
|
||||
// fall through and generate the indexed thumbnail.
|
||||
}
|
||||
}
|
||||
|
||||
if (thumbnailInfo.thumbnailType() == ThumbnailType::None)
|
||||
{
|
||||
return DefaultThumbnailTemporaryFile();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
auto tempFile = pipedal::GetAudioFileThumbnail(
|
||||
this->path / fileNameOnly,
|
||||
width, height,
|
||||
GetTemporaryDirectory());
|
||||
auto thumbnailPath = tempFile.Path();
|
||||
ThumbnailTemporaryFile result;
|
||||
result.Attach(tempFile.Detach(), "image/jpeg");
|
||||
|
||||
// Read the thumbnail data from the temporary file.
|
||||
std::ifstream thumbnailStream(thumbnailPath, std::ios::binary);
|
||||
if (!thumbnailStream)
|
||||
{
|
||||
throw std::runtime_error("Failed to open thumbnail file: " + thumbnailPath.string());
|
||||
}
|
||||
std::vector<uint8_t> thumbnailData(
|
||||
(std::istreambuf_iterator<char>(thumbnailStream)),
|
||||
std::istreambuf_iterator<char>());
|
||||
|
||||
if (!thumbnailData.empty())
|
||||
{
|
||||
audioFilesDb->AddThumbnail(
|
||||
fileNameOnly,
|
||||
width,
|
||||
height,
|
||||
thumbnailData);
|
||||
|
||||
// Set the MIME type for the thumbnail.
|
||||
result.SetMimeType("image/jpeg");
|
||||
audioFilesDb->UpdateThumbnailInfo(
|
||||
fileNameOnly,
|
||||
ThumbnailType::Embedded);
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Thumbnail data is empty.");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (const std::exception &_)
|
||||
{
|
||||
}
|
||||
auto folderFile = GetFolderFile();
|
||||
if (!folderFile.empty())
|
||||
{
|
||||
// We have a folder thumbnail, set the type and return it.
|
||||
fs::path t = this->path / folderFile;
|
||||
int64_t lastModified = fileTimeToInt64(fs::last_write_time(t));
|
||||
DbSetThumbnailType(fileNameOnly, ThumbnailType::Folder, folderFile, lastModified);
|
||||
auto fullFolderPath = this->path / folderFile;
|
||||
return GetFolderTemporaryFile(fullFolderPath);
|
||||
}
|
||||
DbSetThumbnailType(
|
||||
fileNameOnly,
|
||||
ThumbnailType::None); // No thumbnail available, set to None.
|
||||
return DefaultThumbnailTemporaryFile();
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
}
|
||||
return "";
|
||||
|
||||
}
|
||||
return GetUnindexedThunbnail(fileNameOnly, width, height);
|
||||
}
|
||||
|
||||
}
|
||||
ThumbnailTemporaryFile AudioDirectoryInfoImpl::GetUnindexedThunbnail(const std::string &fileNameOnly, int32_t width, int32_t height)
|
||||
{
|
||||
fs::path file = this->path / fileNameOnly;
|
||||
ThumbnailTemporaryFile tempFile = ThumbnailTemporaryFile::CreateTemporaryFile(GetTemporaryDirectory(), "image/jpeg");
|
||||
try
|
||||
{
|
||||
auto thumbnailPath = pipedal::GetAudioFileThumbnail(file, width, height, GetTemporaryDirectory());
|
||||
tempFile.Attach(thumbnailPath.Detach(), "image/jpeg");
|
||||
return tempFile;
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
// If we cannot get the thumbnail, return a default one.
|
||||
return DefaultThumbnailTemporaryFile();
|
||||
}
|
||||
}
|
||||
|
||||
static AudioFileInfoImpl instance;
|
||||
void AudioDirectoryInfoImpl::UpdateMetadata(DbFileInfo *dbFile)
|
||||
{
|
||||
fs::path file = this->path / dbFile->fileName();
|
||||
AudioFileMetadata metadata(file);
|
||||
dbFile->title(metadata.title());
|
||||
dbFile->track(metadata.track());
|
||||
dbFile->duration(metadata.duration());
|
||||
dbFile->album(metadata.album());
|
||||
dbFile->lastModified(GetLastWriteTime(file));
|
||||
dbFile->duration(metadata.duration());
|
||||
}
|
||||
|
||||
AudioFileInfo&AudioFileInfo::GetInstance()
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
ThumbnailTemporaryFile::ThumbnailTemporaryFile(const std::filesystem::path &temporaryDirectory)
|
||||
: TemporaryFile(temporaryDirectory)
|
||||
{
|
||||
// Set a default MIME type for the thumbnail.
|
||||
mimeType = "image/jpeg"; // Default MIME type, can be set later.
|
||||
}
|
||||
|
||||
ThumbnailTemporaryFile ThumbnailTemporaryFile::CreateTemporaryFile(const std::filesystem::path &temporaryDirectory, const std::string &mimeType)
|
||||
{
|
||||
ThumbnailTemporaryFile result(temporaryDirectory);
|
||||
result.SetMimeType(mimeType);
|
||||
return result;
|
||||
}
|
||||
|
||||
ThumbnailTemporaryFile ThumbnailTemporaryFile::FromFile(const std::filesystem::path &filePath, const std::string &mimeType)
|
||||
{
|
||||
ThumbnailTemporaryFile result;
|
||||
result.Attach(filePath, mimeType);
|
||||
return result;
|
||||
}
|
||||
|
||||
void ThumbnailTemporaryFile::Attach(const std::filesystem::path &filePath, const std::string &mimeType)
|
||||
{
|
||||
super::Attach(filePath);
|
||||
SetMimeType(mimeType);
|
||||
}
|
||||
|
||||
size_t AudioDirectoryInfoImpl::TestGetNumberOfThumbnails()
|
||||
{
|
||||
if (!audioFilesDb)
|
||||
{
|
||||
throw std::logic_error("DB not open");
|
||||
}
|
||||
return this->audioFilesDb->GetNumberOfThumbnails();
|
||||
}
|
||||
|
||||
void AudioDirectoryInfoImpl::OpenAudioDb()
|
||||
{
|
||||
if (!this->audioFilesDb) {
|
||||
this->audioFilesDb = std::make_shared<AudioFilesDb>(this->path, indexPath);
|
||||
}
|
||||
}
|
||||
|
||||
ThumbnailTemporaryFile AudioDirectoryInfoImpl::DefaultThumbnailTemporaryFile()
|
||||
{
|
||||
ThumbnailTemporaryFile tempFile;
|
||||
fs::path defaultThumbnail = GetResourceDirectory() / "img/missing_thumbnail.jpg";
|
||||
tempFile.SetNonDeletedPath(defaultThumbnail, "image/jpeg");
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
std::string AudioDirectoryInfoImpl::GetNextAudioFile(const std::string &fileNameOnly)
|
||||
{
|
||||
auto files = this->GetFiles();
|
||||
if (files.empty())
|
||||
{
|
||||
return "";
|
||||
}
|
||||
for (size_t i = 0; i < files.size(); ++i)
|
||||
{
|
||||
if (files[i].fileName() == fileNameOnly)
|
||||
{
|
||||
if (i + 1 < files.size())
|
||||
{
|
||||
return files[i + 1].fileName();
|
||||
}
|
||||
else
|
||||
{
|
||||
return files[0].fileName(); // wrap around to the first file.
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
std::string AudioDirectoryInfoImpl::GetPreviousAudioFile(const std::string &fileNameOnly)
|
||||
{
|
||||
auto files = this->GetFiles();
|
||||
if (files.empty())
|
||||
{
|
||||
return "";
|
||||
}
|
||||
for (size_t i = 0; i < files.size(); ++i)
|
||||
{
|
||||
if (files[i].fileName() == fileNameOnly)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
return files[i - 1].fileName();
|
||||
}
|
||||
else
|
||||
{
|
||||
return files[files.size() - 1].fileName(); // wrap around to the last file.
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
+75
-9
@@ -24,16 +24,82 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include "AudioFileMetadata.hpp"
|
||||
#include "TemporaryFile.hpp"
|
||||
|
||||
namespace pipedal {
|
||||
class AudioFileInfo {
|
||||
protected:
|
||||
AudioFileInfo() { }
|
||||
virtual ~AudioFileInfo() {}
|
||||
public:
|
||||
static AudioFileInfo&GetInstance();
|
||||
|
||||
virtual std::string GetNextAudioFile(const std::string&path) = 0;
|
||||
virtual std::string GetPreviousAudioFile(const std::string&path) = 0;
|
||||
class ThumbnailTemporaryFile: public TemporaryFile {
|
||||
private:
|
||||
// Private constructor to enforce the use of CreateTemporaryFile or the other constructor.
|
||||
explicit ThumbnailTemporaryFile(const std::filesystem::path&temporaryDirectory);
|
||||
public:
|
||||
using super = TemporaryFile;
|
||||
ThumbnailTemporaryFile() = default;
|
||||
ThumbnailTemporaryFile(const ThumbnailTemporaryFile&) = delete;
|
||||
ThumbnailTemporaryFile(ThumbnailTemporaryFile&&) = default;
|
||||
ThumbnailTemporaryFile&operator=(const ThumbnailTemporaryFile&) = delete;
|
||||
ThumbnailTemporaryFile&operator=(ThumbnailTemporaryFile&&) = default;
|
||||
public:
|
||||
static ThumbnailTemporaryFile CreateTemporaryFile(const std::filesystem::path&tempDirectory,const std::string &mimeType);
|
||||
static ThumbnailTemporaryFile FromFile(const std::filesystem::path&filePath,const std::string &mimeType);
|
||||
public:
|
||||
|
||||
void SetNonDeletedPath(const std::filesystem::path&path, const std::string&mimeType = "audio/mpeg") {
|
||||
TemporaryFile::SetNonDeletedPath(path);
|
||||
// Set the MIME type for audio files.
|
||||
this->mimeType = mimeType; // Default MIME type, can be set later.
|
||||
}
|
||||
void Attach(const std::filesystem::path&path, const std::string&mimeType);
|
||||
|
||||
std::string GetMimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
void SetMimeType(const std::string&mimeType) {
|
||||
this->mimeType = mimeType;
|
||||
}
|
||||
private:
|
||||
std::string mimeType = "image/jpeg"; // Default MIME type, can be set later.
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
class AudioDirectoryInfo {
|
||||
protected:
|
||||
AudioDirectoryInfo() { }
|
||||
virtual ~AudioDirectoryInfo() { }
|
||||
public:
|
||||
using self = AudioDirectoryInfo;
|
||||
using Ptr = std::shared_ptr<self>;
|
||||
|
||||
static Ptr Create(const std::string&path);
|
||||
|
||||
virtual std::vector<AudioFileMetadata> GetFiles() = 0;
|
||||
|
||||
virtual ThumbnailTemporaryFile GetThumbnail(const std::string&fileNameOnly, int32_t width, int32_t height) = 0;
|
||||
|
||||
virtual std::string GetNextAudioFile(const std::string&fileNameOnly) = 0;
|
||||
virtual std::string GetPreviousAudioFile(const std::string&fileNameOnly) = 0;
|
||||
|
||||
virtual ThumbnailTemporaryFile DefaultThumbnailTemporaryFile() = 0;
|
||||
|
||||
static void SetTemporaryDirectory(const std::filesystem::path&path);
|
||||
static void SetResourceDirectory(const std::filesystem::path&path);
|
||||
|
||||
|
||||
virtual size_t TestGetNumberOfThumbnails() = 0; // test use only.
|
||||
virtual void TestSetIndexPath(const std::filesystem::path&path) = 0;
|
||||
|
||||
protected:
|
||||
std::filesystem::path GetTemporaryDirectory() const;
|
||||
std::filesystem::path GetResourceDirectory() const;
|
||||
|
||||
private:
|
||||
static std::filesystem::path temporaryDirectory;
|
||||
static std::filesystem::path resourceDirectory;;
|
||||
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,482 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "AudioFilesDb.hpp"
|
||||
#include <stdexcept>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <map>
|
||||
|
||||
using namespace pipedal;
|
||||
using namespace pipedal::impl;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace pipedal::impl
|
||||
{
|
||||
struct DatabaseLockEntry
|
||||
{
|
||||
virtual ~DatabaseLockEntry() = default;
|
||||
|
||||
using ptr = std::shared_ptr<DatabaseLockEntry>;
|
||||
|
||||
std::mutex mutex;
|
||||
};
|
||||
static std::map<std::filesystem::path, std::shared_ptr<DatabaseLockEntry>> databaseLocks;
|
||||
|
||||
std::mutex databaseLockMutex;
|
||||
|
||||
class DatabaseLock
|
||||
{
|
||||
private:
|
||||
fs::path indexFile;
|
||||
std::shared_ptr<DatabaseLockEntry> lockEntry;
|
||||
|
||||
// Disable copy and move constructors
|
||||
DatabaseLock(const DatabaseLock &) = delete;
|
||||
DatabaseLock &operator=(const DatabaseLock &) = delete;
|
||||
DatabaseLock(DatabaseLock &&) = delete;
|
||||
DatabaseLock &operator=(DatabaseLock &&) = delete;
|
||||
|
||||
public:
|
||||
explicit DatabaseLock(const fs::path &lockFilePath)
|
||||
: indexFile(lockFilePath)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(databaseLockMutex);
|
||||
|
||||
auto it = databaseLocks.find(indexFile);
|
||||
if (it == databaseLocks.end())
|
||||
{
|
||||
// Create a new lock entry
|
||||
DatabaseLockEntry::ptr entry = std::make_shared<DatabaseLockEntry>();
|
||||
databaseLocks[indexFile] = entry;
|
||||
lockEntry = entry;
|
||||
}
|
||||
else
|
||||
{
|
||||
lockEntry = it->second;
|
||||
}
|
||||
}
|
||||
lockEntry->mutex.lock(); // we now have exclusive access to the database.
|
||||
|
||||
}
|
||||
|
||||
~DatabaseLock()
|
||||
{
|
||||
lockEntry->mutex.unlock(); // release the lock
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(databaseLockMutex);
|
||||
auto use_count = lockEntry.use_count();
|
||||
|
||||
if (use_count == 2) // one for us, one for the Lockentry
|
||||
{
|
||||
// Remove the entry from the map if no other locks are held.
|
||||
databaseLocks.erase(indexFile);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static std::unique_ptr<DatabaseLock> getDatabaseLock(const std::filesystem::path &path)
|
||||
{
|
||||
// Create a lock file in the same directory as the database.
|
||||
return std::make_unique<DatabaseLock>(path);
|
||||
}
|
||||
|
||||
AudioFilesDb::AudioFilesDb(
|
||||
const std::filesystem::path &path,
|
||||
const std::filesystem::path &indexPath)
|
||||
: path(path)
|
||||
{
|
||||
fs::path dbPathName = this->path / ".pipedal.index";
|
||||
if (!indexPath.empty())
|
||||
{
|
||||
dbPathName = indexPath;
|
||||
}
|
||||
|
||||
dbLock = getDatabaseLock(dbPathName);
|
||||
|
||||
if (!fs::exists(dbPathName))
|
||||
{
|
||||
CreateDb(dbPathName);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->db = std::make_unique<SQLite::Database>(dbPathName, SQLite::OPEN_READWRITE);
|
||||
UpgradeDb();
|
||||
}
|
||||
}
|
||||
|
||||
void AudioFilesDb::UpgradeDb()
|
||||
{
|
||||
int version = QueryVersion();
|
||||
}
|
||||
|
||||
int AudioFilesDb::QueryVersion()
|
||||
{
|
||||
SQLite::Statement query(*db, "SELECT version FROM am_dbInfo LIMIT 1");
|
||||
|
||||
// Fetch the result
|
||||
if (query.executeStep())
|
||||
{
|
||||
int version = query.getColumn(0).getInt();
|
||||
return version;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("QueryVersion failed.");
|
||||
}
|
||||
}
|
||||
|
||||
void AudioFilesDb::CreateDb(const std::filesystem::path &dbPathName)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
this->db = std::make_unique<SQLite::Database>(dbPathName, SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
|
||||
|
||||
try
|
||||
{
|
||||
db->exec("CREATE TABLE am_dbInfo ("
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
"version INTEGER NOT NULL)");
|
||||
|
||||
{
|
||||
SQLite::Statement query(*db, "INSERT INTO am_dbInfo (version) VALUES (?)");
|
||||
query.bind(1, DB_VERSION);
|
||||
query.exec();
|
||||
}
|
||||
|
||||
db->exec(
|
||||
"CREATE TABLE IF NOT EXISTS files ("
|
||||
"idFile INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
"fileName TEXT NOT NULL, "
|
||||
"lastModified INT64 NOT NULL,"
|
||||
"title TEXT NOT NULL,"
|
||||
"track NUMBER NOT NULL,"
|
||||
"album TEXT NOT NULL, "
|
||||
"duration REAL NOT NULL DEFAULT 0.0,"
|
||||
|
||||
"thumbnailType INTEGER NOT NULL DEFAULT 0,"
|
||||
"position INTEGER NOT NULL DEFAULT -1,"
|
||||
"thumbnailFile TEXT NOT NULL DEFAULT \"\","
|
||||
"thumbnailLastModified INT64 NOT NULL DEFAULT 0"
|
||||
")");
|
||||
|
||||
db->exec("CREATE TABLE IF NOT EXISTS thumbnails ("
|
||||
"idThumbnail INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
"idFile INT64, "
|
||||
"thumbnail BLOB, "
|
||||
"width INTEGER, "
|
||||
"height INTEGER)");
|
||||
}
|
||||
catch (const SQLite::Exception &e)
|
||||
{
|
||||
throw std::runtime_error("Failed to create database tables: " + std::string(e.what()));
|
||||
}
|
||||
}
|
||||
catch (const SQLite::Exception &e)
|
||||
{
|
||||
throw std::runtime_error("Failed to create database: " + std::string(e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void AudioFilesDb::DeleteFile(DbFileInfo *dbFile)
|
||||
{
|
||||
DeleteThumbnails(dbFile->idFile());
|
||||
if (!deleteFileQuery)
|
||||
{
|
||||
deleteFileQuery = std::make_unique<SQLite::Statement>(
|
||||
*db,
|
||||
"DELETE FROM files WHERE idFile = ?");
|
||||
}
|
||||
deleteFileQuery->tryReset();
|
||||
deleteFileQuery->bind(1, dbFile->idFile());
|
||||
deleteFileQuery->exec();
|
||||
}
|
||||
|
||||
void AudioFilesDb::DeleteThumbnails(id_t idFile)
|
||||
{
|
||||
if (!updateThumbnailInfoQueryByName)
|
||||
{
|
||||
updateThumbnailInfoQueryByName = std::make_unique<SQLite::Statement>(
|
||||
*db,
|
||||
"DELETE FROM thumbnails WHERE idFile = ?");
|
||||
}
|
||||
updateThumbnailInfoQueryByName->tryReset();
|
||||
updateThumbnailInfoQueryByName->bind(1, idFile);
|
||||
updateThumbnailInfoQueryByName->exec();
|
||||
}
|
||||
|
||||
size_t AudioFilesDb::GetNumberOfThumbnails()
|
||||
{
|
||||
SQLite::Statement query(*db, "SELECT COUNT(*) FROM thumbnails");
|
||||
if (query.executeStep())
|
||||
{
|
||||
return query.getColumn(0).getInt64();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void AudioFilesDb::UpdateThumbnailInfo(
|
||||
const std::string &fileName,
|
||||
ThumbnailType thumbnailType,
|
||||
const std::string &thumbnailFile,
|
||||
int64_t thumbnailLastModified)
|
||||
{
|
||||
if (!updateThumbnailInfoQueryByName)
|
||||
{
|
||||
updateThumbnailInfoQueryByName = std::make_unique<SQLite::Statement>(
|
||||
*db,
|
||||
"UPDATE files SET thumbnailType = ?,thumbnailFile = ?, thumbnailLastModified = ? WHERE fileName = ?");
|
||||
}
|
||||
updateThumbnailInfoQueryByName->tryReset();
|
||||
updateThumbnailInfoQueryByName->bind(1, (int32_t)thumbnailType);
|
||||
updateThumbnailInfoQueryByName->bind(2, thumbnailFile);
|
||||
updateThumbnailInfoQueryByName->bind(3, thumbnailLastModified);
|
||||
updateThumbnailInfoQueryByName->bind(4, fileName);
|
||||
updateThumbnailInfoQueryByName->exec();
|
||||
}
|
||||
|
||||
void AudioFilesDb::UpdateThumbnailInfo(
|
||||
int64_t idFile,
|
||||
ThumbnailType thumbnailType,
|
||||
const std::string &thumbnailFile,
|
||||
int64_t thumbnailLastModified)
|
||||
{
|
||||
if (!updateThumbnailInfoQueryById)
|
||||
{
|
||||
updateThumbnailInfoQueryById = std::make_unique<SQLite::Statement>(
|
||||
*db,
|
||||
"UPDATE files SET thumbnailType = ?,thumbnailFile = ?, thumbnailLastModified = ? WHERE idFile = ?");
|
||||
}
|
||||
updateThumbnailInfoQueryById->bind(1, (int32_t)thumbnailType);
|
||||
updateThumbnailInfoQueryById->bind(2, thumbnailFile);
|
||||
updateThumbnailInfoQueryById->bind(3, thumbnailLastModified);
|
||||
updateThumbnailInfoQueryById->bind(4, idFile);
|
||||
updateThumbnailInfoQueryById->exec();
|
||||
}
|
||||
|
||||
std::unique_ptr<SQLite::Transaction> AudioFilesDb::transaction()
|
||||
{
|
||||
return std::make_unique<SQLite::Transaction>(*db);
|
||||
}
|
||||
|
||||
std::vector<DbFileInfo> AudioFilesDb::QueryTracks()
|
||||
{
|
||||
std::vector<DbFileInfo> result;
|
||||
// get tracks fro the databse.
|
||||
SQLite::Statement query(
|
||||
*db,
|
||||
"SELECT idFile, fileName, "
|
||||
"lastModified, title, track, album,duration, "
|
||||
"thumbnailType, position, "
|
||||
"thumbnailFile, thumbnailLastModified "
|
||||
"FROM files ");
|
||||
|
||||
while (query.executeStep())
|
||||
{
|
||||
DbFileInfo row;
|
||||
row.idFile(query.getColumn(0).getInt64());
|
||||
row.fileName(query.getColumn(1).getText());
|
||||
row.lastModified(query.getColumn(2).getInt64());
|
||||
row.title(query.getColumn(3).getText());
|
||||
row.track(query.getColumn(4).getInt());
|
||||
row.album(query.getColumn(5).getText());
|
||||
row.duration(query.getColumn(6).getDouble());
|
||||
row.thumbnailType((ThumbnailType)query.getColumn(7).getInt());
|
||||
row.position(query.getColumn(8).getInt());
|
||||
row.thumbnailFile(query.getColumn(9).getText());
|
||||
row.thumbnailLastModified(query.getColumn(10).getInt64());
|
||||
result.push_back(std::move(row));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void AudioFilesDb::WriteFile(DbFileInfo *dbFile)
|
||||
{
|
||||
if (dbFile->idFile() == -1)
|
||||
{
|
||||
if (!insertFileQuery)
|
||||
{
|
||||
insertFileQuery = std::make_unique<SQLite::Statement>(
|
||||
*db,
|
||||
"INSERT INTO files ("
|
||||
"fileName, lastModified,"
|
||||
"title,track,album, "
|
||||
"duration, thumbnailType,thumbnailFile, thumbnailLastModified, position "
|
||||
") VALUES (?, ?, ?, ?, ?,?,?,?,?,?)");
|
||||
}
|
||||
insertFileQuery->tryReset();
|
||||
insertFileQuery->bind(1, dbFile->fileName());
|
||||
insertFileQuery->bind(2, dbFile->lastModified());
|
||||
insertFileQuery->bind(3, dbFile->title());
|
||||
insertFileQuery->bind(4, dbFile->track());
|
||||
insertFileQuery->bind(5, dbFile->album());
|
||||
insertFileQuery->bind(6, dbFile->duration());
|
||||
insertFileQuery->bind(7, (int32_t)dbFile->thumbnailType());
|
||||
insertFileQuery->bind(8, dbFile->thumbnailFile());
|
||||
insertFileQuery->bind(9, dbFile->thumbnailLastModified());
|
||||
insertFileQuery->bind(10, dbFile->position());
|
||||
insertFileQuery->exec();
|
||||
dbFile->idFile(db->getLastInsertRowid());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!updateFileQuery)
|
||||
{
|
||||
updateFileQuery = std::make_unique<SQLite::Statement>(
|
||||
*db,
|
||||
"UPDATE files SET "
|
||||
"fileName = ?, lastModified = ?, "
|
||||
"title = ?, track = ?, album = ?, "
|
||||
"duration = ?, thumbnailType = ?, "
|
||||
"thumbnailFile = ?, thumbnailLastModified = ?, position = ? "
|
||||
" WHERE idFile = ?");
|
||||
}
|
||||
updateFileQuery->tryReset();
|
||||
updateFileQuery->bind(1, dbFile->fileName());
|
||||
updateFileQuery->bind(2, dbFile->lastModified());
|
||||
updateFileQuery->bind(3, dbFile->title());
|
||||
updateFileQuery->bind(4, dbFile->track());
|
||||
updateFileQuery->bind(5, dbFile->album());
|
||||
updateFileQuery->bind(6, dbFile->duration());
|
||||
updateFileQuery->bind(7, (int32_t)dbFile->thumbnailType());
|
||||
updateFileQuery->bind(8, dbFile->thumbnailFile());
|
||||
updateFileQuery->bind(9, dbFile->thumbnailLastModified());
|
||||
updateFileQuery->bind(10, dbFile->position());
|
||||
|
||||
updateFileQuery->bind(11, dbFile->idFile());
|
||||
|
||||
updateFileQuery->exec();
|
||||
}
|
||||
}
|
||||
|
||||
ThumbnailInfo AudioFilesDb::GetThumbnailInfo(const std::string &fileNameOnly)
|
||||
{
|
||||
ThumbnailInfo result;
|
||||
SQLite::Statement query(
|
||||
*db,
|
||||
"SELECT thumbnailType, thumbnailFile, thumbnailLastModified FROM files WHERE fileName = ?");
|
||||
query.bind(1, fileNameOnly);
|
||||
if (query.executeStep())
|
||||
{
|
||||
result.thumbnailType((ThumbnailType)query.getColumn(0).getInt());
|
||||
result.thumbnailFile(query.getColumn(1).getText());
|
||||
result.thumbnailLastModified(query.getColumn(2).getInt64());
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("No thumbnail info found for file.");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> AudioFilesDb::GetEmbeddedThumbnail(int64_t idFile, int32_t width, int32_t height)
|
||||
{
|
||||
SQLite::Statement query(*db,
|
||||
"SELECT thumbnail FROM thumbnails "
|
||||
"WHERE idFile = ? AND width = ? AND height = ?");
|
||||
query.bind(1, idFile);
|
||||
query.bind(2, width);
|
||||
query.bind(3, height);
|
||||
|
||||
if (query.executeStep())
|
||||
{
|
||||
const uint8_t *data = (uint8_t *)query.getColumn(0).getBlob();
|
||||
int size = query.getColumn(0).getBytes();
|
||||
return std::vector<uint8_t>(data, data + size);
|
||||
}
|
||||
else
|
||||
{
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint8_t> AudioFilesDb::GetEmbeddedThumbnail(const std::string &fileNameOnly, int32_t width, int32_t height)
|
||||
{
|
||||
int64_t idFile;
|
||||
SQLite::Statement query(*db, "SELECT idFile FROM files WHERE fileName = ?");
|
||||
query.bind(1, fileNameOnly);
|
||||
if (query.executeStep())
|
||||
{
|
||||
idFile = query.getColumn(0).getInt64();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("File not found in database: " + fileNameOnly);
|
||||
}
|
||||
return GetEmbeddedThumbnail(idFile, width, height);
|
||||
}
|
||||
|
||||
void AudioFilesDb::AddThumbnail(
|
||||
const std::string &fileName,
|
||||
int64_t width,
|
||||
int64_t height,
|
||||
const std::vector<uint8_t> &thumbnailData)
|
||||
{
|
||||
int64_t idFile;
|
||||
SQLite::Statement query(*db, "SELECT idFile FROM files WHERE fileName = ?");
|
||||
query.bind(1, fileName);
|
||||
if (query.executeStep())
|
||||
{
|
||||
idFile = query.getColumn(0).getInt64();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("File not found in database: " + fileName);
|
||||
}
|
||||
AddThumbnail(idFile, width, height, thumbnailData);
|
||||
}
|
||||
void AudioFilesDb::AddThumbnail(
|
||||
int64_t idFile,
|
||||
int64_t width,
|
||||
int64_t height,
|
||||
const std::vector<uint8_t> &thumbnailData)
|
||||
{
|
||||
if (thumbnailData.empty())
|
||||
{
|
||||
throw std::runtime_error("Thumbnail data is empty.");
|
||||
}
|
||||
|
||||
SQLite::Statement insertQuery(
|
||||
*db,
|
||||
"INSERT INTO thumbnails (idFile, thumbnail, width, height) VALUES (?, ?, ?, ?)");
|
||||
insertQuery.bind(1, idFile);
|
||||
insertQuery.bind(2, thumbnailData.data(), thumbnailData.size());
|
||||
insertQuery.bind(3, width);
|
||||
insertQuery.bind(4, height);
|
||||
|
||||
try
|
||||
{
|
||||
insertQuery.exec();
|
||||
}
|
||||
catch (const SQLite::Exception &e)
|
||||
{
|
||||
throw std::runtime_error("Failed to add thumbnail: " + std::string(e.what()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include "SQLiteCpp/SQLiteCpp.h"
|
||||
#include "AudioFileMetadata.hpp"
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
|
||||
namespace pipedal::impl {
|
||||
|
||||
class DatabaseLock;
|
||||
|
||||
class DbFileInfo : public AudioFileMetadata
|
||||
{
|
||||
private:
|
||||
int64_t idFile_ = -1;
|
||||
bool present_ = false;
|
||||
bool dirty_ = false;
|
||||
public:
|
||||
int64_t idFile() const { return idFile_;}
|
||||
void idFile(int64_t value) { idFile_ = value;}
|
||||
bool present() const { return present_; }
|
||||
void present(bool value) { present_ = value;}
|
||||
bool dirty() const { return dirty_;}
|
||||
void dirty(bool value) { dirty_ = value;}
|
||||
|
||||
};
|
||||
|
||||
|
||||
class ThumbnailInfo {
|
||||
public:
|
||||
ThumbnailInfo() = default;
|
||||
private:
|
||||
ThumbnailType thumbnailType_ = ThumbnailType::Unknown;
|
||||
std::string thumbnailFile_;
|
||||
int64_t thumbnailLastModified_ = 0;
|
||||
public:
|
||||
ThumbnailType thumbnailType() const { return thumbnailType_; }
|
||||
void thumbnailType(ThumbnailType value) { thumbnailType_ = value; }
|
||||
const std::string &thumbnailFile() const { return thumbnailFile_; }
|
||||
void thumbnailFile(const std::string &value) { thumbnailFile_ = value; }
|
||||
int64_t thumbnailLastModified() const { return thumbnailLastModified_; }
|
||||
void thumbnailLastModified(int64_t value) { thumbnailLastModified_ = value; }
|
||||
};
|
||||
class AudioFilesDb {
|
||||
public:
|
||||
static constexpr int32_t DB_VERSION = 1;
|
||||
AudioFilesDb(
|
||||
const std::filesystem::path &path,
|
||||
const std::filesystem::path &indexPath = "" // if non-empty, forces the location of the ".index.pipedal" file.
|
||||
);
|
||||
public:
|
||||
void DeleteFile(DbFileInfo *dbFile);
|
||||
void WriteFile(DbFileInfo *dbFile);
|
||||
|
||||
// test only.
|
||||
size_t GetNumberOfThumbnails();
|
||||
void UpdateThumbnailInfo(
|
||||
int64_t idFile,
|
||||
ThumbnailType thumbnailType,
|
||||
const std::string &thumbnailFile = "",
|
||||
int64_t thumbnailLastModified = 0);
|
||||
void UpdateThumbnailInfo(
|
||||
const std::string&fileName,
|
||||
ThumbnailType thumbnailType,
|
||||
const std::string &thumbnailFile = "",
|
||||
int64_t thumbnailLastModified = 0);
|
||||
std::unique_ptr<SQLite::Transaction> transaction();
|
||||
std::vector<DbFileInfo> QueryTracks();
|
||||
void DeleteThumbnails(id_t idFile);
|
||||
ThumbnailInfo GetThumbnailInfo(const std::string &fileNameOnly);
|
||||
std::vector<uint8_t> GetEmbeddedThumbnail(const std::string &fileNameOnly, int32_t width, int32_t height);
|
||||
std::vector<uint8_t> GetEmbeddedThumbnail(int64_t idFile, int32_t width, int32_t height);
|
||||
void AddThumbnail(
|
||||
const std::string &fileName,
|
||||
int64_t width,
|
||||
int64_t height,
|
||||
const std::vector<uint8_t> &thumbnailData);
|
||||
void AddThumbnail(
|
||||
int64_t idFile,
|
||||
int64_t width,
|
||||
int64_t height,
|
||||
const std::vector<uint8_t> &thumbnailData);
|
||||
|
||||
private:
|
||||
|
||||
void CreateDb(const std::filesystem::path &dbPathName);
|
||||
void UpgradeDb();
|
||||
int QueryVersion();
|
||||
|
||||
private:
|
||||
std::filesystem::path indexPath;
|
||||
|
||||
// warning: destructor order matters here!
|
||||
std::shared_ptr<DatabaseLock> dbLock; // mutex to protect the index file.
|
||||
std::unique_ptr<SQLite::Database> db;
|
||||
std::unique_ptr<SQLite::Statement> insertFileQuery;
|
||||
std::unique_ptr<SQLite::Statement> updateFileQuery;
|
||||
std::unique_ptr<SQLite::Statement> deleteFileQuery;
|
||||
std::unique_ptr<SQLite::Statement> updateThumbnailInfoQueryByName;
|
||||
std::unique_ptr<SQLite::Statement> updateThumbnailInfoQueryById;
|
||||
std::filesystem::path path;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,536 @@
|
||||
// Copyright (c) 2025 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 "catch.hpp"
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include "AudioFiles.hpp"
|
||||
#include <filesystem>
|
||||
#include "SysExec.hpp"
|
||||
#include "json.hpp"
|
||||
#include "json_variant.hpp"
|
||||
#include "MimeTypes.hpp"
|
||||
|
||||
using namespace std;
|
||||
using namespace pipedal;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
fs::path testDir = fs::temp_directory_path() / "AudioFilesTest" / "files";
|
||||
fs::path sourceDirectory = fs::current_path() / "artifacts" / "TestTracks";
|
||||
fs::path tempDir = fs::temp_directory_path() / "AudioFilesTest" / "tmp";
|
||||
fs::path resourceDir = fs::current_path() / "vite" / "public" / "img";
|
||||
fs::path textIndexPath = fs::temp_directory_path() / "AudioFilesTest" / "TextIndex.pipedal";
|
||||
|
||||
static void CreateTestDirectory()
|
||||
{
|
||||
if (fs::exists(testDir))
|
||||
{
|
||||
fs::remove_all(testDir);
|
||||
}
|
||||
fs::remove_all(tempDir);
|
||||
fs::create_directories(testDir);
|
||||
if (fs::exists(tempDir))
|
||||
{
|
||||
fs::remove_all(tempDir);
|
||||
}
|
||||
fs::create_directories(tempDir);
|
||||
AudioDirectoryInfo::SetTemporaryDirectory(tempDir);
|
||||
AudioDirectoryInfo::SetResourceDirectory(resourceDir);
|
||||
}
|
||||
|
||||
static void CheckForLeakedTemporaryFiles()
|
||||
{
|
||||
bool leakedFiles = false;
|
||||
// Check if the temporary directory exists and is not empty
|
||||
if (fs::exists(tempDir) && fs::is_directory(tempDir))
|
||||
{
|
||||
for (const auto &entry : fs::directory_iterator(tempDir))
|
||||
{
|
||||
if (entry.is_regular_file())
|
||||
{
|
||||
cout << "Leaked temporary file: " << entry.path() << endl;
|
||||
leakedFiles = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fs::exists(tempDir) && fs::is_directory(tempDir / "thumbnails"))
|
||||
{
|
||||
for (const auto &entry : fs::directory_iterator(tempDir / "thumbnails"))
|
||||
{
|
||||
if (entry.is_regular_file())
|
||||
{
|
||||
cout << "Leaked temporary file: " << entry.path() << endl;
|
||||
leakedFiles = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (leakedFiles)
|
||||
{
|
||||
throw std::runtime_error("Leaked temporary files found in: " + tempDir.string());
|
||||
}
|
||||
}
|
||||
|
||||
static bool ContainsTrack(const std::vector<AudioFileMetadata> &files, const std::string &trackName)
|
||||
{
|
||||
for (const auto &file : files)
|
||||
{
|
||||
if (file.title() == trackName)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static bool ContainsFile(const std::vector<AudioFileMetadata> &files, const std::string &fileName)
|
||||
{
|
||||
for (const auto &file : files)
|
||||
{
|
||||
if (file.fileName() == fileName)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static const AudioFileMetadata *GetFile(const std::vector<AudioFileMetadata> &files, const std::string &fileName)
|
||||
{
|
||||
for (const auto &file : files)
|
||||
{
|
||||
if (file.fileName() == fileName)
|
||||
{
|
||||
return &file;
|
||||
}
|
||||
}
|
||||
throw std::logic_error("File not found: " + fileName);
|
||||
}
|
||||
|
||||
static void PrintFiles(const std::vector<AudioFileMetadata> &files)
|
||||
{
|
||||
for (const auto &file : files)
|
||||
{
|
||||
cout << "File: " << file.fileName() << ", Title: " << file.track() << ". " << file.title() << " Album: " << file.album() << endl;
|
||||
}
|
||||
cout << endl;
|
||||
}
|
||||
|
||||
void IndexTest()
|
||||
{
|
||||
CreateTestDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
{
|
||||
AudioDirectoryInfo::Ptr audioDir = AudioDirectoryInfo::Create(testDir.string());
|
||||
|
||||
}
|
||||
AudioDirectoryInfo::Ptr audioDir = AudioDirectoryInfo::Create(testDir.string());
|
||||
REQUIRE(audioDir != nullptr);
|
||||
if (fs::exists(testDir))
|
||||
{
|
||||
fs::remove_all(testDir); // Remove any existing database file
|
||||
}
|
||||
fs::create_directories(testDir);
|
||||
// Copy test files to the test directory
|
||||
for (const auto &entry : fs::directory_iterator(sourceDirectory))
|
||||
{
|
||||
if (entry.is_regular_file())
|
||||
{
|
||||
fs::copy(entry.path(), testDir / entry.path().filename());
|
||||
}
|
||||
}
|
||||
|
||||
auto files = audioDir->GetFiles();
|
||||
PrintFiles(files);
|
||||
|
||||
REQUIRE(files.size() == 3);
|
||||
|
||||
REQUIRE(ContainsTrack(files, "Track 1"));
|
||||
REQUIRE(ContainsTrack(files, "Track 2"));
|
||||
REQUIRE(ContainsTrack(files, "Track 3"));
|
||||
|
||||
for (const auto &file : files)
|
||||
{
|
||||
REQUIRE(!file.fileName().empty());
|
||||
}
|
||||
|
||||
// check indexed results.
|
||||
files = audioDir->GetFiles();
|
||||
REQUIRE(files.size() == 3);
|
||||
for (const auto &file : files)
|
||||
{
|
||||
REQUIRE(!file.fileName().empty());
|
||||
}
|
||||
|
||||
fs::remove(testDir / "Track2.mp3");
|
||||
|
||||
files = audioDir->GetFiles();
|
||||
REQUIRE(files.size() == 2);
|
||||
REQUIRE(!ContainsTrack(files, "Track 2"));
|
||||
REQUIRE(ContainsTrack(files, "Track 1"));
|
||||
REQUIRE(ContainsTrack(files, "Track 3"));
|
||||
|
||||
PrintFiles(files);
|
||||
files = audioDir->GetFiles();
|
||||
REQUIRE(files.size() == 2);
|
||||
REQUIRE(!ContainsTrack(files, "Track 2"));
|
||||
REQUIRE(ContainsTrack(files, "Track 1"));
|
||||
REQUIRE(ContainsTrack(files, "Track 3"));
|
||||
|
||||
// check dbinex results.
|
||||
|
||||
fs::copy_file(sourceDirectory / "Track2.mp3", testDir / "Track3.mp3", fs::copy_options::overwrite_existing);
|
||||
files = audioDir->GetFiles();
|
||||
REQUIRE(files.size() == 2);
|
||||
REQUIRE(ContainsTrack(files, "Track 2"));
|
||||
REQUIRE(ContainsFile(files, "Track3.mp3"));
|
||||
REQUIRE(!ContainsFile(files, "Track2.mp3"));
|
||||
|
||||
fs::copy_file(sourceDirectory / "Track3.mp3", testDir / "Track4.mp3", fs::copy_options::overwrite_existing);
|
||||
files = audioDir->GetFiles();
|
||||
REQUIRE(files.size() == 3);
|
||||
REQUIRE(ContainsTrack(files, "Track 2"));
|
||||
REQUIRE(ContainsFile(files, "Track3.mp3"));
|
||||
REQUIRE(GetFile(files, "Track4.mp3")->title() == "Track 3");
|
||||
|
||||
{
|
||||
auto thumbnail = audioDir->GetThumbnail("Track1.mp3", 200, 200);
|
||||
REQUIRE(audioDir->TestGetNumberOfThumbnails() == 1);
|
||||
}
|
||||
{
|
||||
auto thumbnail = audioDir->GetThumbnail("Track1.mp3", 200, 200);
|
||||
REQUIRE(audioDir->TestGetNumberOfThumbnails() == 1);
|
||||
}
|
||||
{
|
||||
auto thumbnail = audioDir->GetThumbnail("Track1.mp3", 0, 0);
|
||||
REQUIRE(audioDir->TestGetNumberOfThumbnails() == 2);
|
||||
}
|
||||
CheckForLeakedTemporaryFiles();
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
cout << "Exception: " << e.what() << endl;
|
||||
REQUIRE(false);
|
||||
}
|
||||
}
|
||||
|
||||
static void ThumbnailTest()
|
||||
{
|
||||
cout << "ThumbnailTest" << endl;
|
||||
CreateTestDirectory();
|
||||
|
||||
try
|
||||
{
|
||||
AudioDirectoryInfo::Ptr audioDir = AudioDirectoryInfo::Create(testDir.string());
|
||||
REQUIRE(audioDir != nullptr);
|
||||
if (fs::exists(testDir))
|
||||
{
|
||||
fs::remove_all(testDir); // Remove any existing database file
|
||||
}
|
||||
fs::create_directories(testDir);
|
||||
// Copy test files to the test directory
|
||||
for (const auto &entry : fs::directory_iterator(sourceDirectory))
|
||||
{
|
||||
if (entry.is_regular_file())
|
||||
{
|
||||
fs::copy(entry.path(), testDir / entry.path().filename());
|
||||
}
|
||||
}
|
||||
|
||||
auto files = audioDir->GetFiles();
|
||||
|
||||
REQUIRE(files.size() == 3);
|
||||
|
||||
for (const auto &file : files)
|
||||
{
|
||||
ThumbnailTemporaryFile thumbnail = audioDir->GetThumbnail(file.fileName(), 200, 200);
|
||||
REQUIRE(fs::exists(thumbnail.Path()));
|
||||
cout << "Thumbnail for " << file.fileName() << ": " << thumbnail.Path() << endl;
|
||||
|
||||
cout << " " << file.fileName() << "(200,200) size: " << fs::file_size(thumbnail.Path()) << endl;
|
||||
|
||||
thumbnail = audioDir->GetThumbnail(file.fileName(), 0, 0);
|
||||
REQUIRE(fs::exists(thumbnail.Path()));
|
||||
cout << "Thumbnail for " << file.fileName() << ": " << thumbnail.Path() << endl;
|
||||
|
||||
cout << " " << file.fileName() << "(0,0) size: " << fs::file_size(thumbnail.Path()) << endl;
|
||||
}
|
||||
|
||||
REQUIRE(audioDir->TestGetNumberOfThumbnails() == 2 * 2); // 3 files, one file is missing a thumbnail, 2 per file.
|
||||
|
||||
{
|
||||
ThumbnailTemporaryFile thumbnail = audioDir->GetThumbnail("Track1.mp3", 200, 200);
|
||||
REQUIRE(audioDir->TestGetNumberOfThumbnails() == 2 * 2); // 3 files, one file is missing a thumbnail, 2 per file.
|
||||
}
|
||||
fs::remove(testDir / "Track2.mp3");
|
||||
files = audioDir->GetFiles();
|
||||
|
||||
REQUIRE(audioDir->TestGetNumberOfThumbnails() == 2);
|
||||
|
||||
// update the last write time, should trigger a thumbnail update.
|
||||
REQUIRE(GetFile(files, "Track1.mp3")->thumbnailType() == ThumbnailType::Embedded);
|
||||
|
||||
fs::copy(sourceDirectory / "Track1.mp3", testDir / "Track1.mp3", fs::copy_options::overwrite_existing);
|
||||
files = audioDir->GetFiles();
|
||||
REQUIRE(GetFile(files, "Track1.mp3")->thumbnailType() == ThumbnailType::Unknown);
|
||||
REQUIRE(audioDir->TestGetNumberOfThumbnails() == 0);
|
||||
|
||||
audioDir->GetThumbnail("Track1.mp3", 200, 200);
|
||||
REQUIRE(audioDir->TestGetNumberOfThumbnails() == 1);
|
||||
|
||||
CheckForLeakedTemporaryFiles();
|
||||
cout << endl;
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
cout << "Exception: " << e.what() << endl;
|
||||
REQUIRE(false);
|
||||
}
|
||||
}
|
||||
|
||||
void ExecutionTimeTest()
|
||||
{
|
||||
cout << "Timing test" << endl;
|
||||
CreateTestDirectory();
|
||||
|
||||
// deploy sample files.
|
||||
fs::create_directories(testDir);
|
||||
// Copy test files to the test directory
|
||||
for (const auto &entry : fs::directory_iterator(sourceDirectory))
|
||||
{
|
||||
if (entry.is_regular_file())
|
||||
{
|
||||
fs::copy(entry.path(), testDir / entry.path().filename());
|
||||
}
|
||||
}
|
||||
|
||||
AudioDirectoryInfo::Ptr audioDir = AudioDirectoryInfo::Create(testDir.string());
|
||||
audioDir->GetFiles();
|
||||
|
||||
{
|
||||
using clock_t = std::chrono::high_resolution_clock;
|
||||
{
|
||||
auto start = clock_t::now();
|
||||
audioDir->GetThumbnail("Track1.mp3",200,200);
|
||||
auto duration = clock_t::now() - start;
|
||||
cout << "Track1.mp3(200,200): " << std::chrono::duration_cast<std::chrono::milliseconds>(duration).count() <<"ms" << endl;
|
||||
}
|
||||
{
|
||||
auto start = clock_t::now();
|
||||
audioDir->GetThumbnail("Track1.mp3",200,200);
|
||||
auto duration = clock_t::now() - start;
|
||||
cout << "Track1.mp3(200,200): " << std::chrono::duration_cast<std::chrono::milliseconds>(duration).count() <<"ms" << endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
TEST_CASE("AudioFiles test", "[AudioFiles]")
|
||||
{
|
||||
|
||||
IndexTest();
|
||||
ThumbnailTest();
|
||||
ExecutionTimeTest();
|
||||
}
|
||||
|
||||
// from AudioFileMetadata.cpp
|
||||
std::string shell_escape_filename(const std::string &filename);
|
||||
|
||||
static void ScanThumbnails()
|
||||
{
|
||||
const std::string homeDir = getenv("HOME");
|
||||
std::filesystem::path musicDir = std::filesystem::path(homeDir) / "Music";
|
||||
|
||||
for (const auto &entry : std::filesystem::recursive_directory_iterator(musicDir))
|
||||
{
|
||||
if (entry.is_directory())
|
||||
{
|
||||
continue; // Skip directories
|
||||
}
|
||||
|
||||
if (entry.path().extension() == ".mp3" || entry.path().extension() == ".flac")
|
||||
{
|
||||
try
|
||||
{
|
||||
// ffprobe -i ~/tmp/test.flac -show_streams -show_entries stream=index,codec_name,disposition
|
||||
|
||||
std::stringstream args;
|
||||
args << "-log_level error -i " << shell_escape_filename(entry.path())
|
||||
<< " -show_streams -of json -show_entries stream=index,codec_name,disposition 2>/dev/null";
|
||||
|
||||
std::string sargs = args.str();
|
||||
auto output = sysExecForOutput("/usr/bin/ffprobe", sargs, true);
|
||||
if (output.exitCode != 0)
|
||||
{
|
||||
cout << "ffprobe failed for file: " << entry.path() << endl;
|
||||
continue;
|
||||
}
|
||||
std::stringstream ss(output.output);
|
||||
json_reader reader(ss);
|
||||
|
||||
json_variant vt;
|
||||
reader.read(&vt);
|
||||
auto streams = vt.as_object()->at("streams").as_array();
|
||||
struct StreamInfo
|
||||
{
|
||||
int index;
|
||||
std::string codec_name;
|
||||
std::string comment;
|
||||
};
|
||||
std::vector<StreamInfo> thumbnailStreams;
|
||||
for (const auto &stream : *streams)
|
||||
{
|
||||
auto streamObj = stream.as_object();
|
||||
int index = streamObj->at("index").as_int64();
|
||||
std::string codecName = streamObj->at("codec_name").as_string();
|
||||
std::string comment;
|
||||
if (streamObj->contains("tags"))
|
||||
{
|
||||
auto &tagsObj = streamObj->at("tags");
|
||||
auto &tags = tagsObj.as_object();
|
||||
if (tags->contains("comment"))
|
||||
{
|
||||
comment = tags->at("comment").as_string();
|
||||
}
|
||||
}
|
||||
thumbnailStreams.push_back({index, codecName, comment});
|
||||
}
|
||||
if (thumbnailStreams.size() > 2)
|
||||
{
|
||||
cout << "File: " << entry.path() << endl;
|
||||
for (const auto &stream : thumbnailStreams)
|
||||
{
|
||||
cout << " Stream Index: " << stream.index
|
||||
<< ", Codec: " << stream.codec_name
|
||||
<< ", Comment: " << stream.comment << endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
cout << "Error processing file: " << entry.path() << " - " << e.what() << endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TestTimer {
|
||||
public:
|
||||
using clock_t = std::chrono::high_resolution_clock;
|
||||
void Start()
|
||||
{
|
||||
startTime = clock_t::now();
|
||||
}
|
||||
uint64_t Stop()
|
||||
{
|
||||
auto elapsed = clock_t::now()-startTime;
|
||||
elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count();
|
||||
return elapsedMs;
|
||||
|
||||
}
|
||||
double ElapsedMs()
|
||||
{
|
||||
return elapsedMs;
|
||||
}
|
||||
private:
|
||||
clock_t::time_point startTime;
|
||||
uint64_t elapsedMs = 0;
|
||||
};
|
||||
|
||||
static bool isAudioExtension(const std::string &extension)
|
||||
{
|
||||
return MimeTypes::instance().AudioExtensions().contains(extension);
|
||||
}
|
||||
|
||||
|
||||
static bool ContainsMusic(const std::filesystem::path&path)
|
||||
{
|
||||
for (auto dirEntry: fs::directory_iterator(path)) {
|
||||
if (fs::is_regular_file(dirEntry.path())) {
|
||||
if (isAudioExtension(dirEntry.path().extension())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ProfileMusicDirectory() {
|
||||
const std::string homeDir = getenv("HOME");
|
||||
std::filesystem::path musicDir = std::filesystem::path(homeDir) / "Music";
|
||||
|
||||
for (const auto &entry : std::filesystem::recursive_directory_iterator(musicDir))
|
||||
{
|
||||
if (!entry.is_directory())
|
||||
{
|
||||
continue; // Skip directories
|
||||
}
|
||||
if (!ContainsMusic(entry.path())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fs::remove(textIndexPath);
|
||||
AudioDirectoryInfo::Ptr directoryInfo = AudioDirectoryInfo::Create(entry.path());
|
||||
fs::create_directories(textIndexPath.parent_path());
|
||||
directoryInfo->TestSetIndexPath(textIndexPath);
|
||||
|
||||
uint64_t firstTime;
|
||||
uint64_t dbIndexSize;
|
||||
uint64_t entries;
|
||||
{
|
||||
TestTimer t;
|
||||
t.Start();
|
||||
AudioDirectoryInfo::Ptr directoryInfo = AudioDirectoryInfo::Create(entry.path());
|
||||
directoryInfo->TestSetIndexPath(textIndexPath);
|
||||
entries = directoryInfo->GetFiles().size();
|
||||
firstTime = t.Stop();
|
||||
dbIndexSize = fs::file_size(textIndexPath);
|
||||
}
|
||||
uint64_t secondTime;
|
||||
{
|
||||
TestTimer t;
|
||||
t.Start();
|
||||
AudioDirectoryInfo::Ptr directoryInfo = AudioDirectoryInfo::Create(entry.path());
|
||||
directoryInfo->TestSetIndexPath(textIndexPath);
|
||||
directoryInfo->GetFiles();
|
||||
secondTime = t.Stop();
|
||||
}
|
||||
cout << entry.path().string() << endl;
|
||||
cout << " " << "first time: " << firstTime << " second time: " << secondTime << endl;
|
||||
cout << " " << "size: " << dbIndexSize << " entries: " << entries << endl;
|
||||
}
|
||||
|
||||
}
|
||||
TEST_CASE("Search for audio files with multple thumbnails", "[ScanThumbnails]")
|
||||
{
|
||||
AudioDirectoryInfo::SetTemporaryDirectory(tempDir);
|
||||
AudioDirectoryInfo::SetResourceDirectory(resourceDir);
|
||||
ScanThumbnails(); // scan all files in music directory.
|
||||
ProfileMusicDirectory(); // Scans all folders in music directory.
|
||||
|
||||
REQUIRE(true); // Just to ensure the test runs without failure
|
||||
}
|
||||
|
||||
TEST_CASE("Thumbnail performance test", "[ProfileThumbnails]" )
|
||||
{
|
||||
AudioDirectoryInfo::SetTemporaryDirectory(tempDir);
|
||||
AudioDirectoryInfo::SetResourceDirectory(resourceDir);
|
||||
ProfileMusicDirectory(); // Scans all folders in music directory.
|
||||
|
||||
REQUIRE(true); // Just to ensure the test runs without failure
|
||||
}
|
||||
+11
-5
@@ -46,7 +46,6 @@ include(FindPkgConfig)
|
||||
|
||||
find_package(sdbus-c++ REQUIRED)
|
||||
|
||||
# find_package(SQLiteCpp REQUIRED)
|
||||
|
||||
|
||||
|
||||
@@ -153,7 +152,8 @@ add_compile_definitions(DEBIAN_ARCHITECTURE="${DEBIAN_ARCHITECTURE}")
|
||||
if (CMAKE_BUILD_TYPE MATCHES Debug)
|
||||
message(STATUS "Debug build")
|
||||
|
||||
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -g -DDEBUG -D_GLIBCXX_DEBUG" )
|
||||
# Must not -D_GLIBCXX_DEBUG, since it conflict with SQLiteCpp.lol
|
||||
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -g -DDEBUG " )
|
||||
if (USE_SANITIZE)
|
||||
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -static-libasan " )
|
||||
endif()
|
||||
@@ -197,7 +197,9 @@ endif()
|
||||
|
||||
set (PIPEDAL_SOURCES
|
||||
AudioFiles.cpp AudioFiles.hpp
|
||||
AudioFileMetadata.cpp AudioFileMetadata.hpp
|
||||
AudioFileMetadataReader.cpp AudioFileMetadataReader.hpp
|
||||
AudioFileMetadata.hpp AudioFileMetadata.cpp
|
||||
AudioFilesDb.hpp AudioFilesDb.cpp
|
||||
LRUCache.hpp
|
||||
CpuTemperatureMonitor.cpp CpuTemperatureMonitor.hpp
|
||||
SchedulerPriority.hpp SchedulerPriority.cpp
|
||||
@@ -332,7 +334,10 @@ add_library(libpipedald STATIC ${PIPEDAL_SOURCES})
|
||||
target_include_directories(libpipedald PRIVATE ${PIPEDAL_INCLUDES}
|
||||
)
|
||||
|
||||
target_link_libraries(libpipedald PUBLIC PiPedalCommon)
|
||||
target_link_libraries(libpipedald
|
||||
PUBLIC
|
||||
PiPedalCommon
|
||||
SQLiteCpp)
|
||||
|
||||
if(${USE_PCH})
|
||||
target_precompile_headers(libpipedald PRIVATE pch.h)
|
||||
@@ -355,7 +360,7 @@ target_link_libraries(pipedal_kconfig
|
||||
PRIVATE ftxui::component # Not needed for this example.
|
||||
PRIVATE PiPedalCommon
|
||||
systemd
|
||||
)
|
||||
)
|
||||
|
||||
#################################
|
||||
add_executable(pipedald
|
||||
@@ -391,6 +396,7 @@ add_executable(AuxInTest
|
||||
add_executable(pipedaltest
|
||||
testMain.cpp
|
||||
|
||||
AudioFilesTest.cpp
|
||||
LRUCacheTest.cpp
|
||||
ModFileTypesTest.cpp
|
||||
jsonTest.cpp
|
||||
|
||||
@@ -26,6 +26,7 @@ JSON_MAP_BEGIN(FileEntry)
|
||||
JSON_MAP_REFERENCE(FileEntry,displayName)
|
||||
JSON_MAP_REFERENCE(FileEntry,isProtected)
|
||||
JSON_MAP_REFERENCE(FileEntry,isDirectory)
|
||||
JSON_MAP_REFERENCE(FileEntry,metadata)
|
||||
JSON_MAP_END()
|
||||
|
||||
JSON_MAP_BEGIN(BreadcrumbEntry)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "json.hpp"
|
||||
#include "AudioFileMetadata.hpp"
|
||||
|
||||
namespace pipedal {
|
||||
|
||||
@@ -30,11 +31,19 @@ namespace pipedal {
|
||||
:pathname_(pathname), displayName_(displayName), isDirectory_(isDirectory),isProtected_(isProtected)
|
||||
{
|
||||
|
||||
}
|
||||
FileEntry(const std::string&pathname,const std::string &displayName, bool isProtected,
|
||||
std::shared_ptr<AudioFileMetadata> metadata
|
||||
)
|
||||
:pathname_(pathname), displayName_(displayName), isDirectory_(false),isProtected_(isProtected),metadata_(metadata)
|
||||
{
|
||||
|
||||
}
|
||||
std::string pathname_;
|
||||
std::string displayName_;
|
||||
bool isDirectory_ = false;
|
||||
bool isProtected_ = false;
|
||||
std::shared_ptr<AudioFileMetadata> metadata_;
|
||||
|
||||
DECLARE_JSON_MAP(FileEntry);
|
||||
|
||||
|
||||
+22
-3
@@ -58,19 +58,38 @@ static std::string toLower(const std::string&value)
|
||||
return result;
|
||||
}
|
||||
|
||||
const std::set<std::string> playListExtensions = {
|
||||
".m3u",
|
||||
".m3u8",
|
||||
".pls",
|
||||
".wpl"
|
||||
};
|
||||
|
||||
const std::set<std::string> midiMimeTypes = {
|
||||
"audio/midi",
|
||||
"audio/sp-midi",
|
||||
"audio/imelody",
|
||||
"audio/xmf",
|
||||
"audio/x-midi",
|
||||
"audio/x-musicxml+xml"
|
||||
};
|
||||
|
||||
void MimeTypes::AddMimeType(const std::string&extension_, const std::string&mimeType)
|
||||
{
|
||||
std::string extension = SS("." << toLower(extension_));
|
||||
mimeTypeToExtensions[mimeType].insert(extension);
|
||||
mimeTypeToExtension[mimeType] = extension;
|
||||
extensionToMimeType[extension] = mimeType;
|
||||
if (mimeType.starts_with("audio/midi"))
|
||||
if (midiMimeTypes.contains(mimeType))
|
||||
{
|
||||
midiExtensions.insert(extension);
|
||||
} else if (mimeType.starts_with("audio/"))
|
||||
{
|
||||
mimeTypeToExtensions["audio/*"].insert(extension);
|
||||
audioExtensions.insert(extension);
|
||||
if (!playListExtensions.contains(extension))
|
||||
{
|
||||
mimeTypeToExtensions["audio/*"].insert(extension);
|
||||
audioExtensions.insert(extension);
|
||||
}
|
||||
}
|
||||
if (mimeType.starts_with("video/"))
|
||||
{
|
||||
|
||||
@@ -2364,6 +2364,8 @@ FileRequestResult PiPedalModel::GetFileList2(const std::string &relativePath_, c
|
||||
{
|
||||
if (!storage.IsInUploadsDirectory(relativePath))
|
||||
{
|
||||
|
||||
|
||||
// if relativePath is in a resource directory of the plugin, then we have loaded a factory preset or are using a default property.
|
||||
// map the resource path to the corresponding file in the uploads directory.
|
||||
// :-(
|
||||
|
||||
+121
-51
@@ -37,6 +37,7 @@
|
||||
#include <set>
|
||||
#include <MimeTypes.hpp>
|
||||
#include "util.hpp"
|
||||
#include "AudioFiles.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
namespace fs = std::filesystem;
|
||||
@@ -46,7 +47,6 @@ const char *BANKS_FILENAME = "index.banks";
|
||||
|
||||
#define USER_SETTINGS_FILENAME "userSettings.json";
|
||||
|
||||
|
||||
static bool hasSyntheticModRoot(const UiFileProperty &fileProperty)
|
||||
{
|
||||
return (fileProperty.modDirectories().size() > 1 || (fileProperty.modDirectories().size() == 1 && fileProperty.useLegacyModDirectory()));
|
||||
@@ -1600,7 +1600,6 @@ static void ThrowPermissionDeniedError()
|
||||
throw std::logic_error("Permission denied.");
|
||||
}
|
||||
|
||||
|
||||
static bool ensureNoDotDot(const std::filesystem::path &path)
|
||||
{
|
||||
for (auto segment_ : path)
|
||||
@@ -1627,13 +1626,13 @@ static bool ensureNoDotDot(const std::filesystem::path &path)
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static void AddFilesToResult(
|
||||
FileRequestResult &result,
|
||||
const ModFileTypes::ModDirectory *modDirectoryInfo, //yyx
|
||||
const ModFileTypes::ModDirectory *modDirectoryInfo, // yyx
|
||||
const UiFileProperty &fileProperty,
|
||||
const fs::path &rootPath)
|
||||
{
|
||||
|
||||
if (!fs::exists(rootPath))
|
||||
{
|
||||
return; // silently without error.
|
||||
@@ -1641,10 +1640,9 @@ static void AddFilesToResult(
|
||||
auto &resultFiles = result.files_;
|
||||
|
||||
std::set<std::string> validExtensions = fileProperty.GetPermittedFileExtensions(
|
||||
modDirectoryInfo? modDirectoryInfo->modType: "");
|
||||
modDirectoryInfo ? modDirectoryInfo->modType : "");
|
||||
|
||||
|
||||
try
|
||||
try
|
||||
{
|
||||
for (auto const &dir_entry : std::filesystem::directory_iterator(rootPath))
|
||||
{
|
||||
@@ -1657,10 +1655,9 @@ static void AddFilesToResult(
|
||||
bool match = false;
|
||||
if (name.length() > 0 && name[0] != '.') // don't show hidden files.
|
||||
{
|
||||
bool match = validExtensions.size() == 0 || validExtensions.contains(extension)
|
||||
|| validExtensions.contains(".*");
|
||||
bool match = validExtensions.size() == 0 || validExtensions.contains(extension) || validExtensions.contains(".*");
|
||||
|
||||
if (match)
|
||||
if (match && !name.starts_with("."))
|
||||
{
|
||||
resultFiles.push_back(
|
||||
FileEntry(path, name, false, false));
|
||||
@@ -1690,6 +1687,57 @@ static void AddFilesToResult(
|
||||
}
|
||||
return collator->Compare(l.displayName_,r.displayName_) < 0; });
|
||||
}
|
||||
|
||||
static void AddTracksToResult(
|
||||
FileRequestResult &result,
|
||||
const ModFileTypes::ModDirectory *modDirectoryInfo, // yyx
|
||||
const UiFileProperty &fileProperty,
|
||||
const fs::path &rootPath)
|
||||
{
|
||||
|
||||
if (!fs::exists(rootPath))
|
||||
{
|
||||
return; // silently without error.
|
||||
}
|
||||
auto &resultFiles = result.files_;
|
||||
|
||||
std::set<std::string> validExtensions = fileProperty.GetPermittedFileExtensions(
|
||||
modDirectoryInfo ? modDirectoryInfo->modType : "");
|
||||
|
||||
try
|
||||
{
|
||||
// Add directories first.
|
||||
for (auto const &dir_entry : std::filesystem::directory_iterator(rootPath))
|
||||
{
|
||||
const auto &path = dir_entry.path();
|
||||
auto name = path.filename().string();
|
||||
if (dir_entry.is_directory())
|
||||
{
|
||||
resultFiles.push_back(FileEntry{path, name, true, fs::is_symlink(path)});
|
||||
}
|
||||
}
|
||||
auto audioFiles = AudioDirectoryInfo::Create(rootPath);
|
||||
for (const auto &audioFile : audioFiles->GetFiles())
|
||||
{
|
||||
fs::path audioFilePath = rootPath / audioFile.fileName();
|
||||
std::string extension = UiFileProperty::GetFileExtension(audioFilePath);
|
||||
if (validExtensions.size() == 0 || validExtensions.contains(extension) || validExtensions.contains(".*"))
|
||||
{
|
||||
resultFiles.push_back(
|
||||
FileEntry(
|
||||
audioFilePath,
|
||||
audioFile.title(),
|
||||
false,
|
||||
std::make_shared<AudioFileMetadata>(audioFile)));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception &error)
|
||||
{
|
||||
throw std::logic_error("GetFileList failed. Directory not found: " + rootPath.string());
|
||||
}
|
||||
}
|
||||
|
||||
FileRequestResult Storage::GetModFileList2(const std::string &relativePath, const UiFileProperty &fileProperty)
|
||||
{
|
||||
FileRequestResult result;
|
||||
@@ -1771,13 +1819,20 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AddFilesToResult(result,rootModDirectory, fileProperty, relativePath);
|
||||
if (IsInAudioTracksDirectory(relativePath))
|
||||
{
|
||||
AddTracksToResult(result, rootModDirectory, fileProperty, relativePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddFilesToResult(result, rootModDirectory, fileProperty, relativePath);
|
||||
}
|
||||
result.currentDirectory_ = relativePath;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool IsChildDirectory(const fs::path& child, const fs::path&parent) {
|
||||
static bool IsChildDirectory(const fs::path &child, const fs::path &parent)
|
||||
{
|
||||
auto iChild = child.begin();
|
||||
for (auto i = parent.begin(); i != parent.end(); ++i)
|
||||
{
|
||||
@@ -1824,17 +1879,15 @@ FileRequestResult Storage::GetFileList2(const std::string &relativePath_, const
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsChildDirectory(absolutePath,pluginRootDirectory))
|
||||
if (!IsChildDirectory(absolutePath, pluginRootDirectory))
|
||||
{
|
||||
absolutePath = pluginRootDirectory;
|
||||
}
|
||||
|
||||
|
||||
result.currentDirectory_ = absolutePath;
|
||||
|
||||
|
||||
{
|
||||
// watch out for resource files!!!
|
||||
// watch out for resource files!!!
|
||||
result.breadcrumbs_.push_back({"", "Home"});
|
||||
fs::path fsAbsolutePath{absolutePath};
|
||||
auto iAbsolutePath = fsAbsolutePath.begin();
|
||||
@@ -1860,13 +1913,20 @@ FileRequestResult Storage::GetFileList2(const std::string &relativePath_, const
|
||||
throw std::runtime_error(SS("Improper location. " << absolutePath));
|
||||
}
|
||||
|
||||
const ModFileTypes::ModDirectory*pModDirectory = nullptr;
|
||||
const ModFileTypes::ModDirectory *pModDirectory = nullptr;
|
||||
if (fileProperty.modDirectories().size() > 0)
|
||||
{
|
||||
pModDirectory = ModFileTypes::GetModDirectory(fileProperty.modDirectories()[0]);
|
||||
}
|
||||
|
||||
AddFilesToResult(result,pModDirectory, fileProperty, absolutePath);
|
||||
if (IsInAudioTracksDirectory(absolutePath))
|
||||
{
|
||||
AddTracksToResult(result, pModDirectory, fileProperty, absolutePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddFilesToResult(result, pModDirectory, fileProperty, absolutePath);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1960,10 +2020,10 @@ std::filesystem::path Storage::MakeUserFilePath(const std::string &directory, co
|
||||
}
|
||||
return result;
|
||||
}
|
||||
std::string Storage::UploadUserFile(const std::string &directory,
|
||||
UiFileProperty::ptr uiFileProperty,
|
||||
const std::string &filename,
|
||||
std::istream &stream, size_t contentLength)
|
||||
std::string Storage::UploadUserFile(const std::string &directory,
|
||||
UiFileProperty::ptr uiFileProperty,
|
||||
const std::string &filename,
|
||||
std::istream &stream, size_t contentLength)
|
||||
{
|
||||
std::filesystem::path path;
|
||||
if (directory.length() != 0)
|
||||
@@ -1975,9 +2035,11 @@ std::string Storage::UploadUserFile(const std::string &directory,
|
||||
throw std::logic_error("Directory argument not supplied.");
|
||||
}
|
||||
fs::path relativePath;
|
||||
try {
|
||||
relativePath = MakeRelativePath(path,this->GetPluginUploadDirectory());
|
||||
} catch (const std::exception& e)
|
||||
try
|
||||
{
|
||||
relativePath = MakeRelativePath(path, this->GetPluginUploadDirectory());
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
throw std::logic_error("Permission denied. Path is outside the upload storage directory.");
|
||||
}
|
||||
@@ -1986,7 +2048,6 @@ std::string Storage::UploadUserFile(const std::string &directory,
|
||||
{
|
||||
throw std::logic_error("Permission denied. Invalid file extension for this directory.");
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
try
|
||||
@@ -2106,26 +2167,23 @@ void Storage::FillSampleDirectoryTree(FilePropertyDirectoryTree *node, const std
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
static void GetAllExtensions(std::set<std::string>&result, const std::filesystem::path &path)
|
||||
static void GetAllExtensions(std::set<std::string> &result, const std::filesystem::path &path)
|
||||
{
|
||||
assert(fs::is_directory(path));
|
||||
|
||||
for (const auto&dirEnt : fs::directory_iterator(path))
|
||||
for (const auto &dirEnt : fs::directory_iterator(path))
|
||||
{
|
||||
if (dirEnt.is_directory())
|
||||
{
|
||||
GetAllExtensions(result,dirEnt.path());
|
||||
} else {
|
||||
GetAllExtensions(result, dirEnt.path());
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string filename = dirEnt.path().filename();
|
||||
if (!filename.starts_with('.'))
|
||||
if (dirEnt.path().has_extension())
|
||||
{
|
||||
if (dirEnt.path().has_extension())
|
||||
{
|
||||
std::string extension = dirEnt.path().extension();
|
||||
result.insert(extension);
|
||||
}
|
||||
std::string extension = dirEnt.path().extension();
|
||||
result.insert(extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2134,28 +2192,33 @@ static void GetAllExtensions(std::set<std::string>&result, const std::filesystem
|
||||
static std::set<std::string> GetAllExtensions(const std::filesystem::path &path)
|
||||
{
|
||||
std::set<std::string> result;
|
||||
if (fs::is_regular_file(path)) {
|
||||
if (fs::is_regular_file(path))
|
||||
{
|
||||
result.insert(UiFileProperty::GetFileExtension(path));
|
||||
} else {
|
||||
GetAllExtensions(result,path);
|
||||
}
|
||||
else
|
||||
{
|
||||
GetAllExtensions(result, path);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
FilePropertyDirectoryTree::ptr Storage::GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty, const std::filesystem::path&selectedPath)
|
||||
FilePropertyDirectoryTree::ptr Storage::GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty, const std::filesystem::path &selectedPath)
|
||||
{
|
||||
fs::path uploadDirectory = this->GetPluginUploadDirectory();
|
||||
|
||||
fs::path relativePath;
|
||||
try {
|
||||
relativePath = MakeRelativePath(selectedPath,uploadDirectory);
|
||||
} catch (const std::exception&e) {
|
||||
try
|
||||
{
|
||||
relativePath = MakeRelativePath(selectedPath, uploadDirectory);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
// not an upload directory.
|
||||
throw std::runtime_error(SS("Permission denied: " << selectedPath));
|
||||
}
|
||||
|
||||
std::set<std::string> fileExtensions = GetAllExtensions(selectedPath);
|
||||
|
||||
|
||||
if (hasSyntheticModRoot(uiFileProperty))
|
||||
{
|
||||
|
||||
@@ -2172,10 +2235,10 @@ FilePropertyDirectoryTree::ptr Storage::GetFilePropertydirectoryTree(const UiFil
|
||||
auto modDirectoryInfo = ModFileTypes::GetModDirectory(modDirectory);
|
||||
if (modDirectoryInfo)
|
||||
{
|
||||
if (IsSubset(fileExtensions,modDirectoryInfo->fileExtensions) ||
|
||||
if (IsSubset(fileExtensions, modDirectoryInfo->fileExtensions) ||
|
||||
modDirectoryInfo->fileExtensions.contains(".*") ||
|
||||
modDirectoryInfo->fileExtensions.empty() // Private directory that accepts files of any type.
|
||||
)
|
||||
)
|
||||
{
|
||||
auto childPath = uploadDirectory / modDirectoryInfo->pipedalPath;
|
||||
FilePropertyDirectoryTree::ptr child = std::make_unique<FilePropertyDirectoryTree>(
|
||||
@@ -2213,9 +2276,16 @@ FilePropertyDirectoryTree::ptr Storage::GetFilePropertydirectoryTree(const UiFil
|
||||
}
|
||||
}
|
||||
|
||||
bool Storage::IsInUploadsDirectory(const std::filesystem::path&path) const
|
||||
bool Storage::IsInAudioTracksDirectory(const std::filesystem::path &path) const
|
||||
{
|
||||
return IsSubdirectory(path,this->GetPluginUploadDirectory());
|
||||
std::filesystem::path audioTracksDirectory =
|
||||
this->GetPluginUploadDirectory() / "shared/audio/Tracks";
|
||||
return IsSubdirectory(path, audioTracksDirectory);
|
||||
}
|
||||
|
||||
bool Storage::IsInUploadsDirectory(const std::filesystem::path &path) const
|
||||
{
|
||||
return IsSubdirectory(path, this->GetPluginUploadDirectory());
|
||||
}
|
||||
|
||||
const PluginPresetIndex &Storage::GetPluginPresetIndex()
|
||||
|
||||
@@ -155,6 +155,7 @@ public:
|
||||
int64_t DeleteBank(int64_t bankId);
|
||||
|
||||
bool IsInUploadsDirectory(const std::filesystem::path&path) const;
|
||||
bool IsInAudioTracksDirectory(const std::filesystem::path&path) const;
|
||||
|
||||
FileRequestResult GetFileList2(const std::string&relativePath,const UiFileProperty&fileProperty);
|
||||
|
||||
|
||||
+25
-3
@@ -52,14 +52,36 @@ TemporaryFile::TemporaryFile(const std::filesystem::path&directory)
|
||||
this->path = filename;
|
||||
|
||||
}
|
||||
void TemporaryFile::Detach() {
|
||||
this->path.clear();
|
||||
std::filesystem::path TemporaryFile::Detach() {
|
||||
std::filesystem::path result = std::move(this->path);
|
||||
return result;
|
||||
}
|
||||
TemporaryFile::~TemporaryFile()
|
||||
{
|
||||
if (!path.empty())
|
||||
if (!path.empty() && deleteFile)
|
||||
{
|
||||
std::filesystem::remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
void TemporaryFile::SetNonDeletedPath(const std::filesystem::path&path)
|
||||
{
|
||||
this->path = path;
|
||||
this->deleteFile = false; // Do not delete the file on destruction
|
||||
}
|
||||
|
||||
|
||||
|
||||
TemporaryFile::TemporaryFile(TemporaryFile&&other) {
|
||||
this->path = std::move(other.path);
|
||||
this->deleteFile = other.deleteFile;
|
||||
}
|
||||
TemporaryFile&TemporaryFile::operator=(TemporaryFile&&other) {
|
||||
if (!this->path.empty() && this->deleteFile) {
|
||||
std::filesystem::remove(this->path);
|
||||
this->path.clear();
|
||||
}
|
||||
this->path = std::move(other.path);
|
||||
this->deleteFile = other.deleteFile;
|
||||
return *this;
|
||||
}
|
||||
|
||||
+11
-4
@@ -25,18 +25,25 @@ namespace pipedal {
|
||||
TemporaryFile() {}
|
||||
TemporaryFile(const TemporaryFile&) = delete;
|
||||
TemporaryFile&operator=(const TemporaryFile&) = delete;
|
||||
TemporaryFile(TemporaryFile&&other);
|
||||
TemporaryFile&operator=(TemporaryFile&&other);
|
||||
public:
|
||||
explicit TemporaryFile(const std::filesystem::path&parentDirectory);
|
||||
|
||||
TemporaryFile(TemporaryFile&&other) {
|
||||
this->path = std::move(other.path);
|
||||
void Attach(const std::filesystem::path&path) {
|
||||
this->path = path;
|
||||
deleteFile = true; // default to deleting the file on destruction.
|
||||
}
|
||||
TemporaryFile(const std::filesystem::path&parentDirectory);
|
||||
void SetNonDeletedPath(const std::filesystem::path&path);
|
||||
|
||||
void Detach();
|
||||
bool DeleteFile() const { return deleteFile; }
|
||||
std::filesystem::path Detach();
|
||||
~TemporaryFile();
|
||||
const std::filesystem::path&Path()const { return path;}
|
||||
std::string str() const { return path.c_str(); }
|
||||
const char*c_str() const { return path.c_str(); }
|
||||
private:
|
||||
bool deleteFile = true;
|
||||
std::filesystem::path path;
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -738,7 +738,7 @@ namespace pipedal
|
||||
virtual void setBodyFile(std::shared_ptr<TemporaryFile>&bodyFile) override {
|
||||
// cast away const to do what ther request would do if it had that method.
|
||||
|
||||
request.set_body_file(bodyFile->Path(),true);
|
||||
request.set_body_file(bodyFile->Path(),bodyFile->DeleteFile());
|
||||
bodyFile->Detach();
|
||||
}
|
||||
virtual void setBodyFile(std::filesystem::path&path, bool deleteWhenDone) override {
|
||||
|
||||
+156
-35
@@ -35,10 +35,9 @@
|
||||
#include "json.hpp"
|
||||
#include "HotspotManager.hpp"
|
||||
#include "MimeTypes.hpp"
|
||||
#include "AudioFileMetadata.hpp"
|
||||
#include "AudioFileMetadataReader.hpp"
|
||||
#include "AudioFiles.hpp"
|
||||
|
||||
|
||||
#define OLD_PRESET_EXTENSION ".piPreset"
|
||||
#define PRESET_EXTENSION ".piPreset"
|
||||
#define OLD_BANK_EXTENSION ".piBank"
|
||||
@@ -90,6 +89,16 @@ static std::string GetMimeType(const std::filesystem::path &path)
|
||||
auto result = mimeTypes.MimeTypeFromExtension(extension);
|
||||
return result;
|
||||
}
|
||||
|
||||
int32_t ConvertThumbnailSize(const std::string ¶m)
|
||||
{
|
||||
if (param.empty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return static_cast<int32_t > (std::stoi(param));
|
||||
}
|
||||
|
||||
static bool IsZipFile(const std::filesystem::path &path)
|
||||
{
|
||||
std::ifstream f(path);
|
||||
@@ -185,10 +194,48 @@ public:
|
||||
else if (segment == "AudioMetadata")
|
||||
{
|
||||
return true;
|
||||
} else if (segment == "NextAudioFile")
|
||||
}
|
||||
else if (segment == "Thumbnail")
|
||||
{
|
||||
return true;
|
||||
} else if (segment == "PreviousAudioFile")
|
||||
}
|
||||
else if (segment == "PluginPresets")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (segment == "PluginPreset")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (segment == "PluginBanks")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (segment == "PluginBank")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (segment == "GetPluginInfo")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (segment == "GetPluginPresets")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (segment == "GetPreset")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (segment == "GetBank")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (segment == "NextAudioFile")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (segment == "PreviousAudioFile")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -437,62 +484,136 @@ public:
|
||||
}
|
||||
else if (segment == "AudioMetadata")
|
||||
{
|
||||
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
// res.set(HttpField::cache_control, "no-cache");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
fs::path path = request_uri.query("path");
|
||||
|
||||
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
|
||||
{
|
||||
throw PiPedalException("File not found.");
|
||||
}
|
||||
std::string metadata = GetAudioFileMetadataString(path);
|
||||
res.setBody(metadata);
|
||||
AudioDirectoryInfo::Ptr audioDirectory = AudioDirectoryInfo::Create(path.parent_path());
|
||||
|
||||
auto files = audioDirectory->GetFiles();
|
||||
for (const auto &file : files)
|
||||
{
|
||||
std::string fileNameOnly = path.filename();
|
||||
if (file.fileName() == fileNameOnly)
|
||||
{
|
||||
std::stringstream ss;
|
||||
json_writer writer(ss);
|
||||
writer.write(file);
|
||||
res.setBody(ss.str());
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If we get here, the file was not found in the directory.
|
||||
throw PiPedalException("File not found in directory.");
|
||||
}
|
||||
else if (segment == "Thumbnail")
|
||||
{
|
||||
try
|
||||
{
|
||||
fs::path path = request_uri.query("path");
|
||||
|
||||
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"));
|
||||
|
||||
AudioDirectoryInfo::Ptr audioDirectory = AudioDirectoryInfo::Create(path.parent_path());
|
||||
audioDirectory->GetFiles(); // ensure that the .index file is up to date.
|
||||
ThumbnailTemporaryFile thumbnail;
|
||||
try {
|
||||
thumbnail = audioDirectory->GetThumbnail(path.filename(), width, height);
|
||||
} catch (const std::exception &e) {
|
||||
thumbnail = audioDirectory->DefaultThumbnailTemporaryFile();
|
||||
}
|
||||
res.set(HttpField::content_type, thumbnail.GetMimeType());
|
||||
|
||||
res.set(HttpField::cache_control, "max-age=300");
|
||||
res.set(HttpField::content_length, std::to_string(fs::file_size(thumbnail.Path())));
|
||||
|
||||
std::filesystem::path t = thumbnail.Path();
|
||||
res.setBodyFile(t,thumbnail.DeleteFile());
|
||||
thumbnail.Detach();
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
Lv2Log::error("Error getting thumbnail: %s", e.what());
|
||||
res.set(HttpField::location, "/img/missing_thumbnail.jpg");
|
||||
// response = 307
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
else if (segment == "NextAudioFile")
|
||||
{
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
// res.set(HttpField::cache_control, "no-cache");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
fs::path path = request_uri.query("path");
|
||||
|
||||
try {
|
||||
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
|
||||
{
|
||||
throw PiPedalException("File not found.");
|
||||
}
|
||||
|
||||
std::string nextFile = AudioFileInfo::GetInstance().GetNextAudioFile(path);
|
||||
std::stringstream ss;
|
||||
json_writer writer(ss);
|
||||
writer.write(nextFile);
|
||||
|
||||
res.setBody(ss.str());
|
||||
} catch (const std::exception&e)
|
||||
try
|
||||
{
|
||||
res.setBody("''");
|
||||
}
|
||||
}
|
||||
else if (segment == "PreviousAudioFile")
|
||||
{
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
// res.set(HttpField::cache_control, "no-cache");
|
||||
fs::path path = request_uri.query("path");
|
||||
|
||||
try {
|
||||
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
|
||||
{
|
||||
throw PiPedalException("File not found.");
|
||||
}
|
||||
std::string previousFile = AudioFileInfo::GetInstance().GetPreviousAudioFile(path);
|
||||
auto directoryPath = path.parent_path();
|
||||
auto directoryInfo = AudioDirectoryInfo::Create(directoryPath);
|
||||
std::string result = directoryInfo->GetNextAudioFile(path.filename());
|
||||
if (!result.empty())
|
||||
{
|
||||
result = (directoryPath / result).string();
|
||||
}
|
||||
|
||||
// json-encode the result.
|
||||
std::stringstream ss;
|
||||
json_writer writer(ss);
|
||||
writer.write(previousFile);
|
||||
writer.write(result);
|
||||
|
||||
res.setBody(ss.str());
|
||||
} catch (const std::exception&e)
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
res.setBody("\"\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (segment == "PreviousAudioFile")
|
||||
{
|
||||
res.set(HttpField::content_type, "application/json");
|
||||
res.set(HttpField::cache_control, "no-cache");
|
||||
fs::path path = request_uri.query("path");
|
||||
|
||||
try
|
||||
{
|
||||
if (!fs::exists(path) || !this->model->IsInUploadsDirectory(path) || HasDotDot(path))
|
||||
{
|
||||
throw PiPedalException("File not found.");
|
||||
}
|
||||
auto directoryPath = path.parent_path();
|
||||
auto directoryInfo = AudioDirectoryInfo::Create(directoryPath);
|
||||
std::string result = directoryInfo->GetPreviousAudioFile(path.filename());
|
||||
if (!result.empty())
|
||||
{
|
||||
result = (directoryPath / result).string();
|
||||
}
|
||||
|
||||
// json-encode the result.
|
||||
std::stringstream ss;
|
||||
json_writer writer(ss);
|
||||
writer.write(result);
|
||||
|
||||
res.setBody(ss.str());
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
res.setBody("\"\"");
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
|
||||
+13
-1
@@ -45,6 +45,7 @@
|
||||
#include <signal.h>
|
||||
#include <semaphore.h>
|
||||
#include "SchedulerPriority.hpp"
|
||||
#include "AudioFiles.hpp"
|
||||
|
||||
#include <systemd/sd-daemon.h>
|
||||
|
||||
@@ -120,7 +121,7 @@ int main(int argc, char *argv[])
|
||||
{
|
||||
|
||||
#ifndef WIN32
|
||||
umask(002); // newly created files in /var/pipedal get 7cd75-ish permissions, which improves debugging/live-service interaction.
|
||||
umask(002); // newly created files in /var/pipedal get 775-ish permissions, which improves debugging/live-service interaction.
|
||||
#endif
|
||||
|
||||
#if ENABLE_BACKTRACE
|
||||
@@ -234,6 +235,17 @@ int main(int argc, char *argv[])
|
||||
configuration.SetSocketServerEndpoint(portOption);
|
||||
}
|
||||
|
||||
// clean up orphaned temporary files.
|
||||
const std::filesystem::path webTempDirectory = "/var/pipedal/web_temp";
|
||||
if (!webTempDirectory.empty()) {
|
||||
std::filesystem::remove_all(webTempDirectory); //// user must belong to the pipedald grop when debugging.
|
||||
std::filesystem::create_directories(webTempDirectory);
|
||||
}
|
||||
|
||||
// configure AudiDirectoryInfo to use the correct directories.
|
||||
AudioDirectoryInfo::SetResourceDirectory(configuration.GetWebRoot());
|
||||
AudioDirectoryInfo::SetTemporaryDirectory(webTempDirectory / "audiofiles");
|
||||
|
||||
uint16_t port;
|
||||
std::shared_ptr<WebServer> server;
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user