Audio file metadata and thumbnails
This commit is contained in:
@@ -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()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user