/* MusicInfo plugin for purple Displays whatever is currently playing in Winamp or foobar2000 in the user's profile, away message, or available message in Purple Written by Reuben Balik Copyright 2005, 2006, 2007 By Reuben Balik Please ask permission before using any of this code for any other project/product. This code/plugin is provided as-is with no warranty of any kind. Use at your own risk. Last Update: May 4, 2007 */ #include "musicinfo.h" #include "internal.h" #include "status.h" #include "connection.h" #include "signals.h" #include "version.h" #include "notify.h" #include "savedstatuses.h" #include #include static const int maxLen = 2048; //the maximum profile length (I'm not actually sure what the official max AIM profile size is) static const int namesize = 256; //string size for artist and title static const int interval = 15 * 1000; //how often to check the winamp window (milleseconds) //these are all the strings that the plugin searches for static char artist_replace[] = "%ar%"; static char title_replace[] = "%so%"; static char start_songText[] = "(mi)"; static char end_songText[] = "(/mi)"; static char start_noSongText[] = "(ns)"; static char end_noSongText[] = "(/ns)"; //window classes static char winamp_wnd[] = "Winamp v1.x"; static int timeoutint = 0; static GList* accounts = NULL; static GList* prevInfos = NULL; static GList* prevStats = NULL; static gboolean prevAvail = FALSE; static gboolean privateMode = FALSE; static char pModeOn[2][64] = {"MusicInfo Privacy Mode is now ON\0", "Your buddies will not be able to see what you are listening to.\0"}; static char pModeOff[2][64] = {"MusicInfo Privacy Mode is now OFF\0", "Your buddies will be able to see what you are listening to.\0"}; // getSong gets the name of the song and artist from the title of the winamp/foobar window // and puts them in the buffers pointed to by title and artist // len is the size of the title and artist buffers in bytes // getSong returns FALSE if Winamp/foobar is closed or not playing, TRUE if it successfully got a song static gboolean getSong(char* title, char* artist, int len) { if(privateMode) return FALSE; //get handle to winamp window HWND hwndPlayer = FindWindow(winamp_wnd, NULL); if(hwndPlayer == NULL) return FALSE; char this_title[len*2]; int ret = GetWindowText(hwndPlayer,this_title,sizeof(this_title)); gsize written; if(ret != 0) { if(strstr(this_title, "[Stopped]") != NULL) return FALSE; if(strstr(this_title, "[Paused]") != NULL) return FALSE; char temp[len]; gchar* strinutf; char *firsthyphen, *secondhyphen, *period, *ptr; //GError converr; firsthyphen = g_strstr_len(this_title, len, " - "); if(firsthyphen == NULL) //when winamp starts up without playing it doesn't display the song name return FALSE; secondhyphen = g_strrstr_len(this_title, len, " - "); period = g_strstr_len(this_title, len, "."); ptr = period+2; int count = 0; if(firsthyphen != secondhyphen) //we have an artist { while(ptr != firsthyphen && count < len) { temp[count] = *ptr; ptr++; count++; } temp[count] = '\0'; ptr+=3; if(!g_utf8_validate(temp, -1, NULL)) { if( NULL == (strinutf = g_locale_to_utf8(temp, -1, NULL, &written, NULL))) return FALSE; //it couldn't convert for some reason g_strlcpy(artist, strinutf, len); g_free(strinutf); } else { g_strlcpy(artist, temp, len); } } else //no artist { strcpy(artist, "Unknown"); } count=0; while(ptr != secondhyphen && count < len) { temp[count] = *ptr; ptr++; count++; } temp[count] = '\0'; if(!g_utf8_validate(temp, -1, NULL)) { if( NULL == (strinutf = g_locale_to_utf8(temp, -1, NULL, &written, NULL))) return FALSE; //it couldn't convert for some reason g_strlcpy(title, strinutf, len); g_free(strinutf); } else { g_strlcpy(title, temp, len); } return TRUE; } else { return FALSE; } } // replaces the first instance of replaceStr in text with replaceWith // max is the size of text // all strings should already be in utf8 static gboolean replaceText(char* text, char* replaceStr, char* replaceWith, int max) { char* pos = text; char* foundAt = strstr(text, replaceStr); char newString[max]; if(foundAt == NULL) return FALSE; int cur = 0; while(pos != foundAt && cur < max) { newString[cur] = *pos; pos++; cur++; } char* replacePos = replaceWith; while(*replacePos != '\0' && cur < max) { newString[cur] = *replacePos; replacePos++; cur++; } pos+=strlen(replaceStr); while(*pos != '\0' && cur < max) { newString[cur] = *pos; pos++; cur++; } newString[cur] = '\0'; g_strlcpy(text, newString, max); return TRUE; } //hides all text from startReplace to endReplace with a max string size of len static void hideBetween(char* text, char* startReplace, char* endReplace, unsigned int len) { char* hidepos = strstr(text, startReplace); char* endhidepos = strstr(text, endReplace); int noOverFlowHere = strlen(endReplace); endhidepos += noOverFlowHere; char* pos = text; char newString[len]; if(hidepos != NULL && endhidepos != NULL && endhidepos > hidepos) { int n = 0; while(pos != hidepos) { newString[n] = *pos; n++; pos++; } pos = endhidepos; while(*pos != '\0' && n + noOverFlowHere < len) { newString[n] = *pos; n++; pos++; } newString[n] = '\0'; g_strlcpy(text, newString, len); } } //gets the text of whatever the user sets the status to. (not necessarily what status is on the server) static gboolean getStatusText(char* text, gboolean* isAvail, PurpleAccount *account, int len) { PurpleStatus* status; PurpleSavedStatus* savedStatus; const char* savedStatusMessage; gboolean goodToUse = FALSE; PurpleStatusPrimitive prim = PURPLE_STATUS_UNSET; status = purple_account_get_active_status(account); if(status != NULL) { if(purple_status_is_available(status)) { goodToUse = TRUE; *isAvail = TRUE; } else { prim = purple_status_type_get_primitive(purple_status_get_type(status)); if(prim == PURPLE_STATUS_AWAY) { goodToUse = TRUE; *isAvail = FALSE; } } } else return FALSE; if(!goodToUse) return FALSE; savedStatus = purple_savedstatus_get_current(); if(savedStatus != NULL && purple_savedstatus_get_message(savedStatus) != NULL) { savedStatusMessage = purple_savedstatus_get_message(savedStatus); g_strlcpy(text, savedStatusMessage, len); return TRUE; } else { return FALSE; } } // update the info for the connection gc // prevInfo and prevStat are the previously set data for that account (so we don't keep setting the same thing over) static void updateInfo(PurpleConnection *gc, char* prevInfo, char* prevStat, char* title, char* artist, gboolean isPlaying) { PurpleAccount* account; account = purple_connection_get_account(gc); char curAcct[maxLen]; char statusText[maxLen]; const char* sid = NULL; gboolean isAvail = FALSE; if(purple_account_get_user_info(account) != NULL) g_strlcpy(curAcct, purple_account_get_user_info(account), maxLen); else curAcct[0] = '\0'; gboolean hasStatus = getStatusText(statusText, &isAvail, account, maxLen); //had to do this because i couldn't get it to pass back through the getStatusText function sid = purple_status_get_id(purple_account_get_active_status(account)); if(isPlaying) { //replace the appropriate text replaceText(curAcct, start_songText, "\0", maxLen); replaceText(curAcct, end_songText, "\0", maxLen); replaceText(curAcct, artist_replace, artist, maxLen); replaceText(curAcct, title_replace, title, maxLen); //hide everything in the "no song" tag hideBetween(curAcct, start_noSongText, end_noSongText, maxLen); //and do it for the status if(hasStatus) { replaceText(statusText, start_songText, "\0", maxLen); replaceText(statusText, end_songText, "\0", maxLen); replaceText(statusText, artist_replace, artist, maxLen); replaceText(statusText, title_replace, title, maxLen); hideBetween(statusText, start_noSongText, end_noSongText, maxLen); } } else { //replace the appropriate text replaceText(curAcct, start_noSongText, "\0", maxLen); replaceText(curAcct, end_noSongText, "\0", maxLen); //hide everything in the song tag hideBetween(curAcct, start_songText, end_songText, maxLen); //and for the status: if(hasStatus) { replaceText(statusText, start_noSongText, "\0", maxLen); replaceText(statusText, end_noSongText, "\0", maxLen); hideBetween(statusText, start_songText, end_songText, maxLen); } } //this is so we don't update the server if we don't have to if(strcmp(curAcct, prevInfo) != 0) { serv_set_info(gc, curAcct); g_strlcpy(prevInfo, curAcct, maxLen); } if(hasStatus && (strcmp(statusText, prevStat) != 0 || isAvail != prevAvail)) { char message[] = "message"; purple_account_set_status(account, sid, TRUE, message, statusText, NULL); g_strlcpy(prevStat, statusText, maxLen); prevAvail = isAvail; } } // this is the function called by the timer // it loops through all accounts static gboolean callUpdateInfo() { char title[namesize]; char artist[namesize]; gboolean isPlaying = getSong(title, artist, namesize); guint len = g_list_length(accounts); int cur; for(cur = 0; cur < len; cur++) { updateInfo((PurpleConnection*) g_list_nth(accounts, cur)->data, (char*) g_list_nth(prevInfos, cur)->data, (char*) g_list_nth(prevStats, cur)->data, title, artist, isPlaying); } return TRUE; } static void toggle_privacy(PurplePluginAction *action) { privateMode = !privateMode; if(privateMode) purple_notify_info(NULL, "MusicInfo", pModeOn[0], pModeOn[1]); else purple_notify_info(NULL, "MusicInfo", pModeOff[0], pModeOff[1]); } static void online(PurpleConnection *gc) { accounts = g_list_append(accounts, (gpointer)gc); char* prevInfo = (char*) g_malloc(sizeof(char) * maxLen); *prevInfo = '\0'; prevInfos = g_list_append(prevInfos, (gpointer)prevInfo); char* prevStat = (char*) g_malloc(sizeof(char) * maxLen); *prevStat = '\0'; prevStats = g_list_append(prevStats, (gpointer)prevStat); if(timeoutint == 0) { timeoutint = g_timeout_add(interval, callUpdateInfo, NULL); } } static void signoff(PurpleConnection *gc) { guint len = g_list_length(accounts); int cur; gboolean flag = FALSE; for(cur = 0; cur < len && !flag; cur++) { if(g_list_nth(accounts, cur)->data == gc) { flag = TRUE; accounts = g_list_remove(accounts, (gpointer)gc); char* delinfo = g_list_nth(prevInfos, cur)->data; char* delstat = g_list_nth(prevStats, cur)->data; g_free(delinfo); g_free(delstat); prevInfos = g_list_remove(prevInfos, (gpointer)delinfo); prevStats = g_list_remove(prevStats, (gpointer)delstat); } } if(len == 1) { g_source_remove(timeoutint); timeoutint = 0; } } static gboolean plugin_load(PurplePlugin *plugin) { purple_signal_connect(purple_connections_get_handle(), "signed-on", plugin, PURPLE_CALLBACK(online), NULL); purple_signal_connect(purple_connections_get_handle(), "signing-off", plugin, PURPLE_CALLBACK(signoff), NULL); return TRUE; } static gboolean plugin_unload(PurplePlugin *plugin) { purple_signal_disconnect(purple_connections_get_handle(), "signed-on", plugin, PURPLE_CALLBACK(online)); purple_signal_disconnect(purple_connections_get_handle(), "signing-off", plugin, PURPLE_CALLBACK(signoff)); return TRUE; } static GList * miactions(PurplePlugin *plugin, gpointer context) { GList *actlist = NULL; PurplePluginAction *act = NULL; act = purple_plugin_action_new(_("Toggle Privacy Mode"), toggle_privacy); actlist = g_list_append(actlist, act); return actlist; } static PurplePluginInfo info = { PURPLE_PLUGIN_MAGIC, PURPLE_MAJOR_VERSION, PURPLE_MINOR_VERSION, PURPLE_PLUGIN_STANDARD, /**< type */ NULL, /**< ui_requirement */ 0, /**< flags */ NULL, /**< dependencies */ PURPLE_PRIORITY_DEFAULT, /**< priority */ "core-cybrguyrsb-musicinfo", /**< id */ N_("MusicInfo"), /**< name */ "0.7", /**< version */ /** summary */ N_("Share your musical taste with people on your buddy list"), /** description */ N_("Displays whatever song you are listening to in your info, away message, or available message."), "Reuben Balik ", /**< author */ "http://mi.reubenbalik.com", /**< homepage */ plugin_load, /**< load */ plugin_unload, /**< unload */ NULL, /**< destroy */ NULL, /**< ui_info */ NULL, /**< extra_info */ NULL, /**< prefs_info */ miactions, //seems that pidgin added a few extra entries to this struct NULL, NULL, NULL, NULL, }; static void init_plugin(PurplePlugin *plugin) { } PURPLE_INIT_PLUGIN(mig, init_plugin, info)