mirror of
https://github.com/Goldenkrew3000/OSSP_OpenSource.git
synced 2026-02-16 12:15:17 +10:00
Compare commits
11 Commits
9f96c574e9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2853898145 | |||
| 9ae53e0824 | |||
| 1f814fff6e | |||
| 26df10f8fb | |||
| 66aa616286 | |||
| ba554dc716 | |||
| 34f562149e | |||
| db7fb9385d | |||
| cd03819c65 | |||
| 6735603e9c | |||
| 8e7eb4c534 |
@@ -17,7 +17,7 @@ Discord RPC Settings:
|
|||||||
Enable ('enable') - 'true' to enable Discord RPC or 'false' to disable it
|
Enable ('enable') - 'true' to enable Discord RPC or 'false' to disable it
|
||||||
Method ('method'):
|
Method ('method'):
|
||||||
- Method '0' is the official local Discord RPC method, local application to local discord client
|
- 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
|
to allow Discord RPC from a mobile device securely
|
||||||
Show System Details ('showSystemDetails') - 'true' or 'false'
|
Show System Details ('showSystemDetails') - 'true' or 'false'
|
||||||
- Want to show off / Got bragging rights on your _unique_ system?? Well this is perfect for you!
|
- 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
|
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!!!
|
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.
|
- 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
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
# OSSP (OpenSubsonicPlayer)
|
# 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
|
## Notice
|
||||||
OSSP is under heavy development and is NOT ready for daily use.<br>
|
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>
|
Also, I am NOT RESPONSIBLE if you damage your hearing and/or any of your equipment using OSSP.<br>
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|||||||
@@ -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); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
* Goldenkrew3000 2025
|
* Goldenkrew3000 2025
|
||||||
* License: GNU General Public License 3.0
|
* License: GNU General Public License 3.0
|
||||||
* Discord Local RPC Handler
|
* Discord Local RPC Handler
|
||||||
* Note: This provides server auth creds (encoded) directly to Discord, could use Spotify's API instead??
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
@@ -41,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)
|
||||||
@@ -87,7 +66,8 @@ 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) {
|
} else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PLAYING_OPENSUBSONIC) {
|
||||||
|
// 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);
|
||||||
asprintf(&stateString, "by %s", (*discordrpc_struct)->songArtist);
|
asprintf(&stateString, "by %s", (*discordrpc_struct)->songArtist);
|
||||||
@@ -99,6 +79,20 @@ 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_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) {
|
} else if ((*discordrpc_struct)->state == DISCORDRPC_STATE_PAUSED) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,10 @@
|
|||||||
#define _DISCORDRPC_H
|
#define _DISCORDRPC_H
|
||||||
|
|
||||||
#define DISCORDRPC_STATE_IDLE 0
|
#define DISCORDRPC_STATE_IDLE 0
|
||||||
#define DISCORDRPC_STATE_PLAYING 1
|
#define DISCORDRPC_STATE_PLAYING_OPENSUBSONIC 1
|
||||||
#define DISCORDRPC_STATE_PAUSED 2
|
#define DISCORDRPC_STATE_PLAYING_LOCALFILE 2
|
||||||
|
#define DISCORDRPC_STATE_PLAYING_INTERNETRADIO 3
|
||||||
|
#define DISCORDRPC_STATE_PAUSED 4
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int state;
|
int state;
|
||||||
|
|||||||
257
src/localMusicHandler.cpp
Normal file
257
src/localMusicHandler.cpp
Normal 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
26
src/localMusicHandler.hpp
Normal 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
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
380
src/socket.c
Normal file
380
src/socket.c
Normal 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
30
src/socket.h
Normal 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
|
||||||
Reference in New Issue
Block a user