]> git.rustad.me Git - spots/commitdiff
Initial commit
authorBjørn Rustad <bjorn@rustad.me>
Tue, 24 Jul 2012 19:42:18 +0000 (21:42 +0200)
committerBjørn Rustad <bjorn@rustad.me>
Tue, 24 Jul 2012 19:42:18 +0000 (21:42 +0200)
Working command line interface that lets you view your own playlists,
play songs from these, and perform simple searches.

.gitignore [new file with mode: 0644]
Makefile [new file with mode: 0644]
appkey.c [new file with mode: 0644]
appkey.h [new file with mode: 0644]
audio.c [new file with mode: 0644]
audio.h [new file with mode: 0644]
command.c [new file with mode: 0644]
command.h [new file with mode: 0644]
spots.c [new file with mode: 0644]
spots.h [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..1133a11
--- /dev/null
@@ -0,0 +1,8 @@
+*.o
+spots
+*.swp
+.cache/
+.settings/
+libspotify-12.1.51-Linux-x86_64-release/
+libspotify-12.1.51-Linux-x86_64-release.tar.gz
+libspotify/
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..25fb284
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,16 @@
+TARGET   = spots
+CFLAGS  += -I./libspotify/include -g
+CFLAGS  += $(shell pkg-config --cflags libpulse-simple)
+LDFLAGS += -Wl,-rpath,./libspotify/lib -L./libspotify/lib
+LDLIBS  += -lspotify -lpthread -lreadline -lncurses
+LDLIBS  += $(shell pkg-config --libs libpulse-simple)
+
+OBJS    += spots.o appkey.o command.o audio.o
+
+$(TARGET): $(OBJS)
+       $(CC) $(CFLAGS) $(LDFLAGS) $^ $(LDLIBS) -o $@
+
+.PHONY: all clean
+
+clean:
+       rm -f spots *.o
diff --git a/appkey.c b/appkey.c
new file mode 100644 (file)
index 0000000..f59b6a8
--- /dev/null
+++ b/appkey.c
@@ -0,0 +1,28 @@
+#include <stdint.h>
+#include <stdlib.h>
+#include "appkey.h"
+
+const uint8_t g_appkey[] = {
+       0x01, 0x85, 0x53, 0xC5, 0x34, 0x57, 0xD3, 0xF5, 0xD9, 0x3D, 0xE9, 0xA4, 0xF3, 0x27, 0x98, 0xBD,
+       0x69, 0xA7, 0x72, 0x9D, 0x54, 0x70, 0x1E, 0xFF, 0x18, 0xA7, 0xFD, 0x2F, 0x5C, 0x7A, 0xB9, 0x6E,
+       0x8F, 0x16, 0x13, 0xB4, 0x39, 0x22, 0x77, 0xAB, 0xD7, 0xFE, 0xE2, 0x9C, 0xA8, 0x00, 0x9B, 0x76,
+       0x9D, 0x10, 0xED, 0x09, 0x73, 0xEC, 0x7E, 0x28, 0xDA, 0x2D, 0x06, 0x54, 0x92, 0x85, 0x4E, 0x65,
+       0xCC, 0x70, 0xB4, 0xF8, 0xD7, 0xEF, 0x6A, 0x22, 0x03, 0x4C, 0x9B, 0xD5, 0x65, 0x0B, 0x8E, 0xA0,
+       0x66, 0xA6, 0x76, 0x95, 0xEC, 0x4E, 0x8F, 0x5D, 0xCB, 0xEC, 0x28, 0x4C, 0xBD, 0x19, 0x3D, 0xFC,
+       0xB3, 0x12, 0x07, 0x71, 0x0A, 0x31, 0x0A, 0x28, 0x4B, 0xC2, 0x10, 0x1B, 0x8B, 0x85, 0xF6, 0x92,
+       0xB8, 0x3A, 0x9B, 0x42, 0xBA, 0xAB, 0x1A, 0xFE, 0x1C, 0x94, 0x7B, 0x66, 0x9D, 0x20, 0xE3, 0xF1,
+       0x5A, 0x63, 0xC9, 0xA9, 0x1C, 0x3C, 0x11, 0xB3, 0x25, 0xFC, 0x52, 0xFC, 0xA6, 0x0E, 0xF6, 0x9B,
+       0x75, 0x4F, 0x23, 0xB3, 0xCF, 0xDE, 0xC5, 0x78, 0x91, 0x0F, 0xD8, 0xFC, 0xBA, 0x80, 0xF4, 0x1A,
+       0x8A, 0x1D, 0xF5, 0x95, 0x93, 0xD4, 0xA4, 0xA7, 0xC8, 0x80, 0x15, 0x98, 0x02, 0xD4, 0xAD, 0xC9,
+       0xB3, 0xAB, 0x51, 0xD5, 0x92, 0x6C, 0xF8, 0xC2, 0x32, 0x39, 0x5F, 0x30, 0x5F, 0x5F, 0xB7, 0xD6,
+       0x52, 0xE7, 0x8E, 0xB5, 0x00, 0x3B, 0xA7, 0x09, 0xF1, 0xFB, 0x43, 0xFE, 0x9E, 0xE9, 0xD2, 0x54,
+       0xFB, 0x94, 0x33, 0xA2, 0x83, 0x7F, 0xE2, 0xE2, 0x1C, 0x17, 0x9E, 0x79, 0xD3, 0xDE, 0xFA, 0x12,
+       0xD9, 0x5D, 0x9F, 0xED, 0x63, 0xA8, 0x59, 0x8F, 0x70, 0x0C, 0x6C, 0xD2, 0x15, 0xF8, 0x1A, 0xCE,
+       0xE9, 0xEF, 0x1C, 0x7B, 0xE0, 0x37, 0x3B, 0xFE, 0x2E, 0x7F, 0x60, 0xD2, 0x30, 0xAB, 0x97, 0xE3,
+       0xCF, 0x1C, 0x9D, 0x23, 0x97, 0x69, 0x3B, 0x6A, 0x88, 0xDA, 0x1D, 0xC5, 0xC9, 0xAE, 0x9E, 0x6B,
+       0xE3, 0xC1, 0x4E, 0xAE, 0x98, 0xE1, 0x96, 0xEF, 0x9E, 0x7D, 0x07, 0x74, 0x56, 0xD5, 0xFC, 0x56,
+       0x7E, 0x2C, 0xA0, 0x05, 0xF4, 0xB1, 0x3E, 0x0A, 0xE9, 0x93, 0x08, 0xF5, 0x08, 0x9A, 0xA7, 0xC7,
+       0x66, 0x98, 0x7B, 0x4B, 0x88, 0xEE, 0x0A, 0x7F, 0x20, 0x43, 0xA9, 0x4C, 0xF9, 0x76, 0xEF, 0x1D,
+       0x80,
+};
+const size_t g_appkey_size = sizeof(g_appkey);
diff --git a/appkey.h b/appkey.h
new file mode 100644 (file)
index 0000000..ea87004
--- /dev/null
+++ b/appkey.h
@@ -0,0 +1,7 @@
+#ifndef _APPKEY_H
+#define _APPKEY_H
+
+extern const uint8_t g_appkey[];
+extern const size_t g_appkey_size;
+
+#endif
diff --git a/audio.c b/audio.c
new file mode 100644 (file)
index 0000000..1b468f7
--- /dev/null
+++ b/audio.c
@@ -0,0 +1,99 @@
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <pthread.h>
+
+#include "spots.h"
+#include "audio.h"
+
+int play;
+
+pa_simple *pa_handle;
+
+pthread_mutex_t audio_q_mutex;
+pthread_cond_t  audio_q_cond;
+
+struct audio_q_head head;
+
+void play_or_pause (int p)
+{
+       sp_error err;
+
+       fprintf(stderr, "trying to play: %d\n", p);
+
+       err = sp_session_player_play(session, p);
+       pthread_mutex_lock(&audio_q_mutex);
+       play = p;
+       pthread_cond_signal(&audio_q_cond);
+       pthread_mutex_unlock(&audio_q_mutex);
+
+       if (err != SP_ERROR_OK) {
+               fprintf(stderr, "Error pausing: %s\n", sp_error_message(err));
+       }
+}
+
+void audio_q_element_free (struct audio_q_element *e1)
+{
+       free(e1->samples);
+       free(e1);
+}
+
+void audio_q_empty (void)
+{
+       struct audio_q_element *e1, *e2;
+
+       e1 = TAILQ_FIRST(&head);
+       while (e1 != NULL) {
+               e2 = TAILQ_NEXT(e1, entries);
+               audio_q_element_free(e1);
+               e1 = e2;
+       }
+       TAILQ_INIT(&head);
+}
+
+void *audio_thread_function (void *attr)
+{
+       int pa_err;
+       int res;
+       struct audio_q_element *aqe;
+
+       pthread_mutex_lock(&audio_q_mutex);
+       while (1) {
+               aqe = head.tqh_first;
+
+               while (aqe == NULL || !play) {
+                       pthread_cond_wait(&audio_q_cond, &audio_q_mutex);
+                       aqe = head.tqh_first;
+               }
+
+               pthread_mutex_unlock(&audio_q_mutex);
+
+               res = pa_simple_write(
+                               pa_handle,
+                               aqe->samples,
+                               aqe->num_samples * aqe->channels * 2,
+                               &pa_err);
+
+               if (res < 0) {
+                       fprintf(stderr, "pa_simple_write failed: %s\n",
+                                       pa_strerror(pa_err));
+               }
+
+               pthread_mutex_lock(&audio_q_mutex);
+               TAILQ_REMOVE(&head, head.tqh_first, entries);
+               audio_q_element_free(aqe);
+       }
+}
+
+void start_audio_thread (void)
+{
+       int audio_thread_id;
+       pthread_t audio_thread;
+
+       audio_thread_id = pthread_create(
+                       &audio_thread,
+                       NULL,
+                       &audio_thread_function,
+                       NULL);
+}
+
diff --git a/audio.h b/audio.h
new file mode 100644 (file)
index 0000000..c14a03f
--- /dev/null
+++ b/audio.h
@@ -0,0 +1,39 @@
+#ifndef _AUDIO_H
+#define _AUDIO_H
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <pthread.h>
+
+#include <pulse/simple.h>
+#include <pulse/error.h>
+
+TAILQ_HEAD(audio_q_head, audio_q_element);
+
+extern struct audio_q_head head;
+
+extern pthread_mutex_t audio_q_mutex;
+extern pthread_cond_t  audio_q_cond;
+
+extern int play;
+
+extern pa_simple *pa_handle;
+
+struct audio_q_element {
+       void *samples;
+       int num_samples;
+       int sample_rate;
+       int channels;
+       TAILQ_ENTRY(audio_q_element) entries;
+};
+
+void play_or_pause (int p);
+
+void audio_q_element_free (struct audio_q_element *e1);
+void audio_q_empty (void);
+
+void *audio_thread_function (void *attr);
+void start_audio_thread (void);
+
+#endif
diff --git a/command.c b/command.c
new file mode 100644 (file)
index 0000000..66920a8
--- /dev/null
+++ b/command.c
@@ -0,0 +1,308 @@
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <libspotify/api.h>
+
+#include <readline/readline.h>
+#include <readline/history.h>
+
+#include "spots.h"
+#include "command.h"
+#include "audio.h"
+
+void *input_thread_function (void *attr)
+{
+       char *l;
+
+       pthread_mutex_lock(&input_output_mutex);
+       while (1) {
+               while (!show_prompt)
+                       pthread_cond_wait(&input_cond, &input_output_mutex);
+
+               l = readline("> ");
+               show_prompt = 0;
+               pthread_mutex_unlock(&input_output_mutex);
+
+               pthread_mutex_lock(&notify_main_thread_mutex);
+               cmd_line = l;
+               main_thread_notified = 1;
+               pthread_cond_signal(&notify_main_thread_cond);
+               pthread_mutex_unlock(&notify_main_thread_mutex);
+
+               pthread_mutex_lock(&input_output_mutex);
+       }
+}
+
+void start_input_thread (void)
+{
+       int input_thread_id;
+       pthread_t input_thread;
+
+       input_thread_id = pthread_create(
+                       &input_thread,
+                       NULL,
+                       &input_thread_function,
+                       NULL);
+}
+
+void tokenize (char *cmd, int *argc_ptr, char ***argv_ptr)
+{
+       int argc;
+       int argv_len;
+       char **argv;
+       char *saveptr;
+
+       argc = 0;
+       argv_len = 1;
+       argv = malloc(sizeof(char *) * argv_len);
+       argv[0] = strtok_r(cmd, " ", &saveptr);
+       while (argv[argc] != NULL) {
+               argc++;
+
+               if (argv_len <= argc) {
+                       argv_len *= 2;
+                       argv = realloc(argv, sizeof(char *) * argv_len);
+               }
+               if (argv == NULL) {
+                       fprintf(stderr, "Error allocating space for argv.\n");
+                       argc = 0;
+                       argv = NULL;
+                       break;
+               }
+
+               argv[argc] = strtok_r(NULL, " ", &saveptr);
+       }
+
+       *argc_ptr = argc;
+       *argv_ptr = argv;
+}
+
+void parse_and_exec (char *cmd)
+{
+       int i;
+       int argc;
+       int argv_len;
+       int done;
+       char **argv;
+
+       pthread_mutex_lock(&input_output_mutex);
+       tokenize(cmd, &argc, &argv);
+
+       if (argv == NULL) {
+               return;
+       }
+
+       done = -1;
+       for (i = 0; i < commands_length; ++i) {
+               if (strcmp(commands[i].cmd, cmd) == 0) {
+                       done = commands[i].func(argc, argv);
+                       break;
+               }
+       }
+
+       if (done == -1) {
+               fprintf(stderr, "Unknown command %s\n", cmd);
+       }
+       if (done) {
+               show_prompt = 1;
+       }
+
+       fflush(stdout);
+       free(argv);
+
+       pthread_cond_signal(&input_cond);
+       pthread_mutex_unlock(&input_output_mutex);
+}
+
+
+int cmd_print_username (int argc, char *argv[])
+{
+       printf("%s\n", sp_session_user_name(session));
+
+       return 1;
+}
+
+int cmd_print_country (int argc, char *argv[])
+{
+       int country = sp_session_user_country(session);
+
+       printf("%c%c\n", country >> 8, country & 0xff);
+
+       return 1;
+}
+
+int cmd_print_playlists (int argc, char *argv[])
+{
+       int i;
+       sp_playlistcontainer *pc;
+       sp_playlist *p;
+
+               pc = sp_session_playlistcontainer(session);
+       for (i = 0; i < sp_playlistcontainer_num_playlists(pc); ++i) {
+               p = sp_playlistcontainer_playlist(pc, i);
+               printf("%d: %s\n", i, sp_playlist_name(p));
+       }
+
+       return 1;
+}
+
+int cmd_start_playlist_track (int argc, char *argv[])
+{
+       int p_idx;
+       int t_idx;
+
+       sp_error err;
+       sp_playlistcontainer *pc;
+       sp_playlist *p;
+       sp_track *t;
+
+       if (argc < 3) {
+               fprintf(stderr, "Wrong number of arguments.\n");
+               return 1;
+       }
+
+       pc = sp_session_playlistcontainer(session);
+       
+       p_idx = atoi(argv[1]);
+       t_idx = atoi(argv[2]);
+
+       if (p_idx < 0 || p_idx >= sp_playlistcontainer_num_playlists(pc)) {
+               fprintf(stderr, "Invalid playlist number.\n");
+               return 1;
+       }
+
+       p = sp_playlistcontainer_playlist(pc, p_idx);
+
+       if (t_idx < 0 || t_idx >= sp_playlist_num_tracks(p)) {
+               fprintf(stderr, "Invalid track number.\n");
+               return 1;
+       }
+
+       t = sp_playlist_track(p, t_idx);
+
+       play_or_pause(0);
+       pthread_mutex_lock(&audio_q_mutex);
+       audio_q_empty();
+       pthread_mutex_unlock(&audio_q_mutex);
+
+       err = sp_session_player_load(session, t);
+       if (err != SP_ERROR_OK) {
+               fprintf(stderr, "Error loading track: %s\n", sp_error_message(err));
+               return 1;
+       }
+
+       play_or_pause(1);
+
+       printf("Playing %s by ", sp_track_name(t));
+       print_track_artists(t);
+       printf("\n");
+
+       return 1;
+}
+
+int cmd_print_playlist (int argc, char *argv[])
+{
+       int i, j;
+       int num;
+       sp_playlistcontainer *pc;
+       sp_playlist *p;
+       sp_track *t;
+       
+       if (argc < 2) {
+               return 1;
+       }
+
+       pc = sp_session_playlistcontainer(session);
+
+       num = atoi(argv[1]);
+       if (num < 0 || num >= sp_playlistcontainer_num_playlists(pc)) {
+               return 1;
+       }
+
+       p = sp_playlistcontainer_playlist(pc, num);
+       for (i = 0; i < sp_playlist_num_tracks(p); ++i) {
+               t = sp_playlist_track(p, i);
+               print_track(t);
+               printf("\n");
+       }
+
+       return 1;
+}
+
+int cmd_search (int argc, char *argv[])
+{
+       char *query;
+       sp_search *search;
+
+       query = argv[1];
+       sp_search_create(
+                       session, 
+                       query,
+                       0,   // track offset? offset among tracks?
+                       10, // track count? number of tracks?
+                       0,   // album offset?
+                       10, // album count?
+                       0,   // artist offset?
+                       10, // artist count?
+                       0,   // playlist offset?
+                       10, // playlist count?
+                       SP_SEARCH_STANDARD, // can also be SP_SEARCH_SUGGEST
+                       &cb_search_complete,
+                       NULL // userdata
+                       );
+
+       printf("Thank you for your search.\n");
+
+       return 0;
+}
+
+int cmd_logout (int argc, char *argv[])
+{
+       sp_error err;
+
+       printf("Logging out...\n");
+       err = sp_session_logout(session);
+
+       if (err != SP_ERROR_OK) {
+               fprintf(stderr, "Error logging out: %s.\n",
+                               sp_error_message(err));
+       }
+
+       pthread_mutex_lock(&notify_main_thread_mutex);
+
+       log_out = 1;
+       main_thread_notified = 1;
+
+       pthread_cond_signal(&notify_main_thread_cond);
+       pthread_mutex_unlock(&notify_main_thread_mutex);
+
+       return 0;
+}
+
+int cmd_pause (int argc, char *argv[])
+{
+       play_or_pause(0);
+
+       return 1;
+}
+
+int cmd_play (int argc, char *argv[])
+{
+       play_or_pause(1);
+
+       return 1;
+}
+
+command_t commands[] = {
+       { "whoami", &cmd_print_username },
+       { "country", &cmd_print_country },
+       { "playlists", &cmd_print_playlists },
+       { "playlist", &cmd_print_playlist },
+       { "start", &cmd_start_playlist_track },
+       { "play", &cmd_play },
+       { "pause", &cmd_pause },
+       { "search", &cmd_search },
+       { "quit", &cmd_logout },
+};
+
+int commands_length = sizeof(commands)/sizeof(command_t);
diff --git a/command.h b/command.h
new file mode 100644 (file)
index 0000000..fdad1e3
--- /dev/null
+++ b/command.h
@@ -0,0 +1,28 @@
+#ifndef _COMMAND_H
+#define _COMMAND_H
+
+typedef struct {
+       char *cmd;
+       int (*func)(int argc, char *argv[]);
+} command_t;
+
+extern command_t commands[];
+
+extern int commands_length;
+
+int cmd_print_username (int argc, char *argv[]);
+int cmd_print_country (int argc, char *argv[]);
+int cmd_print_playlists (int argc, char *argv[]);
+int cmd_start_playlist_track (int argc, char *argv[]);
+int cmd_print_playlist (int argc, char *argv[]);
+int cmd_search (int argc, char *argv[]);
+int cmd_logout (int argc, char *argv[]);
+int cmd_pause (int argc, char *argv[]);
+int cmd_play (int argc, char *argv[]);
+
+void *input_thread_function (void *attr);
+void start_input_thread (void);
+void tokenize (char *cmd, int *argc_ptr, char ***argv_ptr);
+void parse_and_exec (char *cmd);
+
+#endif
diff --git a/spots.c b/spots.c
new file mode 100644 (file)
index 0000000..5413e0d
--- /dev/null
+++ b/spots.c
@@ -0,0 +1,356 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <ctype.h>
+#include <string.h>
+
+#include <ncurses.h>
+
+#include <pthread.h>
+
+#include <sys/queue.h>
+
+#include <libspotify/api.h>
+
+#include <pulse/simple.h>
+#include <pulse/error.h>
+
+#include "spots.h"
+#include "appkey.h"
+#include "command.h"
+#include "audio.h"
+
+pthread_mutex_t notify_main_thread_mutex;
+pthread_cond_t  notify_main_thread_cond;
+
+pthread_mutex_t input_output_mutex;
+pthread_cond_t  input_cond;
+
+int main_thread_notified;
+int show_prompt;
+int log_out;
+char *cmd_line;
+
+sp_session *session;
+
+void print_track_artists (sp_track *t)
+{
+       int i;
+       sp_artist *a;
+
+       for (i = 0; i < sp_track_num_artists(t); ++i) {
+               a = sp_track_artist(t, i);
+               printf("%s", sp_artist_name(a));
+               if (i < sp_track_num_artists(t) - 1) {
+                       printf(", ");
+               }
+       }
+}
+
+void print_track (sp_track *track)
+{
+       printf("%s\t", sp_track_name(track));
+       print_track_artists(track);
+}
+
+void print_artist (sp_artist *artist)
+{
+       printf("%s", sp_artist_name(artist));
+}
+
+void print_album (sp_album *album)
+{
+       printf("%s", sp_album_name(album));
+}
+
+void print_search (sp_search *search)
+{
+       int i;
+       sp_artist *artist;
+       sp_album *album;
+       sp_track *track;
+       const char *dym;
+
+       printf("The search results are in!\n");
+
+       dym = sp_search_did_you_mean(search);
+       if (dym != NULL && dym[0] != '\0') {
+               printf("Did you mean: %s?\n", dym);
+       }
+
+       printf("=== Tracks ===\n");
+       for (i = 0; i < sp_search_num_tracks(search); ++i) {
+               track = sp_search_track(search, i);
+               print_track(track);
+               printf("\n");
+       }
+
+       printf("=== Artists ===\n");
+       for (i = 0; i < sp_search_num_artists(search); ++i) {
+               artist = sp_search_artist(search, i);
+               print_artist(artist);
+               printf("\n");
+       }
+
+       printf("=== Albums ===\n");
+       for (i = 0; i < sp_search_num_artists(search); ++i) {
+               album = sp_search_album(search, i);
+               print_album(album);
+               printf("\n");
+       }
+}
+
+void trim_end (char *str)
+{
+       char *s = str;
+       while (*s != '\0') ++s;
+
+       while (s != str && isspace(*(s - 1))) --s;
+       *s = '\0';
+}
+
+void cb_search_complete (sp_search *search, void *userdata)
+{
+       pthread_mutex_lock(&input_output_mutex);
+
+       if (sp_search_error(search) != SP_ERROR_OK) {
+               fprintf(stderr, "Search failed: %s\n",
+                               sp_error_message(sp_search_error(search)));
+               return;
+       }
+
+       print_search(search);
+
+       show_prompt = 1;
+       pthread_cond_signal(&input_cond);
+       pthread_mutex_unlock(&input_output_mutex);
+
+       sp_search_release(search);
+}
+
+void cb_logged_in (sp_session *session, sp_error error)
+{
+       if (error == SP_ERROR_OK) {
+               fprintf(stderr, "DEBUG: Login successful.\n");
+       } else {
+               fprintf(stderr, "DEBUG: Login failed: %s.\n",
+                               sp_error_message(error));
+               exit(3);
+       }
+
+       pthread_mutex_lock(&input_output_mutex);
+       show_prompt = 1;
+       pthread_cond_signal(&input_cond);
+       pthread_mutex_unlock(&input_output_mutex);
+}
+
+void cb_logged_out (sp_session *session)
+{
+       fprintf(stderr, "DEBUG: Logged out.\n");
+}
+
+void cb_notify_main_thread (sp_session *session)
+{
+       pthread_mutex_lock(&notify_main_thread_mutex);
+       main_thread_notified = 1;
+       pthread_cond_signal(&notify_main_thread_cond);
+       pthread_mutex_unlock(&notify_main_thread_mutex);
+}
+
+void cb_connection_error (sp_session *session, sp_error error)
+{
+       fprintf(stderr, "Connection error: %s.\n", sp_error_message(error));
+}
+
+int cb_music_delivery (sp_session *session, const sp_audioformat *format,
+               const void *frames, int num_frames)
+{
+       int pa_err;
+
+       struct audio_q_element *aqe;
+
+       fprintf(stderr, "music delivery\n");
+       
+       aqe = malloc(sizeof(struct audio_q_element));
+       aqe->num_samples = num_frames;
+       aqe->sample_rate = format->sample_rate;
+       aqe->channels    = format->channels;
+       aqe->samples     = malloc(num_frames * format->channels * 2);
+
+       memcpy(aqe->samples, frames, num_frames * format->channels * 2);
+
+       pthread_mutex_lock(&audio_q_mutex);
+       TAILQ_INSERT_TAIL(&head, aqe, entries);
+       pthread_cond_signal(&audio_q_cond);
+       pthread_mutex_unlock(&audio_q_mutex);
+
+       return num_frames;
+}
+
+static void finish (int sig)
+{
+       endwin();
+
+       exit(0);
+}
+
+int main (int argc, char *argv[])
+{
+       sp_error err;
+       char username[128];
+       char password[128];
+       int next_timeout;
+       int pa_err;
+       struct timespec ts;
+
+       sp_session_callbacks session_callbacks = {
+               .logged_in = &cb_logged_in,
+               .logged_out = &cb_logged_out,
+               .metadata_updated = NULL,
+               .connection_error = &cb_connection_error,
+               .message_to_user = NULL,
+               .notify_main_thread = &cb_notify_main_thread,
+               .music_delivery = &cb_music_delivery,
+               .play_token_lost = NULL,
+               .log_message = NULL,
+               .end_of_track = NULL,
+               .streaming_error = NULL,
+               .userinfo_updated = NULL,
+               .start_playback = NULL,
+               .stop_playback = NULL,
+               .get_audio_buffer_stats = NULL,
+               .offline_status_updated = NULL,
+               .offline_error = NULL,
+               .credentials_blob_updated = NULL,
+               .connectionstate_updated = NULL,
+               .scrobble_error = NULL,
+               .private_session_mode_changed = NULL,
+       };
+
+       static const pa_sample_spec ss = {
+               .format   = PA_SAMPLE_S16LE,
+               .rate     = 44100,
+               .channels = 2
+       };
+
+       sp_session_config session_config;
+
+       memset(&session_config, 0, sizeof(session_config));
+
+       session_config.api_version = SPOTIFY_API_VERSION;
+       session_config.cache_location = ".cache";
+       session_config.settings_location = ".settings";
+       session_config.application_key = g_appkey;
+       session_config.application_key_size = g_appkey_size;
+       session_config.user_agent = "spshell";
+       session_config.callbacks = &session_callbacks;
+
+       pthread_mutex_init(&notify_main_thread_mutex, NULL);
+       pthread_mutex_init(&input_output_mutex, NULL);
+       pthread_mutex_init(&audio_q_mutex, NULL);
+       pthread_cond_init(&notify_main_thread_cond, NULL);
+       pthread_cond_init(&input_cond, NULL);
+       pthread_cond_init(&audio_q_cond, NULL);
+
+       pthread_mutex_lock(&notify_main_thread_mutex);
+       main_thread_notified = 1;
+       pthread_mutex_unlock(&notify_main_thread_mutex);
+
+       pthread_mutex_lock(&input_output_mutex);
+       show_prompt = 0;
+       pthread_mutex_unlock(&input_output_mutex);
+
+       pthread_mutex_lock(&audio_q_mutex);
+       play = 0;
+       pthread_mutex_unlock(&audio_q_mutex);
+
+       err = sp_session_create(&session_config, &session);
+       if (err != SP_ERROR_OK) {
+               fprintf(stderr, "Could not create session: %s.",
+                               sp_error_message(err));
+               return 1;
+       }
+
+       printf("Please enter your username: ");
+       fgets(username, 128, stdin);
+       trim_end(username);
+       printf("Please enter your password: ");
+       fgets(password, 128, stdin);
+       trim_end(password);
+
+       err = sp_session_login(session, username, password, 0, NULL);
+       if (err != SP_ERROR_OK) {
+               fprintf(stderr, "Could not log in: %s.",
+                               sp_error_message(err));
+               return 2;
+       }
+
+       start_input_thread();
+       start_audio_thread();
+
+       pthread_mutex_lock(&audio_q_mutex);
+       TAILQ_INIT(&head);
+       pa_handle = pa_simple_new(
+                       NULL,
+                       "spots",
+                       PA_STREAM_PLAYBACK,
+                       NULL,
+                       "lamename",
+                       &ss,
+                       NULL,
+                       NULL,
+                       &pa_err);
+       pthread_mutex_unlock(&audio_q_mutex);
+
+       if (pa_handle == NULL) {
+               fprintf(stderr, "Error creating pa_handle: %s\n",
+                               pa_strerror(pa_err));
+               return 3;
+       }
+
+       pthread_mutex_lock(&notify_main_thread_mutex);
+       next_timeout = 0;
+       while (!log_out) {
+               clock_gettime(CLOCK_REALTIME, &ts);
+               ts.tv_sec += next_timeout / 1000;
+
+               if (!main_thread_notified && !cmd_line) {
+                       pthread_cond_timedwait(
+                                       &notify_main_thread_cond,
+                                       &notify_main_thread_mutex,
+                                       &ts);
+               }
+               main_thread_notified = 0;
+
+               if (cmd_line) {
+                       char *l = cmd_line;
+                       cmd_line = NULL;
+
+                       pthread_mutex_unlock(&notify_main_thread_mutex);
+                       parse_and_exec(l);
+                       free(l);
+               } else {
+                       pthread_mutex_unlock(&notify_main_thread_mutex);
+               }
+
+               do {
+                       err = sp_session_process_events(session, &next_timeout);
+                       if (err != SP_ERROR_OK) {
+                               fprintf(stderr, "sp_session_process_events failed: %s.\n",
+                                               sp_error_message(err));
+                               return 3;
+                       }
+
+               } while (next_timeout == 0);
+
+               pthread_mutex_lock(&notify_main_thread_mutex);
+       }
+       pthread_mutex_unlock(&notify_main_thread_mutex);
+
+       play_or_pause(0);
+
+       /* TODO: Kill the other threads? */
+       pa_simple_free(pa_handle);
+       sp_session_release(session);
+
+       return 0;
+}
diff --git a/spots.h b/spots.h
new file mode 100644 (file)
index 0000000..6d0fe4e
--- /dev/null
+++ b/spots.h
@@ -0,0 +1,49 @@
+#ifndef _SPOTS_H
+#define _SPOTS_H
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <pthread.h>
+
+#include <pulse/simple.h>
+#include <pulse/error.h>
+
+#include <sys/queue.h>
+
+#include <libspotify/api.h>
+
+extern pthread_mutex_t notify_main_thread_mutex;
+extern pthread_cond_t  notify_main_thread_cond;
+
+extern pthread_mutex_t input_output_mutex;
+extern pthread_cond_t  input_cond;
+
+extern int main_thread_notified;
+extern int show_prompt;
+extern int log_out;
+extern char *cmd_line;
+
+extern sp_session *session;
+
+void trim_end (char *str);
+void print_track_artists (sp_track *t);
+void print_track (sp_track *track);
+void print_artist (sp_artist *artist);
+void print_album (sp_album *album);
+void print_search (sp_search *search);
+
+void cb_search_complete (sp_search *search, void *userdata);
+void cb_logged_in (sp_session *session, sp_error error);
+void cb_logged_out (sp_session *session);
+void cb_notify_main_thread (sp_session *session);
+void cb_connection_error (sp_session *session, sp_error error);
+int  cb_music_delivery (sp_session *session, const sp_audioformat *format,
+               const void *frames, int num_frames);
+
+void *input_thread_function (void *attr);
+void start_input_thread (void);
+void tokenize (char *cmd, int *argc_ptr, char ***argv_ptr);
+void parse_and_exec (char *cmd);
+
+#endif