bastianovic.dev - Personal toolbox

POSERCRUSHER: A CGI web audio player

2023-12-09

This is POSERCRUSHER, a CGI web audio player written in C, SQLite, HTML5 and JavaScript:

Screenshots

Start page: Artists overview

Artist's page: Albums overview

Album: Player control and song list

Code

posercrusher_cgi.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <ctype.h> #include <sqlite3.h> // // Database // static sqlite3 *db = NULL; // // Counter for page elements (artists, albums, songs) // static int elementindex = 0; // // The query function. // static void sqlquery (const char *sql, void *cb) { char *zErrMsg = 0; int rc = sqlite3_exec (db, sql, cb, 0, &zErrMsg); if (rc != SQLITE_OK) { printf ("<p>SQL error: %s</p>\n", zErrMsg); sqlite3_free (zErrMsg); } } // // Displays an artist list item. // static int display_artist (void *NotUsed, int argc, char **argv, char **azColName) { printf ("<h2><a href=\"./index.cgi?artist=%s\">%s</a></h2>\n", argv[0], argv[1]); return 0; } // // Displays an artist and an album. // static int display_artist_and_album (void *NotUsed, int argc, char **argv, char **azColName) { // // display artist's name // { char sql[1024]; snprintf (sql, sizeof sql, "SELECT * FROM ARTISTS WHERE ID = %s;", argv[1]); sqlquery (sql, display_artist); } printf ("<h3>%s</h3>\n", argv[2]); return 0; } // // Prints an artist list item. // static int list_artists (void *NotUsed, int argc, char **argv, char **azColName) { printf ("· <a href=\"./index.cgi?artist=%s\" id=\"element%d\" " "onmouseover=\"highlightelement(\'%d\', \'#4f6a93\')\" " "onmouseout=\"unhighlightelement(\'%d\')\">%s</a> ", argv[0], elementindex, elementindex, elementindex, argv[1]); ++elementindex; return 0; } // // Lists artist's albums. // static int list_albums (void *NotUsed, int argc, char **argv, char **azColName) { printf ("<li><a href=\"./index.cgi?album=%s\" id=\"element%d\" " "onmouseover=\"highlightelement(\'%d\', \'#4f6a93\')\" " "onmouseout=\"unhighlightelement(\'%d\')\">%s</a></li>", argv[0], elementindex, elementindex, elementindex, argv[2]); ++elementindex; return 0; } // // List album's songs. // static int list_songs (void *NotUsed, int argc, char **argv, char **azColName) { printf ("<div class=\"song\" id=\"element%d\" " "onmouseover=\"highlightelement(\'%d\', \'#993399\')\" " "onmouseout=\"unhighlightelement(\'%d\')\" " "onclick=\"selectsong(%d)\" data-url=\"%s\">\n", elementindex, elementindex, elementindex, elementindex, argv[3]); printf ("%s\n", argv[4]); printf ("</div>\n"); ++elementindex; return 0; } // // main function. // int main (int argc, char *argv[]) { // // Beginning of page (including CSS block) // { printf ("Content-Type: text/html\n\n" "<!DOCTYPE html>\n" "<html lang=\"en\">\n" "<head>\n" "<meta charset=\"utf-8\">\n" "<title>⛧ POSERCRUSHER ⛧</title>\n" "<style>\n" " body {\n" " color: black;\n" " background-color: white;\n" " font-family: Verdana, sans-serif;\n" " text-align: center;\n" " margin:1em auto;\n" " max-width: 40em;\n" " padding:0 .62em 3.24em;\n" " }\n" " h1 {\n" " text-align: center;\n" " }\n" " h1::before {\n" " content: '⛧ ';\n" " }\n" " h1::after {\n" " content: ' ⛧';\n" " }\n" " h2 {\n" " text-align: center;\n" " color: #ff0082;\n" " }\n" " h2::before {\n" " content: '⛧⛧ ';\n" " }\n" " h2::after {\n" " content: ' ⛧⛧';\n" " }\n" " li::before {\n" " content: ' · ';\n" " }\n" " h3 {\n" " text-align: center;\n" " color: #4f6a93;\n" " }\n" " h3::before {\n" " content: '⛧⛧⛧ ';\n" " }\n" " h3::after {\n" " content: ' ⛧⛧⛧';\n" " }\n" " ul {\n" " list-style-type: none;\n" " padding: 0;\n" " }\n" " a {\n" " text-decoration: none;\n" " color: inherit;\n" " }\n" " a:visited {\n" " color: inherit;\n" " }\n" " #header {\n" " color: #993399;\n" " font-size: xxx-large;\n" " transform: scale(1, -1);\n" " -moz-transform: scale(1, -1);\n" " -webkit-transform: scale(1, -1);\n" " -o-transform: scale(1, -1);\n" " -ms-transform: scale(1, -1);\n" " transform: scale(1, -1);\n" " }\n" " #footer {\n" " color: #993399;\n" " font-size: xxx-large;\n" " }\n" "</style>\n" "</head>\n" "<body>\n" "<div id=\"header\">✝</div>\n" "<h1><a href=\"./index.cgi\">POSERCRUSHER WEB AUDIO</a></h1>\n"); } // // Open database // { int rc = sqlite3_open ("posercrusher.db", &db); if (rc) { printf ("<p>Can't open database: %s</p>\n", sqlite3_errmsg (db)); sqlite3_close (db); return (1); } } // // Save query string // const char *query = NULL; { query = getenv ("QUERY_STRING"); } // // index.cgi without parameters: list artists // if (strlen (query) == 0) { puts ("<h2>Artists</h2>"); printf("<p>"); sqlquery ("SELECT * FROM ARTISTS ORDER BY NAME;", list_artists); printf("</p>\n"); } // // index.cgi?artist= : list albums // else if (strncmp (query, "artist=", strlen ("artist=")) == 0) { char *id = strchr (query, '='); if (id) { // Remove first character '=' memmove (id, id + 1, strlen (id)); // // display artist's name // { char sql[1024]; snprintf (sql, sizeof sql, "SELECT * FROM ARTISTS WHERE ID = %s ORDER BY NAME;", id); sqlquery (sql, display_artist); } // // list artist's albums // { puts ("<ul>"); char sql[1024]; snprintf (sql, sizeof sql, "SELECT * FROM ALBUMS " "WHERE ARTISTS_ID = %s " "ORDER BY NAME;", id); sqlquery (sql, list_albums); puts ("\n</ul>"); } } } // // index.cgi?album= : show audio player & list songs // else if (strncmp (query, "album=", strlen ("album=")) == 0) { char *id = strchr (query, '='); elementindex = 0; if (id) { // Remove first character '=' memmove (id, id + 1, strlen (id)); // // display album's name // { char sql[1024]; snprintf (sql, sizeof sql, "SELECT * FROM ALBUMS WHERE ID = %s;", id); sqlquery (sql, display_artist_and_album); } // // show audio player // { printf ("<div>\n" "<audio controls autoplay id=\"player\">\n" "<source src=\"\" type=\"\">\n" "Your browser does not support the audio element.\n" "</audio>\n" "<button id=\"prev\">|<</button>\n" "<button id=\"next\">>|</button>\n" "</div>\n"); } // // list songs // { puts ("<div class=\"songs\">"); char sql[1024]; snprintf (sql, sizeof sql, "SELECT * FROM SONGS " "WHERE ALBUMS_ID = %s " "ORDER BY TITLE;", id); sqlquery (sql, list_songs); puts ("</div>"); } } } // // Close database // { sqlite3_close (db); } // // End of page (including JavaScript block) // { printf ("<div id=\"footer\">&#9956;</div>\n" "<script>\n" "//\n" "// Highlights element with id.\n" "//\n" "function highlightelement(id, color) {\n" " if (id != i) {\n" " const element = document.getElementById(" "\"element\".concat(id));\n" " element.style.fontWeight = \"bold\";\n" " element.style.color = color;\n" " element.style.cursor=\"pointer\";\n" " }\n" "}\n" "//\n" "// Unhighlights element with id.\n" "//\n" "function unhighlightelement(id) {\n" " if (id != i) {\n" " const element = document.getElementById(" "\"element\".concat(id))\n" " element.style.fontWeight = \"normal\";\n" " element.style.color=\"black\";\n" " element.style.cursor=\"default\";\n" " }\n" "}\n" "//\n" "// Select song by id.\n" "//\n" "function selectsong(id) {\n" " i = id;\n" " highlightsong();\n" " play();\n" "}\n" "//\n" "// Play next song.\n" "//\n" "function next() {\n" " if (++i >= audioArray.length) i = 0;\n" " highlightsong();\n" " play();\n" "}\n" "//\n" "// Play previous song.\n" "//\n" "function prev() {\n" " if (--i < 0) i = 0;\n" " highlightsong();\n" " play();\n" "}\n" "//\n" "// Highlights the current song (previous song gets " "unhighlighted).\n" "//\n" "function highlightsong() {\n" " if (previoussong != null) {\n" " previoussong.style.fontWeight = \"normal\";\n" " }\n" " previoussong = document.getElementById(" "\"element\".concat(i));\n" " document.getElementById(" "\"element\".concat(i)).style.color=\"black\";\n" " previoussong.style.fontWeight = \"bold\";\n" "}\n" "//\n" "// Play current song.\n" "//\n" "function play() {\n" " player.src = audioArray[i].getAttribute(\'data-url\');\n" "}\n" "//\n" "// Initialize songs.\n" "//\n" "const audioArray = document.getElementsByClassName(\'song\');\n" "//\n" "// Initialize player.\n" "//\n" "const player = document.getElementById(\'player\');\n" "//\n" "// Index of current song.\n" "//\n" "let i = player == null ? -1 : 0;\n" "//\n" "// For unhighlighting the previous song.\n" "//\n" "let previoussong = null;\n" "//\n" "// Attach events.\n" "//\n" "player.addEventListener(\'ended\', next);\n" "document.getElementById(\"next\").onclick = next;\n" "document.getElementById(\"prev\").onclick = prev;\n" "//\n" "// Highlight current song.\n" "//\n" "highlightsong();\n" "//\n" "// Initialize first song.\n" "//\n" "play();\n" "</script>\n" "</body>\n" "</html>\n"); } }

Compiling

Compile with:
Shell
cc -Wall -Werror -o index.cgi posercrusher_cgi.c -lsqlite3

Database part

POSERCRUSHER get its information from a SQLite database file. The database is created via posercrusher_createdb:

Code

posercrusher_createdb.c
#include <stdio.h> #include <string.h> #include <dirent.h> #include <ctype.h> #include <sqlite3.h> // // Fills database with values. // void filldatabase (sqlite3 * db, const char *directory) { static int level = 0; static int album_id = 0; static int artist_id = 0; static int song_id = 0; char *zErrMsg = 0; struct dirent *files; DIR *dir = opendir (directory); if (dir == NULL) { printf ("Directory cannot be opened!"); return; } while ((files = readdir (dir)) != NULL) { if (files->d_name[0] == '.') { continue; } // + 1 = "/" // + 1 = "\0" char fullname[strlen (directory) + strlen (files->d_name) + 1 + 1]; strcpy (fullname, directory); strcat (fullname, files->d_name); // // Regular file // if (files->d_type == DT_REG) { char *dot = strrchr (files->d_name, '.'); if (dot) { for (int i = 0; i < strlen (dot); ++i) { dot[i] = tolower (dot[i]); } if (strcmp (dot, ".mp3") == 0 || strcmp (dot, ".m4a") == 0 || strcmp (dot, ".ogg") == 0) { char sql[1024]; sprintf (sql, "INSERT INTO SONGS VALUES (" "%d, %d, %d, \"%s\", \"%s\")", song_id, artist_id, album_id, fullname, files->d_name); int rc = sqlite3_exec (db, sql, NULL, 0, &zErrMsg); if (rc != SQLITE_OK) { fprintf (stderr, "SQL error: %s\n", zErrMsg); sqlite3_free (zErrMsg); } ++song_id; } } } // // Directory // else if (files->d_type == DT_DIR) { if (level == 0) { char sql[1024]; sprintf (sql, "INSERT INTO ARTISTS VALUES (%d, \"%s\")", artist_id, files->d_name); int rc = sqlite3_exec (db, sql, NULL, 0, &zErrMsg); if (rc != SQLITE_OK) { fprintf (stderr, "SQL error: %s\n", zErrMsg); sqlite3_free (zErrMsg); } } else if (level == 1) { char sql[1024]; sprintf (sql, "INSERT INTO ALBUMS VALUES (%d, %d, \"%s\")", album_id, artist_id, files->d_name); int rc = sqlite3_exec (db, sql, NULL, 0, &zErrMsg); if (rc != SQLITE_OK) { fprintf (stderr, "SQL error: %s\n", zErrMsg); sqlite3_free (zErrMsg); } } char subdirectory[strlen (fullname) + 1 + 1]; strcpy (subdirectory, fullname); strcat (subdirectory, "/"); ++level; filldatabase (db, subdirectory); --level; // // Increase ids *after* albums and songs have been added // (else off-by-one in database) // if (level == 0) { ++artist_id; } else if (level == 1) { ++album_id; } } } closedir (dir); } // // main function. // int main (void) { // // Open database // sqlite3 *db = NULL; { int rc = sqlite3_open ("posercrusher.db", &db); if (rc) { fprintf (stderr, "Can't open database: %s\n", sqlite3_errmsg (db)); sqlite3_close (db); return (1); } } // // Delete old table ARTISTS // { char *zErrMsg = NULL; int rc = sqlite3_exec (db, "DROP TABLE IF EXISTS ARTISTS;", NULL, 0, &zErrMsg); if (rc != SQLITE_OK) { fprintf (stderr, "SQL error: %s\n", zErrMsg); sqlite3_free (zErrMsg); } } // // Delete old table ALBUMS // { char *zErrMsg = NULL; int rc = sqlite3_exec (db, "DROP TABLE IF EXISTS ALBUMS;", NULL, 0, &zErrMsg); if (rc != SQLITE_OK) { fprintf (stderr, "SQL error: %s\n", zErrMsg); sqlite3_free (zErrMsg); } } // // Delete old table SONGS // { char *zErrMsg = NULL; int rc = sqlite3_exec (db, "DROP TABLE IF EXISTS SONGS;", NULL, 0, &zErrMsg); if (rc != SQLITE_OK) { fprintf (stderr, "SQL error: %s\n", zErrMsg); sqlite3_free (zErrMsg); } } // // Create table ARTISTS // { char *zErrMsg = NULL; int rc = sqlite3_exec (db, "CREATE TABLE ARTISTS (" " id INTEGER PRIMARY KEY," " name TEXT NOT NULL COLLATE NOCASE" ");", NULL, 0, &zErrMsg); if (rc != SQLITE_OK) { fprintf (stderr, "SQL error: %s\n", zErrMsg); sqlite3_free (zErrMsg); } } // // Create table ALBUMS // { char *zErrMsg = NULL; int rc = sqlite3_exec (db, "CREATE TABLE ALBUMS (" " id INTEGER PRIMARY KEY," " artists_id INTEGER," " name TEXT NOT NULL COLLATE NOCASE" ");", NULL, 0, &zErrMsg); if (rc != SQLITE_OK) { fprintf (stderr, "SQL error: %s\n", zErrMsg); sqlite3_free (zErrMsg); } } // // Create table SONGS // { char *zErrMsg = NULL; int rc = sqlite3_exec (db, "CREATE TABLE SONGS (" " id INTEGER PRIMARY KEY," " artists_id INTEGER," " albums_id INTEGER," " fullpath TEXT NOT NULL COLLATE NOCASE," " title TEXT NOT NULL COLLATE NOCASE" ");", NULL, 0, &zErrMsg); if (rc != SQLITE_OK) { fprintf (stderr, "SQL error: %s\n", zErrMsg); sqlite3_free (zErrMsg); } } // // Fill with data // { filldatabase (db, "./audio/"); } // // Close database // { sqlite3_close (db); } }

Compiling

Compile with:

Shell
cc -Wall -Werror -o posercrusher_createdb \
posercrusher_createdb.c -lsqlite3

Expected directory layout

POSERCRUSHER expects a directory named audio in the same directory as index.cgi and posercrusher_createdb with the following structure:

OpenBSD/httpd

To get it working on OpenBSD with httpd from a chroot I had to compile everything static and to give the location of libs and includes:
Shell
cc -Wall -Werror -o index.cgi posercrusher_cgi.c \
-lm -lpthread -lsqlite3 \
-I/usr/local/include -L/usr/local/lib/ \
-static

← Home