Support for LastFM scrobbling (issue #19)

This commit is contained in:
ejurgensen 2014-08-15 23:36:54 +02:00
parent 21cf3ab7d3
commit 6d8e4c67aa
11 changed files with 1085 additions and 22 deletions

View File

@ -29,6 +29,8 @@ sudo apt-get install \
Depending on the version of libav/ffmpeg in your distribution you may also need
libavresample-dev.
To build with LastFM support, you should also install libcurl4-openssl-dev.
Note that while forked-daapd will work with versions of libevent between 2.0.0
and 2.1.3, it is recommended to use either libevent 1 or 2.1.4+. Otherwise you
will not have support for Shoutcast metadata and simultaneous streaming to
@ -108,6 +110,8 @@ Libraries:
from <http://github.com/JonathanBeck/libplist/downloads>
- libspotify (optional - Spotify support)
from <https://developer.spotify.com>
- libcurl (optional - LastFM support)
from <http://curl.haxx.se/libcurl/>
If using binary packages, remember that you need the development packages to
build forked-daapd (usually named -dev or -devel).
@ -161,6 +165,9 @@ though you compiled with --enable-spotify, the executable will still be able
to run on systems without libspotify (the Spotify features will then be
disabled).
Support for LastFM scrobbling is optional. Use --enable-lastfm to enable this
feature.
Support for iTunes Music Library XML format is optional. Use --enable-itunes
to enable this feature.

21
README
View File

@ -4,7 +4,7 @@ forked-daapd
forked-daapd is a Linux/FreeBSD DAAP (iTunes) and RSP (Roku) media server.
It has support for AirPlay devices/speakers, Apple Remote (and compatibles),
internet radio and Spotify.
internet radio, Spotify and LastFM.
DAAP stands for Digital Audio Access Protocol, and is the protocol used
by iTunes and friends to share/stream media libraries over the network.
@ -37,6 +37,7 @@ Contents of this README
- Library
- Command line and web interface
- Spotify
- LastFM
Supported clients
@ -420,3 +421,21 @@ You will not be able to do any playlist management through forked-daapd - use
a Spotify client for that. You also can only listen to your music by letting
forked-daapd do the playback - so that means you can't stream from forked-daapd
to iTunes.
LastFM
------
If forked-daapd was built with LastFM scrobbling enabled (see the INSTALL file)
you can have it scrobble the music you listen to. To set up scrobbling you must
create a text file with the file name ending ".lastfm". The file must have two
lines: The first is your LastFM user name, and the second is your password. Move
the file to your forked-daapd library. Forked-daapd will then log in and get a
permanent session key.
You should delete the .lastfm file immediately after completing the first login.
For safety, forked-daapd will not store your LastFM username/password, only the
session key. The session key does not expire.
To stop scrobbling from forked-daapd, add an empty ".lastfm" file to your
library.

View File

@ -66,10 +66,15 @@ AC_ARG_ENABLE(spotify, AS_HELP_STRING([--enable-spotify], [enable Spotify librar
use_spotify=true;
CPPFLAGS="${CPPFLAGS} -DSPOTIFY")
AC_ARG_ENABLE(lastfm, AS_HELP_STRING([--enable-lastfm], [enable LastFM support (default=no)]),
use_lastfm=true;
CPPFLAGS="${CPPFLAGS} -DLASTFM")
AM_CONDITIONAL(COND_FLAC, test x$use_flac = xtrue)
AM_CONDITIONAL(COND_MUSEPACK, test x$use_musepack = xtrue)
AM_CONDITIONAL(COND_ITUNES, test x$use_itunes = xtrue)
AM_CONDITIONAL(COND_SPOTIFY, test x$use_spotify = xtrue)
AM_CONDITIONAL(COND_LASTFM, test x$use_lastfm = xtrue)
AC_ARG_WITH(oss4, AS_HELP_STRING([--with-oss4=includedir], [use OSS4 with soundcard.h in includedir (default /usr/lib/oss/include/sys)]),
[ case "$withval" in
@ -221,6 +226,12 @@ if test x$use_spotify = xtrue; then
AC_SUBST(SPOTIFY_LIBS)
fi
if test x$use_lastfm = xtrue; then
PKG_CHECK_MODULES(LIBCURL, [ libcurl ])
AC_CHECK_LIB([mxml], [mxmlGetOpaque],
AC_DEFINE(HAVE_MXML_GETOPAQUE, 1, [Define to 1 if your mxml has mxmlGetOpaque.]))
fi
case "$host" in
*-*-linux-*)
if test x$use_oss4 != xtrue; then

View File

@ -2,27 +2,31 @@
sbin_PROGRAMS = forked-daapd
if COND_FLAC
FLACSRC=scan-flac.c
FLAC_SRC=scan-flac.c
endif
if COND_MUSEPACK
MUSEPACKSRC=scan-mpc.c
MUSEPACK_SRC=scan-mpc.c
endif
if COND_ITUNES
ITUNESSRC=filescanner_itunes.c
ITUNES_SRC=filescanner_itunes.c
endif
if COND_SPOTIFY
SPOTIFYSRC=spotify.c spotify.h
SPOTIFY_SRC=spotify.c spotify.h
endif
if COND_LASTFM
LASTFM_SRC=lastfm.c lastfm.h
endif
if COND_ALSA
ALSASRC=laudio_alsa.c
ALSA_SRC=laudio_alsa.c
endif
if COND_OSS4
OSS4SRC=laudio_oss4.c
OSS4_SRC=laudio_oss4.c
endif
if COND_AVIO
@ -78,20 +82,22 @@ forked_daapd_CPPFLAGS = -D_GNU_SOURCE \
forked_daapd_CFLAGS = \
@ZLIB_CFLAGS@ @AVAHI_CFLAGS@ @SQLITE3_CFLAGS@ @LIBAV_CFLAGS@ \
@CONFUSE_CFLAGS@ @TAGLIB_CFLAGS@ @MINIXML_CFLAGS@ @LIBPLIST_CFLAGS@ \
@LIBGCRYPT_CFLAGS@ @GPG_ERROR_CFLAGS@ @ALSA_CFLAGS@ @SPOTIFY_CFLAGS@
@LIBGCRYPT_CFLAGS@ @GPG_ERROR_CFLAGS@ @ALSA_CFLAGS@ @SPOTIFY_CFLAGS@ \
@LIBCURL_CFLAGS@
forked_daapd_LDADD = -lrt \
@ZLIB_LIBS@ @AVAHI_LIBS@ @SQLITE3_LIBS@ @LIBAV_LIBS@ \
@CONFUSE_LIBS@ @FLAC_LIBS@ @TAGLIB_LIBS@ @LIBEVENT_LIBS@ \
@LIBAVL_LIBS@ @MINIXML_LIBS@ @ANTLR3C_LIBS@ @LIBPLIST_LIBS@ \
@LIBGCRYPT_LIBS@ @GPG_ERROR_LIBS@ @ALSA_LIBS@ @LIBUNISTRING@ @SPOTIFY_LIBS@
@LIBGCRYPT_LIBS@ @GPG_ERROR_LIBS@ @ALSA_LIBS@ @LIBUNISTRING@ @SPOTIFY_LIBS@ \
@LIBCURL_LIBS@
forked_daapd_SOURCES = main.c \
db.c db.h \
logger.c logger.h \
conffile.c conffile.h \
filescanner.c filescanner.h \
filescanner_ffmpeg.c filescanner_m3u.c filescanner_icy.c $(ITUNESSRC) \
filescanner_ffmpeg.c filescanner_m3u.c filescanner_icy.c $(ITUNES_SRC) \
mdns_avahi.c mdns.h \
remote_pairing.c remote_pairing.h \
$(EVHTTP_SRC) \
@ -109,12 +115,12 @@ forked_daapd_SOURCES = main.c \
rsp_query.c rsp_query.h \
daap_query.c daap_query.h \
player.c player.h \
$(ALSASRC) $(OSS4SRC) laudio.h \
$(ALSA_SRC) $(OSS4_SRC) laudio.h \
raop.c raop.h \
$(RTSP_SRC) \
scan-wma.c \
$(SPOTIFYSRC) \
$(FLACSRC) $(MUSEPACKSRC)
$(SPOTIFY_SRC) $(LASTFM_SRC) \
$(FLAC_SRC) $(MUSEPACK_SRC)
nodist_forked_daapd_SOURCES = \
$(ANTLR_SOURCES)

View File

@ -63,6 +63,9 @@
#include "remote_pairing.h"
#include "player.h"
#ifdef LASTFM
# include "lastfm.h"
#endif
#ifdef HAVE_SPOTIFY_H
# include "spotify.h"
#endif
@ -687,6 +690,14 @@ process_file(char *file, time_t mtime, off_t size, int type, int flags)
return;
}
#ifdef LASTFM
else if (strcmp(ext, ".lastfm") == 0)
{
lastfm_login(file);
return;
}
#endif
#ifdef HAVE_SPOTIFY_H
else if (strcmp(ext, ".spotify") == 0)
{

986
src/lastfm.c Normal file
View File

@ -0,0 +1,986 @@
/*
* Copyright (C) 2014 Espen Jürgensen <espenjurgensen@gmail.com>
*
* 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <stdio.h>
#include <inttypes.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <string.h>
#include <pthread.h>
#include <gcrypt.h>
#include <mxml.h>
#include <event2/event.h>
#include <event2/buffer.h>
#include <event2/http.h>
#include <curl/curl.h>
#include "db.h"
#include "lastfm.h"
#include "logger.h"
#include "misc.h"
struct lastfm_command;
typedef int (*cmd_func)(struct lastfm_command *cmd);
struct lastfm_command
{
pthread_mutex_t lck;
pthread_cond_t cond;
cmd_func func;
int nonblock;
union {
void *noarg;
int id;
struct keyval *kv;
} arg;
int ret;
};
struct https_client_ctx
{
const char *url;
const char *body;
struct evbuffer *data;
};
/* --- Globals --- */
// lastfm thread
static pthread_t tid_lastfm;
// Event base, pipes and events
struct event_base *evbase_lastfm;
static int g_exit_pipe[2];
static int g_cmd_pipe[2];
static struct event *g_exitev;
static struct event *g_cmdev;
// Tells us if the LastFM thread has been set up
static int g_initialized = 0;
// LastFM becomes disabled if we get a scrobble, try initialising session,
// but can't (probably no session key in db because user does not use LastFM)
static int g_disabled = 0;
/**
* The API key and secret (not so secret being open source) is specific to
* forked-daapd, and is used to identify forked-daapd and to sign requests
*/
const char *g_api_key = "579593f2ed3f49673c7364fd1c9c829b";
const char *g_secret = "ce45a1d275c10b3edf0ecfa27791cb2b";
const char *api_url = "http://ws.audioscrobbler.com/2.0/";
const char *auth_url = "https://ws.audioscrobbler.com/2.0/";
// Session key
char *g_session_key = NULL;
/* --------------------------------- HELPERS ------------------------------- */
/* Reads a LastFM credentials file (1st line username, 2nd line password) */
static int
credentials_read(char *path, char **username, char **password)
{
FILE *fp;
char *u;
char *p;
char buf[256];
int len;
fp = fopen(path, "rb");
if (!fp)
{
DPRINTF(E_LOG, L_LASTFM, "Could not open lastfm credentials file %s: %s\n", path, strerror(errno));
return -1;
}
u = fgets(buf, sizeof(buf), fp);
if (!u)
{
DPRINTF(E_LOG, L_LASTFM, "Empty lastfm credentials file %s\n", path);
fclose(fp);
return -1;
}
len = strlen(u);
if (buf[len - 1] != '\n')
{
DPRINTF(E_LOG, L_LASTFM, "Invalid lastfm credentials file %s: username name too long or missing password\n", path);
fclose(fp);
return -1;
}
while (len)
{
if ((buf[len - 1] == '\r') || (buf[len - 1] == '\n'))
{
buf[len - 1] = '\0';
len--;
}
else
break;
}
if (!len)
{
DPRINTF(E_LOG, L_LASTFM, "Invalid lastfm credentials file %s: empty line where username expected\n", path);
fclose(fp);
return -1;
}
u = strdup(buf);
if (!u)
{
DPRINTF(E_LOG, L_LASTFM, "Out of memory for username while reading %s\n", path);
fclose(fp);
return -1;
}
p = fgets(buf, sizeof(buf), fp);
fclose(fp);
if (!p)
{
DPRINTF(E_LOG, L_LASTFM, "Invalid lastfm credentials file %s: no password\n", path);
free(u);
return -1;
}
len = strlen(p);
while (len)
{
if ((buf[len - 1] == '\r') || (buf[len - 1] == '\n'))
{
buf[len - 1] = '\0';
len--;
}
else
break;
}
p = strdup(buf);
if (!p)
{
DPRINTF(E_LOG, L_LASTFM, "Out of memory for password while reading %s\n", path);
free(u);
return -1;
}
DPRINTF(E_LOG, L_LASTFM, "lastfm credentials file OK, logging in with username %s\n", u);
*username = u;
*password = p;
return 0;
}
/* Converts parameters to a string in application/x-www-form-urlencoded format */
static int
body_print(char **body, struct keyval *kv)
{
struct evbuffer *evbuf;
struct onekeyval *okv;
char *k;
char *v;
evbuf = evbuffer_new();
for (okv = kv->head; okv; okv = okv->next)
{
k = evhttp_encode_uri(okv->name);
if (!k)
continue;
v = evhttp_encode_uri(okv->value);
if (!v)
{
free(k);
continue;
}
evbuffer_add(evbuf, k, strlen(k));
evbuffer_add(evbuf, "=", 1);
evbuffer_add(evbuf, v, strlen(v));
if (okv->next)
evbuffer_add(evbuf, "&", 1);
free(k);
free(v);
}
evbuffer_add(evbuf, "\n", 1);
*body = evbuffer_readln(evbuf, NULL, EVBUFFER_EOL_ANY);
evbuffer_free(evbuf);
DPRINTF(E_DBG, L_LASTFM, "Parameters in request are: %s\n", *body);
return 0;
}
/* Creates an md5 signature of the concatenated parameters and adds it to keyval */
static int
param_sign(struct keyval *kv)
{
struct onekeyval *okv;
char hash[33];
char ebuf[64];
uint8_t *hash_bytes;
size_t hash_len;
gcry_md_hd_t md_hdl;
gpg_error_t gc_err;
int ret;
int i;
gc_err = gcry_md_open(&md_hdl, GCRY_MD_MD5, 0);
if (gc_err != GPG_ERR_NO_ERROR)
{
gpg_strerror_r(gc_err, ebuf, sizeof(ebuf));
DPRINTF(E_LOG, L_LASTFM, "Could not open MD5: %s\n", ebuf);
return -1;
}
for (okv = kv->head; okv; okv = okv->next)
{
gcry_md_write(md_hdl, okv->name, strlen(okv->name));
gcry_md_write(md_hdl, okv->value, strlen(okv->value));
}
gcry_md_write(md_hdl, g_secret, strlen(g_secret));
hash_bytes = gcry_md_read(md_hdl, GCRY_MD_MD5);
if (!hash_bytes)
{
DPRINTF(E_LOG, L_LASTFM, "Could not read MD5 hash\n");
return -1;
}
hash_len = gcry_md_get_algo_dlen(GCRY_MD_MD5);
for (i = 0; i < hash_len; i++)
sprintf(hash + (2 * i), "%02x", hash_bytes[i]);
ret = keyval_add(kv, "api_sig", hash);
gcry_md_close(md_hdl);
return ret;
}
/* For compability with mxml 2.6 */
#ifndef HAVE_MXML_GETOPAQUE
const char * /* O - Opaque string or NULL */
mxmlGetOpaque(mxml_node_t *node) /* I - Node to get */
{
if (!node)
return (NULL);
if (node->type == MXML_OPAQUE)
return (node->value.opaque);
else if (node->type == MXML_ELEMENT &&
node->child &&
node->child->type == MXML_OPAQUE)
return (node->child->value.opaque);
else
return (NULL);
}
#endif
/* ---------------------------- COMMAND EXECUTION -------------------------- */
static int
send_command(struct lastfm_command *cmd)
{
int ret;
if (!cmd->func)
{
DPRINTF(E_LOG, L_LASTFM, "BUG: cmd->func is NULL!\n");
return -1;
}
ret = write(g_cmd_pipe[1], &cmd, sizeof(cmd));
if (ret != sizeof(cmd))
{
DPRINTF(E_LOG, L_LASTFM, "Could not send command: %s\n", strerror(errno));
return -1;
}
return 0;
}
static int
nonblock_command(struct lastfm_command *cmd)
{
int ret;
ret = send_command(cmd);
if (ret < 0)
return -1;
return 0;
}
/* Thread: main */
static void
thread_exit(void)
{
int dummy = 42;
DPRINTF(E_DBG, L_LASTFM, "Killing lastfm thread\n");
if (write(g_exit_pipe[1], &dummy, sizeof(dummy)) != sizeof(dummy))
DPRINTF(E_LOG, L_LASTFM, "Could not write to exit fd: %s\n", strerror(errno));
}
/* --------------------------------- MAIN --------------------------------- */
/* Thread: lastfm */
static size_t
request_cb(char *ptr, size_t size, size_t nmemb, void *userdata)
{
size_t realsize;
struct https_client_ctx *ctx;
int ret;
realsize = size * nmemb;
ctx = (struct https_client_ctx *)userdata;
ret = evbuffer_add(ctx->data, ptr, realsize);
if (ret < 0)
{
DPRINTF(E_LOG, L_LASTFM, "Error adding reply from LastFM to data buffer\n");
return 0;
}
return realsize;
}
static void
response_proces(struct https_client_ctx *ctx)
{
mxml_node_t *tree;
mxml_node_t *s_node;
mxml_node_t *e_node;
char *body;
char *errmsg;
char *sk;
// NULL-terminate the buffer
evbuffer_add(ctx->data, "", 1);
body = (char *)evbuffer_pullup(ctx->data, -1);
if (!body || (strlen(body) == 0))
{
DPRINTF(E_LOG, L_LASTFM, "Empty response\n");
return;
}
DPRINTF(E_SPAM, L_LASTFM, "LastFM response:\n%s\n", body);
tree = mxmlLoadString(NULL, body, MXML_OPAQUE_CALLBACK);
if (!tree)
return;
// Look for errors
e_node = mxmlFindElement(tree, tree, "error", NULL, NULL, MXML_DESCEND);
if (e_node)
{
errmsg = trimwhitespace(mxmlGetOpaque(e_node));
DPRINTF(E_LOG, L_LASTFM, "Request to LastFM failed: %s\n", errmsg);
if (errmsg)
free(errmsg);
mxmlDelete(tree);
return;
}
// Was it a scrobble request? Then do nothing. TODO: Check for error messages
s_node = mxmlFindElement(tree, tree, "scrobbles", NULL, NULL, MXML_DESCEND);
if (s_node)
{
DPRINTF(E_DBG, L_LASTFM, "Scrobble callback\n");
mxmlDelete(tree);
return;
}
// Otherwise an auth request, so get the session key
s_node = mxmlFindElement(tree, tree, "key", NULL, NULL, MXML_DESCEND);
if (!s_node)
{
DPRINTF(E_LOG, L_LASTFM, "Session key not found\n");
mxmlDelete(tree);
return;
}
sk = trimwhitespace(mxmlGetOpaque(s_node));
if (sk)
{
DPRINTF(E_LOG, L_LASTFM, "Got session key from LastFM: %s\n", sk);
db_admin_add("lastfm_sk", sk);
if (g_session_key)
free(g_session_key);
g_session_key = sk;
}
mxmlDelete(tree);
}
// We use libcurl to make the request. We could use libevent and avoid the
// dependency, but for SSL, libevent needs to be v2.1 or better, which is still
// a bit too new to be in the major distros
static int
https_client_request(struct https_client_ctx *ctx)
{
CURL *curl;
CURLcode res;
curl = curl_easy_init();
if (!curl)
{
DPRINTF(E_LOG, L_LASTFM, "Error: Could not get curl handle\n");
return -1;
}
curl_easy_setopt(curl, CURLOPT_USERAGENT, "libcurl-agent/1.0");
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, ctx->body);
curl_easy_setopt(curl, CURLOPT_URL, ctx->url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, request_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, ctx);
ctx->data = evbuffer_new();
if (!ctx->data)
{
DPRINTF(E_LOG, L_LASTFM, "Could not create evbuffer for LastFM response\n");
curl_easy_cleanup(curl);
return -1;
}
res = curl_easy_perform(curl);
if (res != CURLE_OK)
{
DPRINTF(E_LOG, L_LASTFM, "Request to %s failed: %s\n", ctx->url, curl_easy_strerror(res));
curl_easy_cleanup(curl);
return -1;
}
response_proces(ctx);
evbuffer_free(ctx->data);
curl_easy_cleanup(curl);
return 0;
}
static int
request_post(char *method, struct keyval *kv, int auth)
{
struct https_client_ctx ctx;
char *body;
int ret;
ret = keyval_add(kv, "method", method);
if (ret < 0)
return -1;
if (!auth)
ret = keyval_add(kv, "sk", g_session_key);
if (ret < 0)
return -1;
// API requires that we MD5 sign sorted param (without "format" param)
keyval_sort(kv);
ret = param_sign(kv);
if (ret < 0)
{
DPRINTF(E_LOG, L_LASTFM, "Aborting request, param_sign failed\n");
return -1;
}
ret = body_print(&body, kv);
if (ret < 0)
{
DPRINTF(E_LOG, L_LASTFM, "Aborting request, body_print failed\n");
return -1;
}
memset(&ctx, 0, sizeof(struct https_client_ctx));
ctx.url = auth ? auth_url : api_url;
ctx.body = body;
ret = https_client_request(&ctx);
return ret;
}
static int
login(struct lastfm_command *cmd)
{
request_post("auth.getMobileSession", cmd->arg.kv, 1);
keyval_clear(cmd->arg.kv);
return 0;
}
static int
scrobble(struct lastfm_command *cmd)
{
struct media_file_info *mfi;
struct keyval *kv;
char duration[4];
char trackNumber[4];
char timestamp[16];
int ret;
mfi = db_file_fetch_byid(cmd->arg.id);
if (!mfi)
{
DPRINTF(E_LOG, L_LASTFM, "Scrobble failed, track id %d is unknown\n", cmd->arg.id);
return -1;
}
// Don't scrobble songs which are shorter than 30 sec
if (mfi->song_length < 30000)
goto noscrobble;
// Don't scrobble non-music and radio stations
if ((mfi->media_kind != 1) || (mfi->data_kind == 1))
goto noscrobble;
// Don't scrobble songs with unknown artist
if (strcmp(mfi->artist, "Unknown artist") == 0)
goto noscrobble;
kv = keyval_alloc();
if (!kv)
goto noscrobble;
snprintf(duration, sizeof(duration), "%" PRIu32, mfi->song_length);
snprintf(trackNumber, sizeof(trackNumber), "%" PRIu32, mfi->track);
snprintf(timestamp, sizeof(timestamp), "%" PRIi64, (int64_t)time(NULL));
ret = ( (keyval_add(kv, "api_key", g_api_key) == 0) &&
(keyval_add(kv, "sk", g_session_key) == 0) &&
(keyval_add(kv, "artist", mfi->artist) == 0) &&
(keyval_add(kv, "track", mfi->title) == 0) &&
(keyval_add(kv, "album", mfi->album) == 0) &&
(keyval_add(kv, "albumArtist", mfi->album_artist) == 0) &&
(keyval_add(kv, "duration", duration) == 0) &&
(keyval_add(kv, "trackNumber", trackNumber) == 0) &&
(keyval_add(kv, "timestamp", timestamp) == 0)
);
free_mfi(mfi, 0);
if (!ret)
{
keyval_clear(kv);
return -1;
}
DPRINTF(E_INFO, L_LASTFM, "Scrobbling '%s' by '%s'\n", keyval_get(kv, "track"), keyval_get(kv, "artist"));
request_post("track.scrobble", kv, 0);
keyval_clear(kv);
return 0;
noscrobble:
free_mfi(mfi, 0);
return -1;
}
static void *
lastfm(void *arg)
{
int ret;
DPRINTF(E_DBG, L_LASTFM, "Main loop initiating\n");
ret = db_perthread_init();
if (ret < 0)
{
DPRINTF(E_LOG, L_LASTFM, "Error: DB init failed\n");
pthread_exit(NULL);
}
event_base_dispatch(evbase_lastfm);
if (g_initialized)
{
DPRINTF(E_LOG, L_LASTFM, "LastFM event loop terminated ahead of time!\n");
g_initialized = 0;
}
db_perthread_deinit();
DPRINTF(E_DBG, L_LASTFM, "Main loop terminating\n");
pthread_exit(NULL);
}
static void
exit_cb(int fd, short what, void *arg)
{
int dummy;
int ret;
ret = read(g_exit_pipe[0], &dummy, sizeof(dummy));
if (ret != sizeof(dummy))
DPRINTF(E_LOG, L_LASTFM, "Error reading from exit pipe\n");
event_base_loopbreak(evbase_lastfm);
g_initialized = 0;
event_add(g_exitev, NULL);
}
static void
command_cb(int fd, short what, void *arg)
{
struct lastfm_command *cmd;
int ret;
ret = read(g_cmd_pipe[0], &cmd, sizeof(cmd));
if (ret != sizeof(cmd))
{
DPRINTF(E_LOG, L_LASTFM, "Could not read command! (read %d): %s\n", ret, (ret < 0) ? strerror(errno) : "-no error-");
goto readd;
}
if (cmd->nonblock)
{
cmd->func(cmd);
free(cmd);
goto readd;
}
pthread_mutex_lock(&cmd->lck);
ret = cmd->func(cmd);
cmd->ret = ret;
pthread_cond_signal(&cmd->cond);
pthread_mutex_unlock(&cmd->lck);
readd:
event_add(g_cmdev, NULL);
}
/* ---------------------------- Our lastfm API --------------------------- */
static int
lastfm_init(void);
/* Thread: filescanner */
void
lastfm_login(char *path)
{
struct lastfm_command *cmd;
struct keyval *kv;
char *username;
char *password;
int ret;
DPRINTF(E_DBG, L_LASTFM, "Got LastFM login request\n");
// Delete any existing session key
if (g_session_key)
free(g_session_key);
g_session_key = NULL;
db_admin_delete("lastfm_sk");
// Read the credentials file
ret = credentials_read(path, &username, &password);
if (ret < 0)
return;
// Enable LastFM now that we got a login attempt
g_disabled = 0;
kv = keyval_alloc();
if (!kv)
{
free(username);
free(password);
return;
}
ret = ( (keyval_add(kv, "api_key", g_api_key) == 0) &&
(keyval_add(kv, "username", username) == 0) &&
(keyval_add(kv, "password", password) == 0) );
free(username);
free(password);
if (!ret)
{
keyval_clear(kv);
return;
}
// Spawn thread
ret = lastfm_init();
if (ret < 0)
{
g_disabled = 1;
return;
}
g_initialized = 1;
// Send login command to the thread
cmd = (struct lastfm_command *)malloc(sizeof(struct lastfm_command));
if (!cmd)
{
DPRINTF(E_LOG, L_LASTFM, "Could not allocate lastfm_command\n");
return;
}
memset(cmd, 0, sizeof(struct lastfm_command));
cmd->nonblock = 1;
cmd->func = login;
cmd->arg.kv = kv;
nonblock_command(cmd);
return;
}
/* Thread: http and player */
int
lastfm_scrobble(int id)
{
struct lastfm_command *cmd;
int ret;
DPRINTF(E_DBG, L_LASTFM, "Got LastFM scrobble request\n");
// LastFM is disabled because we already tried looking for a session key, but failed
if (g_disabled)
return -1;
// No session key in mem or in db
if ((!g_session_key) || !(g_session_key = db_admin_get("lastfm_sk")))
{
DPRINTF(E_INFO, L_LASTFM, "No valid LastFM session key\n");
g_disabled = 1;
return -1;
}
// Spawn LastFM thread
ret = lastfm_init();
if (ret < 0)
{
g_disabled = 1;
return -1;
}
g_initialized = 1;
// Send scrobble command to the thread
cmd = (struct lastfm_command *)malloc(sizeof(struct lastfm_command));
if (!cmd)
{
DPRINTF(E_LOG, L_LASTFM, "Could not allocate lastfm_command\n");
return -1;
}
memset(cmd, 0, sizeof(struct lastfm_command));
cmd->nonblock = 1;
cmd->func = scrobble;
cmd->arg.id = id;
nonblock_command(cmd);
return 0;
}
static int
lastfm_init(void)
{
int ret;
if (g_initialized)
return 0;
curl_global_init(CURL_GLOBAL_DEFAULT);
# if defined(__linux__)
ret = pipe2(g_exit_pipe, O_CLOEXEC);
# else
ret = pipe(g_exit_pipe);
# endif
if (ret < 0)
{
DPRINTF(E_LOG, L_LASTFM, "Could not create pipe: %s\n", strerror(errno));
goto exit_fail;
}
# if defined(__linux__)
ret = pipe2(g_cmd_pipe, O_CLOEXEC);
# else
ret = pipe(g_cmd_pipe);
# endif
if (ret < 0)
{
DPRINTF(E_LOG, L_LASTFM, "Could not create command pipe: %s\n", strerror(errno));
goto cmd_fail;
}
evbase_lastfm = event_base_new();
if (!evbase_lastfm)
{
DPRINTF(E_LOG, L_LASTFM, "Could not create an event base\n");
goto evbase_fail;
}
#ifdef HAVE_LIBEVENT2
g_exitev = event_new(evbase_lastfm, g_exit_pipe[0], EV_READ, exit_cb, NULL);
if (!g_exitev)
{
DPRINTF(E_LOG, L_LASTFM, "Could not create exit event\n");
goto evnew_fail;
}
g_cmdev = event_new(evbase_lastfm, g_cmd_pipe[0], EV_READ, command_cb, NULL);
if (!g_cmdev)
{
DPRINTF(E_LOG, L_LASTFM, "Could not create cmd event\n");
goto evnew_fail;
}
#else
g_exitev = (struct event *)malloc(sizeof(struct event));
if (!g_exitev)
{
DPRINTF(E_LOG, L_LASTFM, "Could not create exit event\n");
goto evnew_fail;
}
event_set(g_exitev, g_exit_pipe[0], EV_READ, exit_cb, NULL);
event_base_set(evbase_lastfm, g_exitev);
g_cmdev = (struct event *)malloc(sizeof(struct event));
if (!g_cmdev)
{
DPRINTF(E_LOG, L_LASTFM, "Could not create cmd event\n");
goto evnew_fail;
}
event_set(g_cmdev, g_cmd_pipe[0], EV_READ, command_cb, NULL);
event_base_set(evbase_lastfm, g_cmdev);
#endif
event_add(g_exitev, NULL);
event_add(g_cmdev, NULL);
DPRINTF(E_INFO, L_LASTFM, "LastFM thread init\n");
ret = pthread_create(&tid_lastfm, NULL, lastfm, NULL);
if (ret < 0)
{
DPRINTF(E_LOG, L_LASTFM, "Could not spawn LastFM thread: %s\n", strerror(errno));
goto thread_fail;
}
return 0;
thread_fail:
evnew_fail:
event_base_free(evbase_lastfm);
evbase_lastfm = NULL;
evbase_fail:
close(g_cmd_pipe[0]);
close(g_cmd_pipe[1]);
cmd_fail:
close(g_exit_pipe[0]);
close(g_exit_pipe[1]);
exit_fail:
return -1;
}
void
lastfm_deinit(void)
{
int ret;
if (!g_initialized)
return;
curl_global_cleanup();
thread_exit();
ret = pthread_join(tid_lastfm, NULL);
if (ret != 0)
{
DPRINTF(E_FATAL, L_LASTFM, "Could not join lastfm thread: %s\n", strerror(errno));
return;
}
// Free event base (should free events too)
event_base_free(evbase_lastfm);
// Close pipes
close(g_cmd_pipe[0]);
close(g_cmd_pipe[1]);
close(g_exit_pipe[0]);
close(g_exit_pipe[1]);
}

14
src/lastfm.h Normal file
View File

@ -0,0 +1,14 @@
#ifndef __LASTFM_H__
#define __LASTFM_H__
void
lastfm_login(char *path);
int
lastfm_scrobble(int id);
void
lastfm_deinit(void);
#endif /* !__LASTFM_H__ */

View File

@ -43,7 +43,7 @@ static int threshold;
static int console;
static char *logfilename;
static FILE *logfile;
static char *labels[] = { "config", "daap", "db", "httpd", "main", "mdns", "misc", "rsp", "scan", "xcode", "event", "remote", "dacp", "ffmpeg", "artwork", "player", "raop", "laudio", "dmap", "dbperf", "spotify" };
static char *labels[] = { "config", "daap", "db", "httpd", "main", "mdns", "misc", "rsp", "scan", "xcode", "event", "remote", "dacp", "ffmpeg", "artwork", "player", "raop", "laudio", "dmap", "dbperf", "spotify", "lastfm" };
static int

View File

@ -27,8 +27,9 @@
#define L_DMAP 18
#define L_DBPERF 19
#define L_SPOTIFY 20
#define L_LASTFM 21
#define N_LOGDOMAINS 21
#define N_LOGDOMAINS 22
/* Severities */
#define E_FATAL 0

View File

@ -67,6 +67,9 @@ GCRY_THREAD_OPTION_PTHREAD_IMPL;
# include "ffmpeg_url_evbuffer.h"
#endif
#ifdef LASTFM
# include "lastfm.h"
#endif
#ifdef HAVE_SPOTIFY_H
# include "spotify.h"
#endif
@ -798,6 +801,10 @@ main(int argc, char **argv)
player_deinit();
player_fail:
#ifdef LASTFM
DPRINTF(E_LOG, L_MAIN, "LastFM deinit\n");
lastfm_deinit();
#endif
#ifdef HAVE_SPOTIFY_H
DPRINTF(E_LOG, L_MAIN, "Spotify deinit\n");
spotify_deinit();

View File

@ -58,6 +58,10 @@
#include "raop.h"
#include "laudio.h"
#ifdef LASTFM
# include "lastfm.h"
#endif
/* These handle getting the media data */
#include "transcode.h"
#include "pipe.h"
@ -1085,7 +1089,6 @@ source_reshuffle(void)
source_shuffle(head, tail);
// Setting shuffle_head to the current song (head) will show all reshuffled songs in the playlist
if (repeat == REPEAT_ALL)
shuffle_head = head;
}
@ -1231,12 +1234,7 @@ source_next(int force)
if (cur_streaming && (ps == shuffle_head))
{
source_reshuffle();
/* After source_reshuffle with "repeat all", shuffle_head is (re-)set to cur_streaming,
* therefor get the next song in queue and set the start of the playlist to this song.
*/
ps = cur_streaming->shuffle_next;
shuffle_head = ps;
ps = shuffle_head;
}
limit = shuffle_head;
@ -1443,6 +1441,9 @@ source_check(void)
i++;
db_file_inc_playcount((int)cur_playing->id);
#ifdef LASTFM
lastfm_scrobble((int)cur_playing->id);
#endif
/* Stop playback if:
* - at end of playlist (NULL)