#include #include #include #include #include #include #include #include #include #include "moc_library.h" /* State of the server. */ #define STATE_PLAY 0x01 #define STATE_STOP 0x02 #define STATE_PAUSE 0x03 #define CMD_GET_STATE 0x13 /* get the state */ #define CMD_GET_CTIME 0x0d /* get the current song time */ #define CMD_GET_TAGS 0x2c /* get tags for the currently played file */ #define CMD_GET_FILE_TAGS 0x2f /* get tags for the specified file */ #define CMD_GET_SNAME 0x0f /* get the stream file name */ #define CMD_PLAY 0x00 /* play the first element on the list, or a specific file if a non-empty name is given */ #define CMD_LIST_ADD 0x02 /* add an item to the server's playback list */ #define CMD_PAUSE 0x05 #define CMD_UNPAUSE 0x06 #define CMD_STOP 0x04 #define CMD_NEXT 0x10 #define CMD_PREV 0x20 /* Add an item to the synced playlist view (broadcast as EV_PLIST_ADD to * every client, including ourselves). Doesn't touch the server's actual * playback list - that's CMD_LIST_ADD, above. */ #define CMD_CLI_PLIST_ADD 0x24 /* Remove/clear an item from the synced playlist view (broadcast as * EV_PLIST_DEL/EV_PLIST_CLEAR). Like CMD_CLI_PLIST_ADD, these never touch * the server's actual playback list. */ #define CMD_CLI_PLIST_DEL 0x25 #define CMD_CLI_PLIST_CLEAR 0x26 #define MAX_SEND_STRING 4096 #define RANGE(min, val, max) ((val) >= (min) && (val) <= (max)) /* Definition of events sent by server to the client. */ #define EV_STATE 0x01 /* server has changed the state */ #define EV_CTIME 0x02 /* current time of the song has changed */ #define EV_SRV_ERROR 0x04 /* an error occurred */ #define EV_BUSY 0x05 /* another client is connected to the server */ #define EV_DATA 0x06 /* data in response to a request will arrive */ #define EV_BITRATE 0x07 /* the bitrate has changed */ #define EV_RATE 0x08 /* the rate has changed */ #define EV_CHANNELS 0x09 /* the number of channels has changed */ #define EV_EXIT 0x0a /* the server is about to exit */ #define EV_PONG 0x0b /* response for CMD_PING */ #define EV_OPTIONS 0x0c /* the options has changed */ #define EV_SEND_PLIST 0x0d /* request for sending the playlist */ #define EV_TAGS 0x0e /* tags for the current file have changed */ #define EV_STATUS_MSG 0x0f /* followed by a status message */ #define EV_MIXER_CHANGE 0x10 /* the mixer channel was changed */ #define EV_FILE_TAGS 0x11 /* tags in a response for tags request */ #define EV_AVG_BITRATE 0x12 /* average bitrate has changed (new song) */ #define EV_AUDIO_START 0x13 /* playing of audio has started */ #define EV_AUDIO_STOP 0x14 /* playing of audio has stopped */ /* Not a real protocol value - an internal marker pushed onto our pending * request queue while we're waiting for the reply to CMD_GET_SNAME. */ #define PENDING_SNAME -100 /* Events caused by a client that wants to modify the playlist (see * CMD_CLI_PLIST* commands). */ #define EV_PLIST_ADD 0x50 /* add an item, followed by the file name */ #define EV_PLIST_DEL 0x51 /* delete an item, followed by the file name */ #define EV_PLIST_MOVE 0x52 /* move an item, followed by 2 file names */ #define EV_PLIST_CLEAR 0x53 /* clear the playlist */ /* These events, though similar to the four previous are caused by server * which takes care of clients' queue synchronization. */ #define EV_QUEUE_ADD 0x54 #define EV_QUEUE_DEL 0x55 #define EV_QUEUE_MOVE 0x56 #define EV_QUEUE_CLEAR 0x57 /* Ask another connected client (if any) to send us its playlist. */ #define CMD_GET_PLIST 0x22 /* get the playlist from one of the clients */ /* Unlike the playlist, the queue is tracked by the server itself, so this * always succeeds (possibly with an empty queue). */ #define CMD_GET_QUEUE 0x3f /* request the queue from the server */ /* Subscribe to playlist/queue broadcast events; the server won't send them * to us otherwise. */ #define CMD_SEND_PLIST_EVENTS 0x1d /* request for playlist events */ extern const char * moc_str_status(enum moc_status s) { static const char *strings[] = { "INITIALIZING", "CONNECTED", "FAILED_TO_CONNECT", "ERROR", "RECONNECTING" }; return strings[s]; } extern const char * moc_str_state(enum moc_state s) { static const char *strings[] = { "STOPPED", "PLAYING", "PAUSED"}; return strings[s]; } int send_str (int sock, const char *str); char *get_str (int sock); int send_int (int sock, int i); int get_int (int sock, int *i); void reset_data(struct moc *data) { data->filename = NULL; data->title = NULL; data->album = NULL; data->artist = NULL; data->time = -1; data->length = -1; data->state = STOPPED; } #define CMD_QUEUE_SIZE 16 struct cmd_queue { int items[CMD_QUEUE_SIZE]; int head; int tail; }; static void cmd_queue_push (struct cmd_queue *q, int cmd) { q->items[q->tail] = cmd; q->tail = (q->tail + 1) % CMD_QUEUE_SIZE; } /* Returns -1 if nothing is pending. */ static int cmd_queue_pop (struct cmd_queue *q) { int cmd; if (q->head == q->tail) return -1; cmd = q->items[q->head]; q->head = (q->head + 1) % CMD_QUEUE_SIZE; return cmd; } int send_str (int sock, const char *str) { int len; len = strlen (str); if (!send_int (sock, len)) return 0; if (send (sock, str, len, 0) != len) return 0; return 1; } char *get_str (int sock) { int len; int res, nread = 0; char *str; if (!get_int(sock, &len)) return NULL; if (!RANGE(0, len, MAX_SEND_STRING)) { printf("Bad string length."); return NULL; } str = (char *)malloc (sizeof(char) * (len + 1)); while (nread < len) { res = recv (sock, str + nread, len - nread, 0); if (res == -1) { printf("recv() failed when getting string: %s\n", strerror(errno)); free (str); return NULL; } if (res == 0) { printf("Unexpected EOF when getting string\n"); free (str); return NULL; } nread += res; } str[len] = 0; return str; } int send_int (int sock, int i) { int res; res = send (sock, &i, sizeof(int), 0); if (res == -1) printf("send() failed: %s", strerror(errno)); return res == sizeof(int) ? 1 : 0; } int get_int (int sock, int *i) { int res; size_t nread = 0; while (nread < sizeof(int)) { res = recv (sock, (char *)i + nread, sizeof(int) - nread, 0); if (res == -1) { printf("recv() failed when getting int: %s", strerror(errno)); return 0; } if (res == 0) return 0; nread += res; } return 1; } static int get_time (int sock, time_t *t) { int res; size_t nread = 0; while (nread < sizeof(time_t)) { res = recv (sock, (char *)t + nread, sizeof(time_t) - nread, 0); if (res == -1) { printf("recv() failed when getting time_t: %s", strerror(errno)); return 0; } if (res == 0) return 0; nread += res; } return 1; } static int send_time (int sock, time_t t) { int res = send (sock, &t, sizeof(t), 0); if (res == -1) printf("send() failed: %s", strerror(errno)); return res == sizeof(t) ? 1 : 0; } /* Send a minimal/untagged item in the wire format recv_item() expects - * used when adding a file we haven't read tags for ourselves. Our own * EV_PLIST_ADD/EV_QUEUE_ADD handler notices the missing title and asks the * server for real tags afterward. */ static void send_bare_item (int sock, const char *file) { send_str(sock, file); send_str(sock, ""); /* title_tags */ send_str(sock, ""); /* title */ send_str(sock, ""); /* artist */ send_str(sock, ""); /* album */ send_int(sock, -1); /* track */ send_int(sock, -1); /* time */ send_int(sock, 0); /* filled */ send_time(sock, time(NULL)); /* mtime */ } static void plist_item_free (struct moc_plist_item *item) { if (!item) return; free(item->file); free(item->title); free(item->artist); free(item->album); free(item); } /* Receive one playlist/queue item from the socket: file name, and (if the * file name is non-empty) a filename-derived title, full tags, and an mtime. * An empty file name is the "end of playlist" marker used by CMD_GET_PLIST's * bulk transfer; the caller checks item->file[0] for that. Returns NULL on * a read error. */ static struct moc_plist_item *recv_item (int sock) { struct moc_plist_item *item = (struct moc_plist_item *)calloc(1, sizeof(*item)); char *title_tags; int filled; time_t mtime; if (!(item->file = get_str(sock))) { free(item); return NULL; } if (item->file[0]) { if (!(title_tags = get_str(sock))) { plist_item_free(item); return NULL; } free(title_tags); /* filename-derived fallback title; we keep tags->title instead */ if (!(item->title = get_str(sock)) || !(item->artist = get_str(sock)) || !(item->album = get_str(sock)) || !get_int(sock, &item->track) || !get_int(sock, &item->time) || !get_int(sock, &filled) || !get_time(sock, &mtime)) { plist_item_free(item); return NULL; } } return item; } static void playlist_append (struct moc_plist_item **list, struct moc_plist_item *item) { struct moc_plist_item **tail = list; while (*tail) tail = &(*tail)->next; *tail = item; } static void playlist_remove (struct moc_plist_item **list, const char *file) { struct moc_plist_item *item, *prev = NULL; for (item = *list; item; prev = item, item = item->next) { if (!strcmp(item->file, file)) { if (prev) prev->next = item->next; else *list = item->next; plist_item_free(item); return; } } } /* The server's EV_PLIST_MOVE/EV_QUEUE_MOVE just identifies two files that * exchange position, so swap their contents in place. */ static void playlist_swap (struct moc_plist_item *list, const char *from, const char *to) { struct moc_plist_item *a = NULL, *b = NULL, *item; for (item = list; item; item = item->next) { if (!strcmp(item->file, from)) a = item; else if (!strcmp(item->file, to)) b = item; } if (a && b) { char *file = a->file, *title = a->title, *artist = a->artist, *album = a->album; int track = a->track, time = a->time; a->file = b->file; a->title = b->title; a->artist = b->artist; a->album = b->album; a->track = b->track; a->time = b->time; b->file = file; b->title = title; b->artist = artist; b->album = album; b->track = track; b->time = time; } } static void playlist_clear (struct moc_plist_item **list) { struct moc_plist_item *item = *list, *next; while (item) { next = item->next; plist_item_free(item); item = next; } *list = NULL; } static struct moc_plist_item *playlist_find (struct moc_plist_item *list, const char *file) { for (; list; list = list->next) if (!strcmp(list->file, file)) return list; return NULL; } static int moc_server_connect () { struct sockaddr_un sock_name; int sock; /* Create a socket */ if ((sock = socket (PF_LOCAL, SOCK_STREAM, 0)) == -1) return -1; sock_name.sun_family = AF_LOCAL; snprintf(sock_name.sun_path, sizeof(sock_name.sun_path), "%s/.moc/socket2", getenv("HOME")); if (connect(sock, (struct sockaddr *)&sock_name, SUN_LEN(&sock_name)) == -1) { close (sock); fprintf(stderr, "%s at (%s)\n", strerror(errno), sock_name.sun_path); return -1; } return sock; } void update_state(struct moc *data, int state) { if(state == STATE_PLAY) { data->state = PLAYING; } if(state == STATE_PAUSE) { data->state = PAUSED; } if(state == STATE_STOP) { data->state = STOPPED; } } static int moc_reconnect(struct moc *data) { int srv_sock = -1; while(srv_sock == -1) { fprintf(stderr, "moc: reconnecting\n"); srv_sock = moc_server_connect(); sleep(1); } pthread_mutex_lock(&data->lock); data->status = CONNECTED; data->sock = srv_sock; pthread_mutex_unlock(&data->lock); return srv_sock; } /* Block until an EV_DATA marker arrives, discarding the payload of any * other event that shows up first. Only safe to use before we've * subscribed to playlist/queue events and outside the main request queue - * i.e. during the one-time startup sync below, where nothing else we care * about can be in flight yet. */ static void wait_for_ev_data (int sock) { int ev = -1; while (ev != EV_DATA) { get_int(sock, &ev); if (ev == EV_SRV_ERROR || ev == EV_STATUS_MSG) free(get_str(sock)); } } /* Ask another connected client (usually the real mocp ncurses UI, if one is * running) for its current playlist. There may be none, in which case we * just end up with an empty list. */ static void fetch_playlist_snapshot (int sock, struct moc *data) { int exists, serial; struct moc_plist_item *item; pthread_mutex_lock(&data->sock_lock); send_int(sock, CMD_GET_PLIST); pthread_mutex_unlock(&data->sock_lock); wait_for_ev_data(sock); if (!get_int(sock, &exists) || !exists) return; wait_for_ev_data(sock); get_int(sock, &serial); for (;;) { item = recv_item(sock); if (!item || !item->file[0]) { plist_item_free(item); break; } pthread_mutex_lock(&data->lock); playlist_append(&data->playlist, item); pthread_mutex_unlock(&data->lock); } } /* Unlike the playlist, the queue lives on the server, so this always * succeeds (possibly with an empty queue). */ static void fetch_queue_snapshot (int sock, struct moc *data) { struct moc_plist_item *item; pthread_mutex_lock(&data->sock_lock); send_int(sock, CMD_GET_QUEUE); pthread_mutex_unlock(&data->sock_lock); wait_for_ev_data(sock); for (;;) { item = recv_item(sock); if (!item || !item->file[0]) { plist_item_free(item); break; } pthread_mutex_lock(&data->lock); playlist_append(&data->queue, item); pthread_mutex_unlock(&data->lock); } } /* Pull a one-time snapshot of the playlist and queue, then subscribe to * live updates for both going forward. Must run before anything else talks * to the socket, since it does its own raw synchronous reads outside the * normal pending-request queue. */ static void sync_playlists (int sock, struct moc *data) { pthread_mutex_lock(&data->lock); playlist_clear(&data->playlist); playlist_clear(&data->queue); pthread_mutex_unlock(&data->lock); fetch_playlist_snapshot(sock, data); fetch_queue_snapshot(sock, data); pthread_mutex_lock(&data->sock_lock); send_int(sock, CMD_SEND_PLIST_EVENTS); pthread_mutex_unlock(&data->sock_lock); { int plist_n = 0, queue_n = 0; struct moc_plist_item *item; pthread_mutex_lock(&data->lock); for (item = data->playlist; item; item = item->next) plist_n++; for (item = data->queue; item; item = item->next) queue_n++; pthread_mutex_unlock(&data->lock); fprintf(stderr, "moc: playlist sync: %d playlist item(s), %d queued\n", plist_n, queue_n); } } void *moc_loop(void *input) { struct moc *data = (struct moc*)input; int srv_sock = -1; fprintf(stderr, "moc: connecting\n"); srv_sock = moc_reconnect(data); fprintf(stderr, "moc: connected: %d\n", srv_sock); sync_playlists(srv_sock, data); /* Server replies to our requests in the order we sent them, but it can * also send unsolicited notifications (EV_CTIME, EV_STATE, ...) that each * trigger a follow-up request of their own. A single "last_cmd" variable * can't track more than one outstanding request, so if two notifications * arrive back-to-back the second overwrites the first and the next * EV_DATA reply gets matched against the wrong request, desyncing the * whole stream. Use a small FIFO queue of pending request types instead. */ struct cmd_queue pending = { .head = 0, .tail = 0 }; int event = -1; pthread_mutex_lock(&data->sock_lock); send_int(srv_sock, CMD_GET_STATE); pthread_mutex_unlock(&data->sock_lock); cmd_queue_push(&pending, EV_STATE); while(1) { get_int(srv_sock, &event); switch (event) { // handle our data. we MUST know what the last request was otherwise we // don't know how to parse it. case EV_DATA: { int last_cmd = cmd_queue_pop(&pending); if(last_cmd == EV_CTIME) { get_int(srv_sock, &data->time); fprintf(stderr, "moc: update ctime\n"); if(data->state == 0) { pthread_mutex_lock(&data->sock_lock); send_int(srv_sock, CMD_GET_STATE); pthread_mutex_unlock(&data->sock_lock); cmd_queue_push(&pending, EV_STATE); } } // on state change, we need to figure out what file we have and what // tags it has. else if(last_cmd == EV_STATE) { int state; get_int(srv_sock, &state); pthread_mutex_lock(&data->lock); update_state(data, state); if(state != STATE_STOP) { pthread_mutex_unlock(&data->lock); // ask for the filename; the reply is handled below under // PENDING_SNAME once it's actually our turn in the queue. pthread_mutex_lock(&data->sock_lock); send_int(srv_sock, CMD_GET_SNAME); pthread_mutex_unlock(&data->sock_lock); cmd_queue_push(&pending, PENDING_SNAME); } else { reset_data(data); pthread_mutex_unlock(&data->lock); } } // reply to our CMD_GET_SNAME request, queued above. else if(last_cmd == PENDING_SNAME) { char *file = get_str(srv_sock); fprintf(stderr, "moc: current file %s\n", file); pthread_mutex_lock(&data->lock); // if it's the same file as we had, don't update. if(data->filename == NULL || strcmp(file, data->filename) != 0) { fprintf(stderr, "moc: new file, asking for tags\n"); data->filename = file; // get tags. pthread_mutex_lock(&data->sock_lock); send_int(srv_sock, CMD_GET_FILE_TAGS); send_str(srv_sock, file); send_int(srv_sock,TAGS_COMMENTS | TAGS_TIME); pthread_mutex_unlock(&data->sock_lock); } else { free(file); } pthread_mutex_unlock(&data->lock); } else { int unused; get_int(srv_sock, &unused); fprintf(stderr, "moc: UNKNOWN STATE\n"); } break; } case EV_BUSY: break; case EV_CTIME: pthread_mutex_lock(&data->sock_lock); send_int(srv_sock, CMD_GET_CTIME); pthread_mutex_unlock(&data->sock_lock); cmd_queue_push(&pending, EV_CTIME); break; case EV_STATE: pthread_mutex_lock(&data->sock_lock); send_int(srv_sock, CMD_GET_STATE); pthread_mutex_unlock(&data->sock_lock); cmd_queue_push(&pending, EV_STATE); break; case EV_EXIT: pthread_mutex_lock(&data->lock); data->status = RECONNECTING; data->sock = -1; reset_data(data); pthread_mutex_unlock(&data->lock); srv_sock = moc_reconnect(data); pending.head = pending.tail = 0; sync_playlists(srv_sock, data); pthread_mutex_lock(&data->sock_lock); send_int(srv_sock, CMD_GET_STATE); pthread_mutex_unlock(&data->sock_lock); cmd_queue_push(&pending, EV_STATE); break; case EV_BITRATE: break; case EV_RATE: break; case EV_CHANNELS: break; case EV_SRV_ERROR: get_str(srv_sock); break; case EV_OPTIONS: break; case EV_SEND_PLIST: /* We never register via CMD_CAN_SEND_PLIST, so the server should * never ask us to act as the playlist source. Nothing to consume. */ break; case EV_TAGS: break; case EV_PLIST_ADD: case EV_QUEUE_ADD: { struct moc_plist_item *item = recv_item(srv_sock); if (item) { fprintf(stderr, "moc: %s added: %s\n", event == EV_PLIST_ADD ? "playlist item" : "queue item", item->file); pthread_mutex_lock(&data->lock); playlist_append(event == EV_PLIST_ADD ? &data->playlist : &data->queue, item); pthread_mutex_unlock(&data->lock); // freshly added items often arrive untagged (e.g. a directory add // via mocp's SyncPlaylist option sends bare items before reading // tags); ask for them ourselves. The reply comes back as its own // EV_FILE_TAGS event, matched by filename below. if (!item->title || !item->title[0]) { pthread_mutex_lock(&data->sock_lock); send_int(srv_sock, CMD_GET_FILE_TAGS); send_str(srv_sock, item->file); send_int(srv_sock, TAGS_COMMENTS | TAGS_TIME); pthread_mutex_unlock(&data->sock_lock); } } break; } case EV_PLIST_DEL: case EV_QUEUE_DEL: { char *file = get_str(srv_sock); if (file) { pthread_mutex_lock(&data->lock); playlist_remove(event == EV_PLIST_DEL ? &data->playlist : &data->queue, file); pthread_mutex_unlock(&data->lock); free(file); } break; } case EV_PLIST_MOVE: case EV_QUEUE_MOVE: { char *from = get_str(srv_sock); char *to = get_str(srv_sock); if (from && to) { pthread_mutex_lock(&data->lock); playlist_swap(event == EV_PLIST_MOVE ? data->playlist : data->queue, from, to); pthread_mutex_unlock(&data->lock); } free(from); free(to); break; } case EV_PLIST_CLEAR: case EV_QUEUE_CLEAR: pthread_mutex_lock(&data->lock); playlist_clear(event == EV_PLIST_CLEAR ? &data->playlist : &data->queue); pthread_mutex_unlock(&data->lock); break; case EV_STATUS_MSG: free(get_str(srv_sock)); break; case EV_MIXER_CHANGE: break; case EV_FILE_TAGS: { // EV_FILE_TAGS answers whichever CMD_GET_FILE_TAGS request matches // this filename - that could be the "now playing" request from the // EV_STATE/PENDING_SNAME flow above, or one of the lazy per-item // requests we fire off in EV_PLIST_ADD/EV_QUEUE_ADD. There's no // correlation id in the protocol, so we match by filename content. char *filename = get_str(srv_sock); char *title = get_str(srv_sock); char *artist = get_str(srv_sock); char *album = get_str(srv_sock); int track = -1, length = -1, filled = 0; get_int(srv_sock, &track); get_int(srv_sock, &length); get_int(srv_sock, &filled); pthread_mutex_lock(&data->lock); if (filename && data->filename && !strcmp(filename, data->filename)) { free(data->title); free(data->artist); free(data->album); data->title = title; data->artist = artist; data->album = album; data->track = track; data->length = length; data->filled = filled; free(filename); } else if (filename) { struct moc_plist_item *item = playlist_find(data->playlist, filename); if (!item) item = playlist_find(data->queue, filename); if (item) { free(item->title); free(item->artist); free(item->album); item->title = title; item->artist = artist; item->album = album; item->track = track; item->time = length; } else { free(title); free(artist); free(album); } free(filename); } else { free(title); free(artist); free(album); } pthread_mutex_unlock(&data->lock); break; } case EV_AVG_BITRATE: break; case EV_AUDIO_START: break; case EV_AUDIO_STOP: break; default: fprintf(stderr, "Unknown event: 0x%02x!\n", event); break; } } fprintf(stderr, "loop died, you're fucked\n"); } extern struct moc *moc_init() { struct moc *moc = (struct moc*)calloc(1, sizeof(struct moc)); reset_data(moc); pthread_t thread_id; if (pthread_mutex_init(&moc->lock, NULL) != 0) { printf("\n mutex init has failed\n"); return NULL; } if (pthread_mutex_init(&moc->sock_lock, NULL) != 0) { printf("\n mutex init has failed\n"); return NULL; } moc->status = INITIALIZING; moc->sock = -1; pthread_create(&thread_id, NULL, moc_loop, (void *)moc); return moc; } /* Grab the current socket fd, or -1 if we're disconnected/reconnecting. */ static int moc_sock (struct moc *mh) { int sock; pthread_mutex_lock(&mh->lock); sock = mh->sock; pthread_mutex_unlock(&mh->lock); return sock; } void moc_play (struct moc *mh, const char *file) { int sock = moc_sock(mh); if (sock < 0) return; pthread_mutex_lock(&mh->sock_lock); send_int(sock, CMD_PLAY); send_str(sock, file ? file : ""); pthread_mutex_unlock(&mh->sock_lock); } void moc_pause (struct moc *mh) { int sock = moc_sock(mh); if (sock < 0) return; pthread_mutex_lock(&mh->sock_lock); send_int(sock, CMD_PAUSE); pthread_mutex_unlock(&mh->sock_lock); } void moc_unpause (struct moc *mh) { int sock = moc_sock(mh); if (sock < 0) return; pthread_mutex_lock(&mh->sock_lock); send_int(sock, CMD_UNPAUSE); pthread_mutex_unlock(&mh->sock_lock); } void moc_stop (struct moc *mh) { int sock = moc_sock(mh); if (sock < 0) return; pthread_mutex_lock(&mh->sock_lock); send_int(sock, CMD_STOP); pthread_mutex_unlock(&mh->sock_lock); } void moc_next (struct moc *mh) { int sock = moc_sock(mh); if (sock < 0) return; pthread_mutex_lock(&mh->sock_lock); send_int(sock, CMD_NEXT); pthread_mutex_unlock(&mh->sock_lock); } void moc_prev (struct moc *mh) { int sock = moc_sock(mh); if (sock < 0) return; pthread_mutex_lock(&mh->sock_lock); send_int(sock, CMD_PREV); pthread_mutex_unlock(&mh->sock_lock); } /* Register file with the server's actual playback list (so CMD_PLAY can * find it later) and broadcast it to every client's synced playlist view, * including our own - we'll pick it up the normal way via EV_PLIST_ADD. */ void moc_playlist_add (struct moc *mh, const char *file) { int sock = moc_sock(mh); if (sock < 0) return; pthread_mutex_lock(&mh->sock_lock); send_int(sock, CMD_LIST_ADD); send_str(sock, file); send_int(sock, CMD_CLI_PLIST_ADD); send_bare_item(sock, file); pthread_mutex_unlock(&mh->sock_lock); } /* Broadcast removal of a single playlist entry; we'll pick up the change * ourselves the normal way via EV_PLIST_DEL. */ void moc_playlist_remove (struct moc *mh, const char *file) { int sock = moc_sock(mh); if (sock < 0) return; pthread_mutex_lock(&mh->sock_lock); send_int(sock, CMD_CLI_PLIST_DEL); send_str(sock, file); pthread_mutex_unlock(&mh->sock_lock); } void moc_playlist_clear (struct moc *mh) { int sock = moc_sock(mh); if (sock < 0) return; pthread_mutex_lock(&mh->sock_lock); send_int(sock, CMD_CLI_PLIST_CLEAR); pthread_mutex_unlock(&mh->sock_lock); }