diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..6d1b2ba --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Add: ["-std=c++20"] diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/editor.xml b/.idea/editor.xml new file mode 100644 index 0000000..8d0e15e --- /dev/null +++ b/.idea/editor.xml @@ -0,0 +1,345 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..53624c9 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..e69de29 diff --git a/include/Containers.h b/include/Containers.h index 36fe15f..8647576 100644 --- a/include/Containers.h +++ b/include/Containers.h @@ -34,6 +34,7 @@ struct Track { std::string artwork; TagLib::String genre; std::string format; + uint64_t hash; }; using trackPtr = std::shared_ptr; diff --git a/include/Database.h b/include/Database.h index 6306627..5e740da 100644 --- a/include/Database.h +++ b/include/Database.h @@ -2,18 +2,34 @@ #include "Containers.h" +#include +#include +#include #include using queue = std::vector; class Database { public: - Database( const char* filename, sqlite3** db ); + Database( const char* filename ); ~Database(); void fetch( const std::string& query ) const; void fetchAlbum( const std::string& album ) const; - void fetchAll() const; - void addTrack(); + void fetchAll( std::vector& entries ) const; + void fetchAll( std::map& entries ) const; + void fetchHashes( std::set& hashes ); + std::set fetchFilePaths(); + std::optional getArtistId( const std::string& artistName ); + std::optional getAlbumId( const std::string& albumTitle ); + std::optional getGenreId( const std::string& genre ); + void addTrack( Track t); + void addArtist( const std::string& artistName ); + void addAlbum( Track t); + void addGenre( std::string& genre ); + void setActive( bool active ); + void updateFilePath( const std::string& filePath ); + + sqlite3* getDb() { return db_; }; private: sqlite3* db_; sqlite3_stmt* stmnt; diff --git a/include/File.h b/include/File.h index 90af39d..86df0a0 100644 --- a/include/File.h +++ b/include/File.h @@ -10,10 +10,12 @@ namespace fs = std::filesystem; class File { public: - void importFile( std::string fileName ); + Track importFile( const std::string& filePath ); void importFolder( const std::string& folderPath, std::vector& tracks ); void readFileTag( Track& t ); std::string parseFiletype( const std::string& path ); + void updateLibrary(); + uint64_t hashString( Track t ); private: std::string path_; diff --git a/main.o b/main.o deleted file mode 100644 index c1fa833..0000000 Binary files a/main.o and /dev/null differ diff --git a/soundServe b/soundServe deleted file mode 100755 index ecf7453..0000000 Binary files a/soundServe and /dev/null differ diff --git a/sound_serve.db b/sound_serve.db index ba442c8..08668b6 100644 Binary files a/sound_serve.db and b/sound_serve.db differ diff --git a/src/Database.cpp b/src/Database.cpp index 2659510..16be66c 100644 --- a/src/Database.cpp +++ b/src/Database.cpp @@ -1,12 +1,16 @@ #include "../include/Database.h" #include "../include/Player.h" +#include #include #include +#include +#include using db = Database; using queue = std::vector; -db::Database( const char* filename, sqlite3** db ) : db_( *db ){ +db::Database( const char* filename){ + sqlite3** db = &db_; if ( sqlite3_open( filename, db ) != SQLITE_OK ) { throw std::runtime_error( std::string ("Database connection failed!" ) ); }; @@ -74,7 +78,7 @@ void db::fetchAlbum( const std::string& album ) const { sqlite3_finalize( stmt ); } -void db::fetchAll() const { +void db::fetchAll( std::vector& entries ) const { sqlite3_stmt* stmt; const char* sql = "SELECT * FROM music"; @@ -83,7 +87,6 @@ void db::fetchAll() const { if ( rc != SQLITE_OK ) { throw std::runtime_error( sqlite3_errmsg( db_ )); } - while ( (rc = sqlite3_step( stmt )) == SQLITE_ROW ) { Track t; Player p; @@ -95,8 +98,185 @@ void db::fetchAll() const { t.filePath = sqlite3_column_text( stmt, 4 ) ? reinterpret_cast ( sqlite3_column_text( stmt, 4 )) : ""; t.length = sqlite3_column_int( stmt, 5 ) ? sqlite3_column_int ( stmt, 5 ) : 0; t.artwork = sqlite3_column_text( stmt, 6 ) ? reinterpret_cast ( sqlite3_column_text( stmt, 6 )) : ""; - t.genre = sqlite3_column_text( stmt, 7 ) ? reinterpret_cast ( sqlite3_column_text( stmt, 7 )) : ""; - t.format = sqlite3_column_text( stmt, 8 ) ? reinterpret_cast ( sqlite3_column_text( stmt, 8 )) : ""; + t.genre = sqlite3_column_text( stmt, 7 ) ? reinterpret_cast ( sqlite3_column_text( stmt, 7 )) : ""; + t.format = sqlite3_column_text( stmt, 8 ) ? reinterpret_cast ( sqlite3_column_text( stmt, 8 )) : ""; + + entries.push_back(t); } sqlite3_finalize( stmt ); } + +void db::fetchAll( std::map& dbEntries ) const { + sqlite3_stmt* stmt; + const char* sql = "SELECT * FROM music"; + + int rc = sqlite3_prepare_v2( db_, sql, -1, &stmt, nullptr ); + + if ( rc != SQLITE_OK ) { + throw std::runtime_error( sqlite3_errmsg( db_ )); + } + while ( (rc = sqlite3_step( stmt )) == SQLITE_ROW ) { + Track t; + Player p; + + t.artist = sqlite3_column_text( stmt, 0 ) ? reinterpret_cast ( sqlite3_column_text( stmt, 0 )) : ""; + t.title = sqlite3_column_text( stmt, 1 ) ? reinterpret_cast ( sqlite3_column_text( stmt, 1 )) : ""; + t.album = sqlite3_column_text( stmt, 1 ) ? reinterpret_cast ( sqlite3_column_text( stmt, 1 )) : ""; + t.trackNr = sqlite3_column_int( stmt, 3 ) ? sqlite3_column_int( stmt, 3 ) : 0; + t.filePath = sqlite3_column_text( stmt, 4 ) ? reinterpret_cast ( sqlite3_column_text( stmt, 4 )) : ""; + t.length = sqlite3_column_int( stmt, 5 ) ? sqlite3_column_int ( stmt, 5 ) : 0; + t.artwork = sqlite3_column_text( stmt, 6 ) ? reinterpret_cast ( sqlite3_column_text( stmt, 6 )) : ""; + t.genre = sqlite3_column_text( stmt, 7 ) ? reinterpret_cast ( sqlite3_column_text( stmt, 7 )) : ""; + t.format = sqlite3_column_text( stmt, 8 ) ? reinterpret_cast ( sqlite3_column_text( stmt, 8 )) : ""; + + dbEntries.insert( {t.filePath, t} ); + } + sqlite3_finalize( stmt ); +} + +void db::setActive( bool active ){ + sqlite3_stmt* stmt; + const char* sql = "INSERT INTO sound_serve.tracks (tracks.active) { active } WHERE tracks.id == id"; + + int rc = sqlite3_prepare_v2( db_, sql, -1, &stmt, nullptr ); + + if ( rc != SQLITE_OK ) { + throw std::runtime_error( sqlite3_errmsg( db_ )); +} +} + +void db::updateFilePath( const std::string& filePath ) { + sqlite3_stmt* stmt; + std::string fp = ( filePath.empty() ) ? NULL : filePath; + const char* sql = "INSERT INTO tracks ( tracks.filePath ) VALUES ( {fp}) WHERE track.id == id;"; + + int rc = sqlite3_prepare_v2( db_, sql, -1, &stmt, nullptr ); + if ( rc != SQLITE_OK ) { + throw std::runtime_error( sqlite3_errmsg( db_ )); + } + sqlite3_finalize( stmt ); +} + +std::set db::fetchFilePaths() { + std::set filePaths; + sqlite3_stmt* stmt; + const char* sql = "SELECT DISTINCT filePath from tracks;"; + int rc = sqlite3_prepare_v2( db_, sql, -1, &stmt, nullptr ); + + if ( rc != SQLITE_OK ) { + throw std::runtime_error( sqlite3_errmsg( db_ )); + } + while ( (rc = sqlite3_step( stmt )) == SQLITE_ROW ) { + filePaths.insert( reinterpret_cast(sqlite3_column_text( stmt, 0 ))); + } + sqlite3_finalize( stmt ); + return filePaths; +} + +void db::fetchHashes( std::set& hashes ) { + + sqlite3_stmt* stmt; + const char* sql = "SELECT DISTINCT hash from tracks;"; + int rc = sqlite3_prepare_v2( db_, sql, -1, &stmt, nullptr ); + + if ( rc != SQLITE_OK ) { + throw std::runtime_error( sqlite3_errmsg( db_ )); + } + while ( (rc = sqlite3_step( stmt )) == SQLITE_ROW ) { + hashes.insert( static_cast (sqlite3_column_int( stmt, 0 ))); + } + sqlite3_finalize( stmt ); +} + +void db::addTrack( Track t) { + sqlite3_stmt* stmt; + const char* sql = "INSERT INTO tracks (artist, title, album, trackNr, length, filePath, artwork, genre, format, hash) VALUE (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; + + int rc = sqlite3_prepare_v2( db_, sql, -1, &stmt, nullptr ); + if ( rc != SQLITE_OK ) throw std::runtime_error( sqlite3_errmsg( db_ )); + + auto albumId = getAlbumId( t.album.toCString() ); + if ( !albumId ) addAlbum( t ); + + sqlite3_bind_text( stmt, 1,t.artist.toCString(), -1, SQLITE_STATIC ); + sqlite3_bind_text( stmt, 2,t.title.toCString(), -1, SQLITE_STATIC ); + sqlite3_bind_int( stmt, 3, )); + sqlite3_bind_int( stmt, 4, t.trackNr ); + sqlite3_bind_int( stmt, 5, t.length ); + sqlite3_bind_text( stmt, 6,t.filePath.c_str(), -1, SQLITE_STATIC ); + sqlite3_bind_text( stmt, 7,t.artwork.c_str(), -1, SQLITE_STATIC ); + sqlite3_bind_text( stmt, 8,t.genre.toCString(), -1, SQLITE_STATIC ); + sqlite3_bind_text( stmt, 9, t.format.c_str(), -1, SQLITE_STATIC ); + sqlite3_bind_int( stmt, 10, t.hash ); + + rc = sqlite3_step( stmt ); + if ( rc != SQLITE_DONE ) throw std::runtime_error( sqlite3_errmsg( db_ )); + + sqlite3_finalize( stmt ); +} + +void db::addAlbum( Track t ) { + auto albumId = getAlbumId( t.album.toCString()); + + if ( !albumId ) { + sqlite3_stmt* stmt; + const char* sql = "INSERT INTO albums (title, artist, year, genre, artwork) VALUE ( ?, ?, ?, ?, ? );"; + + int rc = sqlite3_prepare_v2( db_, sql, -1, &stmt, nullptr ); + sqlite3_bind_text( stmt, 0, t.album.toCString(), -1, SQLITE_TRANSIENT ); + sqlite3_bind_int( stmt, 1, artistId, -1, SQLITE_TRANSIENT ); + + if ( rc != SQLITE_OK ) throw std::runtime_error( sqlite3_errmsg( db_ )); + } +} + +void db::addArtist( const std::string& artistName ) { + if ( !getArtistId( artistName )) { + sqlite3_stmt* stmt; + const char* sql = "INSERT INTO artists ( name ) VALUE ( ? );"; + int rc = sqlite3_prepare_v2( db_, sql, -1, &stmt, nullptr ); + sqlite3_bind_text( stmt, 0, artistName.c_str(), -1, SQLITE_TRANSIENT ); + + if ( rc != SQLITE_OK ) throw std::runtime_error( sqlite3_errmsg( db_ )); + } +} + +std::optional db::getAlbumId( const std::string& albumTitle ) { + const char* sql = "SELECT id FROM tracks WHERE album = ?;"; + sqlite3_stmt* stmt; + + sqlite3_prepare_v2( db_, sql, -1, &stmt, nullptr ); + sqlite3_bind_text( stmt, 1, albumTitle.c_str(), -1, SQLITE_TRANSIENT ); + + std::optional id; + if (sqlite3_step( stmt ) == SQLITE_ROW ) { + id = sqlite3_column_int( stmt, 0 ); + } + +} + +std::optional db::getArtistId( const std::string& artistName ) { + const char* sql = "SELECT id FROM artists WHERE name = ?;"; + sqlite3_stmt* stmt; + + sqlite3_prepare_v2( db_, sql, -1, &stmt, nullptr ); + sqlite3_bind_text( stmt, 1, artistName.c_str(), -1, SQLITE_TRANSIENT ); + + std::optional id; + if (sqlite3_step( stmt ) == SQLITE_ROW ) { + id = sqlite3_column_int( stmt, 0 ); + } +} + +std::optional db::getGenreId( const std::string& genre ) { + const char* sql = "SELECT id FROM genres WHERE name = ?;"; + sqlite3_stmt* stmt; + + sqlite3_prepare_v2( db_, sql, -1, &stmt, nullptr ); + sqlite3_bind_text( stmt, 1, genre.c_str(), -1, SQLITE_TRANSIENT ); + + std::optional id; + if (sqlite3_step( stmt ) == SQLITE_ROW ) { + id = sqlite3_column_int( stmt, 0 ); + } +} diff --git a/src/File.cpp b/src/File.cpp index d46b5cc..f617e79 100644 --- a/src/File.cpp +++ b/src/File.cpp @@ -1,23 +1,42 @@ #include "../include/File.h" +#include "../include/Database.h" #include "../include/miniaudio.h" + +#include #include +#include #include +#include #include namespace fs = std::filesystem; -void File::importFolder(const std::string& folderPath, std::vector& tracks ) { +Track File::importFile( const std::string& filePath ) { + Track t; + t.filePath = filePath; + t.artwork = "./cover.png"; + readFileTag( t ); + return t; +} +void File::importFolder(const std::string& folderPath, std::vector& tracks ) { for ( const auto& audioFile : fs::directory_iterator{ folderPath }) { - Track t; - t.filePath = static_cast(audioFile.path()); - t.artwork = folderPath + "/" + "cover.png"; - readFileTag( t ); + Track t = importFile( audioFile.path()); tracks.push_back( t ); } } +uint64_t hashString( Track t ) { + std::string s = (t.artist + t.title + t.album).toCString(); + uint64_t hash = 14695981039346656037ULL; + for ( char c : s ) { + hash ^= static_cast ( c ); + hash *= 1099511628211ULL; + } + return hash; +} + void File::readFileTag( Track& t ) { TagLib::FileRef f( reinterpret_cast(&t.filePath )); if ( !f.isNull() ) { @@ -29,6 +48,7 @@ void File::readFileTag( Track& t ) { t.genre = ( f.tag()->genre().isEmpty() ) ? "" : ( f.tag()->genre() ); t.format = parseFiletype( t.filePath ); t.length = ( f.audioProperties()->lengthInSeconds() ) ? f.audioProperties()->lengthInSeconds() : 0; + t.hash = hashString( t ); } } @@ -39,3 +59,31 @@ std::string File::parseFiletype( const std::string& path ){ return path.substr( dot + 1, length - dot ); } + +void File::updateLibrary() { + Database database( "sound_serve.db"); + std::vector musicFolder; + std::vector db; + std::set db_filePaths; + std::set hashes; + + importFolder( musicFolder_, musicFolder); + database.fetchFilePaths(); + database.fetchHashes( hashes ); + + //---- Missing Files ___ + // find missing files and set filePath to NULL; + for ( auto& fp : db_filePaths ) { + if (! fs::exists( fp ) ) { + database.updateFilePath( fp ); + } + } + // --- Add new Files --- + for ( auto file : musicFolder ) { + // compare hash values + if ( !hashes.contains( file.hash )) { + importFile( file.filePath ); + } + } + +} diff --git a/src/File.o b/src/File.o deleted file mode 100644 index 9c0da17..0000000 Binary files a/src/File.o and /dev/null differ diff --git a/src/Tag.o b/src/Tag.o deleted file mode 100644 index 5807f8b..0000000 Binary files a/src/Tag.o and /dev/null differ