mirror of
https://github.com/owntone/owntone-server.git
synced 2025-11-08 21:24:57 -05:00
[filescanner/library] Move filescanner to subfolder
This commit is contained in:
1519
src/library/filescanner.c
Normal file
1519
src/library/filescanner.c
Normal file
File diff suppressed because it is too large
Load Diff
34
src/library/filescanner.h
Normal file
34
src/library/filescanner.h
Normal 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__ */
|
||||
830
src/library/filescanner_ffmpeg.c
Normal file
830
src/library/filescanner_ffmpeg.c
Normal 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;
|
||||
}
|
||||
926
src/library/filescanner_itunes.c
Normal file
926
src/library/filescanner_itunes.c
Normal 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);
|
||||
}
|
||||
322
src/library/filescanner_playlist.c
Normal file
322
src/library/filescanner_playlist.c
Normal 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");
|
||||
}
|
||||
234
src/library/filescanner_smartpl.c
Normal file
234
src/library/filescanner_smartpl.c
Normal 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");
|
||||
}
|
||||
Reference in New Issue
Block a user