/* Shellexec plugin for DeaDBeeF Copyright (C) 2010-2014 Deadbeef team Original developer Viktor Semykin Maintenance, minor improvements Alexey Yakovenko GUI support and bugfixing Azeem Arshad 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, see . */ /* Configuration scheme: shellexec.NN shcmd:title:name:flags @shcmd is the command executed by the shell formating directives are allowed, see format_shell_command function @title is the title of command displayed in UI @name used for referencing command, for example in hotkeys configuration @flags comma-separated of command flags, allowed flags are: single - command allowed for single track multiple - command allowerd for multiple tracks local - command allowed for local files remote - command allowed for non-local files playlist - command allowed for playlist tabs common - command appears in main menu bar */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include "../../deadbeef.h" #include "shellexec.h" //#define trace(...) { fprintf(stderr, __VA_ARGS__); } #define trace(fmt,...) static Shx_plugin_t plugin; static DB_functions_t *deadbeef; static Shx_action_t *actions; DB_plugin_t * shellexec_load (DB_functions_t *api) { deadbeef = api; return DB_PLUGIN (&plugin); } static const char* trim (char* s) { if (!s) { return ""; } char *h, *t; for (h = s; *h == ' ' || *h == '\t'; h++); for (t = s + strlen (s)-1; *t == ' ' || *t == '\t'; t--); *(t+1) = 0; return h; } static int shx_exec_track_cmd (Shx_action_t *action, DB_playItem_t *it) { char cmd[_POSIX_ARG_MAX]; int res = deadbeef->pl_format_title_escaped (it, -1, cmd, sizeof (cmd) - 2, -1, action->shcommand); if (res < 0) { trace ("shellexec: failed to format string for execution (too long?)\n"); return -1; } strcat (cmd, "&"); // replace \' with '"'"' size_t l = strlen (cmd); size_t remaining = _POSIX_ARG_MAX - l - 1; for (int i = 0; cmd[i]; i++) { if (cmd[i] == '\\' && cmd[i+1] == '\'' && remaining >= 3) { memmove (&cmd[i+5], &cmd[i+2], l - i + 1 - 2); memcpy (&cmd[i], "'\"'\"'", 5); l += 3; remaining -= 3; i += 5; } else if (remaining < 3) { fprintf (stderr, "shellexec: command is too long.\n"); return -1; } } trace ("%s\n", cmd); res = system (cmd); return 0; } static int shx_callback (Shx_action_t *action, int ctx) { int res = 0; switch (ctx) { case DDB_ACTION_CTX_MAIN: trace ("%s\n", action->shcommand); int res = system (action->shcommand); break; case DDB_ACTION_CTX_SELECTION: { deadbeef->pl_lock (); ddb_playlist_t *plt = deadbeef->plt_get_curr (); if (plt) { DB_playItem_t **items = NULL; int items_count = deadbeef->plt_getselcount (plt); if (0 < items_count) { items = malloc (sizeof (DB_playItem_t *) * items_count); if (items) { int n = 0; DB_playItem_t *it = deadbeef->pl_get_first (PL_MAIN); while (it) { if (deadbeef->pl_is_selected (it)) { assert (n < items_count); deadbeef->pl_item_ref (it); items[n++] = it; } DB_playItem_t *next = deadbeef->pl_get_next (it, PL_MAIN); deadbeef->pl_item_unref (it); it = next; } } } deadbeef->pl_unlock (); if (items) { for (int i = 0; i < items_count; i++) { res = shx_exec_track_cmd (action, items[i]); deadbeef->pl_item_unref (items[i]); } free (items); } deadbeef->plt_unref (plt); } } break; case DDB_ACTION_CTX_PLAYLIST: { ddb_playlist_t *plt = deadbeef->plt_get_curr (); if (plt) { deadbeef->pl_lock (); DB_playItem_t **items; int items_count = deadbeef->plt_get_item_count (plt, PL_MAIN); if (0 < items_count) { items = malloc (sizeof (DB_playItem_t *) * items_count); if (items) { int n = 0; DB_playItem_t *it = deadbeef->pl_get_first (PL_MAIN); while (it) { items[n++] = it; it = deadbeef->pl_get_next (it, PL_MAIN); } } } deadbeef->pl_unlock (); if (items) { for (int i = 0; i < items_count; i++) { res = shx_exec_track_cmd (action, items[i]); deadbeef->pl_item_unref (items[i]); } free (items); } deadbeef->plt_unref (plt); } } break; case DDB_ACTION_CTX_NOWPLAYING: { DB_playItem_t *it = deadbeef->streamer_get_playing_track (); if (it) { res = shx_exec_track_cmd (action, it); deadbeef->pl_item_unref (it); } } break; } return res; } static DB_plugin_action_t * shx_get_plugin_actions (DB_playItem_t *it) { deadbeef->pl_lock (); int is_local = it ? deadbeef->is_local_file (deadbeef->pl_find_meta (it, ":URI")) : 1; deadbeef->pl_unlock (); Shx_action_t *action; for (action = actions; action; action = (Shx_action_t *)action->parent.next) { if ((!(action->shx_flags & SHX_ACTION_LOCAL_ONLY) && is_local) || (!(action->shx_flags & SHX_ACTION_REMOTE_ONLY) && !is_local)) { action->parent.flags |= DB_ACTION_DISABLED; } else { action->parent.flags &= ~DB_ACTION_DISABLED; } } return (DB_plugin_action_t *)actions; } static char * shx_find_sep (char *str) { while (*str && *str != ':') { if (*str == '"') { str++; while (*str && *str !='"') { str++; } } str++; } return str; } void shx_save_actions (void) { deadbeef->conf_remove_items("shellexec."); Shx_action_t *action = actions; int i = 0; while(action) { // build config line // format- shellexec.NN shcmd:title:name:flags size_t conf_line_length = 100 + strlen(action->shcommand) + 1 + strlen(action->parent.title) + 1 + strlen(action->parent.name) + 1; char conf_line[conf_line_length]; char conf_key[50]; sprintf(conf_key, "shellexec.%d", i); sprintf(conf_line, "%s:%s:%s:", action->shcommand, action->parent.title, action->parent.name); if(action->shx_flags & SHX_ACTION_REMOTE_ONLY) { strcat(conf_line, "remote,"); } if(action->shx_flags & SHX_ACTION_LOCAL_ONLY) { strcat(conf_line, "local,"); } if(action->parent.flags & DB_ACTION_SINGLE_TRACK) { strcat(conf_line, "single,"); } if(action->parent.flags & DB_ACTION_MULTIPLE_TRACKS) { strcat(conf_line, "multiple,"); } if(action->parent.flags & DB_ACTION_COMMON) { strcat(conf_line, "common,"); } deadbeef->conf_set_str(conf_key, conf_line); action = (Shx_action_t*)action->parent.next; i++; } deadbeef->conf_save(); } Shx_action_t* shx_get_actions (DB_plugin_action_callback2_t callback) { Shx_action_t *action_list = NULL; Shx_action_t *prev = NULL; DB_conf_item_t *item = deadbeef->conf_find ("shellexec.", NULL); while (item) { size_t l = strlen (item->value) + 1; char tmp[l]; strcpy (tmp, item->value); trace ("Shellexec: %s\n", tmp); char *args[4] = {0}; int idx = 0; char *p = tmp; while (idx < 4 && p) { char *e = shx_find_sep (p); args[idx++] = p; if (!e) { break; } *e = 0; p = e+1; } if (idx < 2) { fprintf (stderr, "Shellexec: need at least command and title (%s)\n", item->value); continue; } const char *command = trim (args[0]); const char *title = trim (args[1]); const char *name = trim (args[2]); const char *flags = trim (args[3]); if (!name) { name = "noname"; } if (!flags) { flags = "local,single"; } Shx_action_t *action = calloc (sizeof (Shx_action_t), 1); action->parent.title = strdup (title); action->parent.name = strdup (name); action->shcommand = strdup (command); action->parent.callback2 = callback; action->parent.next = NULL; action->parent.flags |= DB_ACTION_ADD_MENU; action->shx_flags = 0; if (strstr (flags, "local")) action->shx_flags |= SHX_ACTION_LOCAL_ONLY; if (strstr (flags, "remote")) action->shx_flags |= SHX_ACTION_REMOTE_ONLY; if (strstr (flags, "single")) action->parent.flags |= DB_ACTION_SINGLE_TRACK; if (strstr (flags, "multiple")) action->parent.flags |= DB_ACTION_MULTIPLE_TRACKS; if (strstr (flags, "common")) action->parent.flags |= DB_ACTION_COMMON; if (prev) prev->parent.next = (DB_plugin_action_t *)action; prev = action; if (!action_list) action_list = action; item = deadbeef->conf_find ("shellexec.", item); } return action_list; } Shx_action_t* shx_action_add (void) { Shx_action_t *a = calloc (sizeof (Shx_action_t), 1); a->parent.callback2 = (DB_plugin_action_callback2_t)shx_callback; if (!actions) { actions = a; } else { for (Shx_action_t *last = actions; last; last = (Shx_action_t *)last->parent.next) { if (!last->parent.next) { last->parent.next = (DB_plugin_action_t *)a; break; } } } return a; } void shx_action_free (Shx_action_t *a) { if (a->shcommand) { free ((char *)a->shcommand); } if (a->parent.title) { free ((char *)a->parent.title); } if (a->parent.name) { free ((char *)a->parent.name); } free (a); } void shx_action_remove (Shx_action_t *action) { Shx_action_t *prev = NULL; for (Shx_action_t *a = actions; a; a = (Shx_action_t *)a->parent.next) { if (a == action) { if (prev) { prev->parent.next = a->parent.next; } else { actions = (Shx_action_t *)a->parent.next; } break; } prev = a; } shx_action_free (action); } static int shx_start () { actions = shx_get_actions((DB_plugin_action_callback2_t)shx_callback); return 0; } static int shx_stop () { Shx_action_t *a = actions; while (a) { Shx_action_t *next = (Shx_action_t *)a->parent.next; if (a->shcommand) { free ((char *)a->shcommand); } if (a->parent.title) { free ((char *)a->parent.title); } if (a->parent.name) { free ((char *)a->parent.name); } free (a); a = next; } actions = NULL; return 0; } // define plugin interface static Shx_plugin_t plugin = { .misc.plugin.api_vmajor = 1, .misc.plugin.api_vminor = 5, .misc.plugin.version_major = 1, .misc.plugin.version_minor = 1, .misc.plugin.type = DB_PLUGIN_MISC, .misc.plugin.id = "shellexec", .misc.plugin.name = "Shell commands", .misc.plugin.descr = "Executes configurable shell commands for tracks\n" "If you don't have shellexecui installed,\n" "you can still use the following syntax in the config file:\n\n" "shellexec.NN shcmd:title:name:flag1,flag2,flag3,...\n\n" "NN is any (unique) number, e.g. 01, 02, 03, etc\n\n" "shcmd is the command to execute, supports title formatting\n\n" "title is the name of command displayed in UI (context menu)\n\n" "name used for referencing commands from other plugins, e.g hotkeys\n\n" "flags are comma-separated list of items, allowed items are:\n" " single - command allowed only for single track\n" " local - command allowed only for local files\n" " remote - command allowed only for non-local files\n" "EXAMPLE: shellexec.00 notify-send \"%a - %t\":Show selected track:notify:single\n" "this would show the name of selected track in notification popup" , .misc.plugin.copyright = "Shellexec plugin for DeaDBeeF\n" "Copyright (C) 2010-2014 Deadbeef team\n" "Original developer Viktor Semykin \n" "Maintenance, minor improvements Alexey Yakovenko \n" "GUI support and bugfixing Azeem Arshad " "\n" "This program is free software; you can redistribute it and/or\n" "modify it under the terms of the GNU General Public License\n" "as published by the Free Software Foundation; either version 2\n" "of the License, or (at your option) any later version.\n" "\n" "This program is distributed in the hope that it will be useful,\n" "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" "GNU General Public License for more details.\n" "\n" "You should have received a copy of the GNU General Public License\n" "along with this program; if not, write to the Free Software\n" "Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n" , .misc.plugin.website = "http://deadbeef.sf.net", .misc.plugin.start = shx_start, .misc.plugin.stop = shx_stop, .misc.plugin.get_actions = shx_get_plugin_actions, .save_actions = shx_save_actions, .action_add = shx_action_add, .action_remove = shx_action_remove, .action_free = shx_action_free, };