taglib.cpp 16.6 KB
Newer Older
1
2
3
4
/*****************************************************************************
 * taglib.cpp: Taglib tag parser/writer
 *****************************************************************************
 * Copyright (C) 2003-2006 the VideoLAN team
dionoea's avatar
dionoea committed
5
 * $Id$
6
7
 *
 * Authors: Clément Stenac <zorglub@videolan.org>
8
 *          Rafaël Carré <funman@videolanorg>
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 *
 * 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., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/
24

25
#include <vlc/vlc.h>
26
#include <vlc_playlist.h>
27
#include <vlc_meta.h>
zorglub's avatar
zorglub committed
28
#include <vlc_demux.h>
29
#include <vlc_strings.h>
30
31
32

#include <fileref.h>
#include <tag.h>
33
#include <tstring.h>
zorglub's avatar
zorglub committed
34
#include <id3v2tag.h>
35
36
#include <textidentificationframe.h>
#include <tbytevector.h>
zorglub's avatar
zorglub committed
37
#include <mpegfile.h>
38
#include <flacfile.h>
39
#include <attachedpictureframe.h>
40
//#include <oggflacfile.h> /* ogg flac files aren't auto-casted by TagLib */
41
42
43
44
#include <flacfile.h>
#include <flacproperties.h>
#include <vorbisfile.h>
#include <vorbisproperties.h>
45
#include <xiphcomment.h>
46
47
#include <uniquefileidentifierframe.h>
#include <textidentificationframe.h>
48
//#include <relativevolumeframe.h> /* parse the tags without TagLib helpers? */
49

50
51
52
static int  ReadMeta    ( vlc_object_t * );
static int  DownloadArt ( vlc_object_t * );
static int  WriteMeta   ( vlc_object_t * );
53
54
55
56

vlc_module_begin();
    set_capability( "meta reader", 1000 );
    set_callbacks( ReadMeta, NULL );
57
58
59
60
61
62
    add_submodule();
        set_capability( "art downloader", 50 );
        set_callbacks( DownloadArt, NULL );
    add_submodule();
        set_capability( "meta writer", 50 );
        set_callbacks( WriteMeta, NULL );
63
vlc_module_end();
zorglub's avatar
zorglub committed
64

65
66
using namespace TagLib;

67
/* Try detecting embedded art */
68
static void DetectImage( FileRef f, demux_t *p_demux )
69
{
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
    demux_meta_t        *p_demux_meta   = (demux_meta_t *)p_demux->p_private;
    vlc_meta_t          *p_meta         = p_demux_meta->p_meta;
    int                 i_score         = -1;

    /* Preferred type of image
     * The 21 types are defined in id3v2 standard:
     * http://www.id3.org/id3v2.4.0-frames */
    static const int pi_cover_score[] = {
        0,  /* Other */
        5,  /* 32x32 PNG image that should be used as the file icon */
        4,  /* File icon of a different size or format. */
        20, /* Front cover image of the album. */
        19, /* Back cover image of the album. */
        13, /* Inside leaflet page of the album. */
        18, /* Image from the album itself. */
        17, /* Picture of the lead artist or soloist. */
        16, /* Picture of the artist or performer. */
        14, /* Picture of the conductor. */
        15, /* Picture of the band or orchestra. */
        9,  /* Picture of the composer. */
        8,  /* Picture of the lyricist or text writer. */
        7,  /* Picture of the recording location or studio. */
        10, /* Picture of the artists during recording. */
        11, /* Picture of the artists during performance. */
        6,  /* Picture from a movie or video related to the track. */
        1,  /* Picture of a large, coloured fish. */
        12, /* Illustration related to the track. */
        3,  /* Logo of the band or performer. */
        2   /* Logo of the publisher (record company). */
    };

101
    if( MPEG::File *mpeg = dynamic_cast<MPEG::File *>(f.file() ) )
102
    {
103
        ID3v2::Tag  *p_tag = mpeg->ID3v2Tag();
104
105
        if( !p_tag )
            return;
106
        ID3v2::FrameList list = p_tag->frameListMap()[ "APIC" ];
107
108
        if( list.isEmpty() )
            return;
109
        ID3v2::AttachedPictureFrame *p_apic;
110
111

        TAB_INIT( p_demux_meta->i_attachments, p_demux_meta->attachments );
112
        for( ID3v2::FrameList::Iterator iter = list.begin();
113
114
                iter != list.end(); iter++ )
        {
115
            p_apic = dynamic_cast<ID3v2::AttachedPictureFrame*>(*iter);
116
117
118
            input_attachment_t *p_attachment;

            const char *psz_name, *psz_mime, *psz_description;
119
            ByteVector p_data_taglib; const char *p_data; int i_data;
120
121

            psz_mime = p_apic->mimeType().toCString(true);
122
123
124
125
126
127
128
129
130
131
132
133

            /* some old iTunes version not only sets incorrectly the mime type
             * but also embeds incorrectly the image.
             * Recent versions seem to behave correctly */
            if( !strncmp( psz_mime, "PNG", 3 ) )
            {
                msg_Warn( p_demux,
                    "%s: Invalid picture embedded by broken iTunes version",
                    f.file()->name() );
                break;
            }

134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
            psz_description = p_apic->description().toCString(true);
            psz_name = psz_description;

            p_data_taglib = p_apic->picture();
            p_data = p_data_taglib.data();
            i_data = p_data_taglib.size();

            msg_Dbg( p_demux, "Found embedded art: %s (%s) is %i bytes",
                    psz_name, psz_mime, i_data );

            p_attachment = vlc_input_attachment_New( psz_name, psz_mime,
                    psz_description, p_data, i_data );
            TAB_APPEND_CAST( (input_attachment_t**),
                    p_demux_meta->i_attachments, p_demux_meta->attachments,
                    p_attachment );

            if( pi_cover_score[p_apic->type()] > i_score )
            {
                i_score = pi_cover_score[p_apic->type()];
                char *psz_url;
                if( asprintf( &psz_url, "attachment://%s",
                        p_attachment->psz_name ) == -1 )
                    return;
                vlc_meta_SetArtURL( p_meta, psz_url );
                free( psz_url );
            }
        }
161
    }
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
    else
    if( Ogg::Vorbis::File *oggv = dynamic_cast<Ogg::Vorbis::File *>(f.file() ) )
    {
        Ogg::XiphComment *p_tag = oggv->tag();
        if( !p_tag )
            return;

        StringList mime_list = p_tag->fieldListMap()[ "COVERARTMIME" ];
        StringList art_list = p_tag->fieldListMap()[ "COVERART" ];

        /* we support only one cover in ogg/vorbis */
        if( mime_list.size() != 1 || art_list.size() != 1 )
            return;

        input_attachment_t *p_attachment;

        const char *psz_name, *psz_mime, *psz_description;
        uint8_t *p_data;
        int i_data;

        psz_name = "cover";
        psz_mime = mime_list[0].toCString(true);
        psz_description = "cover";

        i_data = vlc_b64_decode_binary( &p_data, art_list[0].toCString(true) );

        msg_Dbg( p_demux, "Found embedded art: %s (%s) is %i bytes",
                    psz_name, psz_mime, i_data );

        TAB_INIT( p_demux_meta->i_attachments, p_demux_meta->attachments );
        p_attachment = vlc_input_attachment_New( psz_name, psz_mime,
                psz_description, p_data, i_data );
Rafaël Carré's avatar
Rafaël Carré committed
194
        free( p_data );
195
196
197
198
199
200
201
202

        TAB_APPEND_CAST( (input_attachment_t**),
                p_demux_meta->i_attachments, p_demux_meta->attachments,
                p_attachment );

        vlc_meta_SetArtURL( p_meta, "attachment://cover" );
    }

203
204
#if 0
    //flac embedded images are extracted in the flac demuxer
205
206
    else if( FLAC::File *flac =
             dynamic_cast<FLAC::File *>(f.file() ) )
207
    {
208
209
210
        p_tag = flac->ID3v2Tag();
        if( p_tag )
            return;
211
        ID3v2::FrameList l = p_tag->frameListMap()[ "APIC" ];
212
213
        if( l.isEmpty() )
            return;
214
215
            vlc_meta_SetArtURL( p_meta, "APIC" );
    }
216
#endif
217
#if 0
218
219
220
/* TagLib doesn't support MP4 file yet */
    else if( MP4::File *mp4 =
               dynamic_cast<MP4::File *>( f.file() ) )
221
    {
222
223
        MP4::Tag *mp4tag =
                dynamic_cast<MP4::Tag *>( mp4->tag() );
224
225
226
227
228
        if( mp4tag && mp4tag->cover().size() )
            vlc_meta_SetArtURL( p_meta, "MP4C" );
    }
#endif
}
zorglub's avatar
zorglub committed
229

230
231
static int ReadMeta( vlc_object_t *p_this )
{
232
233
234
    demux_t         *p_demux = (demux_t *)p_this;
    demux_meta_t    *p_demux_meta = (demux_meta_t*)p_demux->p_private;
    vlc_meta_t      *p_meta = p_demux_meta->p_meta;
235

236
237
    TAB_INIT( p_demux_meta->i_attachments, p_demux_meta->attachments );
    p_demux_meta->p_meta = NULL;
Rafaël Carré's avatar
Rafaël Carré committed
238

239
    FileRef f( p_demux->psz_path );
Rafaël Carré's avatar
Rafaël Carré committed
240
241
242
243
244
    if( f.isNull() )
        return VLC_EGENERIC;

    if ( !f.tag() || f.tag()->isEmpty() )
        return VLC_EGENERIC;
245

246
    p_demux_meta->p_meta = p_meta = vlc_meta_New();
247
    Tag *p_tag = f.tag();
Rafaël Carré's avatar
Rafaël Carré committed
248

249
250
    if( MPEG::File *p_mpeg =
        dynamic_cast<MPEG::File *>(f.file() ) )
Rafaël Carré's avatar
Rafaël Carré committed
251
252
    {
        if( p_mpeg->ID3v2Tag() )
253
        {
254
255
256
257
            ID3v2::Tag *p_tag = p_mpeg->ID3v2Tag();
            ID3v2::FrameList list = p_tag->frameListMap()["UFID"];
            ID3v2::UniqueFileIdentifierFrame* p_ufid;
            for( ID3v2::FrameList::Iterator iter = list.begin();
Rafaël Carré's avatar
Rafaël Carré committed
258
                    iter != list.end(); iter++ )
259
            {
260
                p_ufid = dynamic_cast<ID3v2::UniqueFileIdentifierFrame*>(*iter);
Rafaël Carré's avatar
Rafaël Carré committed
261
262
                const char *owner = p_ufid->owner().toCString();
                if (!strcmp( owner, "http://musicbrainz.org" ))
263
                {
Rafaël Carré's avatar
Rafaël Carré committed
264
                    /* ID3v2 UFID contains up to 64 bytes binary data
265
                        * but in our case it will be a '\0'
Rafaël Carré's avatar
Rafaël Carré committed
266
267
268
269
270
271
272
273
274
                        * terminated string */
                    char *psz_ufid = (char*) malloc( 64 );
                    int j = 0;
                    while( ( j < 63 ) &&
                            ( j < p_ufid->identifier().size() ) )
                        psz_ufid[j] = p_ufid->identifier()[j++];
                    psz_ufid[j] = '\0';
                    vlc_meta_SetTrackID( p_meta, psz_ufid );
                    free( psz_ufid );
275
276
                }
            }
Rafaël Carré's avatar
Rafaël Carré committed
277

278
            list = p_tag->frameListMap()["TXXX"];
279
280
            ID3v2::UserTextIdentificationFrame* p_txxx;
            for( ID3v2::FrameList::Iterator iter = list.begin();
Rafaël Carré's avatar
Rafaël Carré committed
281
                    iter != list.end(); iter++ )
282
            {
283
                p_txxx = dynamic_cast<ID3v2::UserTextIdentificationFrame*>(*iter);
Rafaël Carré's avatar
Rafaël Carré committed
284
                const char *psz_desc= p_txxx->description().toCString();
285
                vlc_meta_AddExtra( p_meta, psz_desc,
Rafaël Carré's avatar
Rafaël Carré committed
286
287
288
                            p_txxx->fieldList().toString().toCString());
            }
#if 0
289
            list = p_tag->frameListMap()["RVA2"];
290
291
            ID3v2::RelativeVolumeFrame* p_rva2;
            for( ID3v2::FrameList::Iterator iter = list.begin();
Rafaël Carré's avatar
Rafaël Carré committed
292
293
                    iter != list.end(); iter++ )
            {
294
                p_rva2 = dynamic_cast<ID3v2::RelativeVolumeFrame*>(*iter);
Rafaël Carré's avatar
Rafaël Carré committed
295
                /* TODO: process rva2 frames */
296
297
            }
#endif
298
            list = p_tag->frameList();
299
            ID3v2::Frame* p_t;
Rafaël Carré's avatar
Rafaël Carré committed
300
            char psz_tag[4];
301
            for( ID3v2::FrameList::Iterator iter = list.begin();
Rafaël Carré's avatar
Rafaël Carré committed
302
                    iter != list.end(); iter++ )
303
            {
304
                p_t = dynamic_cast<ID3v2::Frame*> (*iter);
Rafaël Carré's avatar
Rafaël Carré committed
305
306
307
308
309
310
311
312
313
314
315
316
                memcpy( psz_tag, p_t->frameID().data(), 4);

#define SET( foo, bar ) if( !strncmp( psz_tag, foo, 4 ) ) \
vlc_meta_Set##bar( p_meta, p_t->toString().toCString(true))
                SET( "TPUB", Publisher );
                SET( "TCOP", Copyright );
                SET( "TENC", EncodedBy );
                SET( "TLAN", Language );
                //SET( "POPM", Rating ); /* rating needs special handling in id3v2 */
                //if( !strncmp( psz_tag, "RVA2", 4 ) )
                    /* TODO */
#undef SET
317
318
            }
        }
Rafaël Carré's avatar
Rafaël Carré committed
319
    }
320

321
322
    else if( Ogg::Vorbis::File *p_ogg_v =
        dynamic_cast<Ogg::Vorbis::File *>(f.file() ) )
Rafaël Carré's avatar
Rafaël Carré committed
323
324
325
326
327
328
    {
        int i_ogg_v_length = p_ogg_v->audioProperties()->length();

        input_thread_t *p_input = (input_thread_t *)
                vlc_object_find( p_demux,VLC_OBJECT_INPUT, FIND_PARENT );
        if( p_input )
329
        {
Rafaël Carré's avatar
Rafaël Carré committed
330
331
            input_item_t *p_item = input_GetItem( p_input );
            if( p_item )
332
333
                input_item_SetDuration( p_item,
                        (mtime_t) i_ogg_v_length * 1000000 );
Rafaël Carré's avatar
Rafaël Carré committed
334
335
            vlc_object_release( p_input );
        }
336

Rafaël Carré's avatar
Rafaël Carré committed
337
338
339
340
341
342
343
    }
#if 0 /* at this moment, taglib is unable to detect ogg/flac files
* becauses type detection is based on file extension:
* ogg = ogg/vorbis
* flac = flac
* ø = ogg/flac
*/
344
345
    else if( Ogg::FLAC::File *p_ogg_f =
        dynamic_cast<Ogg::FLAC::File *>(f.file() ) )
Rafaël Carré's avatar
Rafaël Carré committed
346
347
348
349
350
351
352
353
    {
        long i_ogg_f_length = p_ogg_f->streamLength();
        input_thread_t *p_input = (input_thread_t *)
                vlc_object_find( p_demux, VLC_OBJECT_INPUT, FIND_PARENT );
        if( p_input )
        {
            input_item_t *p_item = input_GetItem( p_input );
            if( p_item )
354
355
                input_item_SetDuration( p_item,
                        (mtime_t) i_ogg_f_length * 1000000 );
Rafaël Carré's avatar
Rafaël Carré committed
356
357
358
359
            vlc_object_release( p_input );
        }
    }
#endif
360
361
    else if( FLAC::File *p_flac =
        dynamic_cast<FLAC::File *>(f.file() ) )
Rafaël Carré's avatar
Rafaël Carré committed
362
363
364
365
366
367
368
369
    {
        long i_flac_length = p_flac->audioProperties()->length();
        input_thread_t *p_input = (input_thread_t *)
                vlc_object_find( p_demux, VLC_OBJECT_INPUT, FIND_PARENT );
        if( p_input )
        {
            input_item_t *p_item = input_GetItem( p_input );
            if( p_item )
370
371
                input_item_SetDuration( p_item,
                        (mtime_t) i_flac_length * 1000000 );
Rafaël Carré's avatar
Rafaël Carré committed
372
373
374
            vlc_object_release( p_input );
        }
    }
375

376
#define SET( foo, bar ) vlc_meta_Set##foo( p_meta, p_tag->bar ().toCString(true))
377
#define SETINT( foo, bar ) { \
378
379
380
381
        char psz_tmp[10]; \
        snprintf( (char*)psz_tmp, 10, "%d", p_tag->bar() ); \
        vlc_meta_Set##foo( p_meta, (char*)psz_tmp ); \
    }
382

Rafaël Carré's avatar
Rafaël Carré committed
383
384
385
386
387
388
389
    SET( Title, title );
    SET( Artist, artist );
    SET( Album, album );
    SET( Description, comment );
    SET( Genre, genre );
    SETINT( Date, year );
    SETINT( Tracknum , track );
390
#undef SET
391
#undef SETINT
392

393
    DetectImage( f, p_demux );
Rafaël Carré's avatar
Rafaël Carré committed
394

Rafaël Carré's avatar
Rafaël Carré committed
395
    return VLC_SUCCESS;
396
}
397
398
399

static int WriteMeta( vlc_object_t *p_this )
{
Rafaël Carré's avatar
Rafaël Carré committed
400
401
402
    playlist_t *p_playlist = (playlist_t *)p_this;
    meta_export_t *p_export = (meta_export_t *)p_playlist->p_private;
    input_item_t *p_item = p_export->p_item;
403

404
405
406
407
408
    if( p_item == NULL )
    {
        msg_Err( p_this, "Can't save meta data of an empty input" );
        return VLC_EGENERIC;
    }
Rafaël Carré's avatar
Rafaël Carré committed
409

410
    FileRef f( p_export->psz_file );
411
    if( f.isNull() || !f.tag() || f.file()->readOnly() )
412
    {
413
414
415
416
417
418
        msg_Err( p_this, "File %s can't be opened for tag writing\n",
            p_export->psz_file );
        return VLC_EGENERIC;
    }

    msg_Dbg( p_this, "Writing metadata for %s", p_export->psz_file );
419

420
    Tag *p_tag = f.tag();
421

422
    char *psz_meta;
423

424
425
#define SET(a,b) \
        if(b) { \
426
427
            String *psz_##a = new String( b, \
                String::UTF8 ); \
428
429
430
431
432
433
434
435
            p_tag->set##a( *psz_##a ); \
            delete psz_##a; \
        }


    psz_meta = input_item_GetArtist( p_item );
    SET( Artist, psz_meta );
    free( psz_meta );
436

437
438
    psz_meta = input_item_GetTitle( p_item );
    if( !psz_meta ) psz_meta = input_item_GetName( p_item );
439
440
    String *psz_title = new String( psz_meta,
        String::UTF8 );
441
442
443
    p_tag->setTitle( *psz_title );
    delete psz_title;
    free( psz_meta );
444

445
446
447
    psz_meta = input_item_GetAlbum( p_item );
    SET( Album, psz_meta );
    free( psz_meta );
448

449
450
451
    psz_meta = input_item_GetGenre( p_item );
    SET( Genre, psz_meta );
    free( psz_meta );
452

453
454
455
456
457
#undef SET

    psz_meta = input_item_GetDate( p_item );
    if( psz_meta ) p_tag->setYear( atoi( psz_meta ) );
    free( psz_meta );
458

459
460
461
    psz_meta = input_item_GetTrackNum( p_item );
    if( psz_meta ) p_tag->setTrack( atoi( psz_meta ) );
    free( psz_meta );
462

463
464
    if( ID3v2::Tag *p_id3tag =
        dynamic_cast<ID3v2::Tag *>(p_tag) )
465
466
467
468
469
    {
#define WRITE( foo, bar ) \
        psz_meta = input_item_Get##foo( p_item ); \
        if( psz_meta ) \
        { \
470
471
            ByteVector p_byte( bar, 4 ); \
            ID3v2::TextIdentificationFrame p_frame( p_byte ); \
472
473
            p_frame.setText( psz_meta ); \
            p_id3tag->addFrame( &p_frame ); \
474
            free( psz_meta ); \
475
476
477
478
479
480
        } \

        WRITE( Publisher, "TPUB" );
        WRITE( Copyright, "TCOP" );
        WRITE( EncodedBy, "TENC" );
        WRITE( Language, "TLAN" );
481

482
#undef WRITE
483
    }
484
485
486

    f.save();
    return VLC_SUCCESS;
487
488
489
490
491
492
493
494
495
}

static int DownloadArt( vlc_object_t *p_this )
{
    /* We need to be passed the file name
     * Fetch the thing from the file, save it to the cache folder
     */
    return VLC_EGENERIC;
}
496