view src/lastfm/lastfm.c @ 2284:d19b53359b24

cleaned up the sndfile wav plugin, currently limiting it ONLY TO WAV PLAYBACK. if somebody is more experienced with it and wants to restore the other formats, go ahead (maybe change the name of the plugin too?).
author mf0102 <0102@gmx.at>
date Wed, 09 Jan 2008 15:41:22 +0100
parents 39398ff5e162
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)