view src/lastfm/lastfm.c @ 2144:39398ff5e162

fsize_impl should return -1 on a remote stream.
author Yoshiki Yazawa <yaz@cc.rim.or.jp>
date Tue, 30 Oct 2007 22:17:33 +0900
parents b8da6a0b0da2
children 194c2f8c2a92
line wrap: on
line source

/* Audacious lastfm transport plugin
 * Copyright (c) 2007 Cristi Măgherușan <majeru@gentoo.ro>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * 
 *       Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 * 
 *       Redistributions in binary form must reproduce the above copyright notice,
 *       this list of conditions and the following disclaimer in the documentation
 *       and/or other materials provided with the distribution.
 * 
 *       Neither the name of the author nor the names of its contributors may be
 *       used to endorse or promote products derived from this software without
 *       specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/*
 *  Current status and known issues:
 *      - Works fine ant is relatively stable 
 *      - The adjust fails when having 2 or more opened streams at the same time.
 *              * It will randomly adjust to any one of them, because the playlist keeps 
 *                pulling metadata
 *      - When the network is disconnected opening a new track freezes the player 
 *              * This seems to recover after the connection is restablished
 *              * Ordinary mp3 streams seem to share this behavior. Didnt tested if others do.
 */

#include <audacious/vfs.h>
#include <audacious/plugin.h>
#include <audacious/configdb.h>
#include <libmowgli/mowgli_global_storage.h>
#include <curl/curl.h>
#include <glib.h>
#include <string.h>
#include "lastfm.h"
#include <glib/gi18n.h>

#define DEBUG 1

size_t lastfm_store_res(void *ptr, size_t size, size_t nmemb, void *udata)
{
        GString *data = (GString *) udata;
        g_string_append_len(data, ptr, nmemb);
        return size * nmemb;
}


gchar** lastfm_get_data_from_uri(gchar *url)
{
        gchar**result=NULL;
        GString* res=g_string_new(NULL);
        gint i=0;
        CURL *curl = curl_easy_init();
        curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1);
        curl_easy_setopt(curl, CURLOPT_USERAGENT, "Audacious");
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, lastfm_store_res);
        curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
        curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
        curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, LASTFM_CURL_TIMEOUT);
        curl_easy_setopt(curl, CURLOPT_URL, url);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, res);
        gint status = curl_easy_perform(curl);
        curl_easy_cleanup(curl);
        /* g_print(res->str);*/
        if((status==CURLE_OK) && res && res->str)
                result = g_strsplit(res->str, "\n", 20);
        g_string_erase(res, 0, -1);
#if DEBUG
        g_print("Opened URL: '%s'\n",url);
        g_print("LASTFM: (get_uri) received data ---------------\n");
        for (i = 0; result && result[i]; i++)
                g_print("%s\n",result[i]);
        g_print("LASTFM: (get_uri) data ended    ---------------\n");
#endif
        return result;
}


static void show_login_error_dialog(void)
{
        const gchar *markup =
                N_("<b><big>Couldn't initialize the last.fm radio plugin.</big></b>\n\n"
                                "Check if your Scrobbler's plugin login data is set up properly.");

        GtkWidget *dialog =
                gtk_message_dialog_new_with_markup(NULL,
                                GTK_DIALOG_DESTROY_WITH_PARENT,
                                GTK_MESSAGE_ERROR,
                                GTK_BUTTONS_OK,
                                _(markup));
        gtk_dialog_run(GTK_DIALOG(dialog));
        gtk_widget_destroy(dialog);
}

gchar* lastfm_get_login_uri()  /* reads the audioscrobbler login data from the config */
{                              /* and then uses them to create a login URL*/
        ConfigDb *cfg = NULL;
        gchar   *buf=NULL,
                *username = NULL, 
                *password = NULL;
        if ((cfg = aud_cfg_db_open()) != NULL)
        {
                aud_cfg_db_get_string(cfg, "audioscrobbler","username",
                                &username);
                aud_cfg_db_get_string(cfg, "audioscrobbler","password",
                                &password);
                g_free(cfg);
        }
        if (username != NULL && password != NULL)
        {
                buf=g_strdup_printf(LASTFM_HANDSHAKE_URL, username, password);
                g_free(password);
                g_free(username);
                return buf;
        }
        else 
        {
#if DEBUG
                g_print("LASTFM: (get_login_uri) Couldn't find the login data. Use the scrobbler plugin to set it up.\n");
#endif
                show_login_error_dialog();
                return NULL;
        }
}

void lastfm_store(gchar * var_name, gchar * var)  /*mowgli storage wrapper, for storing global data*/
{
        if (mowgli_global_storage_get(var_name))
                mowgli_global_storage_free(var_name);

        mowgli_global_storage_put(var_name,var);
}

int lastfm_login(void)  /*gets the session ID and the mp3 stream URL and stores them.
                          It's called just on the first fopen, since it doesnt change (hopefully!!!)*/
{
        gint    i,
                ret=LASTFM_LOGIN_OK; /*suppose everything goes fine*/
        gchar   **split = NULL;
        if(login_url!=NULL)
                g_free(login_url);
        login_url=lastfm_get_login_uri();
        if(login_url==NULL)
                return LASTFM_MISSING_LOGIN_DATA;
        split = lastfm_get_data_from_uri(login_url);
        if (split)
        {
                for (i = 0; split && split[i]; i++)
                {
                        if (g_str_has_prefix(split[i], "session="))
                                lastfm_store("lastfm_session_id",g_strndup(split[i] + 8, 32));
                        else if (g_str_has_prefix(split[i], "stream_url="))
                                lastfm_store("lastfm_stream_uri" ,g_strdup(split[i] + 11));
                }
                ret = LASTFM_LOGIN_OK;
        }
        else
                ret = LASTFM_LOGIN_ERROR;
        g_strfreev(split);
        g_free(login_url);
        login_url=NULL;
        return ret;
}

static gchar* parse(gchar* input_string,gchar* token)
{
        if (!g_str_has_prefix(input_string, token))
                return NULL;

        return g_strdup(strchr(input_string, '=') + 1);
}

static gpointer lastfm_adjust(gpointer uri)  /*tunes into a channel*/
{
        gchar *fetch_url=NULL,
              *session_id = g_strdup(mowgli_global_storage_get("lastfm_session_id"));
        if (!session_id)
        {
#if DEBUG
                g_print("LASTFM: (adjust) Adjust failed! Session ID not set.\n");
#endif
                return NULL ;
        }
        fetch_url=g_strdup_printf(LASTFM_ADJUST_URL, session_id, (char*)uri);
        gchar** tmp = lastfm_get_data_from_uri(fetch_url);      /*the output doesn't matter*/
        g_strfreev(tmp);
        g_free(fetch_url);
        g_free(session_id);
        return NULL ;
}

gboolean parse_metadata(LastFM * handle,gchar ** split)
{
        int i;

        if(split == NULL)
                return FALSE;
        if(g_str_has_prefix(split[0],"streaming=false"))
        {
#if DEBUG
                g_print("LASTFM: (parse) Metadata is not available\n");
#endif
                return FALSE;
        }
        handle->lastfm_duration=0;
        handle->lastfm_progress=0;

        if (handle->lastfm_artist)
        {
                g_free(handle->lastfm_artist);
                handle->lastfm_artist=NULL;
        }
        if (handle->lastfm_title)
        {
                g_free(handle->lastfm_title);
                handle->lastfm_title=NULL;
        }
        if (handle->lastfm_album)
        {
                g_free(handle->lastfm_album);
                handle->lastfm_album=NULL;
        }
        if (handle->lastfm_station_name)
        {
                g_free(handle->lastfm_station_name);
                handle->lastfm_station_name=NULL;
        }
        for (i = 0; split && split[i]; i++)
        {
                if (g_str_has_prefix(split[i], "artist="))
                        handle->lastfm_artist = parse(split[i],"artist=");

                if (g_str_has_prefix(split[i], "track="))
                        handle->lastfm_title  = parse(split[i],"track=");

                if (g_str_has_prefix(split[i], "album="))
                        lastfm_store("lastfm_album", parse(split[i],"album=" ));

                if (g_str_has_prefix(split[i], "albumcover_medium="))
                        lastfm_store("lastfm_cover", parse(split[i],"albumcover_medium=")); 

                if (g_str_has_prefix(split[i], "station="))
                        handle->lastfm_station_name = parse(split[i],"station=");

                if (g_str_has_prefix(split[i], "trackduration="))
                        handle->lastfm_duration = g_ascii_strtoull(g_strdup(split[i] + 14), NULL, 10);
                if (g_str_has_prefix(split[i], "trackprogress="))
                        handle->lastfm_progress = g_ascii_strtoull(g_strdup(split[i] + 14), NULL, 10);

        }
#if DEBUG
        if(handle->lastfm_station_name!=NULL)
        {
                g_print("nLASTFM: (parse) Duration:%d\n", handle->lastfm_duration);
                g_print("LASTFM: (parse) Station Name: %s\n", handle->lastfm_station_name);
        }
#endif
        return TRUE;
}

int fetch_metadata(LastFM * handle)
{
#if DEBUG
        g_print("LASTFM: (fetch) starting to fetch\n");
#endif
        gchar *uri=NULL;
        gint res=METADATA_FETCH_FAILED;
        if(handle==NULL)
                return res;

        handle->lastfm_session_id=mowgli_global_storage_get("lastfm_session_id");
        if (handle->lastfm_session_id == NULL)
                return res;
        uri=g_strdup_printf(LASTFM_METADATA_URL, handle->lastfm_session_id);
        gchar**fetched_metadata =NULL;
        fetched_metadata = lastfm_get_data_from_uri(uri);
        if(fetched_metadata != NULL)
        {
                if(parse_metadata( handle,fetched_metadata))
                {
                        g_strfreev(fetched_metadata);
                        res=METADATA_FETCH_SUCCEEDED;
#if DEBUG
                        g_print("LASTFM: (fetch) metadata was parsed ok\n");
#endif
                }

        }
        return res;
}

gpointer lastfm_metadata_thread_func(gpointer arg)
{    
        gint    sleep_duration=1,
                previous_track_duration=-1,
                count=1,
                status=0,
                err=0;
        gboolean track_end_expected=FALSE,track_beginning=TRUE;
        LastFM *handle = (LastFM *)arg;
        /*get it right after opened the stream, so it doesnt need the mutex */
        fetch_metadata(handle); 

        /* metadata is fetched 1 second after the stream is opened, 
         * and again after 2 seconds.
         * if metadata was fetched ok i'm waiting for 
         * track_length - fetch_duration - 10 seconds
         * then start polling for new metadata each 2 seconds, until
         * the track gets changed from the previous iteration
         */
        do
        {
                if(count%sleep_duration==0)
                {    
                        g_mutex_lock(metadata_mutex);
                        if(handle==NULL)
                        {
#if DEBUG
                                g_print("LASTFM: (thread) Exiting thread, ID = %p, thread's handle got freed\n", (void *)g_thread_self());
#endif
                                thread_count--;
                                return NULL;       
                        }
                        if(t0->tv_usec==-1)
                                g_get_current_time (t0);
#if DEBUG
                        g_print("LASTFM: (thread) Fetching metadata:\n");
#endif
                        status=fetch_metadata(handle);
                        g_get_current_time (t1);
                        if(status==METADATA_FETCH_SUCCEEDED)
                        {        
                                if(!track_end_expected)
                                {
                                        if(track_beginning)
                                        {       /*first try after track has changed*/
#if DEBUG
                                                g_print("LASTFM: (thread) retrying in 2 seconds\n");
#endif
                                                track_beginning=FALSE;
                                                sleep_duration=2;
                                        }
                                        else 
                                        {
                                                if(g_str_has_prefix(handle->lastfm_station_name, "Previewing 30-second clips"))
                                                        handle->lastfm_duration=30;
                                                sleep_duration=handle->lastfm_duration-(t1->tv_sec - t0->tv_sec)-6;
                                                previous_track_duration=handle->lastfm_duration;
                                                count=err=0;
                                                track_end_expected=TRUE; /*then the track_end will follow*/
                                                track_beginning=TRUE;
                                                t0->tv_usec=-1;
#if DEBUG
                                                g_print("LASTFM: (thread) second fetch after new track started, the next will follow in %d sec\n",sleep_duration);
#endif
                                        }

                                }
                                else
                                {       
                                        /*if the track hasnt yet changed (two tracks are considered identical if they 
                                         * have the same length or the same title)
                                         */
                                        if(handle->lastfm_duration == previous_track_duration)
                                        {      
#if DEBUG                                        
                                                g_print("LASTFM: (thread) it is the same track as before, waiting for new track to start\n");
#endif
                                                sleep_duration=2;
                                        }
                                        else
                                        {
#if DEBUG
                                                g_print("LASTFM: (thread) the track has changed\n");
#endif
                                                track_end_expected=FALSE;
                                                sleep_duration=2;
                                                /*if(previous_track_title)
                                                  {
                                                  g_free(previous_track_title);
                                                  previous_track_title=NULL;
                                                  }*/
                                        }
                                }
#if DEBUG
                                g_print("LASTFM: (thread) Current thread, ID = %p\n", (void *)g_thread_self());
#endif
                        }
                        else 
                        {
                                err++;
                                sleep_duration<<=1;
                        }
#if DEBUG
                        g_print("LASTFM: (thread) Thread_count: %d\n",thread_count);
                        g_print("LASTFM: (thread) sleepping for %d seconds. ",err? sleep_duration/2 :sleep_duration);
                        g_print("Track length = %d sec\n",handle->lastfm_duration);
#endif
                        g_mutex_unlock(metadata_mutex);
                }
                sleep(1);
                count++;
        }
        while ((g_thread_self()==metadata_thread )&& (err<7));

#if DEBUG
        g_print("LASTFM: (thread) Exiting thread, ID = %p\n", (void *)g_thread_self());
#endif
        thread_count--;
        return NULL;
}

VFSFile *lastfm_aud_vfs_fopen_impl(const gchar * path, const gchar * mode)
{
        VFSFile *file = g_new0(VFSFile, 1);
        LastFM  *handle = g_new0(LastFM, 1);
        handle->lastfm_artist=NULL;
        handle->lastfm_title=NULL;
        handle->lastfm_album=NULL;
        handle->lastfm_session_id=NULL;
        handle->lastfm_mp3_stream_url=NULL;
        handle->lastfm_station_name=g_strdup("Couldn't fetch metadata");
        int login_count = 0;
        gchar * temp_path=g_strdup(path);
        if(!mowgli_global_storage_get("lastfm_session_id")) /*login only if really needed*/
        {
                while((login_count++ <= 3)&&(lastfm_login()!= LASTFM_LOGIN_OK))
                        sleep(5);
                if(login_count>3)
                {
                        g_free(handle);
                        g_free(file);
                        return NULL;
                }
        }
        handle->lastfm_session_id = g_strdup(mowgli_global_storage_get("lastfm_session_id"));
        handle->lastfm_mp3_stream_url = g_strdup(mowgli_global_storage_get("lastfm_stream_uri"));
        g_get_current_time(t0);
        g_thread_create(lastfm_adjust,temp_path,FALSE,NULL);
        metadata_thread = g_thread_create(lastfm_metadata_thread_func, handle, FALSE, NULL);
        thread_count++;
        handle->proxy_fd = aud_vfs_fopen(handle->lastfm_mp3_stream_url, mode);
        file->handle = handle;
#if DEBUG
        g_print("LASTFM: (fopen) Thread_count: %d\n",thread_count);
#endif
        return file;
}

gint lastfm_aud_vfs_fclose_impl(VFSFile * file)
{
        gint ret = 0;

        if (file == NULL)
                return -1;
        if (file->handle) 
        {
                g_mutex_lock(metadata_mutex);
                LastFM *handle = file->handle;
                ret = aud_vfs_fclose(handle->proxy_fd);
                if (!ret)
                        handle->proxy_fd = NULL;
                g_free(handle);
                handle=NULL;
                file->handle = NULL;
                g_mutex_unlock(metadata_mutex);
        }
        return ret;
}

size_t lastfm_aud_vfs_fread_impl(gpointer ptr, size_t size, size_t nmemb, VFSFile * file)
{
        LastFM *handle = file->handle;
        size_t ret = aud_vfs_fread(ptr, size, nmemb, handle->proxy_fd);
        return ret;
}

size_t lastfm_aud_vfs_fwrite_impl(gconstpointer ptr, size_t size, size_t nmemb, VFSFile * file)
{
        return -1;
}

gint lastfm_aud_vfs_getc_impl(VFSFile * stream)
{
        LastFM *handle = stream->handle;
        return aud_vfs_getc(handle->proxy_fd);
}

gint lastfm_aud_vfs_ungetc_impl(gint c, VFSFile * stream)
{
        LastFM *handle = stream->handle;
        return aud_vfs_ungetc(c, handle->proxy_fd);
}

gint lastfm_aud_vfs_fseek_impl(VFSFile * file, glong offset, gint whence)
{
        return -1;
}

void lastfm_aud_vfs_rewind_impl(VFSFile * file)
{
        return;
}

glong lastfm_aud_vfs_ftell_impl(VFSFile * file)
{
        LastFM *handle = file->handle;

        return aud_vfs_ftell(handle->proxy_fd);
}

gboolean lastfm_aud_vfs_feof_impl(VFSFile * file)
{
        LastFM *handle = file->handle;

        return aud_vfs_feof(handle->proxy_fd);
}

gint lastfm_aud_vfs_truncate_impl(VFSFile * file, glong size)
{
        return -1;
}

off_t lastfm_aud_vfs_fsize_impl(VFSFile * file)
{
        return -1;
}

gchar *lastfm_aud_vfs_metadata_impl(VFSFile * file, const gchar * field)
{
        LastFM * handle;
        if(file->handle!= NULL)
                handle = file->handle;
        else 
                return NULL;

        if (!g_ascii_strncasecmp(field, "stream-name", 11) && (handle->lastfm_station_name != NULL))
                return g_strdup_printf("last.fm radio: %s", handle->lastfm_station_name);
        if (!g_ascii_strncasecmp(field, "track-name", 10) && (handle->lastfm_title != NULL) && (handle->lastfm_artist != NULL))
                return g_strdup_printf("%s - %s", handle->lastfm_artist, handle->lastfm_title);
        if (!g_ascii_strncasecmp(field, "content-type", 12))
                return g_strdup("audio/mpeg");

        return NULL;
}

VFSConstructor lastfm_const = {
        "lastfm://",
        lastfm_aud_vfs_fopen_impl,
        lastfm_aud_vfs_fclose_impl,
        lastfm_aud_vfs_fread_impl,
        lastfm_aud_vfs_fwrite_impl,
        lastfm_aud_vfs_getc_impl,
        lastfm_aud_vfs_ungetc_impl,
        lastfm_aud_vfs_fseek_impl,
        lastfm_aud_vfs_rewind_impl,
        lastfm_aud_vfs_ftell_impl,
        lastfm_aud_vfs_feof_impl,
        lastfm_aud_vfs_truncate_impl,
        lastfm_aud_vfs_fsize_impl,
        lastfm_aud_vfs_metadata_impl
};

static void init(void)
{       
        aud_vfs_register_transport(&lastfm_const);
        if (!metadata_mutex)
                metadata_mutex = g_mutex_new ();
        t0=g_new0(GTimeVal,1);
        t1=g_new0(GTimeVal,1);
        lastfm_store("lastfm_loaded","TRUE");
}

static void cleanup(void)
{
        g_mutex_free(metadata_mutex);
        mowgli_global_storage_free("lastfm_stream_uri");
#if DEBUG
        g_print("LASTFM: (cleanup) Cleanup finished\n");
#endif
}

DECLARE_PLUGIN(lastfm, init, cleanup, NULL, NULL, NULL, NULL, NULL, NULL)