owntone-server/src/library/filescanner_playlist.c

523 lines
12 KiB
C
Raw Normal View History

/*
* Copyright (C) 2015-2017 Espen Jürgensen <espenjurgensen@gmail.com>
2010-01-05 13:34:00 -05:00
* Copyright (C) 2009-2010 Julien BLACHE <jb@jblache.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <limits.h>
2010-01-09 07:42:23 -05:00
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include "conffile.h"
2009-05-08 11:46:32 -04:00
#include "logger.h"
2009-06-07 12:58:02 -04:00
#include "db.h"
#include "library/filescanner.h"
#include "misc.h"
#include "library.h"
enum playlist_type
{
PLAYLIST_UNKNOWN = 0,
PLAYLIST_PLS,
PLAYLIST_M3U,
PLAYLIST_SMART,
};
static enum playlist_type
playlist_type(const char *path)
{
char *ptr;
ptr = strrchr(path, '.');
if (!ptr)
return PLAYLIST_UNKNOWN;
if (strcasecmp(ptr, ".m3u") == 0)
return PLAYLIST_M3U;
else if (strcasecmp(ptr, ".pls") == 0)
return PLAYLIST_PLS;
else if (strcasecmp(ptr, ".smartpl") == 0)
return PLAYLIST_SMART;
else
return PLAYLIST_UNKNOWN;
}
2013-10-15 07:36:11 -04:00
static int
extinf_read(char **artist, char **title, const char *tag)
2013-10-15 07:36:11 -04:00
{
2013-10-16 05:09:24 -04:00
char *ptr;
2013-10-15 07:36:11 -04:00
ptr = strchr(tag, ',');
2013-10-16 05:09:24 -04:00
if (!ptr || strlen(ptr) < 2)
return -1;
2013-10-15 07:36:11 -04:00
*artist = strdup(ptr + 1);
2013-10-15 07:36:11 -04:00
ptr = strstr(*artist, " -");
2013-10-16 05:09:24 -04:00
if (ptr && strlen(ptr) > 3)
*title = strdup(ptr + 3);
2013-10-16 05:09:24 -04:00
else
*title = strdup("");
2013-10-16 05:09:24 -04:00
if (ptr)
*ptr = '\0';
2013-10-15 07:36:11 -04:00
return 0;
}
static int
extval_read(char **val, const char *tag)
{
char *ptr;
ptr = strchr(tag, ':');
if (!ptr || strlen(ptr) < 2)
return -1;
*val = strdup(ptr + 1);
return 0;
}
// Get metadata from a EXTINF or EXTALB tag
static int
exttag_read(struct media_file_info *mfi, const char *tag)
{
char *artist;
char *title;
char *val;
if (strncmp(tag, "#EXTINF:", strlen("#EXTINF:")) == 0 && extinf_read(&artist, &title, tag) == 0)
{
free(mfi->artist);
free(mfi->title);
mfi->artist = artist;
mfi->title = title;
if (!mfi->album_artist)
mfi->album_artist = strdup(artist);
return 0;
}
if (strncmp(tag, "#EXTALB:", strlen("#EXTALB:")) == 0 && extval_read(&val, tag) == 0)
{
free(mfi->album);
mfi->album = val;
return 0;
}
if (strncmp(tag, "#EXTART:", strlen("#EXTART:")) == 0 && extval_read(&val, tag) == 0)
{
free(mfi->album_artist);
mfi->album_artist = val;
return 0;
}
2020-04-09 12:41:01 -04:00
if (strncmp(tag, "#EXTGENRE:", strlen("#EXTGENRE:")) == 0 && extval_read(&val, tag) == 0)
{
free(mfi->genre);
mfi->genre = val;
return 0;
}
return -1;
2013-10-15 07:36:11 -04:00
}
void
scan_metadata_stream(struct media_file_info *mfi, const char *path)
{
char *pos;
int ret;
mfi->path = strdup(path);
mfi->virtual_path = safe_asprintf("/%s", mfi->path);
pos = strchr(path, '#');
if (pos)
mfi->fname = strdup(pos+1);
else
mfi->fname = strdup(filename_from_path(mfi->path));
mfi->data_kind = DATA_KIND_HTTP;
mfi->time_modified = time(NULL);
mfi->directory_id = DIR_HTTP;
ret = scan_metadata_ffmpeg(mfi, path);
if (ret < 0)
{
DPRINTF(E_LOG, L_SCAN, "Playlist URL '%s' is unavailable for probe/metadata, assuming MP3 encoding\n", path);
mfi->type = strdup("mp3");
mfi->codectype = strdup("mpeg");
mfi->description = strdup("MPEG audio file");
}
if (!mfi->title)
mfi->title = strdup(mfi->fname);
}
static int
process_nested_playlist(int parent_id, const char *path)
{
struct playlist_info *pli;
char *deref = NULL;
int ret;
// First set the type of the parent playlist to folder
pli = db_pl_fetch_byid(parent_id);
if (!pli)
goto error;
pli->type = PL_FOLDER;
ret = library_playlist_save(pli);
if (ret < 0)
goto error;
free_pli(pli, 0);
deref = realpath(path, NULL);
if (!deref)
{
DPRINTF(E_LOG, L_SCAN, "Could not dereference path '%s': %s\n", path, strerror(errno));
return -1;
}
// Do we already have the playlist in the database?
pli = db_pl_fetch_bypath(deref);
if (!pli)
{
pli = calloc(1, sizeof(struct playlist_info));
ret = playlist_fill(pli, deref);
if (ret < 0)
goto error;
// This is a "trick" to make sure the nested playlist will be scanned.
// Otherwise what could happen is that we save the playlist with current
// db_timestamp, and when the scanner finds the actual playlist it will
// conclude from the timestamp that the playlist is unchanged, and thus
// it would never be scanned.
pli->db_timestamp = 1;
}
pli->parent_id = parent_id;
ret = library_playlist_save(pli);
if (ret < 0)
goto error;
free_pli(pli, 0);
free(deref);
return 0;
error:
DPRINTF(E_LOG, L_SCAN, "Error processing nested playlist '%s' in playlist %d\n", path, parent_id);
free_pli(pli, 0);
free(deref);
return -1;
}
static int
process_url(int pl_id, const char *path, struct media_file_info *mfi)
{
struct media_file_info m3u;
int ret;
mfi->id = db_file_id_bypath(path);
if (cfg_getbool(cfg_getsec(cfg, "library"), "m3u_overrides"))
{
memset(&m3u, 0, sizeof(struct media_file_info));
m3u.artist = safe_strdup(mfi->artist);
m3u.album_artist = safe_strdup(mfi->album_artist);
m3u.album = safe_strdup(mfi->album);
m3u.title = safe_strdup(mfi->title);
scan_metadata_stream(mfi, path);
if (m3u.artist)
swap_pointers(&mfi->artist, &m3u.artist);
if (m3u.album_artist)
swap_pointers(&mfi->album_artist, &m3u.album_artist);
if (m3u.album)
swap_pointers(&mfi->album, &m3u.album);
if (m3u.title)
swap_pointers(&mfi->title, &m3u.title);
free_mfi(&m3u, 1);
}
else
scan_metadata_stream(mfi, path);
ret = library_media_save(mfi);
if (ret < 0)
return -1;
return db_pl_add_item_bypath(pl_id, path);
}
static int
process_regular_file(int pl_id, char *path)
{
struct query_params qp;
char filter[PATH_MAX];
const char *a;
const char *b;
char *dbpath;
char *winner;
int score;
int i;
int ret;
// Playlist might be from Windows so we change backslash to forward slash
for (i = 0; i < strlen(path); i++)
{
if (path[i] == '\\')
path[i] = '/';
}
ret = db_snprintf(filter, sizeof(filter), "f.fname = '%q' COLLATE NOCASE", filename_from_path(path));
if (ret < 0)
{
DPRINTF(E_LOG, L_SCAN, "Path in playlist is too long: '%s'\n", path);
return -1;
}
memset(&qp, 0, sizeof(struct query_params));
qp.type = Q_BROWSE_PATH;
qp.sort = S_NONE;
qp.filter = filter;
ret = db_query_start(&qp);
if (ret < 0)
{
db_query_end(&qp);
return -1;
}
winner = NULL;
score = 0;
while ((db_query_fetch_string(&qp, &dbpath) == 0) && dbpath)
{
if (qp.results == 1)
{
free(winner); // This is just here to keep scan-build happy
winner = strdup(dbpath);
break;
}
for (i = 0, a = NULL, b = NULL; (parent_dir(&a, path) == 0) && (parent_dir(&b, dbpath) == 0) && (strcasecmp(a, b) == 0); i++)
;
DPRINTF(E_SPAM, L_SCAN, "Comparison of '%s' and '%s' gave score %d\n", dbpath, path, i);
if (i > score)
{
free(winner);
winner = strdup(dbpath);
score = i;
}
else if (i == score)
{
free(winner);
winner = NULL;
}
}
db_query_end(&qp);
if (!winner)
{
DPRINTF(E_LOG, L_SCAN, "No file in the library matches playlist entry '%s'\n", path);
return -1;
}
DPRINTF(E_DBG, L_SCAN, "Adding '%s' to playlist %d (results %d)\n", winner, pl_id, qp.results);
db_pl_add_item_bypath(pl_id, winner);
free(winner);
return 0;
}
static int
playlist_prepare(const char *path, time_t mtime)
{
struct playlist_info *pli;
int pl_id;
pli = db_pl_fetch_bypath(path);
if (!pli)
{
DPRINTF(E_LOG, L_SCAN, "New playlist found, processing '%s'\n", path);
pl_id = playlist_add(path);
if (pl_id < 0)
{
DPRINTF(E_LOG, L_SCAN, "Error adding playlist '%s'\n", path);
return -1;
}
DPRINTF(E_INFO, L_SCAN, "Added new playlist as id %d\n", pl_id);
return pl_id;
}
db_pl_ping(pli->id);
// mtime == db_timestamp is also treated as a modification because some editors do
// stuff like 1) close the file with no changes (leading us to update db_timestamp),
// 2) copy over a modified version from a tmp file (which may result in a mtime that
// is equal to the newly updated db_timestamp)
if (mtime && (pli->db_timestamp > mtime))
{
DPRINTF(E_LOG, L_SCAN, "Unchanged playlist found, not processing '%s'\n", path);
// Protect this playlist's radio stations from purge after scan
db_pl_ping_items_bymatch("http://", pli->id);
db_pl_ping_items_bymatch("https://", pli->id);
free_pli(pli, 0);
return -1;
}
DPRINTF(E_LOG, L_SCAN, "Modified playlist found, processing '%s'\n", path);
2014-12-21 14:41:44 -05:00
pl_id = pli->id;
free_pli(pli, 0);
db_pl_clear_items(pl_id);
return pl_id;
}
void
scan_playlist(const char *file, time_t mtime, int dir_id)
{
FILE *fp;
struct media_file_info mfi;
struct stat sb;
char buf[PATH_MAX];
char *path;
size_t len;
int pl_id;
int pl_format;
int ntracks;
int nadded;
int ret;
pl_format = playlist_type(file);
if (pl_format != PLAYLIST_M3U && pl_format != PLAYLIST_PLS)
return;
// Will create or update the playlist entry in the database
pl_id = playlist_prepare(file, mtime);
if (pl_id < 0)
return; // Not necessarily an error, could also be that the playlist hasn't changed
ret = stat(file, &sb);
if (ret < 0)
{
DPRINTF(E_LOG, L_SCAN, "Could not stat() '%s': %s\n", file, strerror(errno));
return;
}
fp = fopen(file, "r");
if (!fp)
{
DPRINTF(E_LOG, L_SCAN, "Could not open playlist '%s': %s\n", file, strerror(errno));
return;
}
db_transaction_begin();
2014-04-18 07:08:31 -04:00
memset(&mfi, 0, sizeof(struct media_file_info));
ntracks = 0;
nadded = 0;
while (fgets(buf, sizeof(buf), fp) != NULL)
{
len = strlen(buf);
// rtrim and check that length is sane (ignore blank lines)
2013-10-15 07:36:11 -04:00
while ((len > 0) && isspace(buf[len - 1]))
{
len--;
buf[len] = '\0';
}
2013-10-15 07:36:11 -04:00
if (len < 1)
continue;
// Saves metadata in mfi if EXT metadata line
if ((pl_format == PLAYLIST_M3U) && (exttag_read(&mfi, buf) == 0))
2014-08-27 15:57:16 -04:00
continue;
// For pls files we are only interested in the part after the FileX= entry
2014-08-27 15:57:16 -04:00
path = NULL;
if ((pl_format == PLAYLIST_PLS) && (strncasecmp(buf, "file", strlen("file")) == 0) && (path = strchr(buf, '=')))
path++;
2014-08-27 15:57:16 -04:00
else if (pl_format == PLAYLIST_M3U)
path = buf;
if (!path)
continue;
2013-10-15 07:36:11 -04:00
// Check that first char is sane for a path
2014-08-27 15:57:16 -04:00
if ((!isalnum(path[0])) && (path[0] != '/') && (path[0] != '.'))
2013-10-15 07:36:11 -04:00
continue;
// URLs and playlists will be added to library, tracks should already be there
if (strncasecmp(path, "http://", 7) == 0 || strncasecmp(path, "https://", 8) == 0)
ret = process_url(pl_id, path, &mfi);
else if (playlist_type(path) != PLAYLIST_UNKNOWN)
ret = process_nested_playlist(pl_id, path);
else
ret = process_regular_file(pl_id, path);
ntracks++;
if (ntracks % 200 == 0)
{
DPRINTF(E_LOG, L_SCAN, "Processed %d items...\n", ntracks);
db_transaction_end();
db_transaction_begin();
}
if (ret == 0)
nadded++;
// Clean up in preparation for next item
free_mfi(&mfi, 1);
}
db_transaction_end();
// In case we had some m3u ext metadata that we never got to use, free it now
// (no risk of double free when the free_mfi()'s are content_only)
free_mfi(&mfi, 1);
2015-04-01 17:45:21 -04:00
if (!feof(fp))
DPRINTF(E_LOG, L_SCAN, "Error reading playlist '%s' (only added %d tracks): %s\n", file, nadded, strerror(errno));
else
DPRINTF(E_LOG, L_SCAN, "Done processing playlist, added/modified %d items\n", nadded);
[-] Fix alsa.c null pointer deref + some minor bugs and do some housekeeping Thanks to Denis Denisov and cppcheck for notifying about the below. The leaks are edge cases, but the warning of dereference of avail in alsa.c points at a bug that could probably cause actual crashes. [src/evrtsp/rtsp.c:1352]: (warning) Assignment of function parameter has no effect outside the function. Did you forget dereferencing it? [src/httpd_daap.c:228]: (error) Memory leak: s [src/library.c:280]: (warning) %d in format string (no. 2) requires 'int' but the argument type is 'unsigned int'. [src/library.c:284]: (warning) %d in format string (no. 2) requires 'int' but the argument type is 'unsigned int'. [src/library/filescanner_playlist.c:251]: (error) Resource leak: fp [src/library/filescanner_playlist.c:273]: (error) Resource leak: fp [src/outputs/alsa.c:143]: (warning) Assignment of function parameter has no effect outside the function. Did you forget dereferencing it? [src/outputs/alsa.c:657]: (warning) Possible null pointer dereference: avail [src/outputs/dummy.c:75]: (warning) Assignment of function parameter has no effect outside the function. Did you forget dereferencing it? [src/outputs/fifo.c:245]: (warning) Assignment of function parameter has no effect outside the function. Did you forget dereferencing it? [src/outputs/raop.c:1806]: (warning) Assignment of function parameter has no effect outside the function. Did you forget dereferencing it? [src/outputs/raop.c:1371]: (warning) %u in format string (no. 1) requires 'unsigned int' but the argument type is 'signed int'. [src/outputs/raop.c:1471]: (warning) %u in format string (no. 1) requires 'unsigned int' but the argument type is 'signed int'. [src/outputs/raop_verification.c:705] -> [src/outputs/raop_verification.c:667]: (warning) Either the condition 'if(len_M)' is redundant or there is possible null pointer dereference: len_M.
2017-10-05 16:13:01 -04:00
fclose(fp);
}