2019-10-14 13:38:14 -06:00
|
|
|
#include <mpd/client.h>
|
|
|
|
|
|
|
|
#include <pthread.h>
|
|
|
|
#include <stdio.h>
|
|
|
|
#include <assert.h>
|
|
|
|
#include <string.h>
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <sys/time.h>
|
|
|
|
#include <unistd.h>
|
2019-10-16 08:47:59 -06:00
|
|
|
#include <glib.h>
|
2019-10-17 14:14:59 -06:00
|
|
|
#include <argp.h>
|
|
|
|
|
|
|
|
const char *argp_program_version = "mpd_control 0.3.0";
|
|
|
|
const char *argp_program_bug_address = "<matt-low@zusmail.xyz>";
|
|
|
|
static char doc[] = "MPD blocket for i3blocks.";
|
|
|
|
static char args_doc[] = "[OPTION]...";
|
|
|
|
static struct argp_option options[] = {
|
|
|
|
{ "interval", 'i', "interval", 0, "How frequently to update the status line (in seconds). Default: 1"},
|
|
|
|
{ "length", 'l', "length", 0, "The length of the shown song label (in characters). Default: 25"},
|
|
|
|
{ "step", 's', "step", 0, "How many characters to step the label while scrolling. Default: 3"},
|
2019-10-17 15:15:39 -06:00
|
|
|
{ "padding", 'p', "padding", 0, "The characters to append to the label while scrolling. Default: \" | \""},
|
2019-10-18 03:41:56 -06:00
|
|
|
{ "format", 'f', "format", 0, "The song label format. Default: '{title} - {artist}'"},
|
2019-10-17 14:14:59 -06:00
|
|
|
{ 0 }
|
|
|
|
};
|
|
|
|
|
2019-10-18 06:09:06 -06:00
|
|
|
struct arguments
|
|
|
|
{
|
2019-10-17 14:14:59 -06:00
|
|
|
float interval;
|
|
|
|
int length;
|
|
|
|
int step;
|
2019-10-17 15:15:39 -06:00
|
|
|
char* padding;
|
2019-10-18 03:41:56 -06:00
|
|
|
char* format;
|
2019-10-17 14:14:59 -06:00
|
|
|
};
|
2019-10-18 06:09:06 -06:00
|
|
|
static struct arguments arguments =
|
|
|
|
{
|
|
|
|
.interval = 1.0f,
|
|
|
|
.length = 25,
|
|
|
|
.step = 3,
|
|
|
|
.padding = " | ",
|
|
|
|
.format = "{title} - {artist}",
|
|
|
|
};
|
2019-10-17 14:14:59 -06:00
|
|
|
|
|
|
|
|
2019-10-18 06:09:06 -06:00
|
|
|
static error_t
|
|
|
|
parse_opt(int key, char *arg, struct argp_state *state)
|
|
|
|
{
|
2019-10-17 14:14:59 -06:00
|
|
|
struct arguments *arguments = state->input;
|
|
|
|
switch (key) {
|
|
|
|
case 'i':
|
|
|
|
if (!arg) break;
|
|
|
|
float i = strtof(arg, NULL);
|
|
|
|
if (!i || i < 0) {
|
|
|
|
return EINVAL;
|
|
|
|
}
|
|
|
|
arguments->interval = i;
|
|
|
|
break;
|
|
|
|
case 'l':
|
|
|
|
if (!arg) break;
|
|
|
|
long l = strtol(arg, NULL, 10);
|
|
|
|
if (!l || l < 0) {
|
|
|
|
return EINVAL;
|
|
|
|
}
|
|
|
|
arguments->length = l;
|
|
|
|
break;
|
|
|
|
case 's':
|
|
|
|
if (!arg) break;
|
|
|
|
long s = strtol(arg, NULL, 10);
|
|
|
|
if (!s || s < 0) {
|
|
|
|
return EINVAL;
|
|
|
|
}
|
|
|
|
arguments->step = s; break;
|
2019-10-17 15:15:39 -06:00
|
|
|
case 'p': arguments->padding = arg;
|
2019-10-18 03:41:56 -06:00
|
|
|
case 'f': arguments->format = arg;
|
2019-10-17 14:14:59 -06:00
|
|
|
case ARGP_KEY_ARG: return 0;
|
|
|
|
default: return ARGP_ERR_UNKNOWN;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
2019-10-14 13:38:14 -06:00
|
|
|
|
2019-10-17 14:14:59 -06:00
|
|
|
static struct argp argp = { options, parse_opt, args_doc, doc, 0, 0, 0 };
|
|
|
|
|
2019-10-18 06:09:06 -06:00
|
|
|
struct worker_state
|
|
|
|
{
|
2019-10-15 07:54:08 -06:00
|
|
|
int scroll_index;
|
2019-10-18 02:09:44 -06:00
|
|
|
int song_id;
|
2019-10-17 14:14:59 -06:00
|
|
|
char* full_label;
|
|
|
|
size_t full_label_length;
|
|
|
|
char* padded_label;
|
|
|
|
size_t padded_label_length;
|
2019-10-14 13:38:14 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
static long long
|
2019-10-18 06:09:06 -06:00
|
|
|
current_timestamp()
|
|
|
|
{
|
2019-10-14 13:38:14 -06:00
|
|
|
struct timeval te;
|
|
|
|
gettimeofday(&te, NULL);
|
|
|
|
return te.tv_sec*1000LL + te.tv_usec/1000;
|
|
|
|
}
|
|
|
|
|
2019-10-18 03:41:56 -06:00
|
|
|
static void
|
2019-10-18 06:09:06 -06:00
|
|
|
str_replace(char *target, size_t target_len, const char *needle,
|
|
|
|
const char *replace)
|
2019-10-18 03:41:56 -06:00
|
|
|
{
|
|
|
|
if (target_len == -1) target_len = strlen(target);
|
|
|
|
|
|
|
|
char *end = &target[target_len];
|
|
|
|
size_t needle_len = strlen(needle);
|
|
|
|
size_t replace_len = strlen(replace);
|
|
|
|
|
|
|
|
char *pos;
|
|
|
|
while ((pos = strstr(target, needle)) != NULL) {
|
|
|
|
// Shift portion after needle to its position after replace
|
|
|
|
memmove(pos + replace_len, pos + needle_len, (end - (pos + needle_len)) + 1);
|
|
|
|
end += replace_len - needle_len;
|
|
|
|
|
|
|
|
// Replace needle with repl string
|
|
|
|
memcpy(pos, replace, replace_len);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-15 07:54:08 -06:00
|
|
|
static void
|
2019-10-16 11:32:41 -06:00
|
|
|
scroll_text(const char *text_to_scroll, const size_t text_len, char *buffer,
|
2019-10-18 06:09:06 -06:00
|
|
|
int *index, const int max_length)
|
|
|
|
{
|
2019-10-16 08:47:59 -06:00
|
|
|
if (*index - text_len > 0) *index %= text_len;
|
|
|
|
|
|
|
|
char* first_char = g_utf8_offset_to_pointer(text_to_scroll, *index);
|
|
|
|
const int overflow = text_len - *index - max_length;
|
|
|
|
if (overflow <= 0) {
|
|
|
|
g_utf8_strncpy(buffer, first_char, text_len-*index);
|
|
|
|
g_utf8_strncpy(g_utf8_offset_to_pointer(buffer, text_len-*index),
|
|
|
|
text_to_scroll, max_length-(text_len-*index));
|
|
|
|
} else {
|
|
|
|
g_utf8_strncpy(buffer, first_char, max_length);
|
|
|
|
}
|
2019-10-15 07:54:08 -06:00
|
|
|
}
|
|
|
|
|
2019-10-14 13:38:14 -06:00
|
|
|
static int
|
2019-10-16 15:52:32 -06:00
|
|
|
handle_error(struct mpd_connection *conn, bool renew)
|
2019-10-14 13:38:14 -06:00
|
|
|
{
|
2019-10-16 15:52:32 -06:00
|
|
|
if (mpd_connection_get_error(conn) != MPD_ERROR_SUCCESS) {
|
|
|
|
if (!renew) {
|
|
|
|
fprintf(stderr, "%s\n", mpd_connection_get_error_message(conn));
|
|
|
|
}
|
|
|
|
if (!mpd_connection_clear_error(conn)) {
|
|
|
|
if (renew) {
|
|
|
|
mpd_connection_free(conn);
|
|
|
|
conn = mpd_connection_new(NULL, 0, 0);
|
|
|
|
return handle_error(conn, false);
|
|
|
|
}
|
|
|
|
return EXIT_FAILURE;
|
|
|
|
} else {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0;
|
2019-10-14 13:38:14 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
get_tag(struct mpd_song *song, char *tag, enum mpd_tag_type type)
|
|
|
|
{
|
|
|
|
const char *value;
|
|
|
|
unsigned i = 0;
|
|
|
|
while ((value = mpd_song_get_tag(song, type, i++)) != NULL) {
|
|
|
|
strcpy(tag, value);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
2019-10-17 14:14:59 -06:00
|
|
|
print_status(struct mpd_connection *conn, struct worker_state *state)
|
2019-10-14 13:38:14 -06:00
|
|
|
{
|
2019-10-16 11:32:41 -06:00
|
|
|
struct mpd_status *status = mpd_run_status(conn);
|
2019-10-14 13:38:14 -06:00
|
|
|
|
|
|
|
if (status == NULL)
|
2019-10-16 15:52:32 -06:00
|
|
|
return handle_error(conn, false);
|
2019-10-14 13:38:14 -06:00
|
|
|
|
2019-10-16 11:47:42 -06:00
|
|
|
char *play_icon = NULL;
|
2019-10-17 14:14:59 -06:00
|
|
|
switch (mpd_status_get_state(status)) {
|
2019-10-16 11:47:42 -06:00
|
|
|
case MPD_STATE_PLAY:
|
|
|
|
play_icon = "";
|
|
|
|
break;
|
|
|
|
case MPD_STATE_PAUSE:
|
|
|
|
play_icon = "";
|
|
|
|
break;
|
|
|
|
default:
|
2019-10-17 14:14:59 -06:00
|
|
|
state->scroll_index = 0;
|
2019-10-18 02:09:44 -06:00
|
|
|
state->song_id = -1;
|
2019-10-16 11:47:42 -06:00
|
|
|
printf("\n");
|
|
|
|
mpd_status_free(status);
|
|
|
|
fflush(stdout);
|
2019-10-16 12:29:05 -06:00
|
|
|
return 1;
|
2019-10-15 07:54:08 -06:00
|
|
|
}
|
|
|
|
|
2019-10-14 13:38:14 -06:00
|
|
|
const bool repeat = mpd_status_get_repeat(status);
|
|
|
|
const bool random = mpd_status_get_random(status);
|
|
|
|
|
2019-10-16 11:47:42 -06:00
|
|
|
char state_icon[16];
|
|
|
|
strcpy(state_icon, "");
|
|
|
|
if (repeat)
|
|
|
|
strcat(state_icon, " ");
|
|
|
|
if (random)
|
|
|
|
strcat(state_icon, " ");
|
|
|
|
|
2019-10-14 13:38:14 -06:00
|
|
|
const unsigned queue_pos = mpd_status_get_song_pos(status);
|
|
|
|
const unsigned queue_length = mpd_status_get_queue_length(status);
|
|
|
|
const unsigned elapsed = mpd_status_get_elapsed_time(status);
|
|
|
|
const unsigned remaining = mpd_status_get_total_time(status) - elapsed;
|
|
|
|
const unsigned remaining_mins = remaining / 60;
|
|
|
|
const unsigned remaining_secs = remaining % 60;
|
2019-10-18 02:09:44 -06:00
|
|
|
const int song_id = mpd_status_get_song_id(status);
|
2019-10-14 13:38:14 -06:00
|
|
|
|
|
|
|
mpd_status_free(status);
|
2019-10-16 14:18:24 -06:00
|
|
|
if (mpd_connection_get_error(conn) != MPD_ERROR_SUCCESS)
|
2019-10-16 15:52:32 -06:00
|
|
|
return handle_error(conn, false);
|
2019-10-14 13:38:14 -06:00
|
|
|
|
2019-10-18 02:09:44 -06:00
|
|
|
if (song_id != state->song_id) {
|
|
|
|
state->song_id = song_id;
|
2019-10-14 13:38:14 -06:00
|
|
|
|
2019-10-18 02:09:44 -06:00
|
|
|
char artist[256];
|
|
|
|
char album[256];
|
|
|
|
char title[256];
|
2019-10-14 13:38:14 -06:00
|
|
|
|
2019-10-18 02:09:44 -06:00
|
|
|
struct mpd_song *song = mpd_run_current_song(conn);
|
|
|
|
get_tag(song, artist, MPD_TAG_ARTIST);
|
|
|
|
get_tag(song, album, MPD_TAG_ALBUM);
|
|
|
|
get_tag(song, title, MPD_TAG_TITLE);
|
|
|
|
mpd_song_free(song);
|
|
|
|
if (mpd_connection_get_error(conn) != MPD_ERROR_SUCCESS)
|
|
|
|
return handle_error(conn, false);
|
2019-10-17 14:14:59 -06:00
|
|
|
|
|
|
|
// The song changed, so let's update the full label...
|
2019-10-18 03:41:56 -06:00
|
|
|
state->full_label = strcpy(state->full_label, arguments.format);
|
|
|
|
str_replace(state->full_label, -1, "{title}", title);
|
|
|
|
str_replace(state->full_label, -1, "{album}", album);
|
|
|
|
str_replace(state->full_label, -1, "{artist}", artist);
|
|
|
|
|
2019-10-17 14:14:59 -06:00
|
|
|
state->full_label_length = g_utf8_strlen(state->full_label, -1);
|
|
|
|
|
|
|
|
// ... and the padded label (if the full label is long enough) ...
|
|
|
|
if (state->full_label_length > arguments.length) {
|
2019-10-17 15:15:39 -06:00
|
|
|
sprintf(state->padded_label, "%s%s", state->full_label, arguments.padding);
|
2019-10-17 14:14:59 -06:00
|
|
|
state->padded_label_length = g_utf8_strlen(state->padded_label, -1);
|
|
|
|
}
|
|
|
|
// ... and reset the scroll index
|
|
|
|
state->scroll_index = 0;
|
2019-10-15 07:54:08 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// The label we'll actually print
|
2019-10-17 14:14:59 -06:00
|
|
|
char *label = NULL;
|
2019-10-15 07:54:08 -06:00
|
|
|
|
2019-10-17 14:14:59 -06:00
|
|
|
if (state->full_label_length > arguments.length) {
|
|
|
|
// If length of full label > scroll length, we'll scroll it
|
2019-10-16 08:47:59 -06:00
|
|
|
|
2019-10-17 14:14:59 -06:00
|
|
|
// Allocate enough space for the buffer
|
|
|
|
// Up to 4 bytes per character because utf8.
|
|
|
|
label = alloca((arguments.length*4)+1);
|
|
|
|
label[0] = '\0';
|
2019-10-15 07:54:08 -06:00
|
|
|
|
2019-10-17 14:14:59 -06:00
|
|
|
scroll_text(state->padded_label, state->padded_label_length, label,
|
|
|
|
&state->scroll_index, arguments.length);
|
2019-10-15 07:54:08 -06:00
|
|
|
} else {
|
2019-10-17 14:14:59 -06:00
|
|
|
// Else we'll just point label directly to the full label
|
|
|
|
label = state->full_label;
|
2019-10-15 07:54:08 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
printf("%s%s %s (-%u:%02u) [%u/%u]\n",
|
|
|
|
play_icon, state_icon, label,
|
|
|
|
remaining_mins, remaining_secs,
|
|
|
|
queue_pos+1, queue_length);
|
2019-10-14 13:38:14 -06:00
|
|
|
|
|
|
|
fflush(stdout);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2019-10-17 14:14:59 -06:00
|
|
|
static inline
|
2019-10-18 06:09:06 -06:00
|
|
|
long long max(int a, int b)
|
|
|
|
{
|
2019-10-17 14:14:59 -06:00
|
|
|
return a >= b? a : b;
|
|
|
|
}
|
|
|
|
|
2019-10-14 13:38:14 -06:00
|
|
|
void*
|
2019-10-17 14:14:59 -06:00
|
|
|
status_loop(void* worker_state)
|
2019-10-14 13:38:14 -06:00
|
|
|
{
|
2019-10-17 14:14:59 -06:00
|
|
|
struct worker_state *state = (struct worker_state*)worker_state;
|
2019-10-14 13:38:14 -06:00
|
|
|
struct mpd_connection *conn = mpd_connection_new(NULL, 0, 0);
|
|
|
|
|
2019-10-17 14:14:59 -06:00
|
|
|
for (;;) {
|
2019-10-14 13:38:14 -06:00
|
|
|
long long start = current_timestamp();
|
2019-10-15 07:54:08 -06:00
|
|
|
|
2019-10-17 14:14:59 -06:00
|
|
|
if (handle_error(conn, true) == 0 && print_status(conn, state) == 0) {
|
|
|
|
state->scroll_index += arguments.step;
|
2019-10-16 12:29:05 -06:00
|
|
|
}
|
2019-10-15 07:54:08 -06:00
|
|
|
|
2019-10-16 15:52:32 -06:00
|
|
|
long long elapsed = current_timestamp() - start;
|
2019-10-17 14:14:59 -06:00
|
|
|
usleep(max(0, ((arguments.interval*1000) - elapsed) * 1000));
|
2019-10-14 13:38:14 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
mpd_connection_free(conn);
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
2019-10-16 08:50:56 -06:00
|
|
|
enum click_command {
|
|
|
|
TOGGLE_PAUSE, NEXT, PREVIOUS, SEEK_FWD, SEEK_BCKWD
|
|
|
|
};
|
|
|
|
|
|
|
|
void
|
2019-10-18 06:09:06 -06:00
|
|
|
mpd_run_command(struct worker_state *state, enum click_command command)
|
|
|
|
{
|
2019-10-16 14:18:24 -06:00
|
|
|
struct mpd_connection *conn = mpd_connection_new(NULL, 0, 0);
|
2019-10-16 08:50:56 -06:00
|
|
|
switch (command) {
|
|
|
|
case TOGGLE_PAUSE:
|
|
|
|
mpd_run_toggle_pause(conn);
|
|
|
|
break;
|
|
|
|
case NEXT:
|
|
|
|
mpd_run_next(conn);
|
|
|
|
break;
|
|
|
|
case PREVIOUS:
|
|
|
|
mpd_run_previous(conn);
|
|
|
|
break;
|
|
|
|
case SEEK_FWD:
|
|
|
|
mpd_run_seek_current(conn, 3, true);
|
|
|
|
break;
|
|
|
|
case SEEK_BCKWD:
|
|
|
|
mpd_run_seek_current(conn, -3, true);
|
|
|
|
break;
|
|
|
|
}
|
2019-10-17 14:14:59 -06:00
|
|
|
print_status(conn, state);
|
2019-10-16 14:18:24 -06:00
|
|
|
mpd_connection_free(conn);
|
2019-10-16 08:50:56 -06:00
|
|
|
}
|
|
|
|
|
2019-10-18 06:09:06 -06:00
|
|
|
int main(int argc, char *argv[])
|
|
|
|
{
|
2019-10-17 14:14:59 -06:00
|
|
|
argp_parse(&argp, argc, argv, 0, 0, &arguments);
|
2019-10-16 08:50:56 -06:00
|
|
|
|
2019-10-18 02:09:44 -06:00
|
|
|
struct worker_state state = {0, -1, malloc(1024), 0, malloc(1024), 0};
|
2019-10-14 13:38:14 -06:00
|
|
|
|
2019-10-16 15:52:32 -06:00
|
|
|
struct sigaction new_actn, old_actn;
|
|
|
|
new_actn.sa_handler = SIG_IGN;
|
|
|
|
sigemptyset(&new_actn.sa_mask);
|
|
|
|
new_actn.sa_flags = 0;
|
|
|
|
sigaction(SIGPIPE, &new_actn, &old_actn);
|
|
|
|
|
2019-10-14 13:38:14 -06:00
|
|
|
pthread_t update_thread;
|
2019-10-18 06:09:06 -06:00
|
|
|
if (pthread_create(&update_thread, NULL, status_loop, &state)) {
|
2019-10-14 13:38:14 -06:00
|
|
|
fprintf(stderr, "Error creating thread\n");
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2019-10-16 14:18:24 -06:00
|
|
|
for (;;) {
|
2019-10-16 08:50:56 -06:00
|
|
|
char *line = NULL;
|
2019-10-14 13:38:14 -06:00
|
|
|
size_t size;
|
|
|
|
|
|
|
|
if (getline(&line, &size, stdin) != -1) {
|
|
|
|
if (strcmp(line, "1\n") == 0) {
|
2019-10-17 14:14:59 -06:00
|
|
|
mpd_run_command(&state, PREVIOUS);
|
2019-10-14 13:38:14 -06:00
|
|
|
} else if (strcmp(line, "2\n") == 0) {
|
2019-10-17 14:14:59 -06:00
|
|
|
mpd_run_command(&state, TOGGLE_PAUSE);
|
2019-10-14 13:38:14 -06:00
|
|
|
} else if (strcmp(line, "3\n") == 0) {
|
2019-10-17 14:14:59 -06:00
|
|
|
mpd_run_command(&state, NEXT);
|
2019-10-14 13:38:14 -06:00
|
|
|
} else if (strcmp(line, "4\n") == 0) {
|
2019-10-17 14:14:59 -06:00
|
|
|
mpd_run_command(&state, SEEK_FWD);
|
2019-10-14 13:38:14 -06:00
|
|
|
} else if (strcmp(line, "5\n") == 0) {
|
2019-10-17 14:14:59 -06:00
|
|
|
mpd_run_command(&state, SEEK_BCKWD);
|
2019-10-14 13:38:14 -06:00
|
|
|
}
|
2019-10-16 08:50:56 -06:00
|
|
|
|
|
|
|
free(line);
|
2019-10-14 13:38:14 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-18 06:09:06 -06:00
|
|
|
if (pthread_join(update_thread, NULL)) {
|
2019-10-14 13:38:14 -06:00
|
|
|
fprintf(stderr, "Error joining thread\n");
|
|
|
|
return 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|