aboutsummaryrefslogtreecommitdiffstats
/*
 * (C) 2011-2024 by Christian Hesse <mail@eworm.de>
 *
 * 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 3 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, see <https://www.gnu.org/licenses/>.
 *
 */

#include "mpd-notification.h"

const static char optstring[] = "hH:m:p:s:t:vV";
const static struct option options_long[] = {
	/* name		has_arg			flag	val */
	{ "help",	no_argument,		NULL,	'h' },
	{ "host",	required_argument,	NULL,	'H' },
	{ "music-dir",	required_argument,	NULL,	'm' },
	{ "port",	required_argument,	NULL,	'p' },
	{ "scale",	required_argument,	NULL,	's' },
	{ "timeout",	required_argument,	NULL,	't' },
	{ "verbose",	no_argument,		NULL,	'v' },
	{ "version",	no_argument,		NULL,	'V' },
	{ "notification-file-workaround",
			no_argument,		NULL,	OPT_FILE_WORKAROUND },
	{ 0, 0, 0, 0 }
};

/* global variables */
char *program = NULL;
NotifyNotification * notification = NULL;
struct mpd_connection * conn = NULL;
uint8_t doexit = 0;
uint8_t verbose = 0;
#ifdef HAVE_LIBAV
	magic_t magic = NULL;
#endif

/*** received_signal ***/
void received_signal(int signal) {
	GError * error = NULL;

	switch (signal) {
		case SIGINT:
		case SIGTERM:
			if (verbose > 0)
				printf("%s: Received signal %s, preparing exit.\n", program, strsignal(signal));

			doexit++;
			mpd_send_noidle(conn);
			break;

		case SIGHUP:
		case SIGUSR1:
			if (verbose > 0)
				printf("%s: Received signal %s, showing last notification again.\n", program, strsignal(signal));

			if (notify_notification_show(notification, &error) == FALSE) {
				g_printerr("%s: Error \"%s\" while trying to show notification again.\n", program, error->message);
				g_error_free(error);
			}
			break;
		default:
			fprintf(stderr, "%s: Reveived signal %s (%d), no idea what to do...\n", program, strsignal(signal), signal);
	}
}

/*** retrieve_artwork ***/
GdkPixbuf * retrieve_artwork(const char * music_dir, const char * uri) {
	GdkPixbuf * pixbuf = NULL;
	char * uri_path = NULL, * imagefile = NULL;
	DIR * dir;
	struct dirent * entry;
	regex_t regex;

#ifdef HAVE_LIBAV
	int i;
	const char *magic_mime;
	AVFormatContext * pFormatCtx = NULL;
	GdkPixbufLoader * loader;

	/* try album artwork first */
	if ((uri_path = malloc(strlen(music_dir) + strlen(uri) + 2)) == NULL) {
		fprintf(stderr, "%s: malloc() failed.\n", program);
		goto fail;
	}

	sprintf(uri_path, "%s/%s", music_dir, uri);

	if ((magic_mime = magic_file(magic, uri_path)) == NULL) {
		fprintf(stderr, "%s: We did not get a MIME type...\n", program);
		goto image;
	}

	if (verbose > 0)
		printf("%s: MIME type for %s is: %s\n", program, uri_path, magic_mime);

	/* Are there more mime-types supporting embedded artwork? Tell me! */
	if (strcmp(magic_mime, "audio/mp4") != 0 &&
	    strcmp(magic_mime, "audio/mpeg") != 0 &&
	    strcmp(magic_mime, "audio/x-m4a") != 0)
		goto image;

	if ((pFormatCtx = avformat_alloc_context()) == NULL) {
		fprintf(stderr, "%s: avformat_alloc_context() failed.\n", program);
		goto image;
	}

	if (avformat_open_input(&pFormatCtx, uri_path, NULL, NULL) != 0) {
		fprintf(stderr, "%s: avformat_open_input() failed.\n", program);
		goto image;
	}

	/* find the first attached picture, if available */
	for (i = 0; i < pFormatCtx->nb_streams; i++) {
		if (pFormatCtx->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC) {
			AVPacket pkt;

			if (verbose > 0)
				printf("%s: Found artwork in media file.\n", program);

			pkt = pFormatCtx->streams[i]->attached_pic;

			loader = gdk_pixbuf_loader_new();
			if (gdk_pixbuf_loader_write(loader, pkt.data, pkt.size, NULL) == FALSE) {
				fprintf(stderr, "%s: gdk_pixbuf_loader_write() failed parsing buffer.\n", program);
				goto image;
			}

			if ((pixbuf = gdk_pixbuf_loader_get_pixbuf(loader)) == NULL) {
				fprintf(stderr, "%s: gdk_pixbuf_loader_get_pixbuf() failed creating pixbuf.\n", program);
				goto image;
			}

			gdk_pixbuf_loader_close(loader, NULL);
			goto done;
		}
	}

	if (pixbuf == NULL && verbose > 0)
		printf("%s: No artwork in media file.\n", program);

image:
#endif

	/* cut the file name from path for current directory */
	*strrchr(uri_path, '/') = 0;

	if ((dir = opendir(uri_path)) == NULL) {
		fprintf(stderr, "%s: Could not open directory '%s': %s", program, uri_path, strerror(errno));
		goto fail;
	}

	if (regcomp(&regex, REGEX_ARTWORK, REG_NOSUB + REG_ICASE) != 0) {
		fprintf(stderr, "%s: Could not compile regex.\n", program);
		goto fail;
	}

	while ((entry = readdir(dir))) {
		if (*entry->d_name == '.')
			continue;

		if (regexec(&regex, entry->d_name, 0, NULL, 0) == 0) {
			if (verbose > 0)
				printf("%s: Found image file: %s\n", program, entry->d_name);

			if ((imagefile = malloc(strlen(uri_path) + strlen(entry->d_name) + 2)) == NULL) {
				fprintf(stderr, "%s: malloc() failed.\n", program);
				goto fail;
			}

			sprintf(imagefile, "%s/%s", uri_path, entry->d_name);

			if ((pixbuf = gdk_pixbuf_new_from_file(imagefile, NULL)) == NULL) {
				fprintf(stderr, "%s: gdk_pixbuf_new_from_file() failed loading file: %s\n",
						program, imagefile);
				goto fail;
			}

			free(imagefile);
			break;
		}
	}

	regfree(&regex);
	closedir(dir);

fail:
#ifdef HAVE_LIBAV
done:
	if (pFormatCtx != NULL) {
		avformat_close_input(&pFormatCtx);
		avformat_free_context(pFormatCtx);
	}
#endif

	free(uri_path);

	return pixbuf;
}

/*** format_text ***/
char * format_text(const char* format, const char* title, const char* artist, const char* album, unsigned int duration) {
	char * formatted, * tmp = NULL;
	size_t len;

	if (format == NULL || strlen(format) == 0)
		return NULL;

	formatted = strdup("");
	len = 0;

	do {
		if (*format == '%') {
			format++;

			switch (*format) {
				case 'a':
					tmp = g_markup_escape_text(artist, -1);
					break;

				case 'A':
					tmp = g_markup_escape_text(album, -1);
					break;

				case 'd':
					size_t size;
					size = snprintf(tmp, 0, "%d:%02d", duration / 60, duration % 60) + 1;
					tmp = malloc(size);
					snprintf(tmp, size, "%d:%02d", duration / 60, duration % 60);
					break;

				case 't':
					tmp = g_markup_escape_text(title, -1);
					break;

				default:
					formatted = realloc(formatted, len + 2);
					sprintf(formatted + len, "%%");
					format--;
					break;
			}

			if (tmp != NULL) {
				formatted = realloc(formatted, len + strlen(tmp) + 1);
				sprintf(formatted + len, "%s", tmp);
				free(tmp);
				tmp = NULL;
			}
		} else if (*format == '\\') {
			format++;
			formatted = realloc(formatted, len + 2);

			if (*format == 'n') {
				sprintf(formatted + len, "\n");
			} else {
				sprintf(formatted + len, "\\");
				format--;
			}
		} else {
			formatted = realloc(formatted, len + 2);
			sprintf(formatted + len, "%c", *format);
		}
		len = strlen(formatted);
	} while (*format++);

	return formatted;
}

/*** main ***/
int main(int argc, char ** argv) {
	dictionary * ini = NULL;
	const char * title = NULL, * artist = NULL, * album = NULL;
	char * notifystr = NULL;
	GdkPixbuf * pixbuf = NULL;
	GError * error = NULL;
	enum mpd_state state = MPD_STATE_UNKNOWN, last_state = MPD_STATE_UNKNOWN;
	const char * mpd_host, * mpd_port_str, * music_dir, * text_topic = TEXT_TOPIC,
		* text_play = TEXT_PLAY, * text_pause = TEXT_PAUSE, * text_stop = TEXT_STOP, * uri = NULL;
	unsigned mpd_port = MPD_PORT, mpd_timeout = MPD_TIMEOUT, notification_timeout = NOTIFICATION_TIMEOUT;
	struct mpd_song * song = NULL;
	unsigned int i, version = 0, help = 0, scale = 0, file_workaround = 0, duration;
	int rc = EXIT_FAILURE;

	program = argv[0];

	if ((mpd_host = getenv("MPD_HOST")) == NULL)
		mpd_host = MPD_HOST;

	if ((mpd_port_str = getenv("MPD_PORT")) == NULL)
		mpd_port = MPD_PORT;
	else
		mpd_port = atoi(mpd_port_str);

	music_dir = getenv("XDG_MUSIC_DIR");

	/* parse config file */
	if (chdir(getenv("XDG_CONFIG_HOME")) == 0 && access("mpd-notification.conf", R_OK) == 0) {
		ini = iniparser_load("mpd-notification.conf");
	} else if (chdir(getenv("HOME")) == 0 && access(".config/mpd-notification.conf", R_OK) == 0) {
		ini = iniparser_load(".config/mpd-notification.conf");
	}
	if (ini != NULL) {
		file_workaround = iniparser_getboolean(ini, ":notification-file-workaround", file_workaround);
		mpd_host = iniparser_getstring(ini, ":host", mpd_host);
		mpd_port = iniparser_getint(ini, ":port", mpd_port);
		music_dir = iniparser_getstring(ini, ":music-dir", music_dir);
		notification_timeout = iniparser_getint(ini, ":timeout", notification_timeout);
		scale = iniparser_getint(ini, ":scale", scale);
		text_topic = iniparser_getstring(ini, ":text-topic", text_topic);
		text_play  = iniparser_getstring(ini, ":text-play",  text_play);
		text_pause = iniparser_getstring(ini, ":text-pause", text_pause);
		text_stop  = iniparser_getstring(ini, ":text-stop",  text_stop);
	}

	/* get the verbose status */
	while ((i = getopt_long(argc, argv, optstring, options_long, NULL)) != -1) {
		switch (i) {
			case 'h':
				help++;
				break;
			case 'v':
				verbose++;
				break;
			case 'V':
				verbose++;
				version++;
				break;
		}
	}

	/* reinitialize getopt() by resetting optind to 0 */
	optind = 0;

	/* say hello */
	if (verbose > 0)
		printf("%s: %s v%s"
#ifdef HAVE_SYSTEMD
			" +systemd"
#endif
#ifdef HAVE_LIBAV
			" +libav"
#endif
			" (compiled: " __DATE__ ", " __TIME__ ")\n", program, PROGNAME, VERSION);

	if (help > 0)
		fprintf(stderr, "usage: %s [-h] [-H HOST] [-m MUSIC-DIR] [-p PORT] [-s PIXELS] [-t TIMEOUT] [-v] [-V]\n", program);

	if (version > 0 || help > 0)
		return EXIT_SUCCESS;

	/* get command line options */
	while ((i = getopt_long(argc, argv, optstring, options_long, NULL)) != -1) {
		switch (i) {
			case 'p':
				mpd_port = atoi(optarg);
				if (verbose > 0)
					printf("%s: using port %d\n", program, mpd_port);
				break;
			case 'm':
				music_dir = optarg;
				if (verbose > 0)
					printf("%s: using music-dir %s\n", program, music_dir);
				break;
			case 'H':
				mpd_host = optarg;
				if (verbose > 0)
					printf("%s: using host %s\n", program, mpd_host);
				break;
			case 's':
				scale = atof(optarg);
				break;
			case 't':
				notification_timeout = atof(optarg);
				if (verbose > 0)
					printf("%s: using notification-timeout %d\n", program, notification_timeout);
				break;
			case OPT_FILE_WORKAROUND:
				file_workaround++;
				break;
		}
	}

	/* change directory to music base directory */
	if (music_dir != NULL) {
		if (chdir(music_dir) == -1) {
			fprintf(stderr, "%s: Could not change directory to: %s\n", program, music_dir);
			music_dir = NULL;
		}
	}

#ifdef HAVE_LIBAV
	/* libav */
#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 9, 100)
	av_register_all();
#endif

	/* only fatal messages from libav */
	if (verbose == 0)
		av_log_set_level(AV_LOG_FATAL);

	if ((magic = magic_open(MAGIC_MIME_TYPE)) == NULL) {
		fprintf(stderr, "%s: Could not initialize magic library.\n", program);
		goto out40;
	}

	if (magic_load(magic, NULL) != 0) {
		fprintf(stderr, "%s: Could not load magic database: %s\n", program, magic_error(magic));
		magic_close(magic);
		goto out30;
	}
#endif

	conn = mpd_connection_new(mpd_host, mpd_port, mpd_timeout * 1000);

	if (mpd_connection_get_error(conn) != MPD_ERROR_SUCCESS) {
		fprintf(stderr,"%s: %s\n", program, mpd_connection_get_error_message(conn));
		goto out30;
	}

	if (notify_init(PROGNAME) == FALSE) {
		fprintf(stderr, "%s: Could not initialize notify.\n", program);
		goto out20;
	}

	notification =
#		if NOTIFY_CHECK_VERSION(0, 7, 0)
		notify_notification_new(text_topic, TEXT_NONE, ICON_AUDIO_X_GENERIC);
#		else
		notify_notification_new(text_topic, TEXT_NONE, ICON_AUDIO_X_GENERIC, NULL);
#		endif
	notify_notification_set_category(notification, PROGNAME);
	notify_notification_set_urgency(notification, NOTIFY_URGENCY_NORMAL);
	notify_notification_set_timeout(notification, notification_timeout * 1000);

	struct sigaction act = { 0 };
	act.sa_handler = received_signal;

	sigaction(SIGHUP, &act, NULL);
	sigaction(SIGINT, &act, NULL);
	sigaction(SIGTERM, &act, NULL);
	sigaction(SIGUSR1, &act, NULL);

	/* report ready to systemd */
#ifdef HAVE_SYSTEMD
	sd_notify(0, "READY=1\nSTATUS=Waiting for mpd event...");
#endif

	while (doexit == 0 && mpd_run_idle_mask(conn, MPD_IDLE_PLAYER)) {
		mpd_command_list_begin(conn, true);
		mpd_send_status(conn);
		mpd_send_current_song(conn);
		mpd_command_list_end(conn);

		state = mpd_status_get_state(mpd_recv_status(conn));
		if (state == MPD_STATE_PLAY || state == MPD_STATE_PAUSE) {
			/* There's a bug in libnotify where the server spec version is fetched
			 * too late, which results in issue with image date. Make sure to
			 * show a notification without image data (just generic icon) first. */
			if (last_state != MPD_STATE_PLAY && last_state != MPD_STATE_PAUSE) {
				notify_notification_update(notification, text_topic, "Starting playback...", ICON_AUDIO_X_GENERIC);
				notify_notification_show(notification, NULL);
			}

			mpd_response_next(conn);

			song = mpd_recv_song(conn);

			title  = mpd_song_get_tag(song, MPD_TAG_TITLE, 0);
			artist = mpd_song_get_tag(song, MPD_TAG_ARTIST, 0);
			album  = mpd_song_get_tag(song, MPD_TAG_ALBUM, 0);
			duration = mpd_song_get_duration(song);

			/* ignore if we have no title */
			if (title == NULL)
				goto nonotification;

#ifdef HAVE_SYSTEMD
			sd_notifyf(0, "READY=1\nSTATUS=%s: %s", state == MPD_STATE_PLAY ? "Playing" : "Paused", title);
#endif

			/* get the formatted notification string */
			notifystr = format_text(state == MPD_STATE_PLAY ? text_play : text_pause,
				title, artist ? artist : "unknown artist", album ? album : "unknown album", duration);

			uri = mpd_song_get_uri(song);

			if (music_dir != NULL && uri != NULL) {
				GdkPixbuf * copy;

				pixbuf = retrieve_artwork(music_dir, uri);

				if (pixbuf != NULL && scale > 0) {
					int x, y;

					x = gdk_pixbuf_get_width(pixbuf);
					y = gdk_pixbuf_get_height(pixbuf);

					if ((copy = gdk_pixbuf_scale_simple (pixbuf,
							(x > y ? scale : scale * x / y),
							(y > x ? scale : scale * y / x),
							GDK_INTERP_BILINEAR)) != NULL) {
						g_object_unref(pixbuf);
						pixbuf = copy;
					}
				}


			}

			mpd_song_free(song);
		} else if (state == MPD_STATE_STOP) {
			notifystr = strdup(text_stop);
#ifdef HAVE_SYSTEMD
			sd_notifyf(0, "READY=1\nSTATUS=%s", text_stop);
#endif
		} else
			notifystr = strdup(TEXT_UNKNOWN);

		last_state = state;

		if (verbose > 0)
			printf("%s: %s\n", program, notifystr);

		/* Some notification daemons do not support handing pixbuf data. Write a PNG
		 * file and give the path. */
		if (file_workaround > 0 && pixbuf != NULL) {
			gdk_pixbuf_save(pixbuf, "/tmp/.mpd-notification-artwork.png", "png", NULL, NULL);

			notify_notification_update(notification, text_topic, notifystr, "/tmp/.mpd-notification-artwork.png");
		} else
			notify_notification_update(notification, text_topic, notifystr, ICON_AUDIO_X_GENERIC);

		/* Call this unconditionally! When pixbuf is NULL this clears old image. */
		notify_notification_set_image_from_pixbuf(notification, pixbuf);

		if (notify_notification_show(notification, &error) == FALSE) {
			g_printerr("%s: Error showing notification: %s\n", program, error->message);
			g_error_free(error);

			goto out10;
		}

nonotification:
		if (notifystr != NULL) {
			free(notifystr);
			notifystr = NULL;
		}
		if (pixbuf != NULL) {
			g_object_unref(pixbuf);
			pixbuf = NULL;
		}
		mpd_response_finish(conn);
	}

	if (verbose > 0)
		printf("%s: Exiting...\n", program);

	/* report stopping to systemd */
#ifdef HAVE_SYSTEMD
	sd_notify(0, "STOPPING=1\nSTATUS=Stopping...");
#endif

	rc = EXIT_SUCCESS;

out10:
	g_object_unref(G_OBJECT(notification));
	notify_uninit();

out20:
	if (conn != NULL)
		mpd_connection_free(conn);

out30:
#ifdef HAVE_LIBAV
	if (magic != NULL)
		magic_close(magic);
out40:
#endif

	if (ini != NULL)
		iniparser_freedict(ini);

#ifdef HAVE_SYSTEMD
	sd_notify(0, "STATUS=Stopped. Bye!");
#endif

	return rc;
}