Compare commits

...

11 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
db7fb9385d Adding beginning of AF_UNIX interface 2026-02-08 16:12:32 +10:00
cd03819c65 Adding stance on AI 2026-02-04 15:40:13 +10:00
6735603e9c General cleanup 2026-02-03 04:35:40 +10:00
8e7eb4c534 DiscordRPC: Added internet radio station support 2026-02-02 15:26:15 +10:00
13 changed files with 857 additions and 73 deletions

View File

@@ -17,7 +17,7 @@ Discord RPC Settings:
Enable ('enable') - 'true' to enable Discord RPC or 'false' to disable it
Method ('method'):
- Method '0' is the official local Discord RPC method, local application to local discord client
- Method '1' is a VERY buggy and in-development RPC method that uses a client-server model
- DO NOT USE: Method '1' is a VERY buggy and in-development RPC method that uses a client-server model
to allow Discord RPC from a mobile device securely
Show System Details ('showSystemDetails') - 'true' or 'false'
- Want to show off / Got bragging rights on your _unique_ system?? Well this is perfect for you!
@@ -25,3 +25,20 @@ Show System Details ('showSystemDetails') - 'true' or 'false'
Example: 'on Linux x86_64 6.17.1-arch1-1' will be shown in the RPC dialog
Go on, don't be shy, show everyone you somehow have Discord and OSSP running on fucking s390x!!!
- Setting this to 'false' will simply instead show what playlist you are playing.
Show Cover Art ('showCoverArt') - 'true' or 'false'
- If this is set to true, the cover art for the song playing will be shown on the Discord rich presence.
This can be disabled because the OpenSubsonic API does not have an unauthenticated way of accessing
album art from a server, which means you have to leak an authenticated server URL to Discord (Which
a lot of people would not feel comfortable doing, understandably).
- If this is set to false, Discord rich presence only shows the app icon, and does not leak any authenticated
URLs to Discord servers
Radio Sqlite3 Database Drafting:
Location: $HOME/.config/ossp/radio.db
Table: stations
- Int: id
- String: name
- String: url

View File

@@ -1,5 +1,11 @@
# OSSP (OpenSubsonicPlayer)
## AI Notice
This project does not have ANY AI code other than a few snippets that will be removed in the future.<br>
Yes I comment a LOT, and unfortunately this has become a red flag for excessive AI use.<br>
I am a forgetful person, and have a habit of dropping projects for months at a time, so having
many comments allows me to come back and instantly understand what I was doing.<br>
## Notice
OSSP is under heavy development and is NOT ready for daily use.<br>
Also, I am NOT RESPONSIBLE if you damage your hearing and/or any of your equipment using OSSP.<br>

View File

@@ -12,6 +12,8 @@ find_package(OpenGL REQUIRED)
find_package(SDL2 REQUIRED)
find_package(PkgConfig REQUIRED)
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)
@@ -25,6 +27,8 @@ add_executable(ossp MACOSX_BUNDLE
configHandler.c
discordrpc.c
localRadioDBHandler.c
localMusicHandler.cpp
socket.c
gui/gui_entry.cpp
player/player.c
player/playQueue.cpp
@@ -65,6 +69,6 @@ set_target_properties(ossp PROPERTIES
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_loginSalt = NULL;
(*configObj)->internal_opensubsonic_loginToken = NULL;
(*configObj)->internal_ossp_version = NULL;
(*configObj)->listenbrainz_enable = false;
(*configObj)->listenbrainz_token = NULL;
(*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_right = NULL;
(*configObj)->lv2_reverb_filter_name = NULL;
(*configObj)->local_rootdir = NULL;
// Set internal configuration values
(*configObj)->internal_opensubsonic_version = strdup("1.8.0");
(*configObj)->internal_opensubsonic_clientName = strdup("Hojuix_OSSP");
(*configObj)->internal_ossp_version = strdup("v0.4a");
// Form the path to the config JSON
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);
}
// 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);
logger_log_general(__func__, "Successfully read configuration file.");
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_loginSalt != NULL) { free((*configObj)->internal_opensubsonic_loginSalt); }
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)->lastfm_username != NULL) { free((*configObj)->lastfm_username); }
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_right != NULL) { free((*configObj)->lv2_parax32_frequency_right); }
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); }
}

View File

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

View File

@@ -3,7 +3,6 @@
* Goldenkrew3000 2025
* License: GNU General Public License 3.0
* Discord Local RPC Handler
* Note: This provides server auth creds (encoded) directly to Discord, could use Spotify's API instead??
*/
#include <inttypes.h>
@@ -41,31 +40,11 @@ void discordrpc_struct_deinit(discordrpc_data** 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() {
printf("[DiscordRPC] Initializing...\n");
printf("[DiscordRPC] Initializing.\n");
// TODO Can I just not deal with the handler callbacks at all?
DiscordEventHandlers handlers;
memset(&handlers, 0, sizeof(handlers));
handlers.ready = handleDiscordReady;
handlers.disconnected = handleDiscordDisconnected;
handlers.errored = handleDiscordError;
Discord_Initialize(discordrpc_appid, &handlers, 1, NULL);
// Fetch OS String for RPC (Heap-allocated)
@@ -87,7 +66,8 @@ void discordrpc_update(discordrpc_data** discordrpc_struct) {
if ((*discordrpc_struct)->state == DISCORDRPC_STATE_IDLE) {
asprintf(&detailsString, "Idle");
presence.details = detailsString;
} else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PLAYING) {
} else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PLAYING_OPENSUBSONIC) {
// Playing a song
time_t currentTime = time(NULL);
asprintf(&detailsString, "%s", (*discordrpc_struct)->songTitle);
asprintf(&stateString, "by %s", (*discordrpc_struct)->songArtist);
@@ -99,6 +79,20 @@ void discordrpc_update(discordrpc_data** discordrpc_struct) {
if (configObj->discordrpc_showSysDetails) {
presence.largeImageText = discordrpc_osString;
}
} else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PLAYING_LOCALFILE) {
//
} else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PLAYING_INTERNETRADIO) {
// Playing an internet radio station
time_t currentTime = time(NULL);
asprintf(&detailsString, "%s", (*discordrpc_struct)->songTitle);
asprintf(&stateString, "Internet radio station");
presence.details = detailsString;
presence.state = stateString;
presence.largeImageKey = (*discordrpc_struct)->coverArtUrl;
presence.startTimestamp = (long)currentTime;
if (configObj->discordrpc_showSysDetails) {
presence.largeImageText = discordrpc_osString;
}
} else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PAUSED) {
}

View File

@@ -8,8 +8,10 @@
#define _DISCORDRPC_H
#define DISCORDRPC_STATE_IDLE 0
#define DISCORDRPC_STATE_PLAYING 1
#define DISCORDRPC_STATE_PAUSED 2
#define DISCORDRPC_STATE_PLAYING_OPENSUBSONIC 1
#define DISCORDRPC_STATE_PLAYING_LOCALFILE 2
#define DISCORDRPC_STATE_PLAYING_INTERNETRADIO 3
#define DISCORDRPC_STATE_PAUSED 4
typedef struct {
int state;

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"
// C++ interface for storing song queue data (C interface is in the header)
class SongObject {
class OSSPQ_SongObject {
public:
std::string title;
std::string album;
std::string artist;
std::string id;
std::string streamUrl;
std::string coverArtUrl;
long duration;
int mode;
};
// 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) {
// Append a new song to the end of the queue
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??
std::string cpp_title(title);
std::string cpp_artist(artist);
std::string cpp_id(id);
SongObject songObject;
songObject.title = cpp_title;
songObject.artist = cpp_artist;
songObject.id = cpp_id;
songObject.duration = duration;
OSSPQ_Items.push_back(songObject);
int OSSPQ_AppendToEnd(char* title, char* album, char* artist, char* id, char* streamUrl, char* coverArtUrl, long duration, int mode) {
OSSPQ_SongObject songObject;
if (mode == OSSPQ_MODE_OPENSUBSONIC || mode == OSSPQ_MODE_LOCALFILE) {
// Both Local File and OpenSubsonic playback both take the same arguments
if (mode == OSSPQ_MODE_OPENSUBSONIC) {
printf("[OSSPQ] Appending OpenSubsonic Song: %s by %s.\n", title, artist);
} else if (mode == OSSPQ_MODE_LOCALFILE) {
printf("[OSSPQ] Appending Local Song: %s by %s.\n", title, artist);
}
songObject.title = title;
songObject.album = album;
songObject.artist = artist;
songObject.id = id;
songObject.streamUrl = streamUrl;
songObject.coverArtUrl = coverArtUrl;
songObject.duration = duration;
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;
}
OSSPQ_SongStruct* internal_OSSPQ_PopFromFront() {
if (OSSPQ_Items.empty()) {
// No items in play queue
OSSPQ_SongStruct* OSSPQ_PopFromFront() {
// Check if song queue is empty, if not, then pop oldest
if (OSSPQ_SongQueue.empty()) {
return NULL;
}
OSSPQ_SongObject songObject = OSSPQ_SongQueue.front();
OSSPQ_SongQueue.pop_front();
// Pull the first song off the song queue
SongObject songObject = OSSPQ_Items.front();
OSSPQ_Items.pop_front();
// Allocate, initialize, and fill C compatible song object
OSSPQ_SongStruct* songObjectC = (OSSPQ_SongStruct*)malloc(sizeof(OSSPQ_SongStruct));
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
// NOTE: I am initializing the variables to a known value just in case there is missing information in songObject
OSSPQ_SongStruct* playQueueObject = (OSSPQ_SongStruct*)malloc(sizeof(OSSPQ_SongStruct));
playQueueObject->title = NULL;
playQueueObject->artist = NULL;
playQueueObject->id = NULL;
playQueueObject->duration = 0;
playQueueObject->title = strdup(songObject.title.c_str());
playQueueObject->artist = strdup(songObject.artist.c_str());
playQueueObject->id = strdup(songObject.id.c_str());
playQueueObject->duration = songObject.duration;
return playQueueObject;
if (songObject.mode == OSSPQ_MODE_OPENSUBSONIC || songObject.mode == OSSPQ_MODE_LOCALFILE) {
songObjectC->title = strdup(songObject.title.c_str());
songObjectC->album = strdup(songObject.album.c_str());
songObjectC->artist = strdup(songObject.artist.c_str());
songObjectC->id = strdup(songObject.id.c_str());
songObjectC->streamUrl = strdup(songObject.streamUrl.c_str());
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());
}
return songObjectC;
}
void internal_OSSPQ_FreeSongObject(OSSPQ_SongStruct* songObject) {
if (songObject->title != NULL) { free(songObject->title); }
if (songObject->artist != NULL) { free(songObject->artist); }
if (songObject->id != NULL) { free(songObject->id); }
if (songObject != NULL) { free(songObject); }
void OSSPQ_FreeSongObjectC(OSSPQ_SongStruct* songObjectC) {
printf("[OSSPQ] Freeing SongObjectC.\n");
if (songObjectC->title != NULL) { free(songObjectC->title); }
if (songObjectC->album != NULL) { free(songObjectC->album); }
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) {
return (char*)OSSPQ_Items[idx].title.c_str();
return (char*)OSSPQ_SongQueue[idx].title.c_str();
}
int internal_OSSPQ_GetItemCount() {
return OSSPQ_Items.size();
return OSSPQ_SongQueue.size();
}

View File

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

380
src/socket.c Normal file
View File

@@ -0,0 +1,380 @@
/*
* OpenSubsonicPlayer
* Goldenkrew3000 2025
* License: GNU General Public License 3.0
* Info: Socket Handler
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
#include <stdbool.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include "external/cJSON.h"
#include "configHandler.h"
#include "socket.h"
#define SOCKET_PATH "/tmp/ossp_socket" // TODO Make this configurable through the configuration file
static int server_fd = -1;
static int client_fd = -1;
static int rc = -1;
socklen_t client_len;
struct sockaddr_un server_addr;
struct sockaddr_un client_addr;
extern configHandler_config_t* configObj;
void socketHandler_read();
int SockSig_Length = 4;
const uint32_t OSSP_Sock_ACK = 0x7253FF87;
const uint32_t OSSP_Sock_CliConn = 0xE3566C2E;
const uint32_t OSSP_Sock_GetConnInfo = 0x8E4F6B01;
const uint32_t OSSP_Sock_Size = 0x1F7E8BCF;
const uint32_t OSSP_Sock_ClientGetReq = 0x210829CF;
void socket_setup() {
printf("[SocketHandler] Initializing.\n");
// Create server socket, and ensure that the socket file is removed
server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (server_fd == -1) {
printf("[SocketHandler] Could not open server socket.\n");
// TODO
}
unlink(SOCKET_PATH);
// Bind server socket to SOCKET_PATH
memset(&server_addr, 0, sizeof(struct sockaddr_un));
server_addr.sun_family = AF_UNIX;
strncpy(server_addr.sun_path, SOCKET_PATH, sizeof(server_addr.sun_path) - 1);
rc = bind(server_fd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr_un));
if (rc == -1) {
printf("[SocketHandler] Could not bind server socket.\n");
// TODO
}
rc = listen(server_fd, 5);
if (rc == -1) {
printf("[SocketHandler] Could not listen on server socket.\n");
// TODO
}
// Wait for connection
bool isServerWaiting = true;
while (isServerWaiting) {
client_fd = accept(server_fd, NULL, NULL); // TODO
if (client_fd == -1) {
printf("[SocketHandler] Error accepting connection.\n");
} else {
printf("[SocketHandler] Accepted connection.\n");
isServerWaiting = false;
}
}
//socketHandler_read();
socketHandler_initClientConnection();
printf("------------------------------\n");
// Receive ClientGetReq with Size
int size = 0;
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() {
printf("[SocketHandler] Cleaning up.\n");
close(client_fd);
close(server_fd);
unlink(SOCKET_PATH);
}
// Byte Array to uint32_t Big Endian
uint32_t util_byteArrToUint32BE(char buf[]) {
// NOTE: I could use a combination of memcpy() and htons() here, but bitshifting is a single move
uint32_t retVal = 0x0;
retVal = buf[3] | buf[2] << 8 | buf[1] << 16 | buf[0] << 24;
return retVal;
}
// Byte Array to uint32_t Little Endian
uint32_t util_byteArrToUint32LE(char buf[]) {
uint32_t retVal = 0x0;
retVal = buf[0] | buf[1] << 8 | buf[2] << 16 | buf[3] << 24;
return retVal;
}
int socketHandler_initClientConnection() {
/*
* - Wait for client to send OSSP_Sock_CliConn
* - Send OSSP_Sock_ACK back
* - Wait for client to send OSSP_Sock_GetConnInfo
* - 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;
}
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) {
printf("[SocketHandler] Failed to receive OSSP_Sock_ACK (Signature is invalid. Expected 0x%.8x, Received 0x%.8x).\n", OSSP_Sock_ACK, AckBE);
return 1;
}
printf("[SocketHandler] Received OSSP_Sock_ACK.\n");
return 0;
}
int socketHandler_receiveCliConn() {
char buf[4] = { 0x00 };
rc = read(client_fd, buf, SockSig_Length);
if (rc != SockSig_Length) {
printf("[SocketHandler] Failed to receive OSSP_Sock_CliConn (Signature is the wrong length).\n");
return 1;
}
//uint32_t CliConnBE = util_byteArrToUint32BE(buf);
uint32_t CliConnBE = util_byteArrToUint32LE(buf);
rc = memcmp(&CliConnBE, &OSSP_Sock_CliConn, SockSig_Length);
if (rc != 0) {
printf("[SocketHandler] Failed to receive OSSP_Sock_CliConn (Signature is invalid. Expected 0x%.8x, Received 0x%.8x).\n", OSSP_Sock_CliConn, CliConnBE);
return 1;
}
printf("[SocketHandler] Received OSSP_Sock_CliConn.\n");
return 0;
}
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;
}
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 - --
}

30
src/socket.h Normal file
View File

@@ -0,0 +1,30 @@
#ifndef _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();
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