[filescanner/library] Move filescanner to subfolder

This commit is contained in:
chme
2017-01-11 18:25:49 +01:00
parent 9c9c583b81
commit 528614909c
8 changed files with 8 additions and 10 deletions

1519
src/library/filescanner.c Normal file

File diff suppressed because it is too large Load Diff

34
src/library/filescanner.h Normal file
View File

@@ -0,0 +1,34 @@
#ifndef __FILESCANNER_H__
#define __FILESCANNER_H__
#include "db.h"
#define F_SCAN_TYPE_FILE (1 << 0)
#define F_SCAN_TYPE_PODCAST (1 << 1)
#define F_SCAN_TYPE_AUDIOBOOK (1 << 2)
#define F_SCAN_TYPE_COMPILATION (1 << 3)
#define F_SCAN_TYPE_URL (1 << 4)
#define F_SCAN_TYPE_SPOTIFY (1 << 5)
#define F_SCAN_TYPE_PIPE (1 << 6)
/* Actual scanners */
int
scan_metadata_ffmpeg(const char *file, struct media_file_info *mfi);
int
scan_metadata_icy(char *url, struct media_file_info *mfi);
void
scan_playlist(char *file, time_t mtime, int dir_id);
void
scan_smartpl(char *file, time_t mtime, int dir_id);
#ifdef ITUNES
void
scan_itunes_itml(char *file);
#endif
#endif /* !__FILESCANNER_H__ */

View File

@@ -0,0 +1,830 @@
/*
* Copyright (C) 2009-2011 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 <stdlib.h>
#include <string.h>
#include <time.h>
#include <stdint.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/opt.h>
#include "db.h"
#include "logger.h"
#include "misc.h"
#include "http.h"
/* Mapping between the metadata name(s) and the offset
* of the equivalent metadata field in struct media_file_info */
struct metadata_map {
char *key;
int as_int;
size_t offset;
int (*handler_function)(struct media_file_info *, char *);
};
// Used for passing errors to DPRINTF (can't count on av_err2str being present)
static char errbuf[64];
static inline char *
err2str(int errnum)
{
av_strerror(errnum, errbuf, sizeof(errbuf));
return errbuf;
}
static int
parse_slash_separated_ints(char *string, uint32_t *firstval, uint32_t *secondval)
{
int numvals = 0;
char *ptr;
ptr = strchr(string, '/');
if (ptr)
{
*ptr = '\0';
if (safe_atou32(ptr + 1, secondval) == 0)
numvals++;
}
if (safe_atou32(string, firstval) == 0)
numvals++;
return numvals;
}
static int
parse_track(struct media_file_info *mfi, char *track_string)
{
uint32_t *track = (uint32_t *) ((char *) mfi + mfi_offsetof(track));
uint32_t *total_tracks = (uint32_t *) ((char *) mfi + mfi_offsetof(total_tracks));
return parse_slash_separated_ints(track_string, track, total_tracks);
}
static int
parse_disc(struct media_file_info *mfi, char *disc_string)
{
uint32_t *disc = (uint32_t *) ((char *) mfi + mfi_offsetof(disc));
uint32_t *total_discs = (uint32_t *) ((char *) mfi + mfi_offsetof(total_discs));
return parse_slash_separated_ints(disc_string, disc, total_discs);
}
static int
parse_date(struct media_file_info *mfi, char *date_string)
{
char year_string[21];
uint32_t *year = (uint32_t *) ((char *) mfi + mfi_offsetof(year));
uint32_t *date_released = (uint32_t *) ((char *) mfi + mfi_offsetof(date_released));
struct tm tm = { 0 };
int ret = 0;
if ((*year == 0) && (safe_atou32(date_string, year) == 0))
ret++;
if ( strptime(date_string, "%FT%T%z", &tm) // ISO 8601, %F=%Y-%m-%d, %T=%H:%M:%S
|| strptime(date_string, "%F %T", &tm)
|| strptime(date_string, "%F %H:%M", &tm)
|| strptime(date_string, "%F", &tm)
)
{
*date_released = (uint32_t)mktime(&tm);
ret++;
}
if ((*date_released == 0) && (*year != 0))
{
snprintf(year_string, sizeof(year_string), "%" PRIu32 "-01-01T12:00:00", *year);
if (strptime(year_string, "%FT%T", &tm))
{
*date_released = (uint32_t)mktime(&tm);
ret++;
}
}
return ret;
}
/* Lookup is case-insensitive, first occurrence takes precedence */
static const struct metadata_map md_map_generic[] =
{
{ "title", 0, mfi_offsetof(title), NULL },
{ "artist", 0, mfi_offsetof(artist), NULL },
{ "author", 0, mfi_offsetof(artist), NULL },
{ "album_artist", 0, mfi_offsetof(album_artist), NULL },
{ "album", 0, mfi_offsetof(album), NULL },
{ "genre", 0, mfi_offsetof(genre), NULL },
{ "composer", 0, mfi_offsetof(composer), NULL },
{ "grouping", 0, mfi_offsetof(grouping), NULL },
{ "orchestra", 0, mfi_offsetof(orchestra), NULL },
{ "conductor", 0, mfi_offsetof(conductor), NULL },
{ "comment", 0, mfi_offsetof(comment), NULL },
{ "description", 0, mfi_offsetof(comment), NULL },
{ "track", 1, mfi_offsetof(track), parse_track },
{ "disc", 1, mfi_offsetof(disc), parse_disc },
{ "year", 1, mfi_offsetof(year), NULL },
{ "date", 1, mfi_offsetof(date_released), parse_date },
{ "title-sort", 0, mfi_offsetof(title_sort), NULL },
{ "artist-sort", 0, mfi_offsetof(artist_sort), NULL },
{ "album-sort", 0, mfi_offsetof(album_sort), NULL },
{ "compilation", 1, mfi_offsetof(compilation), NULL },
{ NULL, 0, 0, NULL }
};
static const struct metadata_map md_map_tv[] =
{
{ "stik", 1, mfi_offsetof(media_kind), NULL },
{ "show", 0, mfi_offsetof(tv_series_name), NULL },
{ "episode_id", 0, mfi_offsetof(tv_episode_num_str), NULL },
{ "network", 0, mfi_offsetof(tv_network_name), NULL },
{ "episode_sort", 1, mfi_offsetof(tv_episode_sort), NULL },
{ "season_number",1, mfi_offsetof(tv_season_num), NULL },
{ NULL, 0, 0, NULL }
};
/* NOTE about VORBIS comments:
* Only a small set of VORBIS comment fields are officially designated. Most
* common tags are at best de facto standards. Currently, metadata conversion
* functionality in ffmpeg only adds support for a couple of tags. Specifically,
* ALBUMARTIST and TRACKNUMBER are handled as of Feb 1, 2010 (rev 21587). Tags
* with names that already match the generic ffmpeg scheme--TITLE and ARTIST,
* for example--are of course handled. The rest of these tags are reported to
* have been used by various programs in the wild.
*/
static const struct metadata_map md_map_vorbis[] =
{
{ "albumartist", 0, mfi_offsetof(album_artist), NULL },
{ "album artist", 0, mfi_offsetof(album_artist), NULL },
{ "tracknumber", 1, mfi_offsetof(track), NULL },
{ "tracktotal", 1, mfi_offsetof(total_tracks), NULL },
{ "totaltracks", 1, mfi_offsetof(total_tracks), NULL },
{ "discnumber", 1, mfi_offsetof(disc), NULL },
{ "disctotal", 1, mfi_offsetof(total_discs), NULL },
{ "totaldiscs", 1, mfi_offsetof(total_discs), NULL },
{ NULL, 0, 0, NULL }
};
/* NOTE about ID3 tag names:
* metadata conversion for ID3v2 tags was added in ffmpeg in september 2009
* (rev 20073) for ID3v2.3; support for ID3v2.2 tag names was added in december
* 2009 (rev 20839).
*
* ID3v2.x tags will be removed from the map once a version of ffmpeg containing
* the changes listed above will be generally available. The more entries in the
* map, the slower the filescanner gets.
*/
static const struct metadata_map md_map_id3[] =
{
{ "TT2", 0, mfi_offsetof(title), NULL }, /* ID3v2.2 */
{ "TIT2", 0, mfi_offsetof(title), NULL }, /* ID3v2.3 */
{ "TP1", 0, mfi_offsetof(artist), NULL }, /* ID3v2.2 */
{ "TPE1", 0, mfi_offsetof(artist), NULL }, /* ID3v2.3 */
{ "TP2", 0, mfi_offsetof(album_artist), NULL }, /* ID3v2.2 */
{ "TPE2", 0, mfi_offsetof(album_artist), NULL }, /* ID3v2.3 */
{ "TAL", 0, mfi_offsetof(album), NULL }, /* ID3v2.2 */
{ "TALB", 0, mfi_offsetof(album), NULL }, /* ID3v2.3 */
{ "TCO", 0, mfi_offsetof(genre), NULL }, /* ID3v2.2 */
{ "TCON", 0, mfi_offsetof(genre), NULL }, /* ID3v2.3 */
{ "TCM", 0, mfi_offsetof(composer), NULL }, /* ID3v2.2 */
{ "TCOM", 0, mfi_offsetof(composer), NULL }, /* ID3v2.3 */
{ "TRK", 1, mfi_offsetof(track), parse_track }, /* ID3v2.2 */
{ "TRCK", 1, mfi_offsetof(track), parse_track }, /* ID3v2.3 */
{ "TPA", 1, mfi_offsetof(disc), parse_disc }, /* ID3v2.2 */
{ "TPOS", 1, mfi_offsetof(disc), parse_disc }, /* ID3v2.3 */
{ "TYE", 1, mfi_offsetof(year), NULL }, /* ID3v2.2 */
{ "TYER", 1, mfi_offsetof(year), NULL }, /* ID3v2.3 */
{ "TDA", 1, mfi_offsetof(date_released), parse_date }, /* ID3v2.2 */
{ "TDAT", 1, mfi_offsetof(date_released), parse_date }, /* ID3v2.3 */
{ "TDR", 1, mfi_offsetof(date_released), parse_date }, /* ID3v2.2 */
{ "TDRL", 1, mfi_offsetof(date_released), parse_date }, /* ID3v2.4 */
{ "TSOA", 0, mfi_offsetof(album_sort), NULL }, /* ID3v2.4 */
{ "XSOA", 0, mfi_offsetof(album_sort), NULL }, /* ID3v2.3 */
{ "TSOP", 0, mfi_offsetof(artist_sort), NULL }, /* ID3v2.4 */
{ "XSOP", 0, mfi_offsetof(artist_sort), NULL }, /* ID3v2.3 */
{ "TSOT", 0, mfi_offsetof(title_sort), NULL }, /* ID3v2.4 */
{ "XSOT", 0, mfi_offsetof(title_sort), NULL }, /* ID3v2.3 */
{ "TS2", 0, mfi_offsetof(album_artist_sort), NULL }, /* ID3v2.2 */
{ "TSO2", 0, mfi_offsetof(album_artist_sort), NULL }, /* ID3v2.3 */
{ "ALBUMARTISTSORT", 0, mfi_offsetof(album_artist_sort), NULL }, /* ID3v2.x */
{ "TSC", 0, mfi_offsetof(composer_sort), NULL }, /* ID3v2.2 */
{ "TSOC", 0, mfi_offsetof(composer_sort), NULL }, /* ID3v2.3 */
{ NULL, 0, 0, NULL }
};
static int
#if LIBAVUTIL_VERSION_MAJOR >= 52 || (LIBAVUTIL_VERSION_MAJOR == 51 && LIBAVUTIL_VERSION_MINOR >= 5)
extract_metadata_core(struct media_file_info *mfi, AVDictionary *md, const struct metadata_map *md_map)
#else
extract_metadata_core(struct media_file_info *mfi, AVMetadata *md, const struct metadata_map *md_map)
#endif
{
#if LIBAVUTIL_VERSION_MAJOR >= 52 || (LIBAVUTIL_VERSION_MAJOR == 51 && LIBAVUTIL_VERSION_MINOR >= 5)
AVDictionaryEntry *mdt;
#else
AVMetadataTag *mdt;
#endif
char **strval;
uint32_t *intval;
int mdcount;
int i;
int ret;
#if 0
/* Dump all the metadata reported by ffmpeg */
mdt = NULL;
#if LIBAVUTIL_VERSION_MAJOR >= 52 || (LIBAVUTIL_VERSION_MAJOR == 51 && LIBAVUTIL_VERSION_MINOR >= 5)
while ((mdt = av_dict_get(md, "", mdt, AV_DICT_IGNORE_SUFFIX)) != NULL)
#else
while ((mdt = av_metadata_get(md, "", mdt, AV_METADATA_IGNORE_SUFFIX)) != NULL)
#endif
fprintf(stderr, " -> %s = %s\n", mdt->key, mdt->value);
#endif
mdcount = 0;
/* Extract actual metadata */
for (i = 0; md_map[i].key != NULL; i++)
{
#if LIBAVUTIL_VERSION_MAJOR >= 52 || (LIBAVUTIL_VERSION_MAJOR == 51 && LIBAVUTIL_VERSION_MINOR >= 5)
mdt = av_dict_get(md, md_map[i].key, NULL, 0);
#else
mdt = av_metadata_get(md, md_map[i].key, NULL, 0);
#endif
if (mdt == NULL)
continue;
if ((mdt->value == NULL) || (strlen(mdt->value) == 0))
continue;
if (md_map[i].handler_function)
{
mdcount += md_map[i].handler_function(mfi, mdt->value);
continue;
}
mdcount++;
if (!md_map[i].as_int)
{
strval = (char **) ((char *) mfi + md_map[i].offset);
if (*strval == NULL)
*strval = strdup(mdt->value);
}
else
{
intval = (uint32_t *) ((char *) mfi + md_map[i].offset);
if (*intval == 0)
{
ret = safe_atou32(mdt->value, intval);
if (ret < 0)
continue;
}
}
}
return mdcount;
}
static int
extract_metadata(struct media_file_info *mfi, AVFormatContext *ctx, AVStream *audio_stream, AVStream *video_stream, const struct metadata_map *md_map)
{
int mdcount;
int ret;
mdcount = 0;
if (ctx->metadata)
{
ret = extract_metadata_core(mfi, ctx->metadata, md_map);
mdcount += ret;
DPRINTF(E_DBG, L_SCAN, "Picked up %d tags from file metadata\n", ret);
}
if (audio_stream->metadata)
{
ret = extract_metadata_core(mfi, audio_stream->metadata, md_map);
mdcount += ret;
DPRINTF(E_DBG, L_SCAN, "Picked up %d tags from audio stream metadata\n", ret);
}
if (video_stream && video_stream->metadata)
{
ret = extract_metadata_core(mfi, video_stream->metadata, md_map);
mdcount += ret;
DPRINTF(E_DBG, L_SCAN, "Picked up %d tags from video stream metadata\n", ret);
}
return mdcount;
}
int
scan_metadata_ffmpeg(const char *file, struct media_file_info *mfi)
{
AVFormatContext *ctx;
AVDictionary *options;
const struct metadata_map *extra_md_map;
struct http_icy_metadata *icy_metadata;
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
enum AVCodecID codec_id;
enum AVCodecID video_codec_id;
enum AVCodecID audio_codec_id;
#else
enum CodecID codec_id;
enum CodecID video_codec_id;
enum CodecID audio_codec_id;
#endif
AVStream *video_stream;
AVStream *audio_stream;
char *path;
int mdcount;
int i;
int ret;
ctx = NULL;
options = NULL;
path = strdup(file);
#if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 3)
# ifndef HAVE_FFMPEG
// Without this, libav is slow to probe some internet streams
if (mfi->data_kind == DATA_KIND_HTTP)
{
ctx = avformat_alloc_context();
ctx->probesize = 64000;
}
# endif
if (mfi->data_kind == DATA_KIND_HTTP)
{
free(path);
ret = http_stream_setup(&path, file);
if (ret < 0)
return -1;
av_dict_set(&options, "icy", "1", 0);
mfi->artwork = ARTWORK_HTTP;
}
ret = avformat_open_input(&ctx, path, NULL, &options);
if (options)
av_dict_free(&options);
#else
ret = av_open_input_file(&ctx, path, NULL, 0, NULL);
#endif
if (ret != 0)
{
DPRINTF(E_WARN, L_SCAN, "Cannot open media file '%s': %s\n", path, err2str(ret));
free(path);
return -1;
}
free(path);
#if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 3)
ret = avformat_find_stream_info(ctx, NULL);
#else
ret = av_find_stream_info(ctx);
#endif
if (ret < 0)
{
DPRINTF(E_WARN, L_SCAN, "Cannot get stream info of '%s': %s\n", path, err2str(ret));
#if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 21)
avformat_close_input(&ctx);
#else
av_close_input_file(ctx);
#endif
return -1;
}
#if 0
/* Dump input format as determined by ffmpeg */
av_dump_format(ctx, 0, file, 0);
#endif
DPRINTF(E_DBG, L_SCAN, "File has %d streams\n", ctx->nb_streams);
/* Extract codec IDs, check for video */
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
video_codec_id = AV_CODEC_ID_NONE;
video_stream = NULL;
audio_codec_id = AV_CODEC_ID_NONE;
audio_stream = NULL;
#else
video_codec_id = CODEC_ID_NONE;
video_stream = NULL;
audio_codec_id = CODEC_ID_NONE;
audio_stream = NULL;
#endif
for (i = 0; i < ctx->nb_streams; i++)
{
switch (ctx->streams[i]->codec->codec_type)
{
#if LIBAVCODEC_VERSION_MAJOR >= 53 || (LIBAVCODEC_VERSION_MAJOR == 52 && LIBAVCODEC_VERSION_MINOR >= 64)
case AVMEDIA_TYPE_VIDEO:
#else
case CODEC_TYPE_VIDEO:
#endif
#if LIBAVFORMAT_VERSION_MAJOR >= 55 || (LIBAVFORMAT_VERSION_MAJOR == 54 && LIBAVFORMAT_VERSION_MINOR >= 6)
if (ctx->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC)
{
DPRINTF(E_DBG, L_SCAN, "Found embedded artwork (stream %d)\n", i);
mfi->artwork = ARTWORK_EMBEDDED;
break;
}
#endif
// We treat these as audio no matter what
if (mfi->compilation || (mfi->media_kind & (MEDIA_KIND_PODCAST | MEDIA_KIND_AUDIOBOOK)))
break;
if (!video_stream)
{
DPRINTF(E_DBG, L_SCAN, "File has video (stream %d)\n", i);
mfi->has_video = 1;
video_stream = ctx->streams[i];
video_codec_id = video_stream->codec->codec_id;
}
break;
#if LIBAVCODEC_VERSION_MAJOR >= 53 || (LIBAVCODEC_VERSION_MAJOR == 52 && LIBAVCODEC_VERSION_MINOR >= 64)
case AVMEDIA_TYPE_AUDIO:
#else
case CODEC_TYPE_AUDIO:
#endif
if (!audio_stream)
{
audio_stream = ctx->streams[i];
audio_codec_id = audio_stream->codec->codec_id;
}
break;
default:
break;
}
}
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
if (audio_codec_id == AV_CODEC_ID_NONE)
#else
if (audio_codec_id == CODEC_ID_NONE)
#endif
{
DPRINTF(E_DBG, L_SCAN, "File has no audio streams, discarding\n");
#if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 21)
avformat_close_input(&ctx);
#else
av_close_input_file(ctx);
#endif
return -1;
}
/* Common media information */
if (ctx->duration > 0)
mfi->song_length = ctx->duration / (AV_TIME_BASE / 1000); /* ms */
if (ctx->bit_rate > 0)
mfi->bitrate = ctx->bit_rate / 1000;
else if (ctx->duration > AV_TIME_BASE) /* guesstimate */
mfi->bitrate = ((mfi->file_size * 8) / (ctx->duration / AV_TIME_BASE)) / 1000;
DPRINTF(E_DBG, L_SCAN, "Duration %d ms, bitrate %d kbps\n", mfi->song_length, mfi->bitrate);
/* Try to extract ICY metadata if http stream */
if (mfi->data_kind == DATA_KIND_HTTP)
{
icy_metadata = http_icy_metadata_get(ctx, 0);
if (icy_metadata && icy_metadata->name)
{
DPRINTF(E_DBG, L_SCAN, "Found ICY metadata, name is '%s'\n", icy_metadata->name);
if (mfi->title)
free(mfi->title);
if (mfi->artist)
free(mfi->artist);
if (mfi->album_artist)
free(mfi->album_artist);
mfi->title = strdup(icy_metadata->name);
mfi->artist = strdup(icy_metadata->name);
mfi->album_artist = strdup(icy_metadata->name);
}
if (icy_metadata && icy_metadata->description)
{
DPRINTF(E_DBG, L_SCAN, "Found ICY metadata, description is '%s'\n", icy_metadata->description);
if (mfi->album)
free(mfi->album);
mfi->album = strdup(icy_metadata->description);
}
if (icy_metadata && icy_metadata->genre)
{
DPRINTF(E_DBG, L_SCAN, "Found ICY metadata, genre is '%s'\n", icy_metadata->genre);
if (mfi->genre)
free(mfi->genre);
mfi->genre = strdup(icy_metadata->genre);
}
if (icy_metadata)
http_icy_metadata_free(icy_metadata, 0);
}
/* Get some more information on the audio stream */
if (audio_stream)
{
if (audio_stream->codec->sample_rate != 0)
mfi->samplerate = audio_stream->codec->sample_rate;
/* Try sample format first */
#if LIBAVUTIL_VERSION_MAJOR >= 52 || (LIBAVUTIL_VERSION_MAJOR == 51 && LIBAVUTIL_VERSION_MINOR >= 4)
mfi->bits_per_sample = 8 * av_get_bytes_per_sample(audio_stream->codec->sample_fmt);
#elif LIBAVCODEC_VERSION_MAJOR >= 53
mfi->bits_per_sample = av_get_bits_per_sample_fmt(audio_stream->codec->sample_fmt);
#else
mfi->bits_per_sample = av_get_bits_per_sample_format(audio_stream->codec->sample_fmt);
#endif
if (mfi->bits_per_sample == 0)
{
/* Try codec */
mfi->bits_per_sample = av_get_bits_per_sample(audio_codec_id);
}
DPRINTF(E_DBG, L_SCAN, "samplerate %d, bps %d\n", mfi->samplerate, mfi->bits_per_sample);
}
/* Check codec */
extra_md_map = NULL;
codec_id = (mfi->has_video) ? video_codec_id : audio_codec_id;
switch (codec_id)
{
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
case AV_CODEC_ID_AAC:
#else
case CODEC_ID_AAC:
#endif
DPRINTF(E_DBG, L_SCAN, "AAC\n");
mfi->type = strdup("m4a");
mfi->codectype = strdup("mp4a");
mfi->description = strdup("AAC audio file");
break;
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
case AV_CODEC_ID_ALAC:
#else
case CODEC_ID_ALAC:
#endif
DPRINTF(E_DBG, L_SCAN, "ALAC\n");
mfi->type = strdup("m4a");
mfi->codectype = strdup("alac");
mfi->description = strdup("Apple Lossless audio file");
break;
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
case AV_CODEC_ID_FLAC:
#else
case CODEC_ID_FLAC:
#endif
DPRINTF(E_DBG, L_SCAN, "FLAC\n");
mfi->type = strdup("flac");
mfi->codectype = strdup("flac");
mfi->description = strdup("FLAC audio file");
extra_md_map = md_map_vorbis;
break;
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
case AV_CODEC_ID_APE:
#else
case CODEC_ID_APE:
#endif
DPRINTF(E_DBG, L_SCAN, "APE\n");
mfi->type = strdup("ape");
mfi->codectype = strdup("ape");
mfi->description = strdup("Monkey's audio");
break;
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
case AV_CODEC_ID_MUSEPACK7:
case AV_CODEC_ID_MUSEPACK8:
#else
case CODEC_ID_MUSEPACK7:
case CODEC_ID_MUSEPACK8:
#endif
DPRINTF(E_DBG, L_SCAN, "Musepack\n");
mfi->type = strdup("mpc");
mfi->codectype = strdup("mpc");
mfi->description = strdup("Musepack audio file");
break;
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
case AV_CODEC_ID_MPEG4: /* Video */
case AV_CODEC_ID_H264:
#else
case CODEC_ID_MPEG4: /* Video */
case CODEC_ID_H264:
#endif
DPRINTF(E_DBG, L_SCAN, "MPEG4 video\n");
mfi->type = strdup("m4v");
mfi->codectype = strdup("mp4v");
mfi->description = strdup("MPEG-4 video file");
extra_md_map = md_map_tv;
break;
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
case AV_CODEC_ID_MP3:
#else
case CODEC_ID_MP3:
#endif
DPRINTF(E_DBG, L_SCAN, "MP3\n");
mfi->type = strdup("mp3");
mfi->codectype = strdup("mpeg");
mfi->description = strdup("MPEG audio file");
extra_md_map = md_map_id3;
break;
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
case AV_CODEC_ID_VORBIS:
#else
case CODEC_ID_VORBIS:
#endif
DPRINTF(E_DBG, L_SCAN, "VORBIS\n");
mfi->type = strdup("ogg");
mfi->codectype = strdup("ogg");
mfi->description = strdup("Ogg Vorbis audio file");
extra_md_map = md_map_vorbis;
break;
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
case AV_CODEC_ID_WMAV1:
case AV_CODEC_ID_WMAV2:
case AV_CODEC_ID_WMAVOICE:
#else
case CODEC_ID_WMAV1:
case CODEC_ID_WMAV2:
case CODEC_ID_WMAVOICE:
#endif
DPRINTF(E_DBG, L_SCAN, "WMA Voice\n");
mfi->type = strdup("wma");
mfi->codectype = strdup("wmav");
mfi->description = strdup("WMA audio file");
break;
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
case AV_CODEC_ID_WMAPRO:
#else
case CODEC_ID_WMAPRO:
#endif
DPRINTF(E_DBG, L_SCAN, "WMA Pro\n");
mfi->type = strdup("wmap");
mfi->codectype = strdup("wma");
mfi->description = strdup("WMA audio file");
break;
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
case AV_CODEC_ID_WMALOSSLESS:
#else
case CODEC_ID_WMALOSSLESS:
#endif
DPRINTF(E_DBG, L_SCAN, "WMA Lossless\n");
mfi->type = strdup("wma");
mfi->codectype = strdup("wmal");
mfi->description = strdup("WMA audio file");
break;
#if LIBAVCODEC_VERSION_MAJOR >= 55 || (LIBAVCODEC_VERSION_MAJOR == 54 && LIBAVCODEC_VERSION_MINOR >= 35)
case AV_CODEC_ID_PCM_S16LE ... AV_CODEC_ID_PCM_F64LE:
#else
case CODEC_ID_PCM_S16LE ... CODEC_ID_PCM_F64LE:
#endif
if (strcmp(ctx->iformat->name, "aiff") == 0)
{
DPRINTF(E_DBG, L_SCAN, "AIFF\n");
mfi->type = strdup("aif");
mfi->codectype = strdup("aif");
mfi->description = strdup("AIFF audio file");
break;
}
else if (strcmp(ctx->iformat->name, "wav") == 0)
{
DPRINTF(E_DBG, L_SCAN, "WAV\n");
mfi->type = strdup("wav");
mfi->codectype = strdup("wav");
mfi->description = strdup("WAV audio file");
break;
}
/* WARNING: will fallthrough to default case, don't move */
/* FALLTHROUGH */
default:
DPRINTF(E_DBG, L_SCAN, "Unknown codec 0x%x (video: %s), format %s (%s)\n",
codec_id, (mfi->has_video) ? "yes" : "no", ctx->iformat->name, ctx->iformat->long_name);
mfi->type = strdup("unkn");
mfi->codectype = strdup("unkn");
if (mfi->has_video)
{
mfi->description = strdup("Unknown video file format");
extra_md_map = md_map_tv;
}
else
mfi->description = strdup("Unknown audio file format");
break;
}
mdcount = 0;
if ((!ctx->metadata) && (!audio_stream->metadata)
&& (video_stream && !video_stream->metadata))
{
DPRINTF(E_WARN, L_SCAN, "ffmpeg reports no metadata\n");
goto skip_extract;
}
if (extra_md_map)
{
ret = extract_metadata(mfi, ctx, audio_stream, video_stream, extra_md_map);
mdcount += ret;
DPRINTF(E_DBG, L_SCAN, "Picked up %d tags with extra md_map\n", ret);
}
ret = extract_metadata(mfi, ctx, audio_stream, video_stream, md_map_generic);
mdcount += ret;
DPRINTF(E_DBG, L_SCAN, "Picked up %d tags with generic md_map, %d tags total\n", ret, mdcount);
/* fix up TV metadata */
if (mfi->media_kind == 10)
{
/* I have no idea why this is, but iTunes reports a media kind of 64 for stik==10 (?!) */
mfi->media_kind = MEDIA_KIND_TVSHOW;
}
/* Unspecified video files are "Movies", media_kind 2 */
else if (mfi->has_video == 1)
{
mfi->media_kind = MEDIA_KIND_MOVIE;
}
skip_extract:
#if LIBAVFORMAT_VERSION_MAJOR >= 54 || (LIBAVFORMAT_VERSION_MAJOR == 53 && LIBAVFORMAT_VERSION_MINOR >= 21)
avformat_close_input(&ctx);
#else
av_close_input_file(ctx);
#endif
if (mdcount == 0)
DPRINTF(E_WARN, L_SCAN, "ffmpeg/libav could not extract any metadata\n");
/* Just in case there's no title set ... */
if (mfi->title == NULL)
mfi->title = strdup(mfi->fname);
/* All done */
return 0;
}

View File

@@ -0,0 +1,926 @@
/*
* 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 <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <limits.h>
#include <sys/mman.h>
#include <stdint.h>
#include <inttypes.h>
#include <plist/plist.h>
#include <event2/http.h>
#include "logger.h"
#include "db.h"
#include "conffile.h"
#include "misc.h"
/* Mapping between iTunes library IDs and our DB IDs using a "hash" table of
* size ID_MAP_SIZE
*/
#define ID_MAP_SIZE 16384
struct itml_to_db_map {
uint64_t itml_id;
uint32_t db_id;
struct itml_to_db_map *next;
};
struct itml_to_db_map **id_map;
/* Mapping between iTunes library metadata keys and the offset
* of the equivalent metadata field in struct media_file_info */
struct metadata_map {
char *key;
plist_type type;
size_t offset;
};
static struct metadata_map md_map[] =
{
{ "Name", PLIST_STRING, mfi_offsetof(title) },
{ "Artist", PLIST_STRING, mfi_offsetof(artist) },
{ "Album Artist", PLIST_STRING, mfi_offsetof(album_artist) },
{ "Composer", PLIST_STRING, mfi_offsetof(composer) },
{ "Album", PLIST_STRING, mfi_offsetof(album) },
{ "Genre", PLIST_STRING, mfi_offsetof(genre) },
{ "Comments", PLIST_STRING, mfi_offsetof(comment) },
{ "Track Count", PLIST_UINT, mfi_offsetof(total_tracks) },
{ "Track Number", PLIST_UINT, mfi_offsetof(track) },
{ "Disc Count", PLIST_UINT, mfi_offsetof(total_discs) },
{ "Disc Number", PLIST_UINT, mfi_offsetof(disc) },
{ "Year", PLIST_UINT, mfi_offsetof(year) },
{ "Total Time", PLIST_UINT, mfi_offsetof(song_length) },
{ "Bit Rate", PLIST_UINT, mfi_offsetof(bitrate) },
{ "Sample Rate", PLIST_UINT, mfi_offsetof(samplerate) },
{ "BPM", PLIST_UINT, mfi_offsetof(bpm) },
{ "Rating", PLIST_UINT, mfi_offsetof(rating) },
{ "Compilation", PLIST_BOOLEAN, mfi_offsetof(compilation) },
{ "Date Added", PLIST_DATE, mfi_offsetof(time_added) },
{ NULL, 0, 0 }
};
static void
id_map_free(void)
{
struct itml_to_db_map *map;
int i;
for (i = 0; i < ID_MAP_SIZE; i++)
{
if (!id_map[i])
continue;
for (map = id_map[i]; id_map[i]; map = id_map[i])
{
id_map[i] = map->next;
free(map);
}
}
free(id_map);
}
/* Inserts a linked list item into "hash" position in the id_table */
static int
id_map_add(uint64_t itml_id, uint32_t db_id)
{
struct itml_to_db_map *new_map;
struct itml_to_db_map *cur_map;
int i;
new_map = malloc(sizeof(struct itml_to_db_map));
if (!new_map)
return -1;
new_map->itml_id = itml_id;
new_map->db_id = db_id;
i = itml_id % ID_MAP_SIZE;
cur_map = id_map[i];
new_map->next = cur_map;
id_map[i] = new_map;
return 0;
}
static uint32_t
id_map_get(uint64_t itml_id)
{
struct itml_to_db_map *map;
int i;
i = itml_id % ID_MAP_SIZE;
for (map = id_map[i]; map; map = map->next)
{
if (itml_id == map->itml_id)
return map->db_id;
}
return 0;
}
/* plist helpers */
static int
get_dictval_int_from_key(plist_t dict, const char *key, uint64_t *val)
{
plist_t node;
node = plist_dict_get_item(dict, key);
if (!node)
return -1;
if (plist_get_node_type(node) != PLIST_UINT)
return -1;
plist_get_uint_val(node, val);
return 0;
}
static int
get_dictval_date_from_key(plist_t dict, const char *key, uint32_t *val)
{
plist_t node;
int32_t secs;
int32_t dummy;
node = plist_dict_get_item(dict, key);
if (!node)
return -1;
if (plist_get_node_type(node) != PLIST_DATE)
return -1;
plist_get_date_val(node, &secs, &dummy);
*val = (uint32_t) secs;
return 0;
}
static int
get_dictval_bool_from_key(plist_t dict, const char *key, uint8_t *val)
{
plist_t node;
node = plist_dict_get_item(dict, key);
/* Not present means false */
if (!node)
{
*val = 0;
return 0;
}
if (plist_get_node_type(node) != PLIST_BOOLEAN)
return -1;
plist_get_bool_val(node, val);
return 0;
}
static int
get_dictval_string_from_key(plist_t dict, const char *key, char **val)
{
plist_t node;
node = plist_dict_get_item(dict, key);
if (!node)
return -1;
if (plist_get_node_type(node) != PLIST_STRING)
return -1;
plist_get_string_val(node, val);
return 0;
}
static int
get_dictval_dict_from_key(plist_t dict, const char *key, plist_t *val)
{
plist_t node;
node = plist_dict_get_item(dict, key);
if (!node)
return -1;
if (plist_get_node_type(node) != PLIST_DICT)
return -1;
*val = node;
return 0;
}
static int
get_dictval_array_from_key(plist_t dict, const char *key, plist_t *val)
{
plist_t node;
node = plist_dict_get_item(dict, key);
if (!node)
return -1;
if (plist_get_node_type(node) != PLIST_ARRAY)
return -1;
*val = node;
return 0;
}
/* We don't actually check anything (yet) despite the name */
static int
check_meta(plist_t dict)
{
char *appver;
char *folder;
uint64_t major;
uint64_t minor;
int ret;
ret = get_dictval_int_from_key(dict, "Major Version", &major);
if (ret < 0)
return -1;
ret = get_dictval_int_from_key(dict, "Minor Version", &minor);
if (ret < 0)
return -1;
ret = get_dictval_string_from_key(dict, "Application Version", &appver);
if (ret < 0)
return -1;
ret = get_dictval_string_from_key(dict, "Music Folder", &folder);
if (ret < 0)
{
free(appver);
return -1;
}
DPRINTF(E_INFO, L_SCAN, "iTunes XML playlist Major:%" PRIu64 " Minor:%" PRIu64
" Application:%s Folder:%s\n", major, minor, appver, folder);
free(appver);
free(folder);
return 0;
}
static int
find_track_file(char *location)
{
int ret;
int plen;
int mfi_id;
char *entry;
char *ptr;
location = evhttp_decode_uri(location);
if (!location)
{
DPRINTF(E_LOG, L_SCAN, "Could not decode iTunes XML playlist url.\n");
return 0;
}
plen = strlen("file://");
/* Not a local file ... */
if (strncmp(location, "file://", plen) != 0)
return 0;
/* Now search for the library item where the path has closest match to playlist item */
/* Succes is when we find an unambiguous match, or when we no longer can expand the */
/* the path to refine our search. */
entry = NULL;
do
{
ptr = strrchr(location, '/');
if (entry)
*(entry - 1) = '/';
if (ptr)
{
*ptr = '\0';
entry = ptr + 1;
}
else
entry = location;
DPRINTF(E_SPAM, L_SCAN, "iTunes XML playlist entry is now %s\n", entry);
ret = db_files_get_count_bymatch(entry);
} while (ptr && (ret > 1));
if (ret > 0)
{
mfi_id = db_file_id_bymatch(entry);
DPRINTF(E_DBG, L_SCAN, "Found iTunes XML playlist entry match, id is %d, entry is %s\n", mfi_id, entry);
free(location);
return mfi_id;
}
else
{
DPRINTF(E_DBG, L_SCAN, "No match for iTunes XML playlist entry %s\n", entry);
free(location);
return 0;
}
}
static int
process_track_file(plist_t trk)
{
char *location;
struct media_file_info *mfi;
char *string;
uint64_t integer;
char **strval;
uint32_t *intval;
char *chrval;
uint8_t boolean;
int mfi_id;
int i;
int ret;
ret = get_dictval_string_from_key(trk, "Location", &location);
if (ret < 0)
{
DPRINTF(E_WARN, L_SCAN, "Track type File with no Location\n");
return 0;
}
mfi_id = find_track_file(location);
if (mfi_id <= 0)
{
DPRINTF(E_INFO, L_SCAN, "Could not match location '%s' to any known file\n", location);
free(location);
return 0;
}
free(location);
if (!cfg_getbool(cfg_getsec(cfg, "library"), "itunes_overrides"))
return mfi_id;
/* Override our metadata with what's provided by iTunes */
mfi = db_file_fetch_byid(mfi_id);
if (!mfi)
{
DPRINTF(E_LOG, L_SCAN, "Could not retrieve file info for file id %d\n", mfi_id);
return mfi_id;
}
for (i = 0; md_map[i].key != NULL; i++)
{
switch (md_map[i].type)
{
case PLIST_UINT:
ret = get_dictval_int_from_key(trk, md_map[i].key, &integer);
if (ret < 0)
break;
intval = (uint32_t *) ((char *) mfi + md_map[i].offset);
*intval = (uint32_t)integer;
break;
case PLIST_STRING:
ret = get_dictval_string_from_key(trk, md_map[i].key, &string);
if (ret < 0)
break;
strval = (char **) ((char *) mfi + md_map[i].offset);
if (*strval)
free(*strval);
*strval = string;
break;
case PLIST_BOOLEAN:
ret = get_dictval_bool_from_key(trk, md_map[i].key, &boolean);
if (ret < 0)
break;
chrval = (char *) mfi + md_map[i].offset;
*chrval = boolean;
break;
case PLIST_DATE:
intval = (uint32_t *) ((char *) mfi + md_map[i].offset);
get_dictval_date_from_key(trk, md_map[i].key, intval);
break;
default:
DPRINTF(E_WARN, L_SCAN, "Unhandled metadata type %d\n", md_map[i].type);
break;
}
}
/* Set media_kind to 4 (Podcast) if Podcast is true */
ret = get_dictval_bool_from_key(trk, "Podcast", &boolean);
if ((ret == 0) && boolean)
{
mfi->media_kind = MEDIA_KIND_PODCAST;
}
/* Don't let album_artist set to "Unknown artist" if we've
* filled artist from the iTunes data in the meantime
*/
if (strcmp(mfi->album_artist, "Unknown artist") == 0)
{
free(mfi->album_artist);
mfi->album_artist = strdup(mfi->artist);
}
unicode_fixup_mfi(mfi);
db_file_update(mfi);
free_mfi(mfi, 0);
return mfi_id;
}
static int
process_track_stream(plist_t trk)
{
char *url;
int ret;
ret = get_dictval_string_from_key(trk, "Location", &url);
if (ret < 0)
{
DPRINTF(E_WARN, L_SCAN, "Track type URL with no Location entry!\n");
return 0;
}
ret = db_file_id_byurl(url);
free(url);
return ret;
}
static int
process_tracks(plist_t tracks)
{
plist_t trk;
plist_dict_iter iter;
char *str;
uint64_t trk_id;
uint8_t disabled;
int ntracks;
int mfi_id;
int ret;
if (plist_dict_get_size(tracks) == 0)
{
DPRINTF(E_WARN, L_SCAN, "No tracks in iTunes library\n");
return 0;
}
ntracks = 0;
iter = NULL;
plist_dict_new_iter(tracks, &iter);
plist_dict_next_item(tracks, iter, NULL, &trk);
while (trk)
{
if (plist_get_node_type(trk) != PLIST_DICT)
{
plist_dict_next_item(tracks, iter, NULL, &trk);
continue;
}
ret = get_dictval_int_from_key(trk, "Track ID", &trk_id);
if (ret < 0)
{
DPRINTF(E_WARN, L_SCAN, "Track ID not found!\n");
plist_dict_next_item(tracks, iter, NULL, &trk);
continue;
}
ret = get_dictval_bool_from_key(trk, "Disabled", &disabled);
if (ret < 0)
{
DPRINTF(E_WARN, L_SCAN, "Malformed track record (id %" PRIu64 ")\n", trk_id);
plist_dict_next_item(tracks, iter, NULL, &trk);
continue;
}
if (disabled)
{
DPRINTF(E_INFO, L_SCAN, "Track %" PRIu64 " disabled; skipping\n", trk_id);
plist_dict_next_item(tracks, iter, NULL, &trk);
continue;
}
ret = get_dictval_string_from_key(trk, "Track Type", &str);
if (ret < 0)
{
DPRINTF(E_WARN, L_SCAN, "Track %" PRIu64 " has no track type\n", trk_id);
plist_dict_next_item(tracks, iter, NULL, &trk);
continue;
}
if (strcmp(str, "URL") == 0)
mfi_id = process_track_stream(trk);
else if (strcmp(str, "File") == 0)
mfi_id = process_track_file(trk);
else
{
DPRINTF(E_LOG, L_SCAN, "Unknown track type: %s\n", str);
free(str);
plist_dict_next_item(tracks, iter, NULL, &trk);
continue;
}
free(str);
if (mfi_id <= 0)
{
plist_dict_next_item(tracks, iter, NULL, &trk);
continue;
}
ntracks++;
ret = id_map_add(trk_id, mfi_id);
if (ret < 0)
DPRINTF(E_LOG, L_SCAN, "Out of memory for itml -> db mapping\n");
plist_dict_next_item(tracks, iter, NULL, &trk);
}
free(iter);
return ntracks;
}
static void
process_pl_items(plist_t items, int pl_id)
{
plist_t trk;
uint64_t itml_id;
uint32_t db_id;
uint32_t alen;
uint32_t i;
int ret;
alen = plist_array_get_size(items);
for (i = 0; i < alen; i++)
{
trk = plist_array_get_item(items, i);
if (plist_get_node_type(trk) != PLIST_DICT)
continue;
ret = get_dictval_int_from_key(trk, "Track ID", &itml_id);
if (ret < 0)
{
DPRINTF(E_WARN, L_SCAN, "No Track ID found for playlist item %u\n", i);
continue;
}
db_id = id_map_get(itml_id);
if (!db_id)
{
DPRINTF(E_INFO, L_SCAN, "Track ID %" PRIu64 " dropped\n", itml_id);
continue;
}
ret = db_pl_add_item_byid(pl_id, db_id);
if (ret < 0)
DPRINTF(E_WARN, L_SCAN, "Could not add ID %d to playlist\n", db_id);
}
}
static int
ignore_pl(plist_t pl, char *name)
{
uint64_t kind;
int smart;
uint8_t master;
uint8_t party;
kind = 0;
smart = 0;
master = 0;
party = 0;
/* Special (builtin) playlists */
get_dictval_int_from_key(pl, "Distinguished Kind", &kind);
/* Import smart playlists (optional) */
if (!cfg_getbool(cfg_getsec(cfg, "library"), "itunes_smartpl")
&& (plist_dict_get_item(pl, "Smart Info") || plist_dict_get_item(pl, "Smart Criteria")))
smart = 1;
/* Not interested in the Master playlist */
get_dictval_bool_from_key(pl, "Master", &master);
/* Not interested in Party Shuffle playlists */
get_dictval_bool_from_key(pl, "Party Shuffle", &party);
if ((kind > 0) || smart || party || master)
{
DPRINTF(E_INFO, L_SCAN, "Ignoring playlist '%s' (k %" PRIu64 " s%d p%d m%d)\n", name, kind, smart, party, master);
return 1;
}
return 0;
}
static void
process_pls(plist_t playlists, char *file)
{
plist_t pl;
plist_t items;
struct playlist_info *pli;
char *name;
uint64_t id;
int pl_id;
uint32_t alen;
uint32_t i;
char virtual_path[PATH_MAX];
int ret;
alen = plist_array_get_size(playlists);
for (i = 0; i < alen; i++)
{
pl = plist_array_get_item(playlists, i);
if (plist_get_node_type(pl) != PLIST_DICT)
continue;
ret = get_dictval_int_from_key(pl, "Playlist ID", &id);
if (ret < 0)
{
DPRINTF(E_DBG, L_SCAN, "Playlist ID not found!\n");
continue;
}
ret = get_dictval_string_from_key(pl, "Name", &name);
if (ret < 0)
{
DPRINTF(E_DBG, L_SCAN, "Name not found!\n");
continue;
}
if (ignore_pl(pl, name))
{
free(name);
continue;
}
pli = db_pl_fetch_bytitlepath(name, file);
if (pli)
{
pl_id = pli->id;
free_pli(pli, 0);
db_pl_ping(pl_id);
db_pl_clear_items(pl_id);
}
else
pl_id = 0;
ret = get_dictval_array_from_key(pl, "Playlist Items", &items);
if (ret < 0)
{
DPRINTF(E_INFO, L_SCAN, "Playlist '%s' has no items\n", name);
free(name);
continue;
}
if (pl_id == 0)
{
pli = (struct playlist_info *)malloc(sizeof(struct playlist_info));
if (!pli)
{
DPRINTF(E_LOG, L_SCAN, "Out of memory\n");
return;
}
memset(pli, 0, sizeof(struct playlist_info));
pli->type = PL_PLAIN;
pli->title = strdup(name);
pli->path = strdup(file);
snprintf(virtual_path, PATH_MAX, "/file:%s", file);
pli->virtual_path = strdup(virtual_path);
ret = db_pl_add(pli, &pl_id);
free_pli(pli, 0);
if (ret < 0)
{
DPRINTF(E_LOG, L_SCAN, "Error adding iTunes playlist '%s' (%s)\n", name, file);
free(name);
continue;
}
DPRINTF(E_INFO, L_SCAN, "Added playlist as id %d\n", pl_id);
}
free(name);
process_pl_items(items, pl_id);
}
}
void
scan_itunes_itml(char *file)
{
struct stat sb;
char *itml_xml;
char *ptr;
plist_t itml;
plist_t node;
int fd;
int size;
int ret;
DPRINTF(E_LOG, L_SCAN, "Processing iTunes library: %s\n", file);
fd = open(file, O_RDONLY);
if (fd < 0)
{
DPRINTF(E_LOG, L_SCAN, "Could not open iTunes library '%s': %s\n", file, strerror(errno));
return;
}
ret = fstat(fd, &sb);
if (ret < 0)
{
DPRINTF(E_LOG, L_SCAN, "Could not stat iTunes library '%s': %s\n", file, strerror(errno));
close(fd);
return;
}
itml_xml = mmap(NULL, sb.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (itml_xml == MAP_FAILED)
{
DPRINTF(E_LOG, L_SCAN, "Could not map iTunes library: %s\n", strerror(errno));
close(fd);
return;
}
itml = NULL;
plist_from_xml(itml_xml, sb.st_size, &itml);
ret = munmap(itml_xml, sb.st_size);
if (ret < 0)
DPRINTF(E_LOG, L_SCAN, "Could not unmap iTunes library: %s\n", strerror(errno));
close(fd);
if (!itml)
{
DPRINTF(E_LOG, L_SCAN, "iTunes XML playlist '%s' failed to parse\n", file);
return;
}
if (plist_get_node_type(itml) != PLIST_DICT)
{
DPRINTF(E_LOG, L_SCAN, "Malformed iTunes XML playlist '%s'\n", file);
plist_free(itml);
return;
}
/* Meta data */
ret = check_meta(itml);
if (ret < 0)
{
plist_free(itml);
return;
}
/* Tracks */
ret = get_dictval_dict_from_key(itml, "Tracks", &node);
if (ret < 0)
{
DPRINTF(E_LOG, L_SCAN, "Could not find Tracks dict\n");
plist_free(itml);
return;
}
size = ID_MAP_SIZE * sizeof(struct itml_to_db_map *);
id_map = malloc(size);
if (!id_map)
{
DPRINTF(E_FATAL, L_SCAN, "iTunes library parser could not allocate ID map\n");
plist_free(itml);
return;
}
memset(id_map, 0, size);
ptr = strrchr(file, '/');
if (!ptr)
{
DPRINTF(E_FATAL, L_SCAN, "Invalid filename\n");
id_map_free();
plist_free(itml);
return;
}
*ptr = '\0';
ret = process_tracks(node);
if (ret <= 0)
{
DPRINTF(E_LOG, L_SCAN, "No tracks loaded\n");
id_map_free();
plist_free(itml);
return;
}
*ptr = '/';
DPRINTF(E_INFO, L_SCAN, "Loaded %d tracks from iTunes library\n", ret);
/* Playlists */
ret = get_dictval_array_from_key(itml, "Playlists", &node);
if (ret < 0)
{
DPRINTF(E_LOG, L_SCAN, "Could not find Playlists dict\n");
id_map_free();
plist_free(itml);
return;
}
process_pls(node, file);
id_map_free();
plist_free(itml);
}

View File

@@ -0,0 +1,322 @@
/*
* Copyright (C) 2009-2010 Julien BLACHE <jb@jblache.org>
*
* Rewritten from mt-daapd code:
* Copyright (C) 2003 Ron Pedde (ron@pedde.com)
*
* 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>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include "logger.h"
#include "db.h"
#include "library/filescanner.h"
#include "misc.h"
#include "library.h"
/* Formats we can read so far */
#define PLAYLIST_PLS 1
#define PLAYLIST_M3U 2
/* Get metadata from the EXTINF tag */
static int
extinf_get(char *string, struct media_file_info *mfi, int *extinf)
{
char *ptr;
if (strncmp(string, "#EXTINF:", strlen("#EXTINF:")) != 0)
return 0;
ptr = strchr(string, ',');
if (!ptr || strlen(ptr) < 2)
return 0;
/* New extinf found, so clear old data */
free_mfi(mfi, 1);
*extinf = 1;
mfi->artist = strdup(ptr + 1);
ptr = strstr(mfi->artist, " -");
if (ptr && strlen(ptr) > 3)
mfi->title = strdup(ptr + 3);
else
mfi->title = strdup("");
if (ptr)
*ptr = '\0';
return 1;
}
void
scan_playlist(char *file, time_t mtime, int dir_id)
{
FILE *fp;
struct media_file_info mfi;
struct playlist_info *pli;
struct stat sb;
char buf[PATH_MAX];
char *path;
char *entry;
char *filename;
char *ptr;
size_t len;
int extinf;
int pl_id;
int pl_format;
int mfi_id;
int ret;
char virtual_path[PATH_MAX];
int i;
DPRINTF(E_LOG, L_SCAN, "Processing static playlist: %s\n", file);
ptr = strrchr(file, '.');
if (!ptr)
return;
if (strcasecmp(ptr, ".m3u") == 0)
pl_format = PLAYLIST_M3U;
else if (strcasecmp(ptr, ".pls") == 0)
pl_format = PLAYLIST_PLS;
else
return;
filename = strrchr(file, '/');
if (!filename)
filename = file;
else
filename++;
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;
}
/* Fetch or create playlist */
pli = db_pl_fetch_bypath(file);
if (pli)
{
DPRINTF(E_DBG, L_SCAN, "Found playlist '%s', updating\n", file);
pl_id = pli->id;
db_pl_ping(pl_id);
db_pl_clear_items(pl_id);
}
else
{
pli = (struct playlist_info *)malloc(sizeof(struct playlist_info));
if (!pli)
{
DPRINTF(E_LOG, L_SCAN, "Out of memory\n");
return;
}
memset(pli, 0, sizeof(struct playlist_info));
pli->type = PL_PLAIN;
/* Get only the basename, to be used as the playlist title */
ptr = strrchr(filename, '.');
if (ptr)
*ptr = '\0';
pli->title = strdup(filename);
/* Restore the full filename */
if (ptr)
*ptr = '.';
pli->path = strdup(file);
snprintf(virtual_path, PATH_MAX, "/file:%s", file);
ptr = strrchr(virtual_path, '.');
if (ptr)
*ptr = '\0';
pli->virtual_path = strdup(virtual_path);
pli->directory_id = dir_id;
ret = db_pl_add(pli, &pl_id);
if (ret < 0)
{
DPRINTF(E_LOG, L_SCAN, "Error adding playlist '%s'\n", file);
free_pli(pli, 0);
return;
}
DPRINTF(E_INFO, L_SCAN, "Added playlist as id %d\n", pl_id);
}
free_pli(pli, 0);
extinf = 0;
memset(&mfi, 0, sizeof(struct media_file_info));
while (fgets(buf, sizeof(buf), fp) != NULL)
{
len = strlen(buf);
/* rtrim and check that length is sane (ignore blank lines) */
while ((len > 0) && isspace(buf[len - 1]))
{
len--;
buf[len] = '\0';
}
if (len < 1)
continue;
/* Saves metadata in mfi if EXTINF metadata line */
if ((pl_format == PLAYLIST_M3U) && extinf_get(buf, &mfi, &extinf))
continue;
/* For pls files we are only interested in the part after the FileX= entry */
path = NULL;
if ((pl_format == PLAYLIST_PLS) && (strncasecmp(buf, "file", strlen("file")) == 0))
path = strchr(buf, '=') + 1;
else if (pl_format == PLAYLIST_M3U)
path = buf;
if (!path)
continue;
/* Check that first char is sane for a path */
if ((!isalnum(path[0])) && (path[0] != '/') && (path[0] != '.'))
continue;
/* Check if line is an URL, will be added to library */
if (strncasecmp(path, "http://", strlen("http://")) == 0)
{
DPRINTF(E_DBG, L_SCAN, "Playlist contains URL entry\n");
filename = strdup(path);
if (!filename)
{
DPRINTF(E_LOG, L_SCAN, "Out of memory for playlist filename\n");
continue;
}
if (extinf)
DPRINTF(E_INFO, L_SCAN, "Playlist has EXTINF metadata, artist is '%s', title is '%s'\n", mfi.artist, mfi.title);
library_process_media(filename, mtime, 0, DATA_KIND_HTTP, 0, false, &mfi, DIR_HTTP);
}
/* Regular file, should already be in library */
else
{
/* Playlist might be from Windows so we change backslash to forward slash */
for (i = 0; i < strlen(path); i++)
{
if (path[i] == '\\')
path[i] = '/';
}
/* Now search for the library item where the path has closest match to playlist item */
/* Succes is when we find an unambiguous match, or when we no longer can expand the */
/* the path to refine our search. */
entry = NULL;
do
{
ptr = strrchr(path, '/');
if (entry)
*(entry - 1) = '/';
if (ptr)
{
*ptr = '\0';
entry = ptr + 1;
}
else
entry = path;
DPRINTF(E_SPAM, L_SCAN, "Playlist entry is now %s\n", entry);
ret = db_files_get_count_bymatch(entry);
} while (ptr && (ret > 1));
if (ret > 0)
{
mfi_id = db_file_id_bymatch(entry);
DPRINTF(E_DBG, L_SCAN, "Found playlist entry match, id is %d, entry is %s\n", mfi_id, entry);
filename = db_file_path_byid(mfi_id);
if (!filename)
{
DPRINTF(E_LOG, L_SCAN, "Playlist entry %s matches file id %d, but file path is missing.\n", entry, mfi_id);
continue;
}
}
else
{
DPRINTF(E_DBG, L_SCAN, "No match for playlist entry %s\n", entry);
continue;
}
}
ret = db_pl_add_item_bypath(pl_id, filename);
if (ret < 0)
DPRINTF(E_WARN, L_SCAN, "Could not add %s to playlist\n", filename);
/* Clean up in preparation for next item */
extinf = 0;
free_mfi(&mfi, 1);
free(filename);
}
/* We had some extinf that we never got to use, free it now */
if (extinf)
free_mfi(&mfi, 1);
if (!feof(fp))
{
DPRINTF(E_LOG, L_SCAN, "Error reading playlist '%s': %s\n", file, strerror(errno));
fclose(fp);
return;
}
fclose(fp);
DPRINTF(E_INFO, L_SCAN, "Done processing playlist\n");
}

View File

@@ -0,0 +1,234 @@
/*
* Copyright (C) 2015 Christian Meffert <christian.meffert@googlemail.com>
*
* 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>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include "logger.h"
#include "db.h"
#include "misc.h"
#include "SMARTPLLexer.h"
#include "SMARTPLParser.h"
#include "SMARTPL2SQL.h"
static int
smartpl_parse_file(const char *file, struct playlist_info *pli)
{
pANTLR3_INPUT_STREAM input;
pSMARTPLLexer lxr;
pANTLR3_COMMON_TOKEN_STREAM tstream;
pSMARTPLParser psr;
SMARTPLParser_playlist_return qtree;
pANTLR3_COMMON_TREE_NODE_STREAM nodes;
pSMARTPL2SQL sqlconv;
SMARTPL2SQL_playlist_return plreturn;
int ret;
#if ANTLR3C_NEW_INPUT
input = antlr3FileStreamNew((pANTLR3_UINT8) file, ANTLR3_ENC_8BIT);
#else
input = antlr3AsciiFileStreamNew((pANTLR3_UINT8) file);
#endif
// The input will be created successfully, providing that there is enough memory and the file exists etc
if (input == NULL)
{
DPRINTF(E_LOG, L_SCAN, "Unable to open smart playlist file %s\n", file);
return -1;
}
lxr = SMARTPLLexerNew(input);
// Need to check for errors
if (lxr == NULL)
{
DPRINTF(E_LOG, L_SCAN, "Could not create SMARTPL lexer\n");
ret = -1;
goto lxr_fail;
}
tstream = antlr3CommonTokenStreamSourceNew(ANTLR3_SIZE_HINT, TOKENSOURCE(lxr));
if (tstream == NULL)
{
DPRINTF(E_LOG, L_SCAN, "Could not create SMARTPL token stream\n");
ret = -1;
goto tkstream_fail;
}
// Finally, now that we have our lexer constructed, we can create the parser
psr = SMARTPLParserNew(tstream); // CParserNew is generated by ANTLR3
if (tstream == NULL)
{
DPRINTF(E_LOG, L_SCAN, "Could not create SMARTPL parser\n");
ret = -1;
goto psr_fail;
}
qtree = psr->playlist(psr);
/* Check for parser errors */
if (psr->pParser->rec->state->errorCount > 0)
{
DPRINTF(E_LOG, L_SCAN, "SMARTPL query parser terminated with %d errors\n", psr->pParser->rec->state->errorCount);
ret = -1;
goto psr_error;
}
DPRINTF(E_DBG, L_SCAN, "SMARTPL query AST:\n\t%s\n", qtree.tree->toStringTree(qtree.tree)->chars);
nodes = antlr3CommonTreeNodeStreamNewTree(qtree.tree, ANTLR3_SIZE_HINT);
if (!nodes)
{
DPRINTF(E_LOG, L_SCAN, "Could not create node stream\n");
ret = -1;
goto psr_error;
}
sqlconv = SMARTPL2SQLNew(nodes);
if (!sqlconv)
{
DPRINTF(E_LOG, L_SCAN, "Could not create SQL converter\n");
ret = -1;
goto sql_fail;
}
plreturn = sqlconv->playlist(sqlconv);
/* Check for tree parser errors */
if (sqlconv->pTreeParser->rec->state->errorCount > 0)
{
DPRINTF(E_LOG, L_SCAN, "SMARTPL query tree parser terminated with %d errors\n", sqlconv->pTreeParser->rec->state->errorCount);
ret = -1;
goto sql_error;
}
if (plreturn.title && plreturn.query)
{
DPRINTF(E_DBG, L_SCAN, "SMARTPL SQL title '%s' query: -%s-\n", plreturn.title->chars, plreturn.query->chars);
if (pli->title)
free(pli->title);
pli->title = strdup((char *)plreturn.title->chars);
if (pli->query)
free(pli->query);
pli->query = strdup((char *)plreturn.query->chars);
ret = 0;
}
else
{
DPRINTF(E_LOG, L_SCAN, "Invalid SMARTPL query\n");
ret = -1;
}
sql_error:
sqlconv->free(sqlconv);
sql_fail:
nodes->free(nodes);
psr_error:
psr->free(psr);
psr_fail:
tstream->free(tstream);
tkstream_fail:
lxr->free(lxr);
lxr_fail:
input->close(input);
return ret;
}
void
scan_smartpl(char *file, time_t mtime, int dir_id)
{
struct playlist_info *pli;
int pl_id;
char virtual_path[PATH_MAX];
char *ptr;
int ret;
/* Fetch or create playlist */
pli = db_pl_fetch_bypath(file);
if (!pli)
{
pli = calloc(1, sizeof(struct playlist_info));
if (!pli)
{
DPRINTF(E_LOG, L_SCAN, "Out of memory\n");
return;
}
pli->path = strdup(file);
snprintf(virtual_path, PATH_MAX, "/file:%s", file);
ptr = strrchr(virtual_path, '.');
if (ptr)
*ptr = '\0';
pli->virtual_path = strdup(virtual_path);
pli->type = PL_SMART;
}
pli->directory_id = dir_id;
ret = smartpl_parse_file(file, pli);
if (ret < 0)
{
DPRINTF(E_LOG, L_SCAN, "Error parsing smart playlist '%s'\n", file);
free_pli(pli, 0);
return;
}
if (pli->id)
{
pl_id = pli->id;
ret = db_pl_update(pli);
}
else
{
ret = db_pl_add(pli, &pl_id);
}
if (ret < 0)
{
DPRINTF(E_LOG, L_SCAN, "Error adding smart playlist '%s'\n", file);
free_pli(pli, 0);
return;
}
DPRINTF(E_INFO, L_SCAN, "Added or updated smart playlist as id %d\n", pl_id);
free_pli(pli, 0);
DPRINTF(E_INFO, L_SCAN, "Done processing smart playlist\n");
}