audioscrobbler.c 33 KB
Newer Older
1
2
3
/*****************************************************************************
 * audioscrobbler.c : audioscrobbler submission plugin
 *****************************************************************************
4
 * Copyright (C) 2006-2007 the VideoLAN team
5
6
 * $Id$
 *
7
 * Author: Rafaël Carré <funman at videolanorg>
8
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
26
27
28
/* audioscrobbler protocol version: 1.2
 * http://www.audioscrobbler.net/development/protocol/
 *
 * TODO:    "Now Playing" feature (not mandatory)
 */
29
30
31
32
33
34
35
/*****************************************************************************
 * Preamble
 *****************************************************************************/

#if defined( WIN32 )
#include <time.h>
#endif
36

37
#include <vlc/vlc.h>
zorglub's avatar
zorglub committed
38
#include <vlc_interface.h>
39
40
41
42
43
#include <vlc_meta.h>
#include <vlc_md5.h>
#include <vlc_block.h>
#include <vlc_stream.h>
#include <vlc_url.h>
zorglub's avatar
zorglub committed
44
45
46
#include <vlc_network.h>
#include <vlc_interface.h>
#include <vlc_playlist.h>
47
48
49
50
51

/*****************************************************************************
 * Local prototypes
 *****************************************************************************/

Rafaël Carré's avatar
Rafaël Carré committed
52
/* Keeps track of metadata to be submitted */
53
54
typedef struct audioscrobbler_song_t
{
55
56
57
58
59
60
61
    char        *psz_a;             /**< track artist     */
    char        *psz_t;             /**< track title      */
    char        *psz_b;             /**< track album      */
    char        *psz_n;             /**< track number     */
    int         i_l;                /**< track length     */
    char        *psz_m;             /**< musicbrainz id   */
    time_t      date;               /**< date since epoch */
62
63
64
65
} audioscrobbler_song_t;

struct intf_sys_t
{
66
67
68
69
    audioscrobbler_song_t   p_queue[50];        /**< songs not submitted yet*/
    int                     i_songs;            /**< number of songs        */

    vlc_mutex_t             lock;               /**< p_sys mutex            */
70

Rafaël Carré's avatar
Rafaël Carré committed
71
    /* data about audioscrobbler session */
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
    time_t                  next_exchange;      /**< when can we send data  */
    unsigned int            i_interval;         /**< waiting interval (secs)*/

    /* submission of played songs */
    char                    *psz_submit_host;   /**< where to submit data   */
    int                     i_submit_port;      /**< port to which submit   */
    char                    *psz_submit_file;   /**< file to which submit   */

    /* submission of playing song */
#if 0 //NOT USED
    char                    *psz_nowp_host;     /**< where to submit data   */
    int                     i_nowp_port;        /**< port to which submit   */
    char                    *psz_nowp_file;     /**< file to which submit   */
#endif
    vlc_bool_t              b_handshaked;       /**< are we authenticated ? */
87
    char                    psz_auth_token[33]; /**< Authentication token */
88

Rafaël Carré's avatar
Rafaël Carré committed
89
    /* data about song currently playing */
90
91
92
93
94
95
96
97
98
    audioscrobbler_song_t   p_current_song;     /**< song being played      */

    time_t                  time_pause;         /**< time when vlc paused   */
    time_t                  time_total_pauses;  /**< total time in pause    */

    vlc_bool_t              b_submit;           /**< do we have to submit ? */

    vlc_bool_t              b_state_cb;         /**< if we registered the
                                                 * "state" callback         */
99
100
101

    vlc_bool_t              b_meta_read;        /**< if we read the song's
                                                 * metadata already         */
102
103
};

104
105
106
107
static int  Open            ( vlc_object_t * );
static void Close           ( vlc_object_t * );
static void Unload          ( intf_thread_t * );
static void Run             ( intf_thread_t * );
108

109
static int ItemChange       ( vlc_object_t *, const char *, vlc_value_t,
Rafaël Carré's avatar
Rafaël Carré committed
110
                                vlc_value_t, void * );
111
static int PlayingChange    ( vlc_object_t *, const char *, vlc_value_t,
Rafaël Carré's avatar
Rafaël Carré committed
112
                                vlc_value_t, void * );
113
114
115
116
117
118
119

static int AddToQueue       ( intf_thread_t * );
static int Handshake        ( intf_thread_t * );
static int ReadMetaData     ( intf_thread_t * );
static void DeleteSong      ( audioscrobbler_song_t* );
static int ParseURL         ( char *, char **, char **, int * );
static void HandleInterval  ( time_t *, unsigned int * );
120
121
122
123
124

/*****************************************************************************
 * Module descriptor
 ****************************************************************************/

Rafaël Carré's avatar
Rafaël Carré committed
125
126
127
128
#define USERNAME_TEXT       N_("Username")
#define USERNAME_LONGTEXT   N_("The username of your last.fm account")
#define PASSWORD_TEXT       N_("Password")
#define PASSWORD_LONGTEXT   N_("The password of your last.fm account")
Rafaël Carré's avatar
Rafaël Carré committed
129

130
131
/* This error value is used when last.fm plugin has to be unloaded. */
#define VLC_AUDIOSCROBBLER_EFATAL -69
Rafaël Carré's avatar
Rafaël Carré committed
132

Rafaël Carré's avatar
Rafaël Carré committed
133
/* last.fm client identifier */
134
135
136
137
138
139
#define CLIENT_NAME     PACKAGE
#define CLIENT_VERSION  VERSION

/* HTTP POST request : to submit data */
#define    POST_REQUEST "POST /%s HTTP/1.1\n"                               \
                        "Accept-Encoding: identity\n"                       \
140
                        "Content-length: %u\n"                              \
141
142
143
144
145
146
147
148
149
150
151
152
                        "Connection: close\n"                               \
                        "Content-type: application/x-www-form-urlencoded\n" \
                        "Host: %s\n"                                        \
                        "User-agent: VLC Media Player/%s\r\n"               \
                        "\r\n"                                              \
                        "%s\r\n"                                            \
                        "\r\n"

vlc_module_begin();
    set_category( CAT_INTERFACE );
    set_subcategory( SUBCAT_INTERFACE_CONTROL );
    set_shortname( N_( "Audioscrobbler" ) );
153
    set_description( N_("Submission of played songs to last.fm") );
154
    add_string( "lastfm-username", "", NULL,
155
                USERNAME_TEXT, USERNAME_LONGTEXT, VLC_FALSE );
156
    add_password( "lastfm-password", "", NULL,
157
                PASSWORD_TEXT, PASSWORD_LONGTEXT, VLC_FALSE );
158
159
160
161
162
163
164
165
166
    set_capability( "interface", 0 );
    set_callbacks( Open, Close );
vlc_module_end();

/*****************************************************************************
 * Open: initialize and create stuff
 *****************************************************************************/
static int Open( vlc_object_t *p_this )
{
Rafaël Carré's avatar
Rafaël Carré committed
167
168
    playlist_t      *p_playlist;
    intf_thread_t   *p_intf     = ( intf_thread_t* ) p_this;
169
    intf_sys_t      *p_sys      = calloc( 1, sizeof( intf_sys_t ) );
Rafaël Carré's avatar
Rafaël Carré committed
170

171
    if( !p_sys )
172
        return VLC_ENOMEM;
Rafaël Carré's avatar
Rafaël Carré committed
173

174
175
    p_intf->p_sys = p_sys;

176
177
178
    vlc_mutex_init( p_this, &p_sys->lock );

    p_playlist = pl_Yield( p_intf );
Rafaël Carré's avatar
Rafaël Carré committed
179
    PL_LOCK;
180
    var_AddCallback( p_playlist, "playlist-current", ItemChange, p_intf );
Rafaël Carré's avatar
Rafaël Carré committed
181
    PL_UNLOCK;
182
183
184
185
186
187
188
189
190
191
192
193
    pl_Release( p_playlist );

    p_intf->pf_run = Run;

    return VLC_SUCCESS;
}

/*****************************************************************************
 * Close: destroy interface stuff
 *****************************************************************************/
static void Close( vlc_object_t *p_this )
{
194
    playlist_t                  *p_playlist;
195
    input_thread_t              *p_input;
Rafaël Carré's avatar
Rafaël Carré committed
196
197
    intf_thread_t               *p_intf = ( intf_thread_t* ) p_this;
    intf_sys_t                  *p_sys  = p_intf->p_sys;
198

199
    p_playlist = pl_Yield( p_intf );
200
    PL_LOCK;
201

202
    var_DelCallback( p_playlist, "playlist-current", ItemChange, p_intf );
203

Rafaël Carré's avatar
Rafaël Carré committed
204
    p_input = p_playlist->p_input;
205
    if ( p_input )
206
    {
207
        vlc_object_yield( p_input );
208
209
210
211

        if( p_sys->b_state_cb )
            var_DelCallback( p_input, "state", PlayingChange, p_intf );

212
        vlc_object_release( p_input );
213
214
    }

215
    PL_UNLOCK;
216
217
    pl_Release( p_playlist );

218
219
    p_intf->b_dead = VLC_TRUE;
    /* we lock the mutex in case p_sys is being accessed from a callback */
220
    vlc_mutex_lock ( &p_sys->lock );
221
222
223
    int i;
    for( i = 0; i < p_sys->i_songs; i++ )
        DeleteSong( &p_sys->p_queue[i] );
Rafaël Carré's avatar
Rafaël Carré committed
224
225
    free( p_sys->psz_submit_host );
    free( p_sys->psz_submit_file );
226
227
228
229
#if 0 //NOT USED
    free( p_sys->psz_nowp_host );
    free( p_sys->psz_nowp_file );
#endif
230
231
232
233
234
    vlc_mutex_unlock ( &p_sys->lock );
    vlc_mutex_destroy( &p_sys->lock );
    free( p_sys );
}

235
236
237
238
239
240
241
242
243
244
245
246
247
248

/*****************************************************************************
 * Unload: Unloads the audioscrobbler when encountering fatal errors
 *****************************************************************************/
static void Unload( intf_thread_t *p_this )
{
    vlc_object_kill( p_this );
    vlc_object_detach( p_this );
    if( p_this->p_module )
        module_Unneed( p_this, p_this->p_module );
    vlc_mutex_destroy( &p_this->change_lock );
    vlc_object_destroy( p_this );
}

249
/*****************************************************************************
250
 * Run : call Handshake() then submit songs
251
 *****************************************************************************/
252
static void Run( intf_thread_t *p_intf )
253
{
254
    char                    *psz_submit, *psz_submit_song, *psz_submit_tmp;
Rafaël Carré's avatar
Rafaël Carré committed
255
    int                     i_net_ret;
256
    int                     i_song;
257
258
    uint8_t                 p_buffer[1024];
    char                    *p_buffer_pos;
Rafaël Carré's avatar
Rafaël Carré committed
259
    int                     i_post_socket;
260
    vlc_bool_t              b_die;
261

262
    intf_sys_t *p_sys = p_intf->p_sys;
Rafaël Carré's avatar
Rafaël Carré committed
263

Rafaël Carré's avatar
Rafaël Carré committed
264
    /* main loop */
265
    while( !p_intf->b_die && !p_intf->p_libvlc->b_die )
266
    {
267
268
269
270
271
272
273
274
275
        /* waiting for data to submit, if waiting interval is elapsed */
        vlc_object_lock( p_intf );
        if( time( NULL ) < p_sys->next_exchange )
            b_die = vlc_object_timedwait( p_intf, p_sys->next_exchange );
        else
            b_die = vlc_object_wait( p_intf );
        vlc_object_unlock( p_intf );

        if( b_die )
276
        {
277
            msg_Dbg( p_intf, "audioscrobbler is dying");
278
            return;
279
        }
280
281
282

        /* handshake if needed */
        if( p_sys->b_handshaked == VLC_FALSE )
283
        {
284
285
286
            msg_Dbg( p_intf, "Handshaking with last.fm ..." );

            switch( Handshake( p_intf ) )
287
            {
288
289
290
291
292
293
294
295
                case VLC_ENOMEM:
                    Unload( p_intf );
                    return;

                case VLC_ENOVAR:
                    /* username not set */
                    intf_UserFatal( p_intf, VLC_FALSE,
                        _("Last.fm username not set"),
296
297
298
                        _("Please set a username or disable the "
                        "audioscrobbler plugin, and restart VLC.\n"
                        "Visit http://www.last.fm/join/ to get an account.")
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
                    );
                    Unload( p_intf );
                    return;

                case VLC_SUCCESS:
                    msg_Dbg( p_intf, "Handshake successfull :)" );
                    p_sys->b_handshaked = VLC_TRUE;
                    p_sys->i_interval = 0;
                    time( &p_sys->next_exchange );
                    break;

                case VLC_AUDIOSCROBBLER_EFATAL:
                    msg_Warn( p_intf, "Unloading..." );
                    Unload( p_intf );
                    return;

                case VLC_EGENERIC:
                default:
                    /* protocol error : we'll try later */
                    HandleInterval( &p_sys->next_exchange, &p_sys->i_interval );
                    break;
320
            }
321
322
323
324
325
326
            /* if handshake failed let's restart the loop */
            if( p_sys->b_handshaked == VLC_FALSE )
                continue;
        }

        msg_Dbg( p_intf, "Going to submit some data..." );
327

328
329
330
331
332
        if( !asprintf( &psz_submit, "s=%s", p_sys->psz_auth_token ) )
        {   /* Out of memory */
            Unload( p_intf );
            return;
        }
333

334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
        /* forge the HTTP POST request */
        vlc_mutex_lock( &p_sys->lock );
        audioscrobbler_song_t *p_song;
        for( i_song = 0 ; i_song < p_sys->i_songs ; i_song++ )
        {
            p_song = &p_sys->p_queue[i_song];
            if( !asprintf( &psz_submit_song,
                    "&a%%5B%d%%5D=%s&t%%5B%d%%5D=%s"
                    "&i%%5B%d%%5D=%llu&o%%5B%d%%5D=P&r%%5B%d%%5D="
                    "&l%%5B%d%%5D=%d&b%%5B%d%%5D=%s"
                    "&n%%5B%d%%5D=%s&m%%5B%d%%5D=%s",
                    i_song, p_song->psz_a,           i_song, p_song->psz_t,
                    i_song, (uintmax_t)p_song->date, i_song, i_song,
                    i_song, p_song->i_l,             i_song, p_song->psz_b,
                    i_song, p_song->psz_n,           i_song, p_song->psz_m
            ) )
350
351
352
353
354
            {   /* Out of memory */
                vlc_mutex_unlock( &p_sys->lock );
                Unload( p_intf );
                return;
            }
355
356
357
358
            psz_submit_tmp = psz_submit;
            if( !asprintf( &psz_submit, "%s%s",
                    psz_submit_tmp, psz_submit_song ) )
            {   /* Out of memory */
359
                free( psz_submit_tmp );
360
                free( psz_submit_song );
361
                vlc_mutex_unlock( &p_sys->lock );
362
363
                Unload( p_intf );
                return;
364
            }
365
366
367
368
            free( psz_submit_song );
            free( psz_submit_tmp );
        }
        vlc_mutex_unlock( &p_sys->lock );
369

370
371
        i_post_socket = net_ConnectTCP( p_intf,
            p_sys->psz_submit_host, p_sys->i_submit_port );
372

373
374
375
376
377
        if ( i_post_socket == -1 )
        {
            /* If connection fails, we assume we must handshake again */
            HandleInterval( &p_sys->next_exchange, &p_sys->i_interval );
            p_sys->b_handshaked = VLC_FALSE;
378
            free( psz_submit );
379
380
            continue;
        }
381

382
383
384
385
386
387
388
        /* we transmit the data */
        i_net_ret = net_Printf(
            VLC_OBJECT( p_intf ), i_post_socket, NULL,
            POST_REQUEST, p_sys->psz_submit_file,
            (unsigned)strlen( psz_submit ), p_sys->psz_submit_file,
            VERSION, psz_submit
        );
Rafaël Carré's avatar
Rafaël Carré committed
389

390
391
392
393
394
395
396
397
        free( psz_submit );
        if ( i_net_ret == -1 )
        {
            /* If connection fails, we assume we must handshake again */
            HandleInterval( &p_sys->next_exchange, &p_sys->i_interval );
            p_sys->b_handshaked = VLC_FALSE;
            continue;
        }
398

399
400
401
402
403
404
405
        i_net_ret = net_Read( p_intf, i_post_socket, NULL,
                    p_buffer, 1023, VLC_FALSE );
        if ( i_net_ret <= 0 )
        {
            /* if we get no answer, something went wrong : try again */
            continue;
        }
406

407
408
        net_Close( i_post_socket );
        p_buffer[i_net_ret] = '\0';
409

410
411
412
413
414
415
416
        p_buffer_pos = strstr( ( char * ) p_buffer, "FAILED" );
        if ( p_buffer_pos )
        {
            msg_Warn( p_intf, "%s", p_buffer_pos );
            HandleInterval( &p_sys->next_exchange, &p_sys->i_interval );
            continue;
        }
417

418
419
420
        p_buffer_pos = strstr( ( char * ) p_buffer, "BADSESSION" );
        if ( p_buffer_pos )
        {
421
            msg_Dbg( p_intf, "Authentication failed, handshaking again" );
422
423
424
425
            p_sys->b_handshaked = VLC_FALSE;
            HandleInterval( &p_sys->next_exchange, &p_sys->i_interval );
            continue;
        }
426

427
428
        p_buffer_pos = strstr( ( char * ) p_buffer, "OK" );
        if ( p_buffer_pos )
429
        {
430
431
432
433
434
435
            int i;
            for( i = 0; i < p_sys->i_songs; i++ )
                DeleteSong( &p_sys->p_queue[i] );
            p_sys->i_songs = 0;
            p_sys->i_interval = 0;
            time( &p_sys->next_exchange );
436
            msg_Dbg( p_intf, "Submission successful!" );
437
438
439
        }
        else
        {
440
            msg_Dbg( p_intf, "Authentication failed, handshaking again" );
441
442
443
            p_sys->b_handshaked = VLC_FALSE;
            HandleInterval( &p_sys->next_exchange, &p_sys->i_interval );
            continue;
444
445
446
447
448
449
450
451
452
453
        }
    }
}

/*****************************************************************************
 * PlayingChange: Playing status change callback
 *****************************************************************************/
static int PlayingChange( vlc_object_t *p_this, const char *psz_var,
                       vlc_value_t oldval, vlc_value_t newval, void *p_data )
{
Rafaël Carré's avatar
Rafaël Carré committed
454
455
    intf_thread_t   *p_intf = ( intf_thread_t* ) p_data;
    intf_sys_t      *p_sys  = p_intf->p_sys;
456

457
    VLC_UNUSED( p_this ); VLC_UNUSED( psz_var );
458

459
    if( p_intf->b_dead )
Rafaël Carré's avatar
Rafaël Carré committed
460
461
        return VLC_SUCCESS;

462
    if( p_sys->b_meta_read == VLC_FALSE && newval.i_int == PLAYING_S )
463
464
        ReadMetaData( p_intf );

465
    if( newval.i_int == END_S || newval.i_int == ERROR_S )
466
    {
467
468
469
        /* We'll try to add the previously playing song in the queue */
        if( AddToQueue( p_intf ) == VLC_ENOMEM )
            return VLC_ENOMEM;
470
    }
471
472
    else if( oldval.i_int == PLAYING_S && newval.i_int == PAUSE_S )
        time( &p_sys->time_pause );
473
    else if( oldval.i_int == PAUSE_S && newval.i_int == PLAYING_S )
474
475
476
477
478
479
480
481
482
483
484
485
        p_sys->time_total_pauses += time( NULL ) - p_sys->time_pause;

    return VLC_SUCCESS;
}

/*****************************************************************************
 * ItemChange: Playlist item change callback
 *****************************************************************************/
static int ItemChange( vlc_object_t *p_this, const char *psz_var,
                       vlc_value_t oldval, vlc_value_t newval, void *p_data )
{
    playlist_t          *p_playlist;
486
    input_thread_t      *p_input;
Rafaël Carré's avatar
Rafaël Carré committed
487
488
    intf_thread_t       *p_intf     = ( intf_thread_t* ) p_data;
    intf_sys_t          *p_sys      = p_intf->p_sys;
489
490
491
492
493
494
495
496
497
498
    input_item_t        *p_item;
    vlc_value_t         video_val;

    VLC_UNUSED( p_this ); VLC_UNUSED( psz_var );
    VLC_UNUSED( oldval ); VLC_UNUSED( newval );

    if( p_intf->b_dead )
        return VLC_SUCCESS;

    p_sys->b_state_cb       = VLC_FALSE;
499
    p_sys->b_meta_read      = VLC_FALSE;
500
    p_sys->b_submit         = VLC_FALSE;
501
502
503
504
505

    p_playlist = pl_Yield( p_intf );
    PL_LOCK;
    p_input = p_playlist->p_input;

506
    if( !p_input || p_input->b_dead )
507
508
509
510
511
512
513
514
515
516
    {
        PL_UNLOCK;
        pl_Release( p_playlist );
        return VLC_SUCCESS;
    }

    vlc_object_yield( p_input );
    PL_UNLOCK;
    pl_Release( p_playlist );

517
518
519
520
521
522
    p_item = input_GetItem( p_input );
    if( !p_item )
    {
        vlc_object_release( p_input );
        return VLC_SUCCESS;
    }
523

524
525
526
527
528
529
530
    var_Change( p_input, "video-es", VLC_VAR_CHOICESCOUNT, &video_val, NULL );
    if( ( video_val.i_int > 0 ) || p_item->i_type == ITEM_TYPE_NET )
    {
        msg_Dbg( p_this, "Not an audio local file, not submitting");
        vlc_object_release( p_input );
        return VLC_SUCCESS;
    }
531

532
533
    p_sys->time_total_pauses = 0;
    time( &p_sys->p_current_song.date );
534

535
536
537
    var_AddCallback( p_input, "state", PlayingChange, p_intf );
    p_sys->b_state_cb = VLC_TRUE;

538
539
    if( input_item_IsPreparsed( p_item ) )
        ReadMetaData( p_intf );
540
    /* if the input item was not preparsed, we'll do it in PlayingChange()
541
     * callback, when "state" == PLAYING_S */
542
543
544

    vlc_object_release( p_input );
    return VLC_SUCCESS;
545
546
547
548
549
550
551
552
}

/*****************************************************************************
 * AddToQueue: Add the played song to the queue to be submitted
 *****************************************************************************/
static int AddToQueue ( intf_thread_t *p_this )
{
    time_t                      played_time;
553
554
    intf_sys_t                  *p_sys = p_this->p_sys;

555
    vlc_mutex_lock( &p_sys->lock );
556
557
558
    if( !p_sys->b_submit )
    {
        DeleteSong( &p_sys->p_current_song );
559
        vlc_mutex_unlock( &p_sys->lock );
560
561
        return VLC_SUCCESS;
    }
562

563
564
    /* wait for the user to listen enough before submitting */
    time ( &played_time );
565
    played_time -= p_sys->p_current_song.date;
566
    played_time -= p_sys->time_total_pauses;
567
568
569
570
571
    if( ( played_time < 240 ) &&
        ( played_time < ( p_sys->p_current_song.i_l / 2 ) ) )
    {
        msg_Dbg( p_this, "Song not listened long enough, not submitting" );
        DeleteSong( &p_sys->p_current_song );
572
        vlc_mutex_unlock( &p_sys->lock );
Rafaël Carré's avatar
Rafaël Carré committed
573
        return VLC_SUCCESS;
574
    }
Rafaël Carré's avatar
Rafaël Carré committed
575

576
    if( p_sys->p_current_song.i_l < 30 )
577
    {
578
579
        msg_Dbg( p_this, "Song too short (< 30s), not submitting" );
        DeleteSong( &p_sys->p_current_song );
580
        vlc_mutex_unlock( &p_sys->lock );
581
582
583
        return VLC_SUCCESS;
    }

584
585
    if( !p_sys->p_current_song.psz_a || !*p_sys->p_current_song.psz_a ||
        !p_sys->p_current_song.psz_t || !*p_sys->p_current_song.psz_t )
586
    {
587
588
        msg_Dbg( p_this, "Missing artist or title, not submitting" );
        DeleteSong( &p_sys->p_current_song );
589
        vlc_mutex_unlock( &p_sys->lock );
590
        return VLC_SUCCESS;
591
592
    }

593
    if( p_sys->i_songs == 50 )
594
    {
595
596
        msg_Warn( p_this, "Submission queue is full, not submitting" );
        DeleteSong( &p_sys->p_current_song );
597
        vlc_mutex_unlock( &p_sys->lock );
598
        return VLC_SUCCESS;
599
600
    }

601
    msg_Dbg( p_this, "Song will be submitted." );
602

603
604
#define QUEUE_COPY( a ) \
    p_sys->p_queue[p_sys->i_songs].a = p_sys->p_current_song.a
605

606
607
608
#define QUEUE_COPY_NULL( a ) \
    QUEUE_COPY( a ); \
    p_sys->p_current_song.a = NULL
609

610
611
612
613
614
615
616
617
618
    QUEUE_COPY( i_l );
    QUEUE_COPY_NULL( psz_n );
    QUEUE_COPY_NULL( psz_a );
    QUEUE_COPY_NULL( psz_t );
    QUEUE_COPY_NULL( psz_b );
    QUEUE_COPY_NULL( psz_m );
    QUEUE_COPY( date );
#undef QUEUE_COPY_NULL
#undef QUEUE_COPY
Rafaël Carré's avatar
Rafaël Carré committed
619

620
    p_sys->i_songs++;
621
622
623
624
625
626
627

    /* signal the main loop we have something to submit */
    vlc_object_lock( p_this );
    vlc_cond_signal( &p_this->object_wait );
    vlc_object_unlock( p_this );

    vlc_mutex_unlock( &p_sys->lock );
628
629
    return VLC_SUCCESS;
}
630

631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
/*****************************************************************************
 * ParseURL : Split an http:// URL into host, file, and port
 *
 * Example: "62.216.251.205:80/protocol_1.2"
 *      will be split into "62.216.251.205", 80, "protocol_1.2"
 *
 * psz_url will be freed before returning
 * *psz_file & *psz_host will be freed before use
 *
 * Return value:
 *  VLC_ENOMEM      Out Of Memory
 *  VLC_EGENERIC    Invalid url provided
 *  VLC_SUCCESS     Success
 *****************************************************************************/
static int ParseURL( char *psz_url, char **psz_host, char **psz_file,
                        int *i_port )
{
    int i_pos;
    int i_len = strlen( psz_url );
    FREENULL( *psz_host );
    FREENULL( *psz_file );
652

653
654
655
    i_pos = strcspn( psz_url, ":" );
    if( i_pos == i_len )
        return VLC_EGENERIC;
656

657
658
659
    *psz_host = strndup( psz_url, i_pos );
    if( !*psz_host )
        return VLC_ENOMEM;
660

661
662
663
664
665
666
667
    i_pos++; /* skip the ':' */
    *i_port = atoi( psz_url + i_pos );
    if( *i_port <= 0 )
    {
        FREENULL( *psz_host );
        return VLC_EGENERIC;
    }
668

669
    i_pos = strcspn( psz_url, "/" );
670

671
672
673
674
675
676
677
678
679
680
681
682
    if( i_pos == i_len )
        return VLC_EGENERIC;

    i_pos++; /* skip the '/' */
    *psz_file = strdup( psz_url + i_pos );
    if( !*psz_file )
    {
        FREENULL( *psz_host );
        return VLC_ENOMEM;
    }

    free( psz_url );
683
684
685
686
687
688
689
690
    return VLC_SUCCESS;
}

/*****************************************************************************
 * Handshake : Init audioscrobbler connection
 *****************************************************************************/
static int Handshake( intf_thread_t *p_this )
{
691
692
693
694
695
696
697
    char                *psz_username, *psz_password;
    time_t              timestamp;
    char                psz_timestamp[33];

    struct md5_s        p_struct_md5;
    char                psz_password_md5[33];

698
    stream_t            *p_stream;
699
700
701
702
703
704
    char                *psz_handshake_url;
    uint8_t             p_buffer[1024];
    char                *p_buffer_pos;

    int                 i_ret;
    char                *psz_url;
705

Rafaël Carré's avatar
Rafaël Carré committed
706
707
    intf_thread_t       *p_intf                 = ( intf_thread_t* ) p_this;
    intf_sys_t          *p_sys                  = p_this->p_sys;
708

709
710
711
    psz_username = config_GetPsz( p_this, "lastfm-username" );
    if( !psz_username )
        return VLC_ENOMEM;
712

713
714
715
716
    psz_password = config_GetPsz( p_this, "lastfm-password" );
    if( !psz_password )
    {
        free( psz_username );
Rafaël Carré's avatar
Rafaël Carré committed
717
        return VLC_ENOMEM;
718
    }
Rafaël Carré's avatar
Rafaël Carré committed
719

720
721
722
723
724
725
726
    /* username or password have not been setup */
    if ( !*psz_username || !*psz_password )
    {
        free( psz_username );
        free( psz_password );
        return VLC_ENOVAR;
    }
Rafaël Carré's avatar
Rafaël Carré committed
727

728
    time( &timestamp );
Rafaël Carré's avatar
Rafaël Carré committed
729

730
731
732
733
    /* generates a md5 hash of the password */
    InitMD5( &p_struct_md5 );
    AddMD5( &p_struct_md5, ( uint8_t* ) psz_password, strlen( psz_password ) );
    EndMD5( &p_struct_md5 );
734

735
    free( psz_password );
736

737
738
739
740
741
742
743
744
745
746
    int i;
    for ( i = 0; i < 4; i++ )
    {
        sprintf( &psz_password_md5[8*i], "%02x%02x%02x%02x",
            p_struct_md5.p_digest[i] & 0xff,
            ( p_struct_md5.p_digest[i] >> 8 ) & 0xff,
            ( p_struct_md5.p_digest[i] >> 16 ) & 0xff,
            p_struct_md5.p_digest[i] >> 24
        );
    }
747

748
    snprintf( psz_timestamp, 33, "%llu", (uintmax_t)timestamp );
749

750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
    /* generates a md5 hash of :
     * - md5 hash of the password, plus
     * - timestamp in clear text
     */
    InitMD5( &p_struct_md5 );
    AddMD5( &p_struct_md5, ( uint8_t* ) psz_password_md5, 32 );
    AddMD5( &p_struct_md5, ( uint8_t* ) psz_timestamp, strlen( psz_timestamp ));
    EndMD5( &p_struct_md5 );

    for ( i = 0; i < 4; i++ )
    {
        sprintf( &p_sys->psz_auth_token[8*i], "%02x%02x%02x%02x",
            p_struct_md5.p_digest[i] & 0xff,
            ( p_struct_md5.p_digest[i] >> 8 ) & 0xff,
            ( p_struct_md5.p_digest[i] >> 16 ) & 0xff,
            p_struct_md5.p_digest[i] >> 24
        );
    }

    p_sys->psz_auth_token[32] = '\0';
770

771
772
773
774
775
776
777
778
779
780
781
782
    if( !asprintf( &psz_handshake_url,
    "http://post.audioscrobbler.com/?hs=true&p=1.2&c=%s&v=%s&u=%s&t=%s&a=%s",
        CLIENT_NAME, CLIENT_VERSION, psz_username, psz_timestamp,
        p_sys->psz_auth_token ) )
    {
        free( psz_username );
        return VLC_ENOMEM;
    }
    free( psz_username );

    /* send the http handshake request */
    p_stream = stream_UrlNew( p_intf, psz_handshake_url );
783
784
785
786
787
    free( psz_handshake_url );

    if( !p_stream )
        return VLC_EGENERIC;

Rafaël Carré's avatar
Rafaël Carré committed
788
    /* read answer */
789
790
    i_ret = stream_Read( p_stream, p_buffer, 1023 );
    if( i_ret == 0 )
791
792
    {
        stream_Delete( p_stream );
793
        return VLC_EGENERIC;
794
    }
795
    p_buffer[i_ret] = '\0';
796
797
    stream_Delete( p_stream );

798
    p_buffer_pos = strstr( ( char* ) p_buffer, "FAILED " );
799
800
    if ( p_buffer_pos )
    {
Rafaël Carré's avatar
Rafaël Carré committed
801
        /* handshake request failed, sorry */
802
803
        msg_Warn( p_this, "last.fm handshake failed: %s", p_buffer_pos + 7 );
        return VLC_EGENERIC;
804
805
    }

806
    p_buffer_pos = strstr( ( char* ) p_buffer, "BADAUTH" );
807
808
    if ( p_buffer_pos )
    {
809
810
811
        /* authentication failed, bad username/password combination */
        intf_UserFatal( p_this, VLC_FALSE,
            _("last.fm: Authentication failed"),
812
813
            _("last.fm username or password is incorrect. "
              "Please verify your settings and relaunch VLC." ) );
814
        return VLC_AUDIOSCROBBLER_EFATAL;
815
816
    }

817
    p_buffer_pos = strstr( ( char* ) p_buffer, "BANNED" );
818
819
    if ( p_buffer_pos )
    {
820
821
        /* oops, our version of vlc has been banned by last.fm servers */
        msg_Err( p_intf, "This version of VLC has been banned by last.fm. "
822
                         "You should upgrade VLC, or disable the last.fm plugin." );
823
        return VLC_AUDIOSCROBBLER_EFATAL;
824
825
    }

826
827
    p_buffer_pos = strstr( ( char* ) p_buffer, "BADTIME" );
    if ( p_buffer_pos )
828
    {
829
830
831
832
        /* The system clock isn't good */
        msg_Err( p_intf, "last.fm handshake failed because your clock is too "
                         "much shifted. Please correct it, and relaunch VLC." );
        return VLC_AUDIOSCROBBLER_EFATAL;
833
834
    }

835
836
837
    p_buffer_pos = strstr( ( char* ) p_buffer, "OK" );
    if ( !p_buffer_pos )
        goto proto;
838

839
840
841
842
    p_buffer_pos = strstr( p_buffer_pos, "\n" );
    if( !p_buffer_pos || strlen( p_buffer_pos ) < 34 )
        goto proto;
    p_buffer_pos++; /* we skip the '\n' */
843

844
845
    /* save the session ID */
    snprintf( &p_sys->psz_auth_token[0], 33, "%s", p_buffer_pos );
Rafaël Carré's avatar
Rafaël Carré committed
846

847
848
849
    p_buffer_pos = strstr( p_buffer_pos, "http://" );
    if( !p_buffer_pos || strlen( p_buffer_pos ) == 7 )
        goto proto;
850

851
852
853
854
855
856
    /* We need to read the nowplaying url */
    p_buffer_pos += 7; /* we skip "http://" */
#if 0 //NOT USED
    psz_url = strndup( p_buffer_pos, strcspn( p_buffer_pos, "\n" ) );
    if( !psz_url )
        goto oom;
857

858
859
    switch( ParseURL( psz_url, &p_sys->psz_nowp_host,
                &p_sys->psz_nowp_file, &p_sys->i_nowp_port ) )
860
    {
861
862
863
864
865
866
867
        case VLC_ENOMEM:
            goto oom;
        case VLC_EGENERIC:
            goto proto;
        case VLC_SUCCESS:
        default:
            break;
868
    }
869
870
871
872
873
874
875
876
877
878
879
880
881
#endif
    p_buffer_pos = strstr( p_buffer_pos, "http://" );
    if( !p_buffer_pos || strlen( p_buffer_pos ) == 7 )
        goto proto;

    /* We need to read the submission url */
    p_buffer_pos += 7; /* we skip "http://" */
    psz_url = strndup( p_buffer_pos, strcspn( p_buffer_pos, "\n" ) );
    if( !psz_url )
        goto oom;

    switch( ParseURL( psz_url, &p_sys->psz_submit_host,
                &p_sys->psz_submit_file, &p_sys->i_submit_port ) )
882
    {
883
884
885
886
887
888
889
        case VLC_ENOMEM:
            goto oom;
        case VLC_EGENERIC:
            goto proto;
        case VLC_SUCCESS:
        default:
            break;
890
891
892
    }

    return VLC_SUCCESS;
893
894
895
896
897
898
899

oom:
    return VLC_ENOMEM;

proto:
    msg_Warn( p_intf, "Handshake: can't recognize server protocol" );
    return VLC_EGENERIC;
900
901
902
}

/*****************************************************************************
903
 * DeleteSong : Delete the char pointers in a song
904
 *****************************************************************************/
905
static void DeleteSong( audioscrobbler_song_t* p_song )
906
{
907
908
909
910
911
    FREENULL( p_song->psz_a );
    FREENULL( p_song->psz_b );
    FREENULL( p_song->psz_t );
    FREENULL( p_song->psz_m );
    FREENULL( p_song->psz_n );
912
913
914
}

/*****************************************************************************
915
 * ReadMetaData : Read meta data when parsed by vlc
916
917
918
919
 *****************************************************************************/
static int ReadMetaData( intf_thread_t *p_this )
{
    playlist_t          *p_playlist;
920
921
    input_thread_t      *p_input;
    input_item_t        *p_item;
922

923
    intf_sys_t          *p_sys = p_this->p_sys;
924

925
926
927
928
929
    p_playlist = pl_Yield( p_this );
    PL_LOCK;
    p_input = p_playlist->p_input;
    if( !p_input )
    {
930
931
932
        PL_UNLOCK;
        pl_Release( p_playlist );
        return( VLC_SUCCESS );
933
934
935
936
937
938
    }

    vlc_object_yield( p_input );
    PL_UNLOCK;
    pl_Release( p_playlist );

939
940
    p_item = input_GetItem( p_input );
    if( !p_item )
941
        return VLC_SUCCESS;
Rafaël Carré's avatar
Rafaël Carré committed
942

943
    char *psz_meta;
944
945
946
947
948
949
#define ALLOC_ITEM_META( a, b ) \
    psz_meta = input_item_Get##b( p_item ); \
    if( psz_meta && *psz_meta ) \
    { \
        a = encode_URI_component( psz_meta ); \
        if( !a ) \
Rafaël Carré's avatar
Rafaël Carré committed
950
        { \
951
            free( psz_meta ); \
952
953
954
955
            return VLC_ENOMEM; \
        } \
        free( psz_meta ); \
    }
956

957
958
    vlc_mutex_lock( &p_sys->lock );

959
960
    p_sys->b_meta_read = VLC_TRUE;

961
962
963
    ALLOC_ITEM_META( p_sys->p_current_song.psz_a, Artist )
    else
    {
964
        vlc_mutex_unlock( &p_sys->lock );
965
966
967
968
969
        msg_Dbg( p_this, "No artist.." );
        vlc_object_release( p_input );
        free( psz_meta );
        return VLC_EGENERIC;
    }
970

971
972
    ALLOC_ITEM_META( p_sys->p_current_song.psz_t, Title )
    else
973
    {
974
        vlc_mutex_unlock( &p_sys->lock );
975
976
977
978
979
980
        msg_Dbg( p_this, "No track name.." );
        vlc_object_release( p_input );
        free( p_sys->p_current_song.psz_a );
        free( psz_meta );
        return VLC_EGENERIC;
    }
981

982
983
984
    /* Now we have read the mandatory meta data, so we can submit that info */
    p_sys->b_submit = VLC_TRUE;

985
986
987
    ALLOC_ITEM_META( p_sys->p_current_song.psz_b, Album )
    else
        p_sys->p_current_song.psz_b = calloc( 1, 1 );
988

989
990
991
    ALLOC_ITEM_META( p_sys->p_current_song.psz_m, TrackID )
    else
        p_sys->p_current_song.psz_m = calloc( 1, 1 );
992

993
    p_sys->p_current_song.i_l = input_item_GetDuration( p_item ) / 1000000;
994

995
996
997
998
    ALLOC_ITEM_META( p_sys->p_current_song.psz_n, TrackNum )
    else
        p_sys->p_current_song.psz_n = calloc( 1, 1 );
#undef ALLOC_ITEM_META
999

1000
    msg_Dbg( p_this, "Meta data registered" );
1001

1002
    vlc_mutex_unlock( &p_sys->lock );
1003
1004
1005
1006
    vlc_object_release( p_input );
    return VLC_SUCCESS;

}
1007

1008
1009
1010
1011
1012
1013
static void HandleInterval( time_t *next, unsigned int *i_interval )
{
    if( *i_interval == 0 )
    {
        /* first interval is 1 minute */
        *i_interval = 60;
1014
    }
1015
1016
1017
1018
1019
1020
1021
1022
    else
    {
        /* else we double the previous interval, up to 120 minutes */
        *i_interval = *i_interval * 2;
        if( *i_interval > 60*120 )
            *i_interval = 60*120;
    }
    *next = time( NULL ) + *i_interval;
1023
}
1024