/* * (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(®ex, 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(®ex, 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(®ex); 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; }