itml.c 13.8 KB
Newer Older
1
2
3
/*******************************************************************************
 * itml.c : iTunes Music Library import functions
 *******************************************************************************
Jean-Baptiste Kempf's avatar
LGPL    
Jean-Baptiste Kempf committed
4
 * Copyright (C) 2007 VLC authors and VideoLAN
5
 * $Id$
6
7
8
 *
 * Authors: Yoann Peronneau <yoann@videolan.org>
 *
Jean-Baptiste Kempf's avatar
LGPL    
Jean-Baptiste Kempf committed
9
10
11
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
12
13
14
15
 * (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
Jean-Baptiste Kempf's avatar
LGPL    
Jean-Baptiste Kempf committed
16
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
18
 *
Jean-Baptiste Kempf's avatar
LGPL    
Jean-Baptiste Kempf committed
19
20
21
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
22
23
24
25
26
27
 *******************************************************************************/
/**
 * \file modules/demux/playlist/itml.c
 * \brief iTunes Music Library import functions
 */

28
29
30
31
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

32
#include <vlc_common.h>
33
#include <vlc_demux.h>
ivoire's avatar
ivoire committed
34
35
36
#include <vlc_xml.h>
#include <vlc_strings.h>
#include <vlc_url.h>
37
#include <vlc_fixups.h>
38
39

#include "itml.h"
ivoire's avatar
ivoire committed
40
#include "playlist.h"
41
42
43
44
45
46
47
48
49
50
51

struct demux_sys_t
{
    int i_ntracks;
};

static int Demux( demux_t * );

/**
 * \brief iTML submodule initialization function
 */
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
52
int Import_iTML( vlc_object_t *p_this )
53
{
54
55
56
57
58
59
60
    demux_t *p_demux = (demux_t *)p_this;
    CHECK_FILE();
    if( !demux_IsPathExtension( p_demux, ".xml" )
     && !demux_IsForced( p_demux, "itml" ) )
        return VLC_EGENERIC; \
    STANDARD_DEMUX_INIT_MSG( "using iTunes Media Library reader" );

61
    const uint8_t *p_peek;
62
    const ssize_t i_peek = vlc_stream_Peek( p_demux->s, &p_peek, 128 );
63
64
65
66
67
68
    if ( i_peek < 32 ||
         !strnstr( (const char *) p_peek, "<!DOCTYPE plist ", i_peek ) )
    {
        Close_iTML( p_this );
        return VLC_EGENERIC;
    }
69
70
71
    return VLC_SUCCESS;
}

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
72
void Close_iTML( vlc_object_t *p_this )
73
74
75
76
77
78
79
80
81
82
{
    demux_t *p_demux = (demux_t *)p_this;
    free( p_demux->p_sys );
}

/**
 * \brief demuxer function for iTML parsing
 */
int Demux( demux_t *p_demux )
{
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
83
    xml_reader_t *p_xml_reader;
84
    const char *node;
ivoire's avatar
ivoire committed
85

86
    input_item_t *p_current_input = GetCurrentItem(p_demux);
87
88
89
    p_demux->p_sys->i_ntracks = 0;

    /* create new xml parser from stream */
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
90
    p_xml_reader = xml_ReaderCreate( p_demux, p_demux->s );
ivoire's avatar
ivoire committed
91
92
    if( !p_xml_reader )
        goto end;
93
94

    /* locating the root node */
95
    int type;
ivoire's avatar
ivoire committed
96
    do
97
    {
98
        type = xml_ReaderNextNode( p_xml_reader, &node );
99
        if( type <= 0 )
100
        {
ivoire's avatar
ivoire committed
101
102
            msg_Err( p_demux, "can't read xml stream" );
            goto end;
103
        }
104
105
    }
    while( type != XML_READER_STARTELEM );
106

ivoire's avatar
ivoire committed
107
    /* checking root node name */
108
    if( strcmp( node, "plist" ) )
109
    {
110
        msg_Err( p_demux, "invalid root node <%s>", node );
ivoire's avatar
ivoire committed
111
        goto end;
112
113
    }

jpd's avatar
jpd committed
114
    input_item_node_t *p_subitems = input_item_node_Create( p_current_input );
ivoire's avatar
ivoire committed
115
116
    xml_elem_hnd_t pl_elements[] =
        { {"dict",    COMPLEX_CONTENT, {.cmplx = parse_plist_dict} } };
jpd's avatar
jpd committed
117
    parse_plist_node( p_demux, p_subitems, NULL, p_xml_reader, "plist",
ivoire's avatar
ivoire committed
118
                      pl_elements );
119
    input_item_node_PostAndDelete( p_subitems );
ivoire's avatar
ivoire committed
120
121

end:
122
    if( p_xml_reader )
123
        xml_ReaderDelete( p_xml_reader );
ivoire's avatar
ivoire committed
124
125
126

    /* Needed for correct operation of go back */
    return 0;
127
128
129
130
131
}

/**
 * \brief parse the root node of the playlist
 */
jpd's avatar
jpd committed
132
static bool parse_plist_node( demux_t *p_demux, input_item_node_t *p_input_node,
ivoire's avatar
ivoire committed
133
134
135
                              track_elem_t *p_track, xml_reader_t *p_xml_reader,
                              const char *psz_element,
                              xml_elem_hnd_t *p_handlers )
136
{
137
    VLC_UNUSED(p_track); VLC_UNUSED(psz_element);
138
    const char *attr, *value;
139
    bool b_version_found = false;
140
141

    /* read all playlist attributes */
142
    while( (attr = xml_ReaderNextAttr( p_xml_reader, &value )) != NULL )
143
144
    {
        /* attribute: version */
145
        if( !strcmp( attr, "version" ) )
146
        {
147
            b_version_found = true;
148
            if( strcmp( value, "1.0" ) )
149
150
151
152
                msg_Warn( p_demux, "unsupported iTunes Media Library version" );
        }
        /* unknown attribute */
        else
153
            msg_Warn( p_demux, "invalid <plist> attribute:\"%s\"", attr );
154
    }
155

156
157
158
159
    /* attribute version is mandatory !!! */
    if( !b_version_found )
        msg_Warn( p_demux, "<plist> requires \"version\" attribute" );

jpd's avatar
jpd committed
160
    return parse_dict( p_demux, p_input_node, NULL, p_xml_reader,
161
162
163
164
165
166
167
                       "plist", p_handlers );
}

/**
 * \brief parse a <dict>
 * \param COMPLEX_INTERFACE
 */
jpd's avatar
jpd committed
168
static bool parse_dict( demux_t *p_demux, input_item_node_t *p_input_node,
ivoire's avatar
ivoire committed
169
170
                        track_elem_t *p_track, xml_reader_t *p_xml_reader,
                        const char *psz_element, xml_elem_hnd_t *p_handlers )
171
172
{
    int i_node;
173
    const char *node;
174
175
176
    char *psz_value = NULL;
    char *psz_key = NULL;
    xml_elem_hnd_t *p_handler = NULL;
ivoire's avatar
ivoire committed
177
    bool b_ret = false;
178

179
    while( (i_node = xml_ReaderNextNode( p_xml_reader, &node )) > 0 )
180
181
182
    {
        switch( i_node )
        {
183
        /*  element start tag  */
ivoire's avatar
ivoire committed
184
185
186
        case XML_READER_STARTELEM:
            /* choose handler */
            for( p_handler = p_handlers;
187
                     p_handler->name && strcmp( node, p_handler->name );
188
                     p_handler++ );
ivoire's avatar
ivoire committed
189
190
            if( !p_handler->name )
            {
191
                msg_Err( p_demux, "unexpected element <%s>", node );
ivoire's avatar
ivoire committed
192
193
194
195
196
                goto end;
            }
            /* complex content is parsed in a separate function */
            if( p_handler->type == COMPLEX_CONTENT )
            {
jpd's avatar
jpd committed
197
                if( p_handler->pf_handler.cmplx( p_demux, p_input_node, NULL,
ivoire's avatar
ivoire committed
198
199
                                                 p_xml_reader, p_handler->name,
                                                 NULL ) )
200
                {
ivoire's avatar
ivoire committed
201
202
                    p_handler = NULL;
                    FREE_ATT_KEY();
203
                }
ivoire's avatar
ivoire committed
204
                else
ivoire's avatar
ivoire committed
205
                    goto end;
ivoire's avatar
ivoire committed
206
207
            }
            break;
208

209
        /* simple element content */
ivoire's avatar
ivoire committed
210
211
        case XML_READER_TEXT:
            free( psz_value );
212
213
            psz_value = strdup( node );
            if( unlikely(!psz_value) )
ivoire's avatar
ivoire committed
214
215
                goto end;
            break;
216

217
        /* element end tag */
ivoire's avatar
ivoire committed
218
219
        case XML_READER_ENDELEM:
            /* leave if the current parent node <track> is terminated */
220
            if( !strcmp( node, psz_element ) )
ivoire's avatar
ivoire committed
221
222
            {
                b_ret = true;
ivoire's avatar
ivoire committed
223
                goto end;
ivoire's avatar
ivoire committed
224
225
226
            }
            /* there MUST have been a start tag for that element name */
            if( !p_handler || !p_handler->name
227
                || strcmp( p_handler->name, node ) )
ivoire's avatar
ivoire committed
228
229
            {
                msg_Err( p_demux, "there's no open element left for <%s>",
230
                         node );
ivoire's avatar
ivoire committed
231
232
233
234
235
                goto end;
            }
            /* special case: key */
            if( !strcmp( p_handler->name, "key" ) )
            {
ivoire's avatar
ivoire committed
236
                free( psz_key );
ivoire's avatar
ivoire committed
237
238
239
240
241
242
243
244
245
246
                psz_key = strdup( psz_value );
            }
            /* call the simple handler */
            else if( p_handler->pf_handler.smpl )
            {
                p_handler->pf_handler.smpl( p_track, psz_key, psz_value );
            }
            FREE_ATT();
            p_handler = NULL;
            break;
247
248
        }
    }
249
    msg_Err( p_demux, "unexpected end of XML data" );
ivoire's avatar
ivoire committed
250
251

end:
252
253
    free( psz_value );
    free( psz_key );
ivoire's avatar
ivoire committed
254
    return b_ret;
255
256
}

jpd's avatar
jpd committed
257
static bool parse_plist_dict( demux_t *p_demux, input_item_node_t *p_input_node,
ivoire's avatar
ivoire committed
258
259
260
                              track_elem_t *p_track, xml_reader_t *p_xml_reader,
                              const char *psz_element,
                              xml_elem_hnd_t *p_handlers )
261
{
262
    VLC_UNUSED(p_track); VLC_UNUSED(psz_element); VLC_UNUSED(p_handlers);
263
264
265
266
267
268
269
270
271
272
273
274
    xml_elem_hnd_t pl_elements[] =
        { {"dict",    COMPLEX_CONTENT, {.cmplx = parse_tracks_dict} },
          {"array",   SIMPLE_CONTENT,  {NULL} },
          {"key",     SIMPLE_CONTENT,  {NULL} },
          {"integer", SIMPLE_CONTENT,  {NULL} },
          {"string",  SIMPLE_CONTENT,  {NULL} },
          {"date",    SIMPLE_CONTENT,  {NULL} },
          {"true",    SIMPLE_CONTENT,  {NULL} },
          {"false",   SIMPLE_CONTENT,  {NULL} },
          {NULL,      UNKNOWN_CONTENT, {NULL} }
        };

jpd's avatar
jpd committed
275
    return parse_dict( p_demux, p_input_node, NULL, p_xml_reader,
276
277
278
                       "dict", pl_elements );
}

jpd's avatar
jpd committed
279
static bool parse_tracks_dict( demux_t *p_demux, input_item_node_t *p_input_node,
ivoire's avatar
ivoire committed
280
281
282
                               track_elem_t *p_track, xml_reader_t *p_xml_reader,
                               const char *psz_element,
                               xml_elem_hnd_t *p_handlers )
283
{
284
    VLC_UNUSED(p_track); VLC_UNUSED(psz_element); VLC_UNUSED(p_handlers);
285
286
287
288
289
290
    xml_elem_hnd_t tracks_elements[] =
        { {"dict",    COMPLEX_CONTENT, {.cmplx = parse_track_dict} },
          {"key",     SIMPLE_CONTENT,  {NULL} },
          {NULL,      UNKNOWN_CONTENT, {NULL} }
        };

jpd's avatar
jpd committed
291
    parse_dict( p_demux, p_input_node, NULL, p_xml_reader,
292
293
294
295
296
                "dict", tracks_elements );

    msg_Info( p_demux, "added %i tracks successfully",
              p_demux->p_sys->i_ntracks );

297
    return true;
298
299
}

jpd's avatar
jpd committed
300
static bool parse_track_dict( demux_t *p_demux, input_item_node_t *p_input_node,
ivoire's avatar
ivoire committed
301
302
303
                              track_elem_t *p_track, xml_reader_t *p_xml_reader,
                              const char *psz_element,
                              xml_elem_hnd_t *p_handlers )
304
{
305
    VLC_UNUSED(psz_element); VLC_UNUSED(p_handlers);
306
    input_item_t *p_new_input = NULL;
307
    int i_ret;
308
309
310
311
312
313
314
315
316
317
318
319
320
    p_track = new_track();

    xml_elem_hnd_t track_elements[] =
        { {"array",   COMPLEX_CONTENT, {.cmplx = skip_element} },
          {"key",     SIMPLE_CONTENT,  {.smpl = save_data} },
          {"integer", SIMPLE_CONTENT,  {.smpl = save_data} },
          {"string",  SIMPLE_CONTENT,  {.smpl = save_data} },
          {"date",    SIMPLE_CONTENT,  {.smpl = save_data} },
          {"true",    SIMPLE_CONTENT,  {NULL} },
          {"false",   SIMPLE_CONTENT,  {NULL} },
          {NULL,      UNKNOWN_CONTENT, {NULL} }
        };

jpd's avatar
jpd committed
321
    i_ret = parse_dict( p_demux, p_input_node, p_track,
322
323
324
325
326
327
328
                        p_xml_reader, "dict", track_elements );

    msg_Dbg( p_demux, "name: %s, artist: %s, album: %s, genre: %s, trackNum: %s, location: %s",
             p_track->name, p_track->artist, p_track->album, p_track->genre, p_track->trackNum, p_track->location );

    if( !p_track->location )
    {
329
        msg_Warn( p_demux, "ignoring track without Location entry" );
330
        free_track( p_track );
331
        return true;
332
333
    }

334
    msg_Info( p_demux, "Adding '%s'", p_track->location );
335
    p_new_input = input_item_New( p_track->location, NULL );
336
    input_item_node_AppendItem( p_input_node, p_new_input );
337

338
339
    /* add meta info */
    add_meta( p_new_input, p_track );
340
    input_item_Release( p_new_input );
341

342
    p_demux->p_sys->i_ntracks++;
343
344
345
346
347
348
349

    free_track( p_track );
    return i_ret;
}

static track_elem_t *new_track()
{
350
351
    track_elem_t *p_track = malloc( sizeof *p_track );
    if( likely( p_track ) )
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
    {
        p_track->name = NULL;
        p_track->artist = NULL;
        p_track->album = NULL;
        p_track->genre = NULL;
        p_track->trackNum = NULL;
        p_track->location = NULL;
        p_track->duration = 0;
    }
    return p_track;
}

static void free_track( track_elem_t *p_track )
{
    if ( !p_track )
        return;

ivoire's avatar
ivoire committed
369
370
371
372
373
374
    FREENULL( p_track->name );
    FREENULL( p_track->artist );
    FREENULL( p_track->album );
    FREENULL( p_track->genre );
    FREENULL( p_track->trackNum );
    FREENULL( p_track->location );
375
376
377
378
    p_track->duration = 0;
    free( p_track );
}

ivoire's avatar
ivoire committed
379
380
static bool save_data( track_elem_t *p_track, const char *psz_name,
                       char *psz_value)
381
382
383
{
    /* exit if setting is impossible */
    if( !psz_name || !psz_value || !p_track )
384
        return false;
385
386

    /* re-convert xml special characters inside psz_value */
387
    vlc_xml_decode( psz_value );
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402

#define SAVE_INFO( name, value ) \
    if( !strcmp( psz_name, name ) ) { p_track->value = strdup( psz_value ); }

    SAVE_INFO( "Name", name )
    else SAVE_INFO( "Artist", artist )
    else SAVE_INFO( "Album", album )
    else SAVE_INFO( "Genre", genre )
    else SAVE_INFO( "Track Number", trackNum )
    else SAVE_INFO( "Location", location )
    else if( !strcmp( psz_name, "Total Time" ) )
    {
        long i_num = atol( psz_value );
        p_track->duration = (mtime_t) i_num*1000;
    }
403
#undef SAVE_INFO
404
    return true;
405
406
407
408
409
}

/**
 * \brief handles the supported <track> sub-elements
 */
ivoire's avatar
ivoire committed
410
static bool add_meta( input_item_t *p_input_item, track_elem_t *p_track )
411
412
413
{
    /* exit if setting is impossible */
    if( !p_input_item || !p_track )
414
        return false;
415

416
417
418
419
420
421
422
423
424
#define SET_INFO( type, prop ) \
    if( p_track->prop ) {input_item_Set##type( p_input_item, p_track->prop );}
    SET_INFO( Title, name )
    SET_INFO( Artist, artist )
    SET_INFO( Album, album )
    SET_INFO( Genre, genre )
    SET_INFO( TrackNum, trackNum )
    SET_INFO( Duration, duration )
#undef SET_INFO
425
    return true;
426
427
428
429
430
}

/**
 * \brief skips complex element content that we can't manage
 */
jpd's avatar
jpd committed
431
static bool skip_element( demux_t *p_demux, input_item_node_t *p_input_node,
ivoire's avatar
ivoire committed
432
433
                          track_elem_t *p_track, xml_reader_t *p_xml_reader,
                          const char *psz_element, xml_elem_hnd_t *p_handlers )
434
{
jpd's avatar
jpd committed
435
    VLC_UNUSED(p_demux); VLC_UNUSED(p_input_node);
436
    VLC_UNUSED(p_track); VLC_UNUSED(p_handlers);
437
    const char *node;
438
    int type;
439

440
441
442
    while( (type = xml_ReaderNextNode( p_xml_reader, &node )) > 0 )
        if( type == XML_READER_ENDELEM && !strcmp( psz_element, node ) )
            return true;
443
    return false;
444
}