Compare commits

...

7 Commits

Author SHA1 Message Date
2853898145 playQueue: Finished refactoring 2026-02-14 16:45:33 +10:00
9ae53e0824 discordrpc: Disabled callbacks since they weren't needed and added Local file 2026-02-14 03:47:16 +10:00
1f814fff6e localMusicHandler: General Fixes
Bugs fixed and features:
 - Creating the database and adding songs in the same run would fail
 - Songs with multiple artists are now separated with ', ' instead of the ID3 ';'
 - Excluded .mp4 files from the song list
 - Songs aren't scanned and duplicated on every start
 - General cleanup and memory leak fixes
2026-02-14 02:42:04 +10:00
26df10f8fb localMusicHandler can now recursively search and add music files and metadata to an sqlite database 2026-02-14 01:39:20 +10:00
66aa616286 Started implementing local music playback 2026-02-13 23:07:01 +10:00
ba554dc716 Finished initClientConnection() in socket 2026-02-11 17:09:50 +10:00
34f562149e Added global variable for OSSP version 2026-02-11 17:00:24 +10:00
11 changed files with 700 additions and 170 deletions

View File

@@ -12,6 +12,8 @@ find_package(OpenGL REQUIRED)
find_package(SDL2 REQUIRED) find_package(SDL2 REQUIRED)
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
pkg_check_modules(GSTREAMER REQUIRED gstreamer-1.0) pkg_check_modules(GSTREAMER REQUIRED gstreamer-1.0)
pkg_check_modules(AVFORMAT REQUIRED libavformat)
pkg_check_modules(AVUTIL REQUIRED libavutil)
add_subdirectory(external/discord-rpc) add_subdirectory(external/discord-rpc)
@@ -25,6 +27,8 @@ add_executable(ossp MACOSX_BUNDLE
configHandler.c configHandler.c
discordrpc.c discordrpc.c
localRadioDBHandler.c localRadioDBHandler.c
localMusicHandler.cpp
socket.c
gui/gui_entry.cpp gui/gui_entry.cpp
player/player.c player/player.c
player/playQueue.cpp player/playQueue.cpp
@@ -65,6 +69,6 @@ set_target_properties(ossp PROPERTIES
MACOSX_BUNDLE_GUI_IDENTIFIER "org.hojuix.ossp" MACOSX_BUNDLE_GUI_IDENTIFIER "org.hojuix.ossp"
) )
include_directories(${GSTREAMER_INCLUDE_DIRS}) include_directories(${GSTREAMER_INCLUDE_DIRS} ${AVFORMAT_INCLUDE_DIR} ${AVUTIL_INCLUDE_DIRS})
target_link_libraries(ossp PRIVATE OpenSSL::SSL OpenSSL::Crypto CURL::libcurl SDL2::SDL2 ${OPENGL_LIBRARIES} discord-rpc ${GSTREAMER_LIBRARIES}) target_link_libraries(ossp PRIVATE OpenSSL::SSL OpenSSL::Crypto CURL::libcurl SDL2::SDL2 ${OPENGL_LIBRARIES} discord-rpc ${GSTREAMER_LIBRARIES} ${AVFORMAT_LIBRARIES} ${AVUTIL_LIBRARIES})

View File

@@ -36,6 +36,7 @@ int configHandler_Read(configHandler_config_t** configObj) {
(*configObj)->internal_opensubsonic_clientName = NULL; (*configObj)->internal_opensubsonic_clientName = NULL;
(*configObj)->internal_opensubsonic_loginSalt = NULL; (*configObj)->internal_opensubsonic_loginSalt = NULL;
(*configObj)->internal_opensubsonic_loginToken = NULL; (*configObj)->internal_opensubsonic_loginToken = NULL;
(*configObj)->internal_ossp_version = NULL;
(*configObj)->listenbrainz_enable = false; (*configObj)->listenbrainz_enable = false;
(*configObj)->listenbrainz_token = NULL; (*configObj)->listenbrainz_token = NULL;
(*configObj)->lastfm_enable = false; (*configObj)->lastfm_enable = false;
@@ -67,10 +68,12 @@ int configHandler_Read(configHandler_config_t** configObj) {
(*configObj)->lv2_parax32_frequency_left = NULL; (*configObj)->lv2_parax32_frequency_left = NULL;
(*configObj)->lv2_parax32_frequency_right = NULL; (*configObj)->lv2_parax32_frequency_right = NULL;
(*configObj)->lv2_reverb_filter_name = NULL; (*configObj)->lv2_reverb_filter_name = NULL;
(*configObj)->local_rootdir = NULL;
// Set internal configuration values // Set internal configuration values
(*configObj)->internal_opensubsonic_version = strdup("1.8.0"); (*configObj)->internal_opensubsonic_version = strdup("1.8.0");
(*configObj)->internal_opensubsonic_clientName = strdup("Hojuix_OSSP"); (*configObj)->internal_opensubsonic_clientName = strdup("Hojuix_OSSP");
(*configObj)->internal_ossp_version = strdup("v0.4a");
// Form the path to the config JSON // Form the path to the config JSON
char* config_path = NULL; char* config_path = NULL;
@@ -463,6 +466,19 @@ int configHandler_Read(configHandler_config_t** configObj) {
(*configObj)->lv2_reverb_filter_name = strdup(calf_reverb_filter_name->valuestring); (*configObj)->lv2_reverb_filter_name = strdup(calf_reverb_filter_name->valuestring);
} }
// Make an object from local
cJSON* local_root = cJSON_GetObjectItemCaseSensitive(root, "local");
if (local_root == NULL) {
logger_log_error(__func__, "Error parsing JSON - local does not exist.");
cJSON_Delete(root);
return 1;
}
cJSON* local_root_directory = cJSON_GetObjectItemCaseSensitive(local_root, "rootDirectory");
if (cJSON_IsString(local_root_directory) && local_root_directory->valuestring != NULL) {
(*configObj)->local_rootdir = strdup(local_root_directory->valuestring);
}
cJSON_Delete(root); cJSON_Delete(root);
logger_log_general(__func__, "Successfully read configuration file."); logger_log_general(__func__, "Successfully read configuration file.");
return 0; return 0;
@@ -477,6 +493,7 @@ void configHandler_Free(configHandler_config_t** configObj) {
if ((*configObj)->internal_opensubsonic_clientName != NULL) { free((*configObj)->internal_opensubsonic_clientName); } if ((*configObj)->internal_opensubsonic_clientName != NULL) { free((*configObj)->internal_opensubsonic_clientName); }
if ((*configObj)->internal_opensubsonic_loginSalt != NULL) { free((*configObj)->internal_opensubsonic_loginSalt); } if ((*configObj)->internal_opensubsonic_loginSalt != NULL) { free((*configObj)->internal_opensubsonic_loginSalt); }
if ((*configObj)->internal_opensubsonic_loginToken != NULL) { free((*configObj)->internal_opensubsonic_loginToken); } if ((*configObj)->internal_opensubsonic_loginToken != NULL) { free((*configObj)->internal_opensubsonic_loginToken); }
if ((*configObj)->internal_ossp_version != NULL) { free((*configObj)->internal_ossp_version); }
if ((*configObj)->listenbrainz_token != NULL) { free((*configObj)->listenbrainz_token); } if ((*configObj)->listenbrainz_token != NULL) { free((*configObj)->listenbrainz_token); }
if ((*configObj)->lastfm_username != NULL) { free((*configObj)->lastfm_username); } if ((*configObj)->lastfm_username != NULL) { free((*configObj)->lastfm_username); }
if ((*configObj)->lastfm_password != NULL) { free((*configObj)->lastfm_password); } if ((*configObj)->lastfm_password != NULL) { free((*configObj)->lastfm_password); }
@@ -494,5 +511,6 @@ void configHandler_Free(configHandler_config_t** configObj) {
if ((*configObj)->lv2_parax32_frequency_left != NULL) { free((*configObj)->lv2_parax32_frequency_left); } if ((*configObj)->lv2_parax32_frequency_left != NULL) { free((*configObj)->lv2_parax32_frequency_left); }
if ((*configObj)->lv2_parax32_frequency_right != NULL) { free((*configObj)->lv2_parax32_frequency_right); } if ((*configObj)->lv2_parax32_frequency_right != NULL) { free((*configObj)->lv2_parax32_frequency_right); }
if ((*configObj)->lv2_reverb_filter_name != NULL) { free((*configObj)->lv2_reverb_filter_name); } if ((*configObj)->lv2_reverb_filter_name != NULL) { free((*configObj)->lv2_reverb_filter_name); }
if ((*configObj)->local_rootdir != NULL) { free((*configObj)->local_rootdir); }
if (*configObj != NULL) { free(*configObj); } if (*configObj != NULL) { free(*configObj); }
} }

View File

@@ -25,6 +25,7 @@ typedef struct {
char* internal_opensubsonic_clientName; // (Internal) Opensubsonic Client Name char* internal_opensubsonic_clientName; // (Internal) Opensubsonic Client Name
char* internal_opensubsonic_loginSalt; // (Internal) Opensubsonic Login Salt char* internal_opensubsonic_loginSalt; // (Internal) Opensubsonic Login Salt
char* internal_opensubsonic_loginToken; // (Internal) Opensubsonic Login Token char* internal_opensubsonic_loginToken; // (Internal) Opensubsonic Login Token
char* internal_ossp_version; // (Internal) OSSP Version
// Scrobbler Settings // Scrobbler Settings
bool listenbrainz_enable; // Enable ListenBrainz Scrobbling bool listenbrainz_enable; // Enable ListenBrainz Scrobbling
@@ -64,6 +65,9 @@ typedef struct {
char* lv2_parax32_frequency_left; char* lv2_parax32_frequency_left;
char* lv2_parax32_frequency_right; char* lv2_parax32_frequency_right;
char* lv2_reverb_filter_name; // LV2 Calf Reverb LV2 Name char* lv2_reverb_filter_name; // LV2 Calf Reverb LV2 Name
// Local Settings
char* local_rootdir; // Local Music Root Directory
} configHandler_config_t; } configHandler_config_t;
int configHandler_Read(configHandler_config_t** config); int configHandler_Read(configHandler_config_t** config);

View File

@@ -40,31 +40,11 @@ void discordrpc_struct_deinit(discordrpc_data** discordrpc_struct) {
if (*discordrpc_struct != NULL) { free(*discordrpc_struct); } if (*discordrpc_struct != NULL) { free(*discordrpc_struct); }
} }
static void handleDiscordReady(const DiscordUser* connectedUser)
{
printf("\nDiscord: connected to user %s#%s - %s\n",
connectedUser->username,
connectedUser->discriminator,
connectedUser->userId);
}
static void handleDiscordDisconnected(int errcode, const char* message)
{
printf("\nDiscord: disconnected (%d: %s)\n", errcode, message);
}
static void handleDiscordError(int errcode, const char* message)
{
printf("\nDiscord: error (%d: %s)\n", errcode, message);
}
int discordrpc_init() { int discordrpc_init() {
printf("[DiscordRPC] Initializing.\n"); printf("[DiscordRPC] Initializing.\n");
// TODO Can I just not deal with the handler callbacks at all?
DiscordEventHandlers handlers; DiscordEventHandlers handlers;
memset(&handlers, 0, sizeof(handlers)); memset(&handlers, 0, sizeof(handlers));
handlers.ready = handleDiscordReady;
handlers.disconnected = handleDiscordDisconnected;
handlers.errored = handleDiscordError;
Discord_Initialize(discordrpc_appid, &handlers, 1, NULL); Discord_Initialize(discordrpc_appid, &handlers, 1, NULL);
// Fetch OS String for RPC (Heap-allocated) // Fetch OS String for RPC (Heap-allocated)
@@ -86,7 +66,7 @@ void discordrpc_update(discordrpc_data** discordrpc_struct) {
if ((*discordrpc_struct)->state == DISCORDRPC_STATE_IDLE) { if ((*discordrpc_struct)->state == DISCORDRPC_STATE_IDLE) {
asprintf(&detailsString, "Idle"); asprintf(&detailsString, "Idle");
presence.details = detailsString; presence.details = detailsString;
} else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PLAYING_SONG) { } else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PLAYING_OPENSUBSONIC) {
// Playing a song // Playing a song
time_t currentTime = time(NULL); time_t currentTime = time(NULL);
asprintf(&detailsString, "%s", (*discordrpc_struct)->songTitle); asprintf(&detailsString, "%s", (*discordrpc_struct)->songTitle);
@@ -99,7 +79,9 @@ void discordrpc_update(discordrpc_data** discordrpc_struct) {
if (configObj->discordrpc_showSysDetails) { if (configObj->discordrpc_showSysDetails) {
presence.largeImageText = discordrpc_osString; presence.largeImageText = discordrpc_osString;
} }
} else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PLAYING_RADIO) { } else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PLAYING_LOCALFILE) {
//
} else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PLAYING_INTERNETRADIO) {
// Playing an internet radio station // Playing an internet radio station
time_t currentTime = time(NULL); time_t currentTime = time(NULL);
asprintf(&detailsString, "%s", (*discordrpc_struct)->songTitle); asprintf(&detailsString, "%s", (*discordrpc_struct)->songTitle);

View File

@@ -8,8 +8,9 @@
#define _DISCORDRPC_H #define _DISCORDRPC_H
#define DISCORDRPC_STATE_IDLE 0 #define DISCORDRPC_STATE_IDLE 0
#define DISCORDRPC_STATE_PLAYING_SONG 1 #define DISCORDRPC_STATE_PLAYING_OPENSUBSONIC 1
#define DISCORDRPC_STATE_PLAYING_RADIO 2 #define DISCORDRPC_STATE_PLAYING_LOCALFILE 2
#define DISCORDRPC_STATE_PLAYING_INTERNETRADIO 3
#define DISCORDRPC_STATE_PAUSED 4 #define DISCORDRPC_STATE_PAUSED 4
typedef struct { typedef struct {

257
src/localMusicHandler.cpp Normal file
View File

@@ -0,0 +1,257 @@
/*
* OpenSubsonicPlayer
* Goldenkrew3000 2025
* License: GNU General Public License 3.0
* Info: Local Music File Handler
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <dirent.h>
#include <errno.h>
#include <sys/stat.h>
extern "C" {
#include <libavformat/avformat.h>
#include <libavutil/dict.h>
#include <libavformat/avio.h>
#include "external/sqlite3/sqlite3.h"
}
#include <iostream>
#include <regex>
#include <vector>
#include <deque>
#include "configHandler.h"
#include "localMusicHandler.hpp"
/*
* I'm sorry for this messy and probably unreliable code
* This is the first time I have ever written something like this
* And why C++? Easy to store all of the temporary data without a ton of boilerplate
*/
extern configHandler_config_t* configObj;
std::vector<std::string> localMusicHandler_allFiles;
class localMusicHandler_AudioObject {
public:
std::string path;
std::string songTitle;
std::string albumTitle;
std::string artistTitle;
std::string track;
std::string totalTracks;
uint64_t filesize;
std::string uid;
};
std::deque<localMusicHandler_AudioObject> localMusicHandler_audioItems;
static sqlite3* sqlite_db = NULL;
static char* sqlite_errorMsg = NULL;
void localMusicHandler_scan() {
static int rc = 0;
printf("[LocalMusicHandler] Scanning local music directory recursively for files.\n");
// TODO clear all vectors
// Scan music directory (defined in config file) for all files
localMusicHandler_scanDirectory(configObj->local_rootdir);
printf("[LocalMusicHandler] Found %d files.\n", localMusicHandler_allFiles.size());
// Scan each file to find only music files, and pull ID3 tags
for (int i = 0; i < localMusicHandler_allFiles.size(); i++) {
localMusicHandler_scanFile(i);
}
printf("[LocalMusicHandler] Found %d songs.\n", localMusicHandler_audioItems.size());
// Generate a unique ID for each music file
printf("[LocalMusicHandler] Generating unique IDs for each song.\n");
for (int i = 0; i < localMusicHandler_audioItems.size(); i++) {
// TODO: Technically there is a VERY SMALL chance that 2 id's repeat in the DB
// Figure out what to do about that later
localMusicHandler_generateUid(i);
}
// Store in database
rc = localMusicHandler_initDatabase();
if (rc == -1) {
// ERROR
} else if (rc == 0) {
// Table just made, songs not loaded in yet
for (int i = 0; i < localMusicHandler_audioItems.size(); i++) {
localMusicHandler_moveSongsToDatabase(i);
}
} else if (rc == 1) {
// Table was already made, assume songs were loaded in before
}
}
void localMusicHandler_scanDirectory(char* directory) {
struct dirent* dp;
DIR* dir = opendir(directory);
char path[1000]; // TODO Prevent potential buffer overflow
while ((dp = readdir(dir)) != NULL) {
if (strcmp(dp->d_name, ".") != 0 && strcmp(dp->d_name, "..") != 0) {
sprintf(path, "%s/%s", directory, dp->d_name);
struct stat statbuf;
stat(path, &statbuf);
if (S_ISDIR(statbuf.st_mode)) {
localMusicHandler_scanDirectory(path);
} else if (S_ISREG(statbuf.st_mode)) {
localMusicHandler_allFiles.push_back(path);
}
}
}
closedir(dir);
}
void localMusicHandler_scanFile(int idx) {
AVFormatContext* ctx = NULL;
AVDictionaryEntry* tag = NULL;
static int rc = -1;
rc = avformat_open_input(&ctx, localMusicHandler_allFiles[idx].c_str(), NULL, NULL);
if (rc < 0) {
printf("[LocalMusicHandler] avformat_open_input() failed on idx %d (%s).\n", idx, localMusicHandler_allFiles[idx].c_str());
return;
}
// Ignore files that aren't audio
if (
strcmp(ctx->iformat->name, "lrc") == 0 || // .lrc files
strcmp(ctx->iformat->name, "image2") == 0 || // Pictures
strcmp(ctx->iformat->name, "mov,mp4,m4a,3gp,3g2,mj2") == 0 // .mp4 files
) {
avformat_close_input(&ctx);
return;
}
localMusicHandler_AudioObject audioObject;
audioObject.path = localMusicHandler_allFiles[idx].c_str();
// Get file size (Using libav for this since the file is already opened using it)
audioObject.filesize = 0; // If the following fetch fails, set it to a known value beforehand
if (ctx->pb) {
uint64_t fsize = avio_size(ctx->pb);
if (fsize > 0) {
audioObject.filesize = fsize;
}
}
// Set all strings to known good values before fetching tags that possible don't exist
// NOTE: Honestly don't know if C++ does this by default, but I am not trusting it either way
audioObject.songTitle = "";
audioObject.albumTitle = "";
audioObject.artistTitle = "";
audioObject.track = "";
audioObject.totalTracks = "";
while ((tag = av_dict_get(ctx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) {
if (strcmp(tag->key, "title") == 0) {
audioObject.songTitle = tag->value;
} else if (strcmp(tag->key, "album") == 0) {
audioObject.albumTitle = tag->value;
} else if (strcmp(tag->key, "artist") == 0) {
// In ID3, multiple artists are stored as 'Artist A;Artist B'. Replace ';' with ', '
audioObject.artistTitle = std::regex_replace(tag->value, std::regex(";"), ", ");
} else if (strcmp(tag->key, "track") == 0) {
audioObject.track = tag->value;
} else if (strcmp(tag->key, "totaltracks") == 0) {
audioObject.totalTracks = tag->value;
}
}
localMusicHandler_audioItems.push_back(audioObject);
avformat_close_input(&ctx);
}
void localMusicHandler_generateUid(int idx) {
// TODO: Add other operating support here, such as in libopensubsonic/crypto.c
char uuidBytes[20];
char uuidString[40];
for (int i = 0; i < 20; i++) {
uuidBytes[i] = arc4random() & 0xFF;
}
snprintf(uuidString, 40, "%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x",
uuidBytes[0], uuidBytes[1], uuidBytes[2], uuidBytes[3], uuidBytes[4],
uuidBytes[5], uuidBytes[6], uuidBytes[7], uuidBytes[8], uuidBytes[9],
uuidBytes[10], uuidBytes[11], uuidBytes[12], uuidBytes[13], uuidBytes[14],
uuidBytes[15], uuidBytes[16], uuidBytes[17], uuidBytes[18], uuidBytes[19]
);
localMusicHandler_audioItems[idx].uid = uuidString;
}
int localMusicHandler_initDatabase() {
// Code returns: -1 -> Error, 0 -> No songs in table, 1 -> Songs already in table (Table already existed)
static int createTable = 0;
static int rc = 0;
char* dbPath = NULL;
rc = asprintf(&dbPath, "%s/.config/ossp/local.db", getenv("HOME"));
if (rc == -1) {
printf("[LocalMusicHandler] asprintf() failed.\n");
return -1;
}
struct stat st;
if (stat(dbPath, &st) == 0) {
printf("[LocalMusicHandler] Database found, is %ld bytes.\n", st.st_size);
} else {
printf("[LocalMusicHandler] Database does not exist, creating.\n");
createTable = 1;
}
rc = sqlite3_open(dbPath, &sqlite_db);
if (rc) {
printf("[LocalMusicHandler] Could not create database: %s\n", sqlite3_errmsg(sqlite_db));
free(dbPath);
return -1;
} else {
printf("[LocalMusicHandler] Created/Opened database.\n");
}
if (createTable == 1) {
const char* sqlQuery = "CREATE TABLE local_songs(uid TEXT, songTitle TEXT, albumTitle TEXT, artistTitle TEXT, track TEXT, totalTracks TEXT, path TEXT, filesize INT)";
rc = sqlite3_exec(sqlite_db, sqlQuery, NULL, 0, &sqlite_errorMsg);
if (rc != SQLITE_OK) {
printf("[LocalMusicHandler] Could not make table: %s\n", sqlite_errorMsg);
sqlite3_free(sqlite_errorMsg);
free(dbPath);
return -1;
}
printf("[LocalMusicHandler] Made table.\n");
free(dbPath);
return 0;
}
free(dbPath);
return 1;
}
void localMusicHandler_moveSongsToDatabase(int idx) {
sqlite3_stmt* sqlite_stmt;
const char* sqlQuery = "INSERT INTO local_songs VALUES(?, ?, ?, ?, ?, ?, ?, ?)";
if (sqlite3_prepare_v2(sqlite_db, sqlQuery, -1, &sqlite_stmt, NULL) != SQLITE_OK) {
printf("[LocalMusicHandler] Prepare error: %s\n", sqlite3_errmsg(sqlite_db));
return; // TODO
}
sqlite3_bind_text(sqlite_stmt, 1, localMusicHandler_audioItems[idx].uid.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(sqlite_stmt, 2, localMusicHandler_audioItems[idx].songTitle.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(sqlite_stmt, 3, localMusicHandler_audioItems[idx].albumTitle.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(sqlite_stmt, 4, localMusicHandler_audioItems[idx].artistTitle.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(sqlite_stmt, 5, localMusicHandler_audioItems[idx].track.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(sqlite_stmt, 6, localMusicHandler_audioItems[idx].totalTracks.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(sqlite_stmt, 7, localMusicHandler_audioItems[idx].path.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(sqlite_stmt, 8, localMusicHandler_audioItems[idx].filesize);
if (sqlite3_step(sqlite_stmt) != SQLITE_DONE) {
printf("[LocalMusicHandler] Execution error: %s\n", sqlite3_errmsg(sqlite_db));
}
sqlite3_finalize(sqlite_stmt);
}

26
src/localMusicHandler.hpp Normal file
View File

@@ -0,0 +1,26 @@
/*
* OpenSubsonicPlayer
* Goldenkrew3000 2025
* License: GNU General Public License 3.0
*/
#ifndef _LOCALMUSICHANDLER_H
#define _LOCALMUSICHANDLER_H
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
void localMusicHandler_scan();
void localMusicHandler_scanDirectory(char* directory);
void localMusicHandler_scanFile(int idx);
void localMusicHandler_generateUid(int idx);
int localMusicHandler_initDatabase();
void localMusicHandler_moveSongsToDatabase(int idx);
#ifdef __cplusplus
}
#endif // __cplusplus
#endif // _LOCALMUSICHANDLER_H

View File

@@ -20,69 +20,105 @@
#include "playQueue.hpp" #include "playQueue.hpp"
// C++ interface for storing song queue data (C interface is in the header) // C++ interface for storing song queue data (C interface is in the header)
class SongObject { class OSSPQ_SongObject {
public: public:
std::string title; std::string title;
std::string album;
std::string artist; std::string artist;
std::string id; std::string id;
std::string streamUrl;
std::string coverArtUrl;
long duration; long duration;
int mode;
}; };
// NOTE: Acronym is OpenSubsonicPlayerQueue // NOTE: Acronym is OpenSubsonicPlayerQueue
std::deque<SongObject> OSSPQ_Items; std::deque<OSSPQ_SongObject> OSSPQ_SongQueue;
int internal_OSSPQ_AppendToEnd(char* title, char* artist, char* id, long duration) { int OSSPQ_AppendToEnd(char* title, char* album, char* artist, char* id, char* streamUrl, char* coverArtUrl, long duration, int mode) {
// Append a new song to the end of the queue OSSPQ_SongObject songObject;
printf("Title: %s\nArtist: %s\nID: %s\nDuration: %ld\n", title, artist, id, duration);
// TODO: Find a neater way of converting a C string to a C++ string?? if (mode == OSSPQ_MODE_OPENSUBSONIC || mode == OSSPQ_MODE_LOCALFILE) {
std::string cpp_title(title); // Both Local File and OpenSubsonic playback both take the same arguments
std::string cpp_artist(artist); if (mode == OSSPQ_MODE_OPENSUBSONIC) {
std::string cpp_id(id); printf("[OSSPQ] Appending OpenSubsonic Song: %s by %s.\n", title, artist);
SongObject songObject; } else if (mode == OSSPQ_MODE_LOCALFILE) {
songObject.title = cpp_title; printf("[OSSPQ] Appending Local Song: %s by %s.\n", title, artist);
songObject.artist = cpp_artist; }
songObject.id = cpp_id;
songObject.title = title;
songObject.album = album;
songObject.artist = artist;
songObject.id = id;
songObject.streamUrl = streamUrl;
songObject.coverArtUrl = coverArtUrl;
songObject.duration = duration; songObject.duration = duration;
OSSPQ_Items.push_back(songObject); songObject.mode = mode;
} else if (mode == OSSPQ_MODE_INTERNETRADIO) {
printf("[OSSPQ] Appending Internet Radio Station: %s.\n", title);
songObject.title = title;
songObject.id = id;
songObject.streamUrl = streamUrl;
}
OSSPQ_SongQueue.push_back(songObject);
return 0; return 0;
} }
OSSPQ_SongStruct* internal_OSSPQ_PopFromFront() { OSSPQ_SongStruct* OSSPQ_PopFromFront() {
if (OSSPQ_Items.empty()) { // Check if song queue is empty, if not, then pop oldest
// No items in play queue if (OSSPQ_SongQueue.empty()) {
return NULL; return NULL;
} }
OSSPQ_SongObject songObject = OSSPQ_SongQueue.front();
OSSPQ_SongQueue.pop_front();
// Pull the first song off the song queue // Allocate, initialize, and fill C compatible song object
SongObject songObject = OSSPQ_Items.front(); OSSPQ_SongStruct* songObjectC = (OSSPQ_SongStruct*)malloc(sizeof(OSSPQ_SongStruct));
OSSPQ_Items.pop_front(); songObjectC->title = NULL;
songObjectC->album = NULL;
songObjectC->artist = NULL;
songObjectC->id = NULL;
songObjectC->streamUrl = NULL;
songObjectC->coverArtUrl = NULL;
songObjectC->duration = 0;
songObjectC->mode = 0;
// Move song data into a C readable format if (songObject.mode == OSSPQ_MODE_OPENSUBSONIC || songObject.mode == OSSPQ_MODE_LOCALFILE) {
// NOTE: I am initializing the variables to a known value just in case there is missing information in songObject songObjectC->title = strdup(songObject.title.c_str());
OSSPQ_SongStruct* playQueueObject = (OSSPQ_SongStruct*)malloc(sizeof(OSSPQ_SongStruct)); songObjectC->album = strdup(songObject.album.c_str());
playQueueObject->title = NULL; songObjectC->artist = strdup(songObject.artist.c_str());
playQueueObject->artist = NULL; songObjectC->id = strdup(songObject.id.c_str());
playQueueObject->id = NULL; songObjectC->streamUrl = strdup(songObject.streamUrl.c_str());
playQueueObject->duration = 0; songObjectC->coverArtUrl = strdup(songObject.coverArtUrl.c_str());
songObjectC->duration = songObject.duration;
songObjectC->mode = songObject.mode;
} else if (songObject.mode == OSSPQ_MODE_INTERNETRADIO) {
songObjectC->title = strdup(songObject.title.c_str());
songObjectC->id = strdup(songObject.id.c_str());
songObjectC->streamUrl = strdup(songObject.streamUrl.c_str());
}
playQueueObject->title = strdup(songObject.title.c_str()); return songObjectC;
playQueueObject->artist = strdup(songObject.artist.c_str());
playQueueObject->id = strdup(songObject.id.c_str());
playQueueObject->duration = songObject.duration;
return playQueueObject;
} }
void internal_OSSPQ_FreeSongObject(OSSPQ_SongStruct* songObject) { void OSSPQ_FreeSongObjectC(OSSPQ_SongStruct* songObjectC) {
if (songObject->title != NULL) { free(songObject->title); } printf("[OSSPQ] Freeing SongObjectC.\n");
if (songObject->artist != NULL) { free(songObject->artist); } if (songObjectC->title != NULL) { free(songObjectC->title); }
if (songObject->id != NULL) { free(songObject->id); } if (songObjectC->album != NULL) { free(songObjectC->album); }
if (songObject != NULL) { free(songObject); } if (songObjectC->artist != NULL) { free(songObjectC->artist); }
if (songObjectC->id != NULL) { free(songObjectC->id); }
if (songObjectC->streamUrl != NULL) { free(songObjectC->streamUrl); }
if (songObjectC->coverArtUrl != NULL) { free(songObjectC->coverArtUrl); }
if (songObjectC != NULL) { free(songObjectC); }
} }
// TODO Bullshit functions for dealing with Imgui
char* internal_OSSPQ_GetTitleAtIndex(int idx) { char* internal_OSSPQ_GetTitleAtIndex(int idx) {
return (char*)OSSPQ_Items[idx].title.c_str(); return (char*)OSSPQ_SongQueue[idx].title.c_str();
} }
int internal_OSSPQ_GetItemCount() { int internal_OSSPQ_GetItemCount() {
return OSSPQ_Items.size(); return OSSPQ_SongQueue.size();
} }

View File

@@ -7,6 +7,10 @@
#ifndef _PLAYQUEUE_H #ifndef _PLAYQUEUE_H
#define _PLAYQUEUE_H #define _PLAYQUEUE_H
#define OSSPQ_MODE_OPENSUBSONIC 101
#define OSSPQ_MODE_LOCALFILE 102
#define OSSPQ_MODE_INTERNETRADIO 103
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif // __cplusplus #endif // __cplusplus
@@ -14,14 +18,20 @@ extern "C" {
// C interface for sending song queue data (C++ interface is in the C++ file) // C interface for sending song queue data (C++ interface is in the C++ file)
typedef struct { typedef struct {
char* title; char* title;
char* album;
char* artist; char* artist;
char* id; char* id;
char* streamUrl;
char* coverArtUrl;
long duration; long duration;
int mode;
} OSSPQ_SongStruct; } OSSPQ_SongStruct;
int internal_OSSPQ_AppendToEnd(char* title, char* artist, char* id, long duration); int OSSPQ_AppendToEnd(char* title, char* album, char* artist, char* id, char* streamUrl, char* coverArtUrl, long duration, int mode);
OSSPQ_SongStruct* internal_OSSPQ_PopFromFront(); OSSPQ_SongStruct* OSSPQ_PopFromFront();
void internal_OSSPQ_FreeSongObject(OSSPQ_SongStruct* songObject); void OSSPQ_FreeSongObjectC(OSSPQ_SongStruct* songObjectC);
// TODO
char* internal_OSSPQ_GetTitleAtIndex(int idx); char* internal_OSSPQ_GetTitleAtIndex(int idx);
int internal_OSSPQ_GetItemCount(); int internal_OSSPQ_GetItemCount();

View File

@@ -8,14 +8,14 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <unistd.h>
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include <sys/types.h> #include <sys/types.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <sys/un.h> #include <sys/un.h>
#include <unistd.h> #include "external/cJSON.h"
#include "configHandler.h"
#include "socket.h" #include "socket.h"
#define SOCKET_PATH "/tmp/ossp_socket" // TODO Make this configurable through the configuration file #define SOCKET_PATH "/tmp/ossp_socket" // TODO Make this configurable through the configuration file
@@ -25,19 +25,21 @@ static int rc = -1;
socklen_t client_len; socklen_t client_len;
struct sockaddr_un server_addr; struct sockaddr_un server_addr;
struct sockaddr_un client_addr; struct sockaddr_un client_addr;
extern configHandler_config_t* configObj;
void socketHandler_read(); void socketHandler_read();
void socketHandler_initClientConnection();
void socketHandler_getConnInfo(); int SockSig_Length = 4;
#define SOCKSIG_CLIENT_CONNECT 0xE3566C2E
const uint32_t SockSig_Client_Connect = 0xE3566C2E; const uint32_t OSSP_Sock_ACK = 0x7253FF87;
#define SOCKSIG_SIZE 0x1F7E8BCF const uint32_t OSSP_Sock_CliConn = 0xE3566C2E;
#define SOCKSIG_ACK 0x7253FF87 const uint32_t OSSP_Sock_GetConnInfo = 0x8E4F6B01;
const uint32_t SockSig_Ack = 0x7253FF87; const uint32_t OSSP_Sock_Size = 0x1F7E8BCF;
const uint32_t SockSig_GetConnInfo = 0x8E4F6B01;
const uint32_t SockSig_Size = 0x1F7E8BCF; const uint32_t OSSP_Sock_ClientGetReq = 0x210829CF;
void socket_setup() { void socket_setup() {
@@ -87,16 +89,21 @@ void socket_setup() {
//char buffer[256]; printf("------------------------------\n");
//client_len = sizeof(struct sockaddr_un);
//read(client_fd, buffer, 256);
//printf("Received: %s\n", buffer);
//printf("Client len: %d\n", client_len);
// Cleanup
//close(client_fd); // Receive ClientGetReq with Size
//close(server_fd); int size = 0;
//unlink(SOCKET_PATH); socketHandler_receiveCliGetReq(&size);
printf("Size to alloc: %d bytes\n", size);
char* reqBuf = malloc(size);
// Send ACK
socketHandler_sendAck();
// Receive JSON data
socketHandler_receiveJson(&reqBuf, size);
printf("Received JSON: %s\n", reqBuf);
} }
void socketHandler_cleanup() { void socketHandler_cleanup() {
@@ -107,14 +114,10 @@ void socketHandler_cleanup() {
unlink(SOCKET_PATH); unlink(SOCKET_PATH);
} }
void socketHandler_read() {
char buf[16];
int buflen = 16;
for (int i = 0; i < buflen; i++) { buf[i] = 0x00; }
rc = read(client_fd, buf, buflen);
printf("Read %d: %s\n", buflen, buf);
printf("RC was %d\n", rc);
}
// Byte Array to uint32_t Big Endian // Byte Array to uint32_t Big Endian
uint32_t util_byteArrToUint32BE(char buf[]) { uint32_t util_byteArrToUint32BE(char buf[]) {
@@ -124,89 +127,254 @@ uint32_t util_byteArrToUint32BE(char buf[]) {
return retVal; return retVal;
} }
void socketHandler_initClientConnection() { // Byte Array to uint32_t Little Endian
// If reached here, a client has connected to the AF_UNIX socket uint32_t util_byteArrToUint32LE(char buf[]) {
uint32_t retVal = 0x0;
retVal = buf[0] | buf[1] << 8 | buf[2] << 16 | buf[3] << 24;
return retVal;
}
// Receive CliConn int socketHandler_initClientConnection() {
int cliConnSize = 4; /*
char buf[4] = { 0x00 }; * - Wait for client to send OSSP_Sock_CliConn
rc = read(client_fd, buf, cliConnSize); * - Send OSSP_Sock_ACK back
if (rc != cliConnSize) { * - Wait for client to send OSSP_Sock_GetConnInfo
printf("Received data was not correct.\n"); * - Send back OSSP_Sock_Size
* - Wait for client to send OSSP_Sock_ACK
* - Send JSON
* - Wait for client to send OSSP_Sock_ACK
*/
if (socketHandler_receiveCliConn() != 0) {
printf("[SocketHandler] initClientConnection() failed.\n");
return 1;
} }
uint32_t cliConn = util_byteArrToUint32BE(buf);
rc = memcmp(&cliConn, &SockSig_Client_Connect, 4); if (socketHandler_sendAck() != 0) {
printf("[SocketHandler] initClientConnection() failed.\n");
return 1;
}
if (socketHandler_receiveGetConnInfo() != 0) {
printf("[SockerHandler] initClientConnection() failed.\n");
return 1;
}
// Form JSON of connection info
char* serverAddr = NULL;
rc = asprintf(&serverAddr, "%s://%s", configObj->opensubsonic_protocol, configObj->opensubsonic_server);
if (rc == -1) {
printf("[SocketHandler] asprintf() failed.\n");
return 1;
}
cJSON* connInfoObj = cJSON_CreateObject();
cJSON_AddItemToObject(connInfoObj, "ossp_version", cJSON_CreateString(configObj->internal_ossp_version));
cJSON_AddItemToObject(connInfoObj, "server_addr", cJSON_CreateString(serverAddr));
char* connInfoStr = cJSON_PrintUnformatted(connInfoObj);
int connInfoLen = strlen(connInfoStr);
free(serverAddr);
cJSON_Delete(connInfoObj);
if (socketHandler_sendSize(connInfoLen) != 0) {
printf("[SockerHandler] initClientConnection() failed.\n");
return 1;
}
if (socketHandler_receiveAck() != 0) {
printf("[SockerHandler] initClientConnection() failed.\n");
return 1;
}
if (socketHandler_sendJson(connInfoStr, connInfoLen) != 0) {
printf("[SockerHandler] initClientConnection() failed.\n");
return 1;
}
if (socketHandler_receiveAck() != 0) {
printf("[SockerHandler] initClientConnection() failed.\n");
return 1;
}
return 0;
}
int socketHandler_sendAck() {
printf("[SocketHandler] Sending OSSP_Sock_ACK.\n");
rc = send(client_fd, &OSSP_Sock_ACK, SockSig_Length, 0);
if (rc != SockSig_Length) {
printf("[SocketHandler] Failed to send OSSP_Sock_ACK.\n");
return 1;
}
return 0;
}
int socketHandler_receiveAck() {
char buf[4] = { 0x00 };
rc = read(client_fd, buf, SockSig_Length);
if (rc != SockSig_Length) {
printf("[SocketHandler] Failed to receive OSSP_Sock_ACK (Signature is the wrong length).\n");
return 1;
}
uint32_t AckBE = util_byteArrToUint32LE(buf);
rc = memcmp(&AckBE, &OSSP_Sock_ACK, SockSig_Length);
if (rc != 0) { if (rc != 0) {
printf("Received CliConn signature is not valid.\n"); printf("[SocketHandler] Failed to receive OSSP_Sock_ACK (Signature is invalid. Expected 0x%.8x, Received 0x%.8x).\n", OSSP_Sock_ACK, AckBE);
// TODO return 1;
} else {
printf("Client connected!\n");
} }
printf("[SocketHandler] Received OSSP_Sock_ACK.\n");
// Send Server ACK return 0;
rc = send(client_fd, &SockSig_Ack, 4, 0);
if (rc != 4) {
printf("Failed to send Server ACK.\n");
} else {
printf("Sent Server ACK.\n");
}
// Deal with connection info
socketHandler_getConnInfo();
} }
typedef struct {
uint32_t signature;
uint16_t size;
} __attribute__((packed)) OSSP_Sock_Size_Data;
void socketHandler_getConnInfo() {
// Receive OSSP_Sock_GetConnInfo int socketHandler_receiveCliConn() {
int sigSize = 4;
char buf[4] = { 0x00 }; char buf[4] = { 0x00 };
rc = read(client_fd, buf, sigSize); rc = read(client_fd, buf, SockSig_Length);
if (rc != sigSize) { if (rc != SockSig_Length) {
printf("OSSP_Sock_GetConnInfo Error 1\n"); printf("[SocketHandler] Failed to receive OSSP_Sock_CliConn (Signature is the wrong length).\n");
// TODO return 1;
} else {
printf("Signature verified.\n");
} }
// Assemble JSON //uint32_t CliConnBE = util_byteArrToUint32BE(buf);
char* jsonInfo = "{ 'ossp_version': '0.3a', 'server_addr': 'https://eumak.hojuix.org' }"; uint32_t CliConnBE = util_byteArrToUint32LE(buf);
int jsonLen = strlen(jsonInfo);
printf("JSON: %s\n", jsonInfo);
printf("JSON Length: %d\n", jsonLen);
// Send OSSP_Sock_Size rc = memcmp(&CliConnBE, &OSSP_Sock_CliConn, SockSig_Length);
OSSP_Sock_Size_Data ossp_sock; if (rc != 0) {
ossp_sock.signature = SockSig_Size; printf("[SocketHandler] Failed to receive OSSP_Sock_CliConn (Signature is invalid. Expected 0x%.8x, Received 0x%.8x).\n", OSSP_Sock_CliConn, CliConnBE);
ossp_sock.size = jsonLen; return 1;
int sizeLen = 6; }
rc = send(client_fd, (char*)&ossp_sock, sizeLen, 0); printf("[SocketHandler] Received OSSP_Sock_CliConn.\n");
if (rc != sizeLen) { return 0;
printf("OSSP_Sock_Size failed.\n");
} else { printf("OSSP_Sock_Size sent.\n"); }
// Wait for ACK
char bufb[4] = { 0x00 };
rc = read(client_fd, bufb, sigSize);
if (rc != sigSize) {
printf("Invalid ACK sig size");
} else { printf("Sig verified\n"); }
// Send JSON
rc = send(client_fd, jsonInfo, jsonLen, 0);
if (rc != jsonLen) {
printf("Failed to send JSON.\n");
} else { printf("Sent JSON.\n"); }
} }
void sockerHandler_sendAck() {
// Send OSSP_Sock_ACK to the client
int socketHandler_receiveGetConnInfo() {
char buf[4] = { 0x00 };
rc = read(client_fd, buf, SockSig_Length);
if (rc != SockSig_Length) {
printf("[SocketHandler] Failed to receive OSSP_Sock_GetConnInfo (Signature is the wrong length).\n");
return 1;
}
uint32_t GetConnInfoBE = util_byteArrToUint32LE(buf);
rc = memcmp(&GetConnInfoBE, &OSSP_Sock_GetConnInfo, SockSig_Length);
if (rc != 0) {
printf("[SocketHandler] Failed to receive OSSP_Sock_GetConnInfo (Signature is invalid. Expected 0x%.8x, Received 0x%.8x).\n", OSSP_Sock_GetConnInfo, GetConnInfoBE);
return 1;
}
printf("[SocketHandler] Received OSSP_Sock_GetConnInfo.\n");
return 0;
} }
void socketHandler_receiveAck() {
// Receive OSSP_Sock_ACK from the client
int socketHandler_sendSize(uint32_t size) {
printf("[SocketHandler] Sending OSSP_Sock_Size.\n");
OSSP_Sock_Size_t sizeData;
sizeData.signature = OSSP_Sock_Size;
sizeData.size = size;
rc = send(client_fd, (char*)&sizeData, 8, 0);
if (rc != 8) {
printf("[SocketHandler] Failed to send OSSP_Sock_Size.\n");
return 1;
}
return 0;
}
int socketHandler_receiveSize(uint32_t* size) {
char buf[8] = { 0x00 };
rc = read(client_fd, buf, 8);
if (rc != 8) {
printf("[SocketHandler] Failed to receive OSSP_Sock_Size (Invalid size).\n");
return 1;
}
OSSP_Sock_Size_t* sizeData = (OSSP_Sock_Size_t*)&buf;
rc = memcmp(&sizeData->signature, &OSSP_Sock_Size, 4);
if (rc != 0) {
printf("[SocketHandler] Failed to receive OSSP_Sock_Size (Signature is invalid. Expected 0x%.8x, Received 0x%.8x).\n", OSSP_Sock_Size, sizeData->signature);
return 1;
}
printf("[SocketHandler] Received OSSP_Sock_Size.\n");
*size = sizeData->size;
return 0;
}
int socketHandler_receiveCliGetReq(int* size) {
char buf[8] = { 0x00 };
rc = read(client_fd, buf, 8);
if (rc != 8) {
printf("[SocketHandler] Failed to receive OSSP_Sock_ClientGetReq (Invalid size).\n");
return 1;
}
OSSP_Sock_ClientGetReq_t* clientGetReq = (OSSP_Sock_ClientGetReq_t*)&buf;
rc = memcmp(&clientGetReq->signature, &OSSP_Sock_ClientGetReq, 4);
if (rc != 0) {
printf("[SocketHandler] Failed to receive OSSP_Sock_ClientGetReq (Signature is invalid. Expected 0x%.8x, Received 0x%.8x).\n", OSSP_Sock_ClientGetReq, clientGetReq->signature);
return 1;
}
printf("[SocketHandler] Received OSSP_Sock_ClientGetReq.\n");
*size = clientGetReq->size;
return 0;
}
int socketHandler_receiveJson(char** data, int size) {
rc = read(client_fd, *data, size);
if (rc != size) {
printf("[SocketHandler] Failed to receive generic JSON data.\n");
return 1;
}
printf("[SocketHandler] Received generic JSON data.\n");
return 0;
}
int socketHandler_sendJson(char* json, int size) {
printf("[SocketHandler] Sending JSON.\n");
rc = send(client_fd, json, size, 0);
if (rc != size) {
printf("[SocketHandler] Failed to send JSON.\n");
return 1;
}
return 0;
}
int socketHandler_processClientGetReq() {
// Step 1 - Client sends GetReq with size
// Step 2 - Server allocates memory for future client request
// Step 3 - Server responds ACK
// Step 4 - --
} }

View File

@@ -1,6 +1,30 @@
#ifndef _SOCKET_H #ifndef _SOCKET_H
#define _SOCKET_H #define _SOCKET_H
#include <stdint.h>
typedef struct {
uint32_t signature;
uint32_t size;
} __attribute__((packed)) OSSP_Sock_Size_t;
typedef struct {
uint32_t signature;
uint32_t size;
} __attribute__((packed)) OSSP_Sock_ClientGetReq_t;
void socket_setup(); void socket_setup();
int socketHandler_initClientConnection();
int socketHandler_sendAck();
int socketHandler_receiveAck();
int socketHandler_receiveCliConn();
int socketHandler_receiveGetConnInfo();
int socketHandler_sendSize(uint32_t size);
int socketHandler_receiveSize(uint32_t* size);
int socketHandler_receiveCliGetReq(int* size);
int socketHandler_receiveJson(char** data, int size);
int socketHandler_sendJson(char* json, int size);
#endif #endif