Rev 4 | Rev 6 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
/*
* Copyright (C) 2015 by Andreas Theofilu <andreas@theosys.at>
*
* All rights reserved. No warranty, explicit or implicit, provided.
*
* NOTICE: All information contained herein is, and remains
* the property of Andreas Theofilu and his suppliers, if any.
* The intellectual and technical concepts contained
* herein are proprietary to Andreas Theofilu and its suppliers and
* may be covered by European and Foreign Patents, patents in process,
* and are protected by trade secret or copyright law.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from Andreas Theofilu.
*/
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <stdlib.h>
#include <libgen.h>
#include <ctype.h>
#include <math.h>
#include <time.h>
#include <signal.h>
#include <syslog.h>
#include <errno.h>
#include <pthread.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <netdb.h>
#include <dirent.h>
#include <netinet/in.h>
#include <sys/param.h>
#include <sys/ioctl.h>
#include <sqlite3.h>
#include <id3.h>
#include "config.h"
#include "helplib.h"
#include "mdb.h"
#include "list.h"
static pthread_mutex_t fastmutex_proc = PTHREAD_MUTEX_INITIALIZER;
int musicFilter(const struct dirent *dir);
int grabMusicFiles(char *dir);
static int callback(void *hint, int argc, char **argv, char **azColName);
void evaluateMusicFile(char *file);
/* Global variables */
int file_found = 0;
char cmd_error[512];
char cmd_message[512];
/*
* This is called from the main listening thread as a thread of its own.
* The function gets the commands from the client, work with them and give
* back the result.
*/
void *processCommands(void *pV_data)
{
char ch, buf[512];
int i, s1, s;
struct SOCKETS *soc;
soc = (struct SOCKETS *)pV_data;
s1 = soc->newfd;
s = soc->sockfd;
memset(&buf[0], 0, sizeof(buf));
memset(cmd_message, 0, sizeof(cmd_message));
i = 0;
while (read(s1,&ch,1) > 0)
{
if (i < (int)(sizeof(buf) - 1))
{
buf[i] = ch;
if (ch == ';' || ch == 0x0d)
{
int pstat;
pthread_mutex_lock (&fastmutex_proc);
if (!strncmp(buf, "quit", 4))
break;
if ((pstat = parseCommand(s1, buf)) == FALSE)
{
char hv0[128];
char *p = urlencode(cmd_error);
if (p != NULL)
{
sprintf(&hv0[0], "INVALID COMMAND: %s: %s;", buf, p);
free(p);
}
else
sprintf(&hv0[0], "INVALID COMMAND: %s;", buf);
write(s1, hv0, strlen(hv0));
}
else if (pstat == 2)
write(s1, "NAK;", 4);
else
{
if (strlen(cmd_message) > 0)
write(s1, cmd_message, strlen(cmd_message));
else
write(s1, "OK;", 3);
}
memset(&buf[0], 0, sizeof(buf));
i = 0;
pthread_mutex_unlock(&fastmutex_proc);
continue;
}
}
i++;
}
close(s1);
}
/*
* This function parses the commands given by a network client.
*/
int parseCommand(int s1, char *cmd)
{
char bef[32],par[1024], *p;
char hv0[256];
int i,j;
memset(bef, 0, sizeof(bef));
memset(par, 0, sizeof(par));
memset(cmd_error, 0, sizeof(cmd_error));
if ((p = strchr(cmd, ':')) != NULL) /* do we have a parameter? */
{
strncpy(bef, cmd, p - cmd);
strncpy(par, p+1, strlen(p) - 2); /* Cut off the trailing semi colon */
}
else
strncpy(bef, cmd, strlen(cmd) - 1);
cmd_error[0] = 0;
cmd_message[0] = 0;
syslog(LOG_DEBUG, "bef=%s, par=%s", bef, par);
if (!strcasecmp(bef, "RESCAN")) /* Rescan all directories */
{
int fd;
char fname[512];
if (access(configs.Pathfile, R_OK))
{
syslog(LOG_WARNING, "Error accessing file %s", configs.Pathfile);
sprintf(cmd_error, "Error accessing file >%s<", configs.Pathfile);
return FALSE;
}
if ((fd = open(configs.Pathfile, O_RDONLY)) < 0)
{
syslog(LOG_WARNING, "Error opening file %s: %s", configs.Pathfile, strerror(errno));
sprintf(cmd_error, "Error opening file >%s<", configs.Pathfile);
return FALSE;
}
i = 0;
/* Delete the database file before we start to scan.
* This makes shure, that there is no double entry.
*/
strcpy(fname, configs.home);
strcat(fname, "/music.db");
if (!access(fname, R_OK | W_OK))
{
if (unlink(fname) != 0)
syslog(LOG_WARNING, "Error deleting file %s: %s", fname, strerror(errno));
}
/* Scan all directories in the config file recursievly. */
while (readLine(fd, &hv0[0], sizeof(hv0)) != NULL)
{
grabMusicFiles(hv0);
i++;
}
sprintf(cmd_message, "DIRECTORIES:%d;", i);
close(fd);
}
/*
* Syntax: LIST:<type>:<start>:<length>;
* <type> TITLE | ARTIST | ALBUM | GENRE
* <start> The line to start from
* <length> The number of lines to report
*
* Returns: LINE:<type>:<id>:<line>:<title>:<artist>:<album>:<genre>;
* <id> The unique ID of the file. Needed to play the file!
* <line> The line number counting from 1
*/
if (!strcasecmp(bef, "LIST")) /* List content */
{
char p_type[16];
int start, length;
memset(p_type, 0, sizeof(p_type));
remove_string(par, ":", &hv0[0]);
start = strlen(hv0);
hv0[start-1] = 0;
strncpy(p_type, hv0, sizeof(p_type)-1);
start = atoi(par);
remove_string(par, ":", &hv0[0]);
length = atoi(par);
return listSongs(s1, p_type, start, length);
}
/*
* Syntax: FOLDER:<type>:<start>:<length>;
* <type> TITLE | ARTIST | ALBUM | GENRE
* <start> The line to start from
* <length> The number of lines to report
*
* Returns: FOLDER:<type>:<id>:<line>:<content>;
* <id> The unique ID of the file. Needed to play the file!
* <line> The line number counting from 1
* <content> The name of the folder
*/
if (!strcasecmp(bef, "FOLDER")) /* List folders */
{
char p_type[16];
int start, length;
memset(p_type, 0, sizeof(p_type));
remove_string(par, ":", &hv0[0]);
start = strlen(hv0);
hv0[start-1] = 0;
strncpy(p_type, hv0, sizeof(p_type)-1);
start = atoi(par);
remove_string(par, ":", &hv0[0]);
length = atoi(par);
return listFolders(s1, p_type, start, length);
}
return TRUE; /* cmd was OK */
}
/*
* Callback function of directory scan.
* This function desides whether a directory entry will be treated as a
* music file or not.
*/
int musicFilter(const struct dirent *dir)
{
if (dir == NULL)
return 0;
if (dir->d_type == DT_REG)
{
char *p = strrchr(dir->d_name, '.');
if (p != NULL && strcasestr(p, ".mp3") != NULL)
return 1;
}
else if (dir->d_type == DT_DIR)
{
if (strcmp(dir->d_name, ".") != 0 && strcmp(dir->d_name, "..") != 0)
return 1;
}
return 0;
}
/*
* This function scans a directory for music files. If it finds some file name
* who is a possible music file, it gives it to a function to evaluate and
* store it.
*/
int grabMusicFiles(char *dir)
{
struct dirent **namelist;
int n;
if (dir == NULL || strlen(dir) == 0)
return FALSE;
n = scandir(dir, &namelist, musicFilter, alphasort);
if (n < 0)
{
syslog(LOG_DAEMON, "Error scanning directory %s: %s", dir, strerror(errno));
return FALSE;
}
else
{
while(n--)
{
char hv0[512];
strcpy(hv0, dir);
strcat(hv0, "/");
strcat(hv0, namelist[n]->d_name);
if (namelist[n]->d_type == DT_DIR)
{
grabMusicFiles(hv0);
syslog(LOG_DEBUG, "Directory: %s", namelist[n]->d_name);
}
else
evaluateMusicFile(hv0);
free(namelist[n]);
}
free(namelist);
}
}
static int callback(void *hint, int argc, char **argv, char **azColName)
{
/* int i; */
if (hint != NULL && strcmp(hint, "EXIST"))
{
file_found = TRUE;
syslog(LOG_DEBUG, "File exist");
return 0;
}
/* for(i = 0; i < argc; i++)
{
printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL");
}
printf("\n");
*/
return 0;
}
void evaluateMusicFile(char *file)
{
sqlite3 *db;
char *zErrMsg = 0;
int rc;
char fname[256];
char query[1024];
ID3Tag *id3;
ID3Frame *frame;
ID3Field *field;
char id3_title[256];
char id3_singer[256];
char id3_album[256];
char id3_genre[256];
char *title, *singer, *album, *hp;
if (access(file, R_OK))
return;
strcpy(fname, configs.home);
strcat(fname, "/music.db");
if (!access(fname, R_OK | W_OK))
file_found = TRUE;
else /* create the directory, if it doesn't exitst */
{
if (access(configs.home, R_OK | W_OK))
{
if (mkdir(configs.home, 0775) != 0)
{
syslog(LOG_WARNING, "Error creating directory %s: %s", configs.home, strerror(errno));
return;
}
else
syslog(LOG_INFO, "Directory %s was created.", configs.home);
}
sprintf(query, "%s/Covers", configs.home);
if (access(query, R_OK | W_OK))
{
if (mkdir(query, 0775) != 0)
syslog(LOG_WARNING, "Error creating directory %s: %s", query, strerror(errno));
else
syslog(LOG_INFO, "Directory %s was created.", query);
}
}
rc = sqlite3_open(fname, &db);
if (rc)
{
syslog(LOG_WARNING, "Error opening database %s: %s", fname, sqlite3_errmsg(db));
return;
}
if (!file_found)
{
strcpy(query, "create table \"main\".\"musicdb\" (\"id\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,");
strcat(query, "\"path\" TEXT NOT NULL, \"title\" TEXT, \"interpret\" TEXT,");
strcat(query, "\"album\" TEXT, \"genre\" TEXT, \"cover\" TEXT)");
rc = sqlite3_exec(db, query, callback, (void *)"CREATE", &zErrMsg);
if (rc != SQLITE_OK)
{
syslog(LOG_WARNING, "SQL error [%s]: %s", query, zErrMsg);
sqlite3_free(zErrMsg);
sqlite3_close(db);
return;
}
else
syslog(LOG_INFO, "Database \"musicdb\" (%s) was successfully created.", fname);
}
file_found = FALSE;
/* Make sure, the file is not already in the database */
sprintf(query, "select path from musicdb where path = \"%s\"", file);
if ((rc = sqlite3_exec(db, query, callback, (void *)"EXIST", &zErrMsg)) != SQLITE_OK)
{
syslog(LOG_DAEMON, "SQL Error [%s]: %s", query, zErrMsg);
sqlite3_free(zErrMsg);
sqlite3_close(db);
return;
}
if (file_found) /* This is true, if the file is already in the database. */
{
file_found = FALSE;
sqlite3_close(db);
return;
}
/* Open the file and get the ID3 tag. */
id3 = ID3Tag_New();
ID3Tag_Link(id3, file);
memset(&id3_title, 0, sizeof(id3_title));
memset(&id3_singer, 0, sizeof(id3_singer));
memset(&id3_album, 0, sizeof(id3_album));
memset(&id3_genre, 0, sizeof(id3_genre));
/* Get the required frames */
if ((frame = ID3Tag_FindFrameWithID(id3, ID3FID_TITLE)) != NULL)
{
field = ID3Frame_GetField(frame, ID3FN_TEXT);
ID3Field_GetASCII(field, &id3_title[0], sizeof(id3_title));
char_replace(id3_title, '"', '`');
char_replace(id3_title, '\\', ' ');
}
if ((frame = ID3Tag_FindFrameWithID(id3, ID3FID_LEADARTIST)) != NULL)
{
field = ID3Frame_GetField(frame, ID3FN_TEXT);
ID3Field_GetASCII(field, &id3_singer[0], sizeof(id3_singer));
char_replace(id3_singer, '"', '`');
char_replace(id3_singer, '\\', ' ');
}
if ((frame = ID3Tag_FindFrameWithID(id3, ID3FID_ALBUM)) != NULL)
{
field = ID3Frame_GetField(frame, ID3FN_TEXT);
ID3Field_GetASCII(field, &id3_album[0], sizeof(id3_album));
char_replace(id3_album, '"', '`');
char_replace(id3_album, '\\', ' ');
}
if ((frame = ID3Tag_FindFrameWithID(id3, ID3FID_CONTENTTYPE)) != NULL)
{
field = ID3Frame_GetField(frame, ID3FN_TEXT);
ID3Field_GetASCII(field, &id3_genre[0], sizeof(id3_genre));
if (strchr(id3_genre, '(') && strchr(id3_genre, ')') && isdigit(id3_genre[1]))
{
uint32 gen;
gen = ID3Field_GetINT(field);
if (gen < ID3_NR_OF_V1_GENRES)
strncpy (id3_genre, ID3_v1_genre_description[gen], sizeof(id3_genre));
else
strcpy (id3_genre, "Pop");
}
else
{
char *genre;
if ((genre = ASCIItoUTF8(id3_genre)) != NULL)
{
strncpy (id3_genre, genre, sizeof(id3_genre));
free(genre);
}
}
char_replace(id3_genre, '"', '`');
char_replace(id3_genre, '\\', ' ');
}
ID3Tag_Delete(id3);
strcpy (query, "insert into musicdb (\"path\", \"title\", \"interpret\", \"album\", \"genre\") values ");
strcat (query, "(\"");
strcat (query, file);
strcat (query, "\", \"");
if ((title = ASCIItoUTF8(id3_title)) != NULL)
{
strcat (query, title);
free(title);
}
else
strcat (query, id3_title);
strcat (query, "\", \"");
if ((singer = ASCIItoUTF8(id3_singer)) != NULL)
{
strcat (query, singer);
free(singer);
}
else
strcat (query, id3_singer);
strcat (query, "\", \"");
if ((album = ASCIItoUTF8(id3_album)) != NULL)
{
strcat (query, album);
free(album);
}
else
strcat (query, id3_album);
strcat (query, "\", \"");
strcat (query, id3_genre);
strcat (query, "\")");
if ((rc = sqlite3_exec(db, query, callback, (void *)"INSERT", &zErrMsg)) != SQLITE_OK)
{
syslog(LOG_DAEMON, "SQL Error [%s]: %s", query, zErrMsg);
sqlite3_free(zErrMsg);
sqlite3_close(db);
return;
}
sqlite3_close(db);
}