audioscrobbler.c 34.2 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
87
    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 ? */
    char                    psz_auth_token[33]; /**< authentification 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
99
100
    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         */
    vlc_bool_t              b_preparsed_cb;     /**< if we registered the
                                                 * "meta-preparsed" callback*/
101
102
};

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

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

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 * );
121
122
123
124
125

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

Rafaël Carré's avatar
Rafaël Carré committed
126
127
128
129
#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
130

131
132
/* 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
133

Rafaël Carré's avatar
Rafaël Carré committed
134
/* last.fm client identifier */
135
136
137
138
139
140
#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"                       \
141
                        "Content-length: %u\n"                              \
142
143
144
145
146
147
148
149
150
151
152
153
                        "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" ) );
154
    set_description( N_("Submission of played songs to last.fm") );
155
    add_string( "lastfm-username", "", NULL,
156
                USERNAME_TEXT, USERNAME_LONGTEXT, VLC_FALSE );
157
    add_password( "lastfm-password", "", NULL,
158
                PASSWORD_TEXT, PASSWORD_LONGTEXT, VLC_FALSE );
159
160
161
162
163
164
165
166
167
    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
168
169
    playlist_t      *p_playlist;
    intf_thread_t   *p_intf     = ( intf_thread_t* ) p_this;
170
    intf_sys_t      *p_sys      = calloc( 1, sizeof( intf_sys_t ) );
Rafaël Carré's avatar
Rafaël Carré committed
171

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

175
176
    p_intf->p_sys = p_sys;

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

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

    p_intf->pf_run = Run;

    return VLC_SUCCESS;
}

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

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

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

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

        if( p_sys->b_state_cb )
            var_DelCallback( p_input, "state", PlayingChange, p_intf );
        if( p_sys->b_preparsed_cb )
            var_DelCallback( p_input, "meta-preparsed", MetaPreparsed, p_intf );

215
        vlc_object_release( p_input );
216
217
    }

218
    PL_UNLOCK;
219
220
    pl_Release( p_playlist );

221
222
    p_intf->b_dead = VLC_TRUE;
    /* we lock the mutex in case p_sys is being accessed from a callback */
223
    vlc_mutex_lock ( &p_sys->lock );
224
225
226
    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
227
228
    free( p_sys->psz_submit_host );
    free( p_sys->psz_submit_file );
229
230
231
232
#if 0 //NOT USED
    free( p_sys->psz_nowp_host );
    free( p_sys->psz_nowp_file );
#endif
233
234
235
236
237
    vlc_mutex_unlock ( &p_sys->lock );
    vlc_mutex_destroy( &p_sys->lock );
    free( p_sys );
}

238
239
240
241
242
243
244
245
246
247
248
249
250
251

/*****************************************************************************
 * 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 );
}

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

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

Rafaël Carré's avatar
Rafaël Carré committed
267
    /* main loop */
268
    while( !p_intf->b_die && !p_intf->p_libvlc->b_die )
269
    {
270
271
272
273
274
275
276
277
278
        /* 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 )
279
280
        {
            msg_Dbg( p_intf, "audioscrobbler is dying\n");
281
            return;
282
        }
283
284
285

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

            switch( Handshake( p_intf ) )
290
            {
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
                case VLC_ENOMEM:
                    Unload( p_intf );
                    return;

                case VLC_ENOVAR:
                    /* username not set */
                    intf_UserFatal( p_intf, VLC_FALSE,
                        _("Last.fm username not set"),
                        _("Please set an username or disable "
                        "audioscrobbler plugin, and then restart VLC.\n"
                        "Visit https://www.last.fm/join/ to get an account")
                    );
                    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;
323
            }
324
325
326
327
328
329
            /* 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..." );
330

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

337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
        /* 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
            ) )
353
354
355
356
357
            {   /* Out of memory */
                vlc_mutex_unlock( &p_sys->lock );
                Unload( p_intf );
                return;
            }
358
359
360
361
            psz_submit_tmp = psz_submit;
            if( !asprintf( &psz_submit, "%s%s",
                    psz_submit_tmp, psz_submit_song ) )
            {   /* Out of memory */
362
                free( psz_submit_tmp );
363
                free( psz_submit_song );
364
                vlc_mutex_unlock( &p_sys->lock );
365
366
                Unload( p_intf );
                return;
367
            }
368
369
370
371
            free( psz_submit_song );
            free( psz_submit_tmp );
        }
        vlc_mutex_unlock( &p_sys->lock );
372

373
374
        i_post_socket = net_ConnectTCP( p_intf,
            p_sys->psz_submit_host, p_sys->i_submit_port );
375

376
377
378
379
380
        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;
381
            free( psz_submit );
382
383
            continue;
        }
384

385
386
387
388
389
390
391
        /* 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
392

393
394
395
396
397
398
399
400
        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;
        }
401

402
403
404
405
406
407
408
        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;
        }
409

410
411
        net_Close( i_post_socket );
        p_buffer[i_net_ret] = '\0';
412

413
414
415
416
417
418
419
        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;
        }
420

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

430
431
        p_buffer_pos = strstr( ( char * ) p_buffer, "OK" );
        if ( p_buffer_pos )
432
        {
433
434
435
436
437
438
439
440
441
442
443
444
445
446
            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 );
            msg_Dbg( p_intf, "Submission successfull!" );
        }
        else
        {
            msg_Dbg( p_intf, "Authentification failed, handshaking again" );
            p_sys->b_handshaked = VLC_FALSE;
            HandleInterval( &p_sys->next_exchange, &p_sys->i_interval );
            continue;
447
448
449
450
451
452
453
454
455
456
        }
    }
}

/*****************************************************************************
 * 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
457
458
    intf_thread_t   *p_intf = ( intf_thread_t* ) p_data;
    intf_sys_t      *p_sys  = p_intf->p_sys;
459

460
    VLC_UNUSED( p_this ); VLC_UNUSED( psz_var );
461

462
    if( p_intf->b_dead )
Rafaël Carré's avatar
Rafaël Carré committed
463
464
        return VLC_SUCCESS;

465
    if( newval.i_int == END_S || newval.i_int == ERROR_S )
466
    {
467
468
469
470
471
472
473
474
475
        playlist_t *p_playlist = pl_Yield( p_intf );
        PL_LOCK;
        if( p_playlist->request.i_status == PLAYLIST_STOPPED )
        {
            /* if we stopped, we won't submit playing song */
            p_sys->b_submit = VLC_FALSE;
        }
        PL_UNLOCK;
        pl_Release( p_playlist );
476
    }
477
478
    else if( oldval.i_int == PLAYING_S && newval.i_int == PAUSE_S )
        time( &p_sys->time_pause );
479
    else if( oldval.i_int == PAUSE_S && newval.i_int == PLAYING_S )
480
481
482
483
484
        p_sys->time_total_pauses += time( NULL ) - p_sys->time_pause;

    return VLC_SUCCESS;
}

485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
/*****************************************************************************
 * MetaPreparsed: Meta data reading callback
 *****************************************************************************/
static int MetaPreparsed( vlc_object_t *p_this, const char *psz_var,
                       vlc_value_t oldval, vlc_value_t newval, void *p_data )
{
    intf_thread_t       *p_intf     = ( intf_thread_t* ) p_data;

    if( p_intf->b_dead )
        return VLC_SUCCESS;

    ReadMetaData( p_intf );
    return VLC_SUCCESS;
}

500
501
502
503
504
505
506
/*****************************************************************************
 * 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;
507
    input_thread_t      *p_input;
Rafaël Carré's avatar
Rafaël Carré committed
508
509
    intf_thread_t       *p_intf     = ( intf_thread_t* ) p_data;
    intf_sys_t          *p_sys      = p_intf->p_sys;
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
    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_preparsed_cb   = VLC_FALSE;
    p_sys->b_state_cb       = VLC_FALSE;

    /* We'll try to add the previously playing song in the queue */
    if( AddToQueue( p_intf ) == VLC_ENOMEM )
        return VLC_ENOMEM;
525
526
527
528
529

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

530
    if( !p_input || p_input->b_dead )
531
532
533
    {
        PL_UNLOCK;
        pl_Release( p_playlist );
534
        p_sys->b_submit = VLC_FALSE;
535
536
537
538
539
540
541
        return VLC_SUCCESS;
    }

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

542
543
544
545
546
547
548
    p_item = input_GetItem( p_input );
    if( !p_item )
    {
        vlc_object_release( p_input );
        p_sys->b_submit = VLC_FALSE;
        return VLC_SUCCESS;
    }
549

550
551
552
553
554
555
556
557
    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 );
        p_sys->b_submit = VLC_FALSE;
        return VLC_SUCCESS;
    }
558

559
560
561
    p_sys->b_submit = VLC_TRUE;
    p_sys->time_total_pauses = 0;
    time( &p_sys->p_current_song.date );
562

563
564
565
    var_AddCallback( p_input, "state", PlayingChange, p_intf );
    p_sys->b_state_cb = VLC_TRUE;

566
567
568
569
570
571
572
573
    if( input_item_IsPreparsed( p_item ) )
    {
        ReadMetaData( p_intf );
        vlc_object_release( p_input );
        return VLC_SUCCESS;
    }
    else
    {
574
575
576
#if 0
        /* XXX this won't work. One way would be to monitor p_input "state",
         * once it is no more INIT_S, meta should be parsed */
577
578
579
        /* We'll read the meta data when it will be preparsed */
        var_AddCallback( p_input, "meta-preparsed", MetaPreparsed, p_intf );
        p_sys->b_preparsed_cb = VLC_TRUE;
580
#endif
581
582
583
        vlc_object_release( p_input );
        return VLC_SUCCESS;
    }
584
585
586
587
588
589
590
591
}

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

594
    vlc_mutex_lock( &p_sys->lock );
595
596
597
    if( !p_sys->b_submit )
    {
        DeleteSong( &p_sys->p_current_song );
598
        vlc_mutex_unlock( &p_sys->lock );
599
600
        return VLC_SUCCESS;
    }
601

602
603
    /* wait for the user to listen enough before submitting */
    time ( &played_time );
604
    played_time -= p_sys->p_current_song.date;
605
    played_time -= p_sys->time_total_pauses;
606
607
608
609
610
    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 );
611
        vlc_mutex_unlock( &p_sys->lock );
Rafaël Carré's avatar
Rafaël Carré committed
612
        return VLC_SUCCESS;
613
    }
Rafaël Carré's avatar
Rafaël Carré committed
614

615
    if( p_sys->p_current_song.i_l < 30 )
616
    {
617
618
        msg_Dbg( p_this, "Song too short (< 30s), not submitting" );
        DeleteSong( &p_sys->p_current_song );
619
        vlc_mutex_unlock( &p_sys->lock );
620
621
622
        return VLC_SUCCESS;
    }

623
624
    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 )
625
    {
626
627
        msg_Dbg( p_this, "Missing artist or title, not submitting" );
        DeleteSong( &p_sys->p_current_song );
628
        vlc_mutex_unlock( &p_sys->lock );
629
        return VLC_SUCCESS;
630
631
    }

632
    if( p_sys->i_songs == 50 )
633
    {
634
635
        msg_Warn( p_this, "Submission queue is full, not submitting" );
        DeleteSong( &p_sys->p_current_song );
636
        vlc_mutex_unlock( &p_sys->lock );
637
        return VLC_SUCCESS;
638
639
    }

640
    msg_Dbg( p_this, "Song will be submitted." );
641

642
643
#define QUEUE_COPY( a ) \
    p_sys->p_queue[p_sys->i_songs].a = p_sys->p_current_song.a
644

645
646
647
#define QUEUE_COPY_NULL( a ) \
    QUEUE_COPY( a ); \
    p_sys->p_current_song.a = NULL
648

649
650
651
652
653
654
655
656
657
    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
658

659
    p_sys->i_songs++;
660
661
662
663
664
665
666

    /* 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 );
667
668
    return VLC_SUCCESS;
}
669

670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
/*****************************************************************************
 * 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 );
691

692
693
694
    i_pos = strcspn( psz_url, ":" );
    if( i_pos == i_len )
        return VLC_EGENERIC;
695

696
697
698
    *psz_host = strndup( psz_url, i_pos );
    if( !*psz_host )
        return VLC_ENOMEM;
699

700
701
702
703
704
705
706
    i_pos++; /* skip the ':' */
    *i_port = atoi( psz_url + i_pos );
    if( *i_port <= 0 )
    {
        FREENULL( *psz_host );
        return VLC_EGENERIC;
    }
707

708
    i_pos = strcspn( psz_url, "/" );
709

710
711
712
713
714
715
716
717
718
719
720
721
    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 );
722
723
724
725
726
727
728
729
    return VLC_SUCCESS;
}

/*****************************************************************************
 * Handshake : Init audioscrobbler connection
 *****************************************************************************/
static int Handshake( intf_thread_t *p_this )
{
730
731
732
733
734
735
736
    char                *psz_username, *psz_password;
    time_t              timestamp;
    char                psz_timestamp[33];

    struct md5_s        p_struct_md5;
    char                psz_password_md5[33];

737
    stream_t            *p_stream;
738
739
740
741
742
743
    char                *psz_handshake_url;
    uint8_t             p_buffer[1024];
    char                *p_buffer_pos;

    int                 i_ret;
    char                *psz_url;
744

Rafaël Carré's avatar
Rafaël Carré committed
745
746
    intf_thread_t       *p_intf                 = ( intf_thread_t* ) p_this;
    intf_sys_t          *p_sys                  = p_this->p_sys;
747

748
749
750
    psz_username = config_GetPsz( p_this, "lastfm-username" );
    if( !psz_username )
        return VLC_ENOMEM;
751

752
753
754
755
    psz_password = config_GetPsz( p_this, "lastfm-password" );
    if( !psz_password )
    {
        free( psz_username );
Rafaël Carré's avatar
Rafaël Carré committed
756
        return VLC_ENOMEM;
757
    }
Rafaël Carré's avatar
Rafaël Carré committed
758

759
760
761
762
763
764
765
    /* 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
766

767
    time( &timestamp );
Rafaël Carré's avatar
Rafaël Carré committed
768

769
770
771
772
    /* 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 );
773

774
    free( psz_password );
775

776
777
778
779
780
781
782
783
784
785
    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
        );
    }
786

787
    snprintf( psz_timestamp, 33, "%llu", (uintmax_t)timestamp );
788

789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
    /* 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';
809

810
811
812
813
814
815
816
817
818
819
820
821
    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 );
822
823
824
825
826
    free( psz_handshake_url );

    if( !p_stream )
        return VLC_EGENERIC;

Rafaël Carré's avatar
Rafaël Carré committed
827
    /* read answer */
828
829
    i_ret = stream_Read( p_stream, p_buffer, 1023 );
    if( i_ret == 0 )
830
831
    {
        stream_Delete( p_stream );
832
        return VLC_EGENERIC;
833
    }
834
    p_buffer[i_ret] = '\0';
835
836
    stream_Delete( p_stream );

837
    p_buffer_pos = strstr( ( char* ) p_buffer, "FAILED " );
838
839
    if ( p_buffer_pos )
    {
Rafaël Carré's avatar
Rafaël Carré committed
840
        /* handshake request failed, sorry */
841
842
        msg_Warn( p_this, "last.fm handshake failed: %s", p_buffer_pos + 7 );
        return VLC_EGENERIC;
843
844
    }

845
    p_buffer_pos = strstr( ( char* ) p_buffer, "BADAUTH" );
846
847
    if ( p_buffer_pos )
    {
848
849
850
851
852
853
        /* authentication failed, bad username/password combination */
        intf_UserFatal( p_this, VLC_FALSE,
            _("last.fm: Authentication failed"),
            _("last.fm Username or Password is incorrect,"
              " please verify your settings and relaunch VLC." ) );
        return VLC_AUDIOSCROBBLER_EFATAL;
854
855
    }

856
    p_buffer_pos = strstr( ( char* ) p_buffer, "BANNED" );
857
858
    if ( p_buffer_pos )
    {
859
860
861
862
        /* 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. "
                         "You should upgrade VLC, or disable last.fm plugin." );
        return VLC_AUDIOSCROBBLER_EFATAL;
863
864
    }

865
866
    p_buffer_pos = strstr( ( char* ) p_buffer, "BADTIME" );
    if ( p_buffer_pos )
867
    {
868
869
870
871
        /* 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;
872
873
    }

874
875
876
    p_buffer_pos = strstr( ( char* ) p_buffer, "OK" );
    if ( !p_buffer_pos )
        goto proto;
877

878
879
880
881
    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' */
882

883
884
    /* 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
885

886
887
888
    p_buffer_pos = strstr( p_buffer_pos, "http://" );
    if( !p_buffer_pos || strlen( p_buffer_pos ) == 7 )
        goto proto;
889

890
891
892
893
894
895
    /* 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;
896

897
898
    switch( ParseURL( psz_url, &p_sys->psz_nowp_host,
                &p_sys->psz_nowp_file, &p_sys->i_nowp_port ) )
899
    {
900
901
902
903
904
905
906
        case VLC_ENOMEM:
            goto oom;
        case VLC_EGENERIC:
            goto proto;
        case VLC_SUCCESS:
        default:
            break;
907
    }
908
909
910
911
912
913
914
915
916
917
918
919
920
#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 ) )
921
    {
922
923
924
925
926
927
928
        case VLC_ENOMEM:
            goto oom;
        case VLC_EGENERIC:
            goto proto;
        case VLC_SUCCESS:
        default:
            break;
929
930
931
    }

    return VLC_SUCCESS;
932
933
934
935
936
937
938

oom:
    return VLC_ENOMEM;

proto:
    msg_Warn( p_intf, "Handshake: can't recognize server protocol" );
    return VLC_EGENERIC;
939
940
941
}

/*****************************************************************************
942
 * DeleteSong : Delete the char pointers in a song
943
 *****************************************************************************/
944
static void DeleteSong( audioscrobbler_song_t* p_song )
945
{
946
947
948
949
950
    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 );
951
952
953
}

/*****************************************************************************
954
 * ReadMetaData : Read meta data when parsed by vlc
955
956
957
958
 *****************************************************************************/
static int ReadMetaData( intf_thread_t *p_this )
{
    playlist_t          *p_playlist;
959
960
    input_thread_t      *p_input;
    input_item_t        *p_item;
961

962
    intf_sys_t          *p_sys = p_this->p_sys;
963

964
965
966
967
968
    p_playlist = pl_Yield( p_this );
    PL_LOCK;
    p_input = p_playlist->p_input;
    if( !p_input )
    {
969
970
971
        PL_UNLOCK;
        pl_Release( p_playlist );
        return( VLC_SUCCESS );
972
973
974
975
976
977
    }

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

978
979
    p_item = input_GetItem( p_input );
    if( !p_item )
980
        return VLC_SUCCESS;
Rafaël Carré's avatar
Rafaël Carré committed
981

982
    char *psz_meta;
983
984
985
986
987
988
#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
989
        { \
990
            free( psz_meta ); \
991
992
993
994
            return VLC_ENOMEM; \
        } \
        free( psz_meta ); \
    }
995

996
997
    vlc_mutex_lock( &p_sys->lock );

998
999
1000
    ALLOC_ITEM_META( p_sys->p_current_song.psz_a, Artist )
    else
    {
1001
        vlc_mutex_unlock( &p_sys->lock );
1002
1003
1004
1005
1006
        msg_Dbg( p_this, "No artist.." );
        vlc_object_release( p_input );
        free( psz_meta );
        return VLC_EGENERIC;
    }
1007

1008
1009
    ALLOC_ITEM_META( p_sys->p_current_song.psz_t, Title )
    else
1010
    {
1011
        vlc_mutex_unlock( &p_sys->lock );
1012
1013
1014
1015
1016
1017
        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;
    }
1018

1019
1020
1021
    ALLOC_ITEM_META( p_sys->p_current_song.psz_b, Album )
    else
        p_sys->p_current_song.psz_b = calloc( 1, 1 );
1022

1023
1024
1025
    ALLOC_ITEM_META( p_sys->p_current_song.psz_m, TrackID )
    else
        p_sys->p_current_song.psz_m = calloc( 1, 1 );
1026

1027
    p_sys->p_current_song.i_l = input_item_GetDuration( p_item ) / 1000000;
1028

1029
1030
1031
1032
    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
1033

1034
    msg_Dbg( p_this, "Meta data registered" );
1035

1036
    vlc_mutex_unlock( &p_sys->lock );
1037
1038
1039
1040
    vlc_object_release( p_input );
    return VLC_SUCCESS;

}
1041

1042
1043
1044
1045
1046
1047
static void HandleInterval( time_t *next, unsigned int *i_interval )
{
    if( *i_interval == 0 )
    {
        /* first interval is 1 minute */
        *i_interval = 60;
1048
    }
1049
1050
1051
1052
1053
1054
1055
1056
    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;
1057
}
1058