For many years I've been using a skin called "Dirty Simulated Woodtype". Long ago it was with Winamp 2.x on windows. When I moved to linux I began using XMMS, but eventually easy support for gtk1 applications, and the way XMMS interacted with ALSA and/or Pulseaudio became... impaired... in distributions I used. Wanting to stay with a small memory footprint, no nonsense music player I switched over to Audacious 2.3. Unfortunately Audacious isn't completely cross compatible with all Winamp style skins. So I unzipped the skin and spent a few hours trial and error editing bitmaps till the corruptions when away. This is it; the same old skin. But it now works in Audacious 2.x. I tried to contact the creator of this skin but all the email addresses I found just bounced. So I'm hosting it here instead. The .wsz file is just a renamed zip containing the bitmaps. It should load fine in any app supporting winamp skins, and, of course, Audacious.
Simulated Woodtype Winamp2 skin fixed for Audacious 2.x
[comment on this post] Append "/@say/your message here" to the URL in the location bar and hit enter.
The old plugin wasn't working with modern libre.fm scrobbling and since it is the future now I decided to vibe code a simple scrobbling plugin instead of trying to fix the old one. This is the result. It requires libcurl and glib against ancient ubuntu 10.04 audacious-dev libraries. I doubt anyone would use this besides me in 2026, but just in case and to keep this page accurate, here's what I use: librefm-scrobber.c. And you should never use this, compile it yourself, but: scrobbler.so
Build it like,
gcc -shared -fPIC -o scrobbler.so librefm-scrobbler.c \
$(pkg-config --cflags --libs audacious glib-2.0) \
-lcurl -lpthread
Then create a config file at ~/.config/audacious/librefm-scrobbler.conf with your username and password,
username=YOUR_LIBREFM_USERNAME password_plain=YOUR_PASSWORD -- OR -- password=PRECOMPUTED_MD5_HEX
The libre.fm scrobbling URL is baked into the code. In order to change scrobbling services you have edit,
#define HS_URL "https://turtle.libre.fm/"
and then the cosmetic console output,
fprintf(stderr, PLUGIN_NAME ": handshaking with turtle.libre.fm ...\n");
and then recompile and re-copy the new scrobbler.so file into /usr/lib/audacious/General/
/*
* librefm-scrobbler.c
* Audacious 2.x General Plugin: scrobbles to libre.fm via AudioScrobbler 1.2
*
* Tested against Audacious 2.3 on Ubuntu 10.04 (audacious-dev package).
*
* BUILD:
* gcc -shared -fPIC -o scrobbler.so librefm-scrobbler.c \
* $(pkg-config --cflags --libs audacious glib-2.0) \
* -lcurl -lpthread
*
* INSTALL:
* sudo cp scrobbler.so /usr/lib/audacious/General/
*
* CONFIG: ~/.config/audacious/librefm-scrobbler.conf
* username=YOUR_LIBREFM_USERNAME
* password_plain=YOUR_PASSWORD (plugin MD5s it at startup)
* -- OR --
* password=PRECOMPUTED_MD5_HEX (echo -n 'pass' | md5sum)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
#include <glib.h>
#include <curl/curl.h>
/*
* plugin.h itself does: #include "libaudcore/tuple.h"
* so including it gives us Tuple, FIELD_*, tuple_get_string, tuple_get_int.
* All the aud_* macros we use expand to the real function names via plugin.h.
*
* Confirmed API (from header grep):
* aud_playlist_get_active() -> gint
* aud_playlist_get_position(gint playlist) -> gint
* playlist_entry_get_tuple(gint pl, gint entry) -> const Tuple *
* tuple_get_string(const Tuple *, gint field, NULL) -> const gchar *
* tuple_get_int(const Tuple *, gint field, NULL) -> gint
* aud_tuple_free(void *) == mowgli_object_unref
* aud_drct_get_length() -> gint (ms)
* DECLARE_PLUGIN(name, init, fini, ip, op, ep, gp, vp, interface)
*/
#include <audacious/plugin.h>
/* ================================================================
* Constants
* ================================================================ */
#define PLUGIN_NAME "Libre.fm Scrobbler"
#define CLIENT_ID "aud"
#define CLIENT_VER "1"
#define HS_URL "https://turtle.libre.fm/"
#define CONFIG_FILE "librefm-scrobbler.conf"
#define MIN_PLAY_SEC 240
#define MIN_TRACK_SEC 30
/*
MIN_TRACK_SEC 30 — a track must be at least 30 seconds long to be eligible at all. Anything shorter (jingles, intros, sound effects) is simply never scrobbled regardless of how long you listen.
MIN_PLAY_SEC 240 — this is not "the track must be 4 minutes long". It's "you must have played 240 seconds of it". It's one of two ways a scrobble can qualify. The two conditions are OR'd together:
You've been playing it for 240 seconds or more, OR
You've been playing it for at least half its total length
*/
/* ================================================================
* State
* ================================================================ */
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
static pthread_t worker;
static gboolean running = FALSE;
static char session_id[256] = "";
static char np_url[512] = "";
static char sub_url[512] = "";
static time_t session_expiry = 0;
static char cfg_user[256] = "";
static char cfg_pass[64] = ""; /* MD5 hex of password */
typedef struct {
char artist[512];
char title[512];
char album[256];
int length; /* seconds, -1 = unknown */
int trackno;
time_t started;
gboolean valid;
} TrackInfo;
static TrackInfo cur = { "", "", "", -1, -1, 0, FALSE };
static TrackInfo pend = { "", "", "", -1, -1, 0, FALSE };
static gboolean want_np = FALSE;
static gboolean want_sub = FALSE;
/* ================================================================
* MD5 via GLib (available in Ubuntu 10.04's glib 2.22)
* ================================================================ */
static void md5hex(const char *s, char out[33])
{
gchar *h = g_compute_checksum_for_string(G_CHECKSUM_MD5, s, -1);
if (h) { g_strlcpy(out, h, 33); g_free(h); }
else out[0] = '\0';
}
/* ================================================================
* Config
* ================================================================ */
static void load_config(void)
{
gchar *path = g_build_filename(g_get_home_dir(), ".config",
"audacious", CONFIG_FILE, NULL);
FILE *f = fopen(path, "r");
g_free(path);
if (!f) {
g_warning(PLUGIN_NAME ": no config (~/.config/audacious/%s)", CONFIG_FILE);
return;
}
char line[1024];
while (fgets(line, sizeof(line), f)) {
line[strcspn(line, "\r\n")] = '\0';
char *eq = strchr(line, '=');
if (!eq) continue;
*eq = '\0';
const char *k = line, *v = eq + 1;
if (!strcmp(k, "username")) g_strlcpy(cfg_user, v, sizeof(cfg_user));
else if (!strcmp(k, "password")) g_strlcpy(cfg_pass, v, sizeof(cfg_pass));
else if (!strcmp(k, "password_plain")) md5hex(v, cfg_pass);
}
fclose(f);
}
/* ================================================================
* CURL helpers
* ================================================================ */
typedef struct { char *buf; size_t len; } Cbuf;
static size_t on_data(void *ptr, size_t size, size_t n, void *ud)
{
size_t total = size * n;
Cbuf *b = (Cbuf *)ud;
b->buf = g_realloc(b->buf, b->len + total + 1);
memcpy(b->buf + b->len, ptr, total);
b->len += total;
b->buf[b->len] = '\0';
return total;
}
static char *do_get(const char *url)
{
Cbuf b = {NULL, 0};
CURL *c = curl_easy_init();
if (!c) return NULL;
curl_easy_setopt(c, CURLOPT_URL, url);
curl_easy_setopt(c, CURLOPT_CAINFO, "/etc/ssl/certs/ca-certificates.crt");
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, on_data);
curl_easy_setopt(c, CURLOPT_WRITEDATA, &b);
curl_easy_setopt(c, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(c, CURLOPT_USERAGENT, PLUGIN_NAME "/" CLIENT_VER);
CURLcode r = curl_easy_perform(c);
curl_easy_cleanup(c);
if (r != CURLE_OK) { g_free(b.buf); return NULL; }
return b.buf;
}
static char *do_post(const char *url, const char *fields)
{
Cbuf b = {NULL, 0};
CURL *c = curl_easy_init();
if (!c) return NULL;
curl_easy_setopt(c, CURLOPT_URL, url);
curl_easy_setopt(c, CURLOPT_CAINFO, "/etc/ssl/certs/ca-certificates.crt");
curl_easy_setopt(c, CURLOPT_POSTFIELDS, fields);
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, on_data);
curl_easy_setopt(c, CURLOPT_WRITEDATA, &b);
curl_easy_setopt(c, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(c, CURLOPT_USERAGENT, PLUGIN_NAME "/" CLIENT_VER);
CURLcode r = curl_easy_perform(c);
curl_easy_cleanup(c);
if (r != CURLE_OK) { g_free(b.buf); return NULL; }
return b.buf;
}
static char *urlencode(const char *s)
{
CURL *c = curl_easy_init();
if (!c) return g_strdup(s);
char *e = curl_easy_escape(c, s, 0);
char *ret = g_strdup(e ? e : s);
curl_free(e);
curl_easy_cleanup(c);
return ret;
}
/* ================================================================
* Handshake
* AudioScrobbler 1.2:
* GET http://turtle.libre.fm/?hs=true&p=1.2&c=CLIENT&v=VER&u=USER&t=TS&a=TOKEN
* TOKEN = md5( md5(password) + timestamp_string )
* Response: OK\n<session>\n<np_url>\n<sub_url>\n
* ================================================================ */
static gboolean do_handshake(void)
{
if (!cfg_user[0] || !cfg_pass[0]) {
g_warning(PLUGIN_NAME ": no credentials – edit ~/.config/audacious/%s",
CONFIG_FILE);
return FALSE;
}
char ts_str[32];
snprintf(ts_str, sizeof(ts_str), "%ld", (long)time(NULL));
char tmp[128];
snprintf(tmp, sizeof(tmp), "%s%s", cfg_pass, ts_str);
char token[33];
md5hex(tmp, token);
char *ue = urlencode(cfg_user);
char *url = g_strdup_printf("%s?hs=true&p=1.2&c=%s&v=%s&u=%s&t=%s&a=%s",
HS_URL, CLIENT_ID, CLIENT_VER, ue, ts_str, token);
g_free(ue);
fprintf(stderr, PLUGIN_NAME ": handshaking with turtle.libre.fm ...\n");
fflush(stderr);
char *resp = do_get(url);
g_free(url);
if (!resp) { fprintf(stderr, PLUGIN_NAME ": handshake: no response\n"); fflush(stderr); return FALSE; }
gchar **lines = g_strsplit(resp, "\n", 0);
g_free(resp);
gboolean ok = FALSE;
if (lines && lines[0]) {
if (g_str_has_prefix(lines[0], "OK") &&
lines[1] && lines[2] && lines[3]) {
g_strlcpy(session_id, g_strstrip(lines[1]), sizeof(session_id));
g_strlcpy(np_url, g_strstrip(lines[2]), sizeof(np_url));
g_strlcpy(sub_url, g_strstrip(lines[3]), sizeof(sub_url));
session_expiry = time(NULL) + 3600;
fprintf(stderr, PLUGIN_NAME ": session OK\n"); fflush(stderr);
ok = TRUE;
} else {
fprintf(stderr, PLUGIN_NAME ": handshake error: %s\n", lines[0]); fflush(stderr);
}
}
g_strfreev(lines);
return ok;
}
static gboolean ensure_session(void)
{
if (session_id[0] && time(NULL) < session_expiry) return TRUE;
session_id[0] = np_url[0] = sub_url[0] = '\0';
session_expiry = 0;
return do_handshake();
}
/* ================================================================
* Now Playing
* ================================================================ */
static void send_nowplaying(const TrackInfo *t)
{
if (!ensure_session()) return;
char *a = urlencode(t->artist);
char *ti = urlencode(t->title);
char *b = urlencode(t->album);
char len[16] = "", trk[16] = "";
if (t->length > 0) snprintf(len, sizeof(len), "%d", t->length);
if (t->trackno > 0) snprintf(trk, sizeof(trk), "%d", t->trackno);
char *le = urlencode(len), *te = urlencode(trk);
char *post = g_strdup_printf("s=%s&a=%s&t=%s&b=%s&l=%s&n=%s&m=",
session_id, a, ti, b, le, te);
g_free(a); g_free(ti); g_free(b); g_free(le); g_free(te);
fprintf(stderr, PLUGIN_NAME ": now playing: %s - %s\n", t->artist, t->title); fflush(stderr);
char *resp = do_post(np_url, post);
g_free(post);
if (resp) {
if (!g_str_has_prefix(resp, "OK")) {
fprintf(stderr, PLUGIN_NAME ": now-playing response: %s\n", resp); fflush(stderr);
}
if (g_str_has_prefix(resp, "BADSESSION")) session_expiry = 0;
g_free(resp);
}
}
/* ================================================================
* Scrobble submit
* ================================================================ */
static void submit_scrobble(const TrackInfo *t)
{
if (!ensure_session()) return;
char ts[32];
snprintf(ts, sizeof(ts), "%ld", (long)t->started);
char *a = urlencode(t->artist);
char *ti = urlencode(t->title);
char *b = urlencode(t->album);
char *tse = urlencode(ts);
char len[16] = "", trk[16] = "";
if (t->length > 0) snprintf(len, sizeof(len), "%d", t->length);
if (t->trackno > 0) snprintf(trk, sizeof(trk), "%d", t->trackno);
char *le = urlencode(len);
char *tre = urlencode(trk);
char *post = g_strdup_printf(
"s=%s&a[0]=%s&t[0]=%s&i[0]=%s&o[0]=P&r[0]=&l[0]=%s&b[0]=%s&n[0]=%s&m[0]=",
session_id, a, ti, tse, le, b, tre);
g_free(a); g_free(ti); g_free(b); g_free(tse); g_free(le); g_free(tre);
fprintf(stderr, PLUGIN_NAME ": scrobbling: %s - %s\n", t->artist, t->title); fflush(stderr);
char *resp = do_post(sub_url, post);
g_free(post);
if (resp) {
if (g_str_has_prefix(resp, "OK")) {
fprintf(stderr, PLUGIN_NAME ": scrobble accepted\n"); fflush(stderr);
} else {
fprintf(stderr, PLUGIN_NAME ": submit response: %s\n", resp); fflush(stderr);
}
if (g_str_has_prefix(resp, "BADSESSION")) session_expiry = 0;
g_free(resp);
}
}
/* ================================================================
* Worker thread
* ================================================================ */
static void *worker_func(void *unused)
{
(void)unused;
pthread_mutex_lock(&mtx);
while (running) {
while (running && !want_np && !want_sub)
pthread_cond_wait(&cond, &mtx);
if (!running) break;
if (want_sub) {
TrackInfo t = pend; want_sub = FALSE;
pthread_mutex_unlock(&mtx);
submit_scrobble(&t);
pthread_mutex_lock(&mtx);
}
if (want_np) {
TrackInfo t = cur; want_np = FALSE;
pthread_mutex_unlock(&mtx);
send_nowplaying(&t);
pthread_mutex_lock(&mtx);
}
}
pthread_mutex_unlock(&mtx);
return NULL;
}
/* ================================================================
* Metadata from Audacious 2.x
*
* Confirmed from header grep:
* aud_playlist_get_active() -> gint
* aud_playlist_get_position(gint pl) -> gint
* playlist_entry_get_tuple(gint pl, gint entry) -> const Tuple *
* tuple_get_string(const Tuple *, gint, NULL) -> const gchar *
* tuple_get_int(const Tuple *, gint, NULL) -> gint
* aud_tuple_free(ptr) == mowgli_object_unref
* aud_drct_get_length() -> gint ms
*
* FIELD_* constants come from libaudcore/tuple.h which plugin.h includes.
* ================================================================ */
static void fill_track(TrackInfo *t)
{
memset(t, 0, sizeof(*t));
t->length = t->trackno = -1;
t->valid = FALSE;
gint len_ms = audacious_drct_get_length();
if (len_ms > 0) t->length = len_ms / 1000;
gint pl = aud_playlist_get_active();
gint pos = aud_playlist_get_position(pl);
Tuple *tup = (Tuple *) aud_playlist_entry_get_tuple(pl, pos);
if (!tup) return;
const gchar *artist = tuple_get_string(tup, FIELD_ARTIST, NULL);
const gchar *title = tuple_get_string(tup, FIELD_TITLE, NULL);
const gchar *album = tuple_get_string(tup, FIELD_ALBUM, NULL);
gint trackno = tuple_get_int (tup, FIELD_TRACK_NUMBER, NULL);
if (artist) g_strlcpy(t->artist, artist, sizeof(t->artist));
if (title) g_strlcpy(t->title, title, sizeof(t->title));
if (album) g_strlcpy(t->album, album, sizeof(t->album));
if (trackno > 0) t->trackno = trackno;
/* Do not free the tuple - we don't own it in Audacious 2.x */
t->valid = (t->artist[0] != '\0' && t->title[0] != '\0');
}
/* ================================================================
* Eligibility
* ================================================================ */
static gboolean should_scrobble(const TrackInfo *t)
{
if (!t->valid || t->started == 0) return FALSE;
if (t->length > 0 && t->length < MIN_TRACK_SEC) return FALSE;
time_t elapsed = time(NULL) - t->started;
if (elapsed >= MIN_PLAY_SEC) return TRUE;
if (t->length > 0 && elapsed >= t->length / 2) return TRUE;
return FALSE;
}
/* ================================================================
* Hook callbacks
* ================================================================ */
static void on_playback_begin(gpointer hook_data, gpointer user_data)
{
(void)hook_data; (void)user_data;
pthread_mutex_lock(&mtx);
if (should_scrobble(&cur)) { pend = cur; want_sub = TRUE; }
fill_track(&cur);
cur.started = time(NULL);
if (cur.valid) { want_np = TRUE; pthread_cond_signal(&cond); }
pthread_mutex_unlock(&mtx);
}
static void on_playback_stop(gpointer hook_data, gpointer user_data)
{
(void)hook_data; (void)user_data;
pthread_mutex_lock(&mtx);
if (should_scrobble(&cur)) {
pend = cur; want_sub = TRUE;
pthread_cond_signal(&cond);
}
memset(&cur, 0, sizeof(cur));
cur.length = cur.trackno = -1;
pthread_mutex_unlock(&mtx);
}
/* ================================================================
* Plugin init / cleanup (void return in Audacious 2.x)
* ================================================================ */
static void scrobbler_init(void)
{
curl_global_init(CURL_GLOBAL_DEFAULT);
load_config();
running = TRUE;
pthread_create(&worker, NULL, worker_func, NULL);
aud_hook_associate("playback begin", on_playback_begin, NULL);
aud_hook_associate("playback stop", on_playback_stop, NULL);
aud_hook_associate("playback end", on_playback_stop, NULL);
g_message(PLUGIN_NAME ": loaded (user: %s)",
cfg_user[0] ? cfg_user : "unconfigured");
}
static void scrobbler_cleanup(void)
{
aud_hook_dissociate("playback begin", on_playback_begin);
aud_hook_dissociate("playback stop", on_playback_stop);
aud_hook_dissociate("playback end", on_playback_stop);
pthread_mutex_lock(&mtx);
running = FALSE;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mtx);
pthread_join(worker, NULL);
curl_global_cleanup();
g_message(PLUGIN_NAME ": unloaded");
}
/* ================================================================
* Plugin registration
*
* From header grep, DECLARE_PLUGIN signature is:
* (name, init, fini, ip_list, op_list, ep_list, gp_list, vp_list, interface)
* ================================================================ */
static GeneralPlugin gp_info = {
.description = (gchar *) PLUGIN_NAME,
.init = scrobbler_init,
.cleanup = scrobbler_cleanup,
};
GeneralPlugin *gen_plugin_list[] = { &gp_info, NULL };
DECLARE_PLUGIN(librefm_scrobbler, NULL, NULL,
NULL, NULL, NULL,
gen_plugin_list,
NULL, NULL);
$ sudo apt-get install audacious-dev $ sudo aptitude install libcurl4-dev $ wget http://distfiles.atheme.org/audacious-plugins-2.2.tgz $ tar -zxvf audacious-plugins-2.2.tgz $ cd audacious-plugins-2.2
$ ./configure --enable-dependency-tracking --enable-lastfm --enable-scrobbler --disable-aac --disable-icecast --disable-adplug --disable-jack --disable-alsa --disable-altivec --disable-libFLACtest --disable-amidiplug --disable-libmadtest --disable-amidiplug-alsa --disable-lirc --disable-amidiplug-dummy --disable-mms --disable-amidiplug-flsyn --disable-modplug --disable-aosd --disable-mp3 --disable-aosd-xcomp --disable-mtp_up --disable-bluetooth --disable-neon --disable-bs2b --disable-nls --disable-cdaudio --disable-option-checking --disable-coreaudio --disable-oss --disable-cue --disable-paranormal --disable-dbus --disable-projectm --disable-dependency-tracking --disable-projectm-1.0 --disable-dockalbumart --disable-pulse --disable-esd --disable-rocklight --disable-evdevplug --disable-rpath --disable-ffaudio --disable-sid --disable-filewriter --disable-sndfile --disable-filewriter_flac --disable-sse2 --disable-filewriter_mp3 --disable-statusicon --disable-filewriter_vorbis --disable-streambrowser --disable-flacng --disable-vorbis --disable-gio --disable-wavpack --disable-gnomeshortcuts --disable-xmltest --disable-hotkey --disable-xspf
Use an editor to open (in audacious-plugins-2.2) configure (line 7498) and configure.ac (line 126),
change:
INPUT_PLUGINS="tonegen console psf xsf metronom vtx" OUTPUT_PLUGINS="crossfade null" EFFECT_PLUGINS="audiocompress crystalizer ladspa voice_removal sndstretch stereo_plugin echo_plugin" GENERAL_PLUGINS="song_change alarm skins vfstrace gtkui" VISUALIZATION_PLUGINS="blur_scope spectrum" CONTAINER_PLUGINS="m3u pls"
to:
INPUT_PLUGINS="" OUTPUT_PLUGINS="" EFFECT_PLUGINS="" GENERAL_PLUGINS="song_change" VISUALIZATION_PLUGINS="" CONTAINER_PLUGINS=""
Then,
$ make $ cd src/scrobbler/ $ sudo cp scrobbler.so /usr/lib/audacious/General
Type, "/@say/Your message here." after the end of any URL on my site and hit enter to leave a comment. You can view them here. An example would be, http://superkuh.com/rtlsdr.html/@say/Your message here.
You may not access or use the site superkuh.com if you are under 90 years of age. If you do not agree then you must leave now.
The US Dept. of Justice has determined that violating a website's terms of service is a felony under CFAA 1030(a)2(c). Under this same law I can declare that you may only use one IP address to access this site; circumvention is a felony. Absurd, isn't it?
It is my policy to regularly delete server logs. I don't log at all for the tor onion service.
search. (via google)
I enjoy recursion, dissipating local energy gradients, lipid bilayers, palindromic dimerization, particle acceleration, heliophysics instrumentation and generally anything with a high rate of change in electrical current. This site is a combination of my efforts to archive what I find interesting and my shoddy attempts to implement it as cheap as possible. A living embodiment of "a little knowledge is a dangerous thing".
I get all email sent to @superkuh.com
Make-up any address *@superkuh.com
If I don't respond check your "spam" folder. Megacorps like google used to mark me as spam.