taglib.cpp 16.2 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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148

            psz_mime = p_apic->mimeType().toCString(true);
            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 );
            }
        }
149
    }
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
    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
182
        free( p_data );
183
184
185
186
187
188
189
190

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

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

191
192
#if 0
    //flac embedded images are extracted in the flac demuxer
193
194
    else if( FLAC::File *flac =
             dynamic_cast<FLAC::File *>(f.file() ) )
195
    {
196
197
198
        p_tag = flac->ID3v2Tag();
        if( p_tag )
            return;
199
        ID3v2::FrameList l = p_tag->frameListMap()[ "APIC" ];
200
201
        if( l.isEmpty() )
            return;
202
203
            vlc_meta_SetArtURL( p_meta, "APIC" );
    }
204
#endif
205
#if 0
206
207
208
/* TagLib doesn't support MP4 file yet */
    else if( MP4::File *mp4 =
               dynamic_cast<MP4::File *>( f.file() ) )
209
    {
210
211
        MP4::Tag *mp4tag =
                dynamic_cast<MP4::Tag *>( mp4->tag() );
212
213
214
215
216
        if( mp4tag && mp4tag->cover().size() )
            vlc_meta_SetArtURL( p_meta, "MP4C" );
    }
#endif
}
zorglub's avatar
zorglub committed
217

218
219
static int ReadMeta( vlc_object_t *p_this )
{
220
221
222
    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;
223

224
225
    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
226

227
    FileRef f( p_demux->psz_path );
Rafaël Carré's avatar
Rafaël Carré committed
228
229
230
231
232
    if( f.isNull() )
        return VLC_EGENERIC;

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

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

237
238
    if( MPEG::File *p_mpeg =
        dynamic_cast<MPEG::File *>(f.file() ) )
Rafaël Carré's avatar
Rafaël Carré committed
239
240
    {
        if( p_mpeg->ID3v2Tag() )
241
        {
242
243
244
245
            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
246
                    iter != list.end(); iter++ )
247
            {
248
                p_ufid = dynamic_cast<ID3v2::UniqueFileIdentifierFrame*>(*iter);
Rafaël Carré's avatar
Rafaël Carré committed
249
250
                const char *owner = p_ufid->owner().toCString();
                if (!strcmp( owner, "http://musicbrainz.org" ))
251
                {
Rafaël Carré's avatar
Rafaël Carré committed
252
                    /* ID3v2 UFID contains up to 64 bytes binary data
253
                        * but in our case it will be a '\0'
Rafaël Carré's avatar
Rafaël Carré committed
254
255
256
257
258
259
260
261
262
                        * 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 );
263
264
                }
            }
Rafaël Carré's avatar
Rafaël Carré committed
265

266
            list = p_tag->frameListMap()["TXXX"];
267
268
            ID3v2::UserTextIdentificationFrame* p_txxx;
            for( ID3v2::FrameList::Iterator iter = list.begin();
Rafaël Carré's avatar
Rafaël Carré committed
269
                    iter != list.end(); iter++ )
270
            {
271
                p_txxx = dynamic_cast<ID3v2::UserTextIdentificationFrame*>(*iter);
Rafaël Carré's avatar
Rafaël Carré committed
272
                const char *psz_desc= p_txxx->description().toCString();
273
                vlc_meta_AddExtra( p_meta, psz_desc,
Rafaël Carré's avatar
Rafaël Carré committed
274
275
276
                            p_txxx->fieldList().toString().toCString());
            }
#if 0
277
            list = p_tag->frameListMap()["RVA2"];
278
279
            ID3v2::RelativeVolumeFrame* p_rva2;
            for( ID3v2::FrameList::Iterator iter = list.begin();
Rafaël Carré's avatar
Rafaël Carré committed
280
281
                    iter != list.end(); iter++ )
            {
282
                p_rva2 = dynamic_cast<ID3v2::RelativeVolumeFrame*>(*iter);
Rafaël Carré's avatar
Rafaël Carré committed
283
                /* TODO: process rva2 frames */
284
285
            }
#endif
286
            list = p_tag->frameList();
287
            ID3v2::Frame* p_t;
Rafaël Carré's avatar
Rafaël Carré committed
288
            char psz_tag[4];
289
            for( ID3v2::FrameList::Iterator iter = list.begin();
Rafaël Carré's avatar
Rafaël Carré committed
290
                    iter != list.end(); iter++ )
291
            {
292
                p_t = dynamic_cast<ID3v2::Frame*> (*iter);
Rafaël Carré's avatar
Rafaël Carré committed
293
294
295
296
297
298
299
300
301
302
303
304
                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
305
306
            }
        }
Rafaël Carré's avatar
Rafaël Carré committed
307
    }
308

309
310
    else if( Ogg::Vorbis::File *p_ogg_v =
        dynamic_cast<Ogg::Vorbis::File *>(f.file() ) )
Rafaël Carré's avatar
Rafaël Carré committed
311
312
313
314
315
316
    {
        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 )
317
        {
Rafaël Carré's avatar
Rafaël Carré committed
318
319
            input_item_t *p_item = input_GetItem( p_input );
            if( p_item )
320
321
                input_item_SetDuration( p_item,
                        (mtime_t) i_ogg_v_length * 1000000 );
Rafaël Carré's avatar
Rafaël Carré committed
322
323
            vlc_object_release( p_input );
        }
324

Rafaël Carré's avatar
Rafaël Carré committed
325
326
327
328
329
330
331
    }
#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
*/
332
333
    else if( Ogg::FLAC::File *p_ogg_f =
        dynamic_cast<Ogg::FLAC::File *>(f.file() ) )
Rafaël Carré's avatar
Rafaël Carré committed
334
335
336
337
338
339
340
341
    {
        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 )
342
343
                input_item_SetDuration( p_item,
                        (mtime_t) i_ogg_f_length * 1000000 );
Rafaël Carré's avatar
Rafaël Carré committed
344
345
346
347
            vlc_object_release( p_input );
        }
    }
#endif
348
349
    else if( FLAC::File *p_flac =
        dynamic_cast<FLAC::File *>(f.file() ) )
Rafaël Carré's avatar
Rafaël Carré committed
350
351
352
353
354
355
356
357
    {
        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 )
358
359
                input_item_SetDuration( p_item,
                        (mtime_t) i_flac_length * 1000000 );
Rafaël Carré's avatar
Rafaël Carré committed
360
361
362
            vlc_object_release( p_input );
        }
    }
363

364
#define SET( foo, bar ) vlc_meta_Set##foo( p_meta, p_tag->bar ().toCString(true))
365
#define SETINT( foo, bar ) { \
366
367
368
369
        char psz_tmp[10]; \
        snprintf( (char*)psz_tmp, 10, "%d", p_tag->bar() ); \
        vlc_meta_Set##foo( p_meta, (char*)psz_tmp ); \
    }
370

Rafaël Carré's avatar
Rafaël Carré committed
371
372
373
374
375
376
377
    SET( Title, title );
    SET( Artist, artist );
    SET( Album, album );
    SET( Description, comment );
    SET( Genre, genre );
    SETINT( Date, year );
    SETINT( Tracknum , track );
378
#undef SET
379
#undef SETINT
380

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

Rafaël Carré's avatar
Rafaël Carré committed
383
    return VLC_SUCCESS;
384
}
385
386
387

static int WriteMeta( vlc_object_t *p_this )
{
Rafaël Carré's avatar
Rafaël Carré committed
388
389
390
    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;
391

392
393
394
395
396
    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
397

398
    FileRef f( p_export->psz_file );
399
    if( f.isNull() || !f.tag() || f.file()->readOnly() )
400
    {
401
402
403
404
405
406
        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 );
407

408
    Tag *p_tag = f.tag();
409

410
    char *psz_meta;
411

412
413
#define SET(a,b) \
        if(b) { \
414
415
            String *psz_##a = new String( b, \
                String::UTF8 ); \
416
417
418
419
420
421
422
423
            p_tag->set##a( *psz_##a ); \
            delete psz_##a; \
        }


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

425
426
    psz_meta = input_item_GetTitle( p_item );
    if( !psz_meta ) psz_meta = input_item_GetName( p_item );
427
428
    String *psz_title = new String( psz_meta,
        String::UTF8 );
429
430
431
    p_tag->setTitle( *psz_title );
    delete psz_title;
    free( psz_meta );
432

433
434
435
    psz_meta = input_item_GetAlbum( p_item );
    SET( Album, psz_meta );
    free( psz_meta );
436

437
438
439
    psz_meta = input_item_GetGenre( p_item );
    SET( Genre, psz_meta );
    free( psz_meta );
440

441
442
443
444
445
#undef SET

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

447
448
449
    psz_meta = input_item_GetTrackNum( p_item );
    if( psz_meta ) p_tag->setTrack( atoi( psz_meta ) );
    free( psz_meta );
450

451
452
    if( ID3v2::Tag *p_id3tag =
        dynamic_cast<ID3v2::Tag *>(p_tag) )
453
454
455
456
457
    {
#define WRITE( foo, bar ) \
        psz_meta = input_item_Get##foo( p_item ); \
        if( psz_meta ) \
        { \
458
459
            ByteVector p_byte( bar, 4 ); \
            ID3v2::TextIdentificationFrame p_frame( p_byte ); \
460
461
            p_frame.setText( psz_meta ); \
            p_id3tag->addFrame( &p_frame ); \
462
            free( psz_meta ); \
463
464
465
466
467
468
        } \

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

470
#undef WRITE
471
    }
472
473
474

    f.save();
    return VLC_SUCCESS;
475
476
477
478
479
480
481
482
483
}

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;
}
484