mirror of
https://git.code.sf.net/p/minidlna/git
synced 2024-10-31 16:58:06 +00:00
579 lines
12 KiB
C
579 lines
12 KiB
C
/* MiniDLNA media server
|
|
* Copyright (C) 2008-2017 Justin Maggard
|
|
*
|
|
* This file is part of MiniDLNA.
|
|
*
|
|
* MiniDLNA is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* MiniDLNA is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with MiniDLNA. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "config.h"
|
|
|
|
#include <stdio.h>
|
|
#include <ctype.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <sys/param.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <limits.h>
|
|
#include <fcntl.h>
|
|
#include <errno.h>
|
|
|
|
#include "minidlnatypes.h"
|
|
#include "upnpglobalvars.h"
|
|
#include "utils.h"
|
|
#include "log.h"
|
|
|
|
int
|
|
xasprintf(char **strp, char *fmt, ...)
|
|
{
|
|
va_list args;
|
|
int ret;
|
|
|
|
va_start(args, fmt);
|
|
ret = vasprintf(strp, fmt, args);
|
|
va_end(args);
|
|
if( ret < 0 )
|
|
{
|
|
DPRINTF(E_WARN, L_GENERAL, "xasprintf: allocation failed\n");
|
|
*strp = NULL;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
int
|
|
ends_with(const char * haystack, const char * needle)
|
|
{
|
|
const char * end;
|
|
int nlen = strlen(needle);
|
|
int hlen = strlen(haystack);
|
|
|
|
if( nlen > hlen )
|
|
return 0;
|
|
end = haystack + hlen - nlen;
|
|
|
|
return (strcasecmp(end, needle) ? 0 : 1);
|
|
}
|
|
|
|
char *
|
|
trim(char *str)
|
|
{
|
|
int i;
|
|
int len;
|
|
|
|
if (!str)
|
|
return(NULL);
|
|
|
|
len = strlen(str);
|
|
for (i=len-1; i >= 0 && isspace(str[i]); i--)
|
|
{
|
|
str[i] = '\0';
|
|
len--;
|
|
}
|
|
while (isspace(*str))
|
|
{
|
|
str++;
|
|
len--;
|
|
}
|
|
|
|
if (str[0] == '"' && str[len-1] == '"')
|
|
{
|
|
str[0] = '\0';
|
|
str[len-1] = '\0';
|
|
str++;
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
/* Find the first occurrence of p in s, where s is terminated by t */
|
|
char *
|
|
strstrc(const char *s, const char *p, const char t)
|
|
{
|
|
char *endptr;
|
|
size_t slen, plen;
|
|
|
|
endptr = strchr(s, t);
|
|
if (!endptr)
|
|
return strstr(s, p);
|
|
|
|
plen = strlen(p);
|
|
slen = endptr - s;
|
|
while (slen >= plen)
|
|
{
|
|
if (*s == *p && strncmp(s+1, p+1, plen-1) == 0)
|
|
return (char*)s;
|
|
s++;
|
|
slen--;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
char *
|
|
strcasestrc(const char *s, const char *p, const char t)
|
|
{
|
|
char *endptr;
|
|
size_t slen, plen;
|
|
|
|
endptr = strchr(s, t);
|
|
if (!endptr)
|
|
return strcasestr(s, p);
|
|
|
|
plen = strlen(p);
|
|
slen = endptr - s;
|
|
while (slen >= plen)
|
|
{
|
|
if (*s == *p && strncasecmp(s+1, p+1, plen-1) == 0)
|
|
return (char*)s;
|
|
s++;
|
|
slen--;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
char *
|
|
modifyString(char *string, const char *before, const char *after, int noalloc)
|
|
{
|
|
int oldlen, newlen, chgcnt = 0;
|
|
char *s, *p;
|
|
|
|
/* If there is no match, just return */
|
|
s = strstr(string, before);
|
|
if (!s)
|
|
return string;
|
|
|
|
oldlen = strlen(before);
|
|
newlen = strlen(after);
|
|
if (newlen > oldlen)
|
|
{
|
|
if (noalloc)
|
|
return string;
|
|
|
|
while ((p = strstr(s, before)))
|
|
{
|
|
chgcnt++;
|
|
s = p + oldlen;
|
|
}
|
|
s = realloc(string, strlen(string)+((newlen-oldlen)*chgcnt)+1);
|
|
/* If we failed to realloc, return the original alloc'd string */
|
|
if( s )
|
|
string = s;
|
|
else
|
|
return string;
|
|
}
|
|
|
|
s = string;
|
|
while (s)
|
|
{
|
|
p = strstr(s, before);
|
|
if (!p)
|
|
return string;
|
|
memmove(p + newlen, p + oldlen, strlen(p + oldlen) + 1);
|
|
memcpy(p, after, newlen);
|
|
s = p + newlen;
|
|
}
|
|
|
|
return string;
|
|
}
|
|
|
|
char *
|
|
unescape_tag(const char *tag, int force_alloc)
|
|
{
|
|
char *esc_tag = NULL;
|
|
|
|
if (strchr(tag, '&') &&
|
|
(strstr(tag, "&") || strstr(tag, "<") || strstr(tag, ">") ||
|
|
strstr(tag, """) || strstr(tag, "'")))
|
|
{
|
|
esc_tag = strdup(tag);
|
|
esc_tag = modifyString(esc_tag, "&", "&", 1);
|
|
esc_tag = modifyString(esc_tag, "<", "<", 1);
|
|
esc_tag = modifyString(esc_tag, ">", ">", 1);
|
|
esc_tag = modifyString(esc_tag, """, "\"", 1);
|
|
esc_tag = modifyString(esc_tag, "'", "'", 1);
|
|
}
|
|
else if( force_alloc )
|
|
esc_tag = strdup(tag);
|
|
|
|
return esc_tag;
|
|
}
|
|
|
|
char *
|
|
escape_tag(const char *tag, int force_alloc)
|
|
{
|
|
char *esc_tag = NULL;
|
|
|
|
if( strchr(tag, '&') || strchr(tag, '<') || strchr(tag, '>') || strchr(tag, '"') )
|
|
{
|
|
esc_tag = strdup(tag);
|
|
esc_tag = modifyString(esc_tag, "&", "&amp;", 0);
|
|
esc_tag = modifyString(esc_tag, "<", "&lt;", 0);
|
|
esc_tag = modifyString(esc_tag, ">", "&gt;", 0);
|
|
esc_tag = modifyString(esc_tag, "\"", "&quot;", 0);
|
|
}
|
|
else if( force_alloc )
|
|
esc_tag = strdup(tag);
|
|
|
|
return esc_tag;
|
|
}
|
|
|
|
char *
|
|
duration_str(int msec)
|
|
{
|
|
char *str;
|
|
|
|
xasprintf(&str, "%d:%02d:%02d.%03d",
|
|
(msec / 3600000),
|
|
(msec / 60000 % 60),
|
|
(msec / 1000 % 60),
|
|
(msec % 1000));
|
|
|
|
return str;
|
|
}
|
|
|
|
char *
|
|
strip_ext(char *name)
|
|
{
|
|
char *period;
|
|
|
|
if (!name)
|
|
return NULL;
|
|
period = strrchr(name, '.');
|
|
if (period)
|
|
*period = '\0';
|
|
|
|
return period;
|
|
}
|
|
|
|
/* Code basically stolen from busybox */
|
|
int
|
|
make_dir(char * path, mode_t mode)
|
|
{
|
|
char * s = path;
|
|
char c;
|
|
struct stat st;
|
|
|
|
do {
|
|
c = '\0';
|
|
|
|
/* Before we do anything, skip leading /'s, so we don't bother
|
|
* trying to create /. */
|
|
while (*s == '/')
|
|
++s;
|
|
|
|
/* Bypass leading non-'/'s and then subsequent '/'s. */
|
|
while (*s) {
|
|
if (*s == '/') {
|
|
do {
|
|
++s;
|
|
} while (*s == '/');
|
|
c = *s; /* Save the current char */
|
|
*s = '\0'; /* and replace it with nul. */
|
|
break;
|
|
}
|
|
++s;
|
|
}
|
|
|
|
if (mkdir(path, mode) < 0) {
|
|
/* If we failed for any other reason than the directory
|
|
* already exists, output a diagnostic and return -1.*/
|
|
if ((errno != EEXIST && errno != EISDIR)
|
|
|| (stat(path, &st) < 0 || !S_ISDIR(st.st_mode))) {
|
|
DPRINTF(E_WARN, L_GENERAL, "make_dir: cannot create directory '%s'\n", path);
|
|
if (c)
|
|
*s = c;
|
|
return -1;
|
|
}
|
|
}
|
|
if (!c)
|
|
return 0;
|
|
|
|
/* Remove any inserted nul from the path. */
|
|
*s = c;
|
|
|
|
} while (1);
|
|
}
|
|
|
|
/* Simple, efficient hash function from Daniel J. Bernstein */
|
|
unsigned int
|
|
DJBHash(uint8_t *data, int len)
|
|
{
|
|
unsigned int hash = 5381;
|
|
unsigned int i = 0;
|
|
|
|
for(i = 0; i < len; data++, i++)
|
|
{
|
|
hash = ((hash << 5) + hash) + (*data);
|
|
}
|
|
|
|
return hash;
|
|
}
|
|
|
|
const char *
|
|
mime_to_ext(const char * mime)
|
|
{
|
|
switch( *mime )
|
|
{
|
|
/* Audio extensions */
|
|
case 'a':
|
|
if( strcmp(mime+6, "mpeg") == 0 )
|
|
return "mp3";
|
|
else if( strcmp(mime+6, "mp4") == 0 )
|
|
return "m4a";
|
|
else if( strcmp(mime+6, "x-ms-wma") == 0 )
|
|
return "wma";
|
|
else if( strcmp(mime+6, "x-flac") == 0 )
|
|
return "flac";
|
|
else if( strcmp(mime+6, "flac") == 0 )
|
|
return "flac";
|
|
else if( strcmp(mime+6, "x-wav") == 0 )
|
|
return "wav";
|
|
else if( strncmp(mime+6, "L16", 3) == 0 )
|
|
return "pcm";
|
|
else if( strcmp(mime+6, "3gpp") == 0 )
|
|
return "3gp";
|
|
else if( strcmp(mime, "application/ogg") == 0 )
|
|
return "ogg";
|
|
else if( strcmp(mime+6, "x-dsd") == 0 )
|
|
return "dsd";
|
|
break;
|
|
case 'v':
|
|
if( strcmp(mime+6, "avi") == 0 )
|
|
return "avi";
|
|
else if( strcmp(mime+6, "divx") == 0 )
|
|
return "avi";
|
|
else if( strcmp(mime+6, "x-msvideo") == 0 )
|
|
return "avi";
|
|
else if( strcmp(mime+6, "mpeg") == 0 )
|
|
return "mpg";
|
|
else if( strcmp(mime+6, "mp4") == 0 )
|
|
return "mp4";
|
|
else if( strcmp(mime+6, "x-ms-wmv") == 0 )
|
|
return "wmv";
|
|
else if( strcmp(mime+6, "x-matroska") == 0 )
|
|
return "mkv";
|
|
else if( strcmp(mime+6, "x-mkv") == 0 )
|
|
return "mkv";
|
|
else if( strcmp(mime+6, "x-flv") == 0 )
|
|
return "flv";
|
|
else if( strcmp(mime+6, "vnd.dlna.mpeg-tts") == 0 )
|
|
return "mpg";
|
|
else if( strcmp(mime+6, "quicktime") == 0 )
|
|
return "mov";
|
|
else if( strcmp(mime+6, "3gpp") == 0 )
|
|
return "3gp";
|
|
else if( strncmp(mime+6, "x-tivo-mpeg", 11) == 0 )
|
|
return "TiVo";
|
|
break;
|
|
case 'i':
|
|
if( strcmp(mime+6, "jpeg") == 0 )
|
|
return "jpg";
|
|
else if( strcmp(mime+6, "png") == 0 )
|
|
return "png";
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return "dat";
|
|
}
|
|
|
|
int
|
|
is_video(const char * file)
|
|
{
|
|
return (ends_with(file, ".mpg") || ends_with(file, ".mpeg") ||
|
|
ends_with(file, ".avi") || ends_with(file, ".divx") ||
|
|
ends_with(file, ".asf") || ends_with(file, ".wmv") ||
|
|
ends_with(file, ".mp4") || ends_with(file, ".m4v") ||
|
|
ends_with(file, ".mts") || ends_with(file, ".m2ts") ||
|
|
ends_with(file, ".m2t") || ends_with(file, ".mkv") ||
|
|
ends_with(file, ".vob") || ends_with(file, ".ts") ||
|
|
ends_with(file, ".flv") || ends_with(file, ".xvid") ||
|
|
#ifdef TIVO_SUPPORT
|
|
ends_with(file, ".TiVo") ||
|
|
#endif
|
|
ends_with(file, ".mov") || ends_with(file, ".3gp") ||
|
|
ends_with(file, ".rm") || ends_with(file, ".rmvb") ||
|
|
ends_with(file, ".webm"));
|
|
}
|
|
|
|
int
|
|
is_audio(const char * file)
|
|
{
|
|
return (ends_with(file, ".mp3") || ends_with(file, ".flac") ||
|
|
ends_with(file, ".wma") || ends_with(file, ".asf") ||
|
|
ends_with(file, ".fla") || ends_with(file, ".flc") ||
|
|
ends_with(file, ".m4a") || ends_with(file, ".aac") ||
|
|
ends_with(file, ".mp4") || ends_with(file, ".m4p") ||
|
|
ends_with(file, ".wav") || ends_with(file, ".ogg") ||
|
|
ends_with(file, ".pcm") || ends_with(file, ".3gp") ||
|
|
ends_with(file, ".dsf") || ends_with(file, ".dff"));
|
|
}
|
|
|
|
int
|
|
is_image(const char * file)
|
|
{
|
|
return (ends_with(file, ".jpg") || ends_with(file, ".jpeg"));
|
|
}
|
|
|
|
int
|
|
is_playlist(const char * file)
|
|
{
|
|
return (ends_with(file, ".m3u") || ends_with(file, ".pls"));
|
|
}
|
|
|
|
int
|
|
is_caption(const char * file)
|
|
{
|
|
return (ends_with(file, ".srt") || ends_with(file, ".smi"));
|
|
}
|
|
|
|
media_types
|
|
get_media_type(const char *file)
|
|
{
|
|
const char *ext = strrchr(file, '.');
|
|
if (!ext)
|
|
return NO_MEDIA;
|
|
if (is_image(ext))
|
|
return TYPE_IMAGE;
|
|
if (is_video(ext))
|
|
return TYPE_VIDEO;
|
|
if (is_audio(ext))
|
|
return TYPE_AUDIO;
|
|
if (is_playlist(ext))
|
|
return TYPE_PLAYLIST;
|
|
if (is_caption(ext))
|
|
return TYPE_CAPTION;
|
|
if (is_nfo(ext))
|
|
return TYPE_NFO;
|
|
return NO_MEDIA;
|
|
}
|
|
|
|
int
|
|
is_album_art(const char * name)
|
|
{
|
|
struct album_art_name_s * album_art_name;
|
|
|
|
/* Check if this file name matches one of the default album art names */
|
|
for( album_art_name = album_art_names; album_art_name; album_art_name = album_art_name->next )
|
|
{
|
|
if( album_art_name->wildcard )
|
|
{
|
|
if( strncmp(album_art_name->name, name, strlen(album_art_name->name)) == 0 )
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
if( strcmp(album_art_name->name, name) == 0 )
|
|
break;
|
|
}
|
|
}
|
|
|
|
return (album_art_name ? 1 : 0);
|
|
}
|
|
|
|
int
|
|
resolve_unknown_type(const char * path, media_types dir_type)
|
|
{
|
|
struct stat entry;
|
|
enum file_types type = TYPE_UNKNOWN;
|
|
char str_buf[PATH_MAX];
|
|
ssize_t len;
|
|
|
|
if( lstat(path, &entry) == 0 )
|
|
{
|
|
if( S_ISLNK(entry.st_mode) )
|
|
{
|
|
if( (len = readlink(path, str_buf, PATH_MAX-1)) > 0 )
|
|
{
|
|
str_buf[len] = '\0';
|
|
//DEBUG DPRINTF(E_DEBUG, L_GENERAL, "Checking for recursive symbolic link: %s (%s)\n", path, str_buf);
|
|
if( strncmp(path, str_buf, strlen(str_buf)) == 0 )
|
|
{
|
|
DPRINTF(E_DEBUG, L_GENERAL, "Ignoring recursive symbolic link: %s (%s)\n", path, str_buf);
|
|
return type;
|
|
}
|
|
}
|
|
stat(path, &entry);
|
|
}
|
|
|
|
if( S_ISDIR(entry.st_mode) )
|
|
{
|
|
type = TYPE_DIR;
|
|
}
|
|
else if( S_ISREG(entry.st_mode) )
|
|
{
|
|
media_types mtype = get_media_type(path);
|
|
if (dir_type & mtype)
|
|
type = TYPE_FILE;
|
|
}
|
|
}
|
|
return type;
|
|
}
|
|
|
|
media_types
|
|
valid_media_types(const char *path)
|
|
{
|
|
struct media_dir_s *media_dir;
|
|
|
|
for (media_dir = media_dirs; media_dir; media_dir = media_dir->next)
|
|
{
|
|
if (strncmp(path, media_dir->path, strlen(media_dir->path)) == 0)
|
|
return media_dir->types;
|
|
}
|
|
|
|
return ALL_MEDIA;
|
|
}
|
|
|
|
/*
|
|
* Add and subtract routines for timevals.
|
|
* N.B.: subtract routine doesn't deal with
|
|
* results which are before the beginning,
|
|
* it just gets very confused in this case.
|
|
* Caveat emptor.
|
|
*/
|
|
static void timevalfix(struct timeval *);
|
|
void
|
|
timevaladd(struct timeval *t1, const struct timeval *t2)
|
|
{
|
|
|
|
t1->tv_sec += t2->tv_sec;
|
|
t1->tv_usec += t2->tv_usec;
|
|
timevalfix(t1);
|
|
}
|
|
|
|
void
|
|
timevalsub(struct timeval *t1, const struct timeval *t2)
|
|
{
|
|
|
|
t1->tv_sec -= t2->tv_sec;
|
|
t1->tv_usec -= t2->tv_usec;
|
|
timevalfix(t1);
|
|
}
|
|
|
|
static void
|
|
timevalfix(struct timeval *t1)
|
|
{
|
|
|
|
if (t1->tv_usec < 0) {
|
|
t1->tv_sec--;
|
|
t1->tv_usec += 1000000;
|
|
}
|
|
if (t1->tv_usec >= 1000000) {
|
|
t1->tv_sec++;
|
|
t1->tv_usec -= 1000000;
|
|
}
|
|
}
|