From: Richard Kettlewell Date: Sun, 18 Oct 2009 21:54:18 +0000 (+0100) Subject: Merge from disorder.dev. X-Git-Tag: 5.0~86^2 X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/disorder/commitdiff_plain/4942ee7d61bf22ba38bf026c7d05028cb7db0d54?hp=-c Merge from disorder.dev. --- 4942ee7d61bf22ba38bf026c7d05028cb7db0d54 diff --combined disobedience/queue-generic.c index 60ff794,aa29c28..f14f424 --- a/disobedience/queue-generic.c +++ b/disobedience/queue-generic.c @@@ -129,7 -129,7 +129,7 @@@ const char *column_length(const struct else { if(!last_playing) return NULL; - time(&now); + xtime(&now); l = playing_track->sofar + (now - last_playing); } byte_xasprintf(&played, "%ld:%02ld/%s", l / 60, l % 60, length); @@@ -143,7 -143,7 +143,7 @@@ /** @brief Return the @ref queue_entry corresponding to @p iter * @param model Model that owns @p iter * @param iter Tree iterator - * @return ID string + * @return Pointer to queue entry */ struct queue_entry *ql_iter_to_q(GtkTreeModel *model, GtkTreeIter *iter) { @@@ -402,120 -402,6 +402,120 @@@ void ql_new_queue(struct queuelike *ql --suppress_actions; } +/* Drag and drop has to be figured out experimentally, because it is not well + * documented. + * + * First you get a row-inserted. The path argument points to the destination + * row but this will not yet have had its values set. The source row is still + * present. AFAICT the iter argument points to the same place. + * + * Then you get a row-deleted. The path argument identifies the row that was + * deleted. By this stage the row inserted above has acquired its values. + * + * A complication is that the deletion will move the inserted row. For + * instance, if you do a drag that moves row 1 down to after the track that was + * formerly on row 9, in the row-inserted call it will show up as row 10, but + * in the row-deleted call, row 1 will have been deleted thus making the + * inserted row be row 9. + * + * So when we see the row-inserted we have no idea what track to move. + * Therefore we stash it until we see a row-deleted. + */ + +/** @brief row-inserted callback */ +static void ql_row_inserted(GtkTreeModel attribute((unused)) *treemodel, + GtkTreePath *path, + GtkTreeIter attribute((unused)) *iter, + gpointer user_data) { + struct queuelike *const ql = user_data; + if(!suppress_actions) { +#if 0 + char *ps = gtk_tree_path_to_string(path); + GtkTreeIter piter[1]; + gboolean pi = gtk_tree_model_get_iter(treemodel, piter, path); + struct queue_entry *pq = pi ? ql_iter_to_q(treemodel, piter) : 0; + struct queue_entry *iq = ql_iter_to_q(treemodel, iter); + + fprintf(stderr, "row-inserted %s path=%s pi=%d pq=%p path=%s iq=%p iter=%s\n", + ql->name, + ps, + pi, + pq, + (pi + ? (pq ? pq->track : "(pq=0)") + : "(pi=FALSE)"), + iq, + iq ? iq->track : "(iq=0)"); + + GtkTreeIter j[1]; + gboolean jt = gtk_tree_model_get_iter_first(treemodel, j); + int row = 0; + while(jt) { + struct queue_entry *q = ql_iter_to_q(treemodel, j); + fprintf(stderr, " %2d %s\n", row++, q ? q->track : "(no q)"); + jt = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), j); + } + g_free(ps); +#endif + /* Remember an iterator pointing at the insertion target */ + if(ql->drag_target) + gtk_tree_path_free(ql->drag_target); + ql->drag_target = gtk_tree_path_copy(path); + } +} + +/** @brief row-deleted callback */ +static void ql_row_deleted(GtkTreeModel attribute((unused)) *treemodel, + GtkTreePath *path, + gpointer user_data) { + struct queuelike *const ql = user_data; + + if(!suppress_actions) { +#if 0 + char *ps = gtk_tree_path_to_string(path); + fprintf(stderr, "row-deleted %s path=%s ql->drag_target=%s\n", + ql->name, ps, gtk_tree_path_to_string(ql->drag_target)); + GtkTreeIter j[1]; + gboolean jt = gtk_tree_model_get_iter_first(treemodel, j); + int row = 0; + while(jt) { + struct queue_entry *q = ql_iter_to_q(treemodel, j); + fprintf(stderr, " %2d %s\n", row++, q ? q->track : "(no q)"); + jt = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), j); + } + g_free(ps); +#endif + if(!ql->drag_target) { + error(0, "%s: unsuppressed row-deleted with no row-inserted", + ql->name); + return; + } + + /* Get the source and destination row numbers. */ + int srcrow = gtk_tree_path_get_indices(path)[0]; + int dstrow = gtk_tree_path_get_indices(ql->drag_target)[0]; + //fprintf(stderr, "srcrow=%d dstrow=%d\n", srcrow, dstrow); + + /* Note that the source row is computed AFTER the destination has been + * inserted, since GTK+ does the insert before the delete. Therefore if + * the source row is south (higher row number) of the destination, it will + * be one higher than expected. + * + * For instance if we drag row 1 to before row 0 we will see row-inserted + * for row 0 but then a row-deleted for row 2. + */ + if(srcrow > dstrow) + --srcrow; + + /* Tell the queue implementation */ + ql->drop(srcrow, dstrow); + + /* Dispose of stashed data */ + gtk_tree_path_free(ql->drag_target); + ql->drag_target = 0; + } +} + /** @brief Initialize a @ref queuelike */ GtkWidget *init_queuelike(struct queuelike *ql) { D(("init_queuelike")); @@@ -561,17 -447,6 +561,17 @@@ g_signal_connect(ql->view, "button-press-event", G_CALLBACK(ql_button_release), ql); + /* Drag+drop*/ + if(ql->drop) { + gtk_tree_view_set_reorderable(GTK_TREE_VIEW(ql->view), TRUE); + g_signal_connect(ql->store, + "row-inserted", + G_CALLBACK(ql_row_inserted), ql); + g_signal_connect(ql->store, + "row-deleted", + G_CALLBACK(ql_row_deleted), ql); + } + /* TODO style? */ ql->init(); diff --combined disobedience/queue.c index b6eae61,7779c0f..a051090 --- a/disobedience/queue.c +++ b/disobedience/queue.c @@@ -97,7 -97,7 +97,7 @@@ static void playing_completed(void attr } actual_playing_track = q; queue_playing_changed(); - time(&last_playing); + xtime(&last_playing); } /** @brief Schedule an update to the queue @@@ -152,61 -152,6 +152,61 @@@ static void queue_init(void) g_timeout_add(1000/*ms*/, playing_periodic, 0); } +static void queue_move_completed(void attribute((unused)) *v, + const char *err) { + if(err) { + popup_protocol_error(0, err); + return; + } + /* The log should tell us the queue changed so we do no more here */ +} + +/** @brief Called when drag+drop completes */ +static void queue_drop(int src, int dst) { + struct queue_entry *sq, *dq; + int n; + + //fprintf(stderr, "queue_drop %d -> %d\n", src, dst); + if(playing_track) { + /* If there's a playing track then you can't drag it anywhere */ + if(src == 0) { + //fprintf(stderr, "cannot drag playing track\n"); + queue_playing_changed(); + return; + } + /* If you try to drop before the playing track we assume you missed and + * mean after instead */ + if(!dst) + dst = 1; + //fprintf(stderr, "...adjusted to %d -> %d\n\n", src, dst); + } + /* Find the entry to move */ + for(n = 0, sq = ql_queue.q; n < src; ++n) + sq = sq->next; + /*fprintf(stderr, "source=%s (%s)\n", + sq->id, sq->track);*/ + const int after = dst - 1; + if(after == -1) + dq = 0; + else + /* Find the entry to insert after */ + for(n = 0, dq = ql_queue.q; n < after; ++n) + dq = dq->next; + if(dq == playing_track) + dq = 0; +#if 0 + if(dq) + fprintf(stderr, "after=%s (%s)\n", + dq->id, dq->track); + else + fprintf(stderr, "after=NULL\n"); +#endif + disorder_eclient_moveafter(client, + dq ? dq->id : "", + 1, &sq->id, + queue_move_completed, NULL); +} + /** @brief Columns for the queue */ static const struct queue_column queue_columns[] = { { "When", column_when, 0, COL_RIGHT }, @@@ -233,10 -178,161 +233,10 @@@ struct queuelike ql_queue = .columns = queue_columns, .ncolumns = sizeof queue_columns / sizeof *queue_columns, .menuitems = queue_menuitems, - .nmenuitems = sizeof queue_menuitems / sizeof *queue_menuitems + .nmenuitems = sizeof queue_menuitems / sizeof *queue_menuitems, + .drop = queue_drop }; -/* Drag and drop has to be figured out experimentally, because it is not well - * documented. - * - * First you get a row-inserted. The path argument points to the destination - * row but this will not yet have had its values set. The source row is still - * present. AFAICT the iter argument points to the same place. - * - * Then you get a row-deleted. The path argument identifies the row that was - * deleted. By this stage the row inserted above has acquired its values. - * - * A complication is that the deletion will move the inserted row. For - * instance, if you do a drag that moves row 1 down to after the track that was - * formerly on row 9, in the row-inserted call it will show up as row 10, but - * in the row-deleted call, row 1 will have been deleted thus making the - * inserted row be row 9. - * - * So when we see the row-inserted we have no idea what track to move. - * Therefore we stash it until we see a row-deleted. - */ - -/** @brief Target row for drag */ -static int queue_drag_target = -1; - -static void queue_move_completed(void attribute((unused)) *v, - const char *err) { - if(err) { - popup_protocol_error(0, err); - return; - } - /* The log should tell us the queue changed so we do no more here */ -} - -static void queue_row_deleted(GtkTreeModel *treemodel, - GtkTreePath *path, - gpointer attribute((unused)) user_data) { - if(!suppress_actions) { -#if 0 - char *ps = gtk_tree_path_to_string(path); - fprintf(stderr, "row-deleted path=%s queue_drag_target=%d\n", - ps, queue_drag_target); - GtkTreeIter j[1]; - gboolean jt = gtk_tree_model_get_iter_first(treemodel, j); - int row = 0; - while(jt) { - struct queue_entry *q = ql_iter_to_q(treemodel, j); - fprintf(stderr, " %2d %s\n", row++, q ? q->track : "(no q)"); - jt = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql_queue.store), j); - } - g_free(ps); -#endif - if(queue_drag_target < 0) { - error(0, "unsuppressed row-deleted with no row-inserted"); - return; - } - int drag_source = gtk_tree_path_get_indices(path)[0]; - - /* If the drag is downwards (=towards higher row numbers) then the target - * will have been moved upwards (=towards lower row numbers) by one row. */ - if(drag_source < queue_drag_target) - --queue_drag_target; - - /* Find the track to move */ - GtkTreeIter src[1]; - gboolean srcv = gtk_tree_model_iter_nth_child(treemodel, src, NULL, - queue_drag_target); - if(!srcv) { - error(0, "cannot get iterator to drag target %d", queue_drag_target); - queue_playing_changed(); - queue_drag_target = -1; - return; - } - struct queue_entry *srcq = ql_iter_to_q(treemodel, src); - assert(srcq); - //fprintf(stderr, "move %s %s\n", srcq->id, srcq->track); - - /* Don't allow the currently playing track to be moved. As above, we put - * the queue back into the right order straight away. */ - if(srcq == playing_track) { - //fprintf(stderr, "cannot move currently playing track\n"); - queue_playing_changed(); - queue_drag_target = -1; - return; - } - - /* Find the destination */ - struct queue_entry *dstq; - if(queue_drag_target) { - GtkTreeIter dst[1]; - gboolean dstv = gtk_tree_model_iter_nth_child(treemodel, dst, NULL, - queue_drag_target - 1); - if(!dstv) { - error(0, "cannot get iterator to drag target predecessor %d", - queue_drag_target - 1); - queue_playing_changed(); - queue_drag_target = -1; - return; - } - dstq = ql_iter_to_q(treemodel, dst); - assert(dstq); - if(dstq == playing_track) - dstq = 0; - } else - dstq = 0; - /* NB if the user attempts to move a queued track before the currently - * playing track we assume they just missed a bit, and put it after. */ - //fprintf(stderr, " target %s %s\n", dstq ? dstq->id : "(none)", dstq ? dstq->track : "(none)"); - /* Now we know what is to be moved. We need to know the preceding queue - * entry so we can move it. */ - disorder_eclient_moveafter(client, - dstq ? dstq->id : "", - 1, &srcq->id, - queue_move_completed, NULL); - queue_drag_target = -1; - } -} - -static void queue_row_inserted(GtkTreeModel attribute((unused)) *treemodel, - GtkTreePath *path, - GtkTreeIter attribute((unused)) *iter, - gpointer attribute((unused)) user_data) { - if(!suppress_actions) { -#if 0 - char *ps = gtk_tree_path_to_string(path); - GtkTreeIter piter[1]; - gboolean pi = gtk_tree_model_get_iter(treemodel, piter, path); - struct queue_entry *pq = pi ? ql_iter_to_q(treemodel, piter) : 0; - struct queue_entry *iq = ql_iter_to_q(treemodel, iter); - - fprintf(stderr, "row-inserted path=%s pi=%d pq=%p path=%s iq=%p iter=%s\n", - ps, - pi, - pq, - (pi - ? (pq ? pq->track : "(pq=0)") - : "(pi=FALSE)"), - iq, - iq ? iq->track : "(iq=0)"); - - GtkTreeIter j[1]; - gboolean jt = gtk_tree_model_get_iter_first(treemodel, j); - int row = 0; - while(jt) { - struct queue_entry *q = ql_iter_to_q(treemodel, j); - fprintf(stderr, " %2d %s\n", row++, q ? q->track : "(no q)"); - jt = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql_queue.store), j); - } - g_free(ps); -#endif - queue_drag_target = gtk_tree_path_get_indices(path)[0]; - } -} - /** @brief Called when a key is pressed in the queue tree view */ static gboolean queue_key_press(GtkWidget attribute((unused)) *widget, GdkEventKey *event, @@@ -257,6 -353,14 +257,6 @@@ GtkWidget *queue_widget(void) { GtkWidget *const w = init_queuelike(&ql_queue); - /* Enable drag+drop */ - gtk_tree_view_set_reorderable(GTK_TREE_VIEW(ql_queue.view), TRUE); - g_signal_connect(ql_queue.store, - "row-inserted", - G_CALLBACK(queue_row_inserted), &ql_queue); - g_signal_connect(ql_queue.store, - "row-deleted", - G_CALLBACK(queue_row_deleted), &ql_queue); /* Catch keypresses */ g_signal_connect(ql_queue.view, "key-press-event", G_CALLBACK(queue_key_press), &ql_queue); diff --combined lib/eclient.c index 676aa06,101a7bf..fad9e1b --- a/lib/eclient.c +++ b/lib/eclient.c @@@ -92,7 -92,6 +92,7 @@@ typedef void operation_callback(disorde struct operation { struct operation *next; /**< @brief next operation */ char *cmd; /**< @brief command to send or 0 */ + char **body; /**< @brief command body */ operation_callback *opcallback; /**< @brief internal completion callback */ void (*completed)(); /**< @brief user completion callback or 0 */ void *v; /**< @brief data for COMPLETED */ @@@ -166,8 -165,6 +166,8 @@@ static void stash_command(disorder_ecli operation_callback *opcallback, void (*completed)(), void *v, + int nbody, + char **body, const char *cmd, ...); static void log_opcallback(disorder_eclient *c, struct operation *op); @@@ -190,9 -187,6 +190,9 @@@ static void logentry_user_delete(disord static void logentry_user_edit(disorder_eclient *c, int nvec, char **vec); static void logentry_rights_changed(disorder_eclient *c, int nvec, char **vec); static void logentry_adopted(disorder_eclient *c, int nvec, char **vec); +static void logentry_playlist_created(disorder_eclient *c, int nvec, char **vec); +static void logentry_playlist_deleted(disorder_eclient *c, int nvec, char **vec); +static void logentry_playlist_modified(disorder_eclient *c, int nvec, char **vec); /* Tables ********************************************************************/ @@@ -214,9 -208,6 +214,9 @@@ static const struct logentry_handler lo LE(failed, 2, 2), LE(moved, 1, 1), LE(playing, 1, 2), + LE(playlist_created, 2, 2), + LE(playlist_deleted, 1, 1), + LE(playlist_modified, 2, 2), LE(queue, 2, INT_MAX), LE(recent_added, 2, INT_MAX), LE(recent_removed, 1, 1), @@@ -335,24 -326,6 +335,24 @@@ static int protocol_error(disorder_ecli /* State machine *************************************************************/ +/** @brief Send an operation (into the output buffer) + * @param op Operation to send + */ +static void op_send(struct operation *op) { + disorder_eclient *const c = op->client; + put(c, op->cmd, strlen(op->cmd)); + if(op->body) { + for(int n = 0; op->body[n]; ++n) { + if(op->body[n][0] == '.') + put(c, ".", 1); + put(c, op->body[n], strlen(op->body[n])); + put(c, "\n", 1); + } + put(c, ".\n", 2); + } + op->sent = 1; +} + /** @brief Called when there's something to do * @param c Client * @param mode bitmap of @ref DISORDER_POLL_READ and/or @ref DISORDER_POLL_WRITE. @@@ -406,7 -379,7 +406,7 @@@ void disorder_eclient_polled(disorder_e D(("state_connected")); /* We just connected. Initiate the authentication protocol. */ stash_command(c, 1/*queuejump*/, authbanner_opcallback, - 0/*completed*/, 0/*v*/, 0/*cmd*/); + 0/*completed*/, 0/*v*/, -1/*nbody*/, 0/*body*/, 0/*cmd*/); /* We never stay is state_connected very long. We could in principle jump * straight to state_cmdresponse since there's actually no command to * send, but that would arguably be cheating. */ @@@ -422,13 -395,17 +422,13 @@@ if(c->authenticated) { /* Transmit all unsent operations */ for(op = c->ops; op; op = op->next) { - if(!op->sent) { - put(c, op->cmd, strlen(op->cmd)); - op->sent = 1; - } + if(!op->sent) + op_send(op); } } else { /* Just send the head operation */ - if(c->ops->cmd && !c->ops->sent) { - put(c, c->ops->cmd, strlen(c->ops->cmd)); - c->ops->sent = 1; - } + if(c->ops->cmd && !c->ops->sent) + op_send(c->ops); } /* Awaiting response for the operation at the head of the list */ c->state = state_cmdresponse; @@@ -440,7 -417,7 +440,7 @@@ /* Queue up a byte to send */ if(c->state == state_log && c->output.nvec == 0 - && time(&now) - c->last_prod > LOG_PROD_INTERVAL) { + && xtime(&now) - c->last_prod > LOG_PROD_INTERVAL) { put(c, "x", 1); c->last_prod = now; } @@@ -624,7 -601,6 +624,7 @@@ static void authbanner_opcallback(disor return; } stash_command(c, 1/*queuejump*/, authuser_opcallback, 0/*completed*/, 0/*v*/, + -1/*nbody*/, 0/*body*/, "user", quoteutf8(config->username), quoteutf8(res), (char *)0); } @@@ -649,7 -625,6 +649,7 @@@ static void authuser_opcallback(disorde if(c->log_callbacks && !(c->ops && c->ops->opcallback == log_opcallback)) /* We are a log client, switch to logging mode */ stash_command(c, 0/*queuejump*/, log_opcallback, 0/*completed*/, c->log_v, + -1/*nbody*/, 0/*body*/, "log", (char *)0); } @@@ -812,8 -787,6 +812,8 @@@ static void stash_command_vector(disord operation_callback *opcallback, void (*completed)(), void *v, + int nbody, + char **body, int ncmd, char **cmd) { struct operation *op = xmalloc(sizeof *op); @@@ -832,13 -805,6 +832,13 @@@ op->cmd = d.vec; } else op->cmd = 0; /* usually, awaiting challenge */ + if(nbody >= 0) { + op->body = xcalloc(nbody + 1, sizeof (char *)); + for(n = 0; n < nbody; ++n) + op->body[n] = xstrdup(body[n]); + op->body[n] = 0; + } else + op->body = NULL; op->opcallback = opcallback; op->completed = completed; op->v = v; @@@ -864,8 -830,6 +864,8 @@@ static void vstash_command(disorder_ecl operation_callback *opcallback, void (*completed)(), void *v, + int nbody, + char **body, const char *cmd, va_list ap) { char *arg; struct vector vec; @@@ -877,11 -841,9 +877,11 @@@ while((arg = va_arg(ap, char *))) vector_append(&vec, arg); stash_command_vector(c, queuejump, opcallback, completed, v, - vec.nvec, vec.vec); + nbody, body, vec.nvec, vec.vec); } else - stash_command_vector(c, queuejump, opcallback, completed, v, 0, 0); + stash_command_vector(c, queuejump, opcallback, completed, v, + nbody, body, + 0, 0); } static void stash_command(disorder_eclient *c, @@@ -889,14 -851,12 +889,14 @@@ operation_callback *opcallback, void (*completed)(), void *v, + int nbody, + char **body, const char *cmd, ...) { va_list ap; va_start(ap, cmd); - vstash_command(c, queuejump, opcallback, completed, v, cmd, ap); + vstash_command(c, queuejump, opcallback, completed, v, nbody, body, cmd, ap); va_end(ap); } @@@ -1048,8 -1008,6 +1048,8 @@@ static void list_response_opcallback(di D(("list_response_callback")); if(c->rc / 100 == 2) completed(op->v, NULL, c->vec.nvec, c->vec.vec); + else if(c->rc == 555) + completed(op->v, NULL, -1, NULL); else completed(op->v, errorstring(c), 0, 0); } @@@ -1081,24 -1039,7 +1081,24 @@@ static int simple(disorder_eclient *c va_list ap; va_start(ap, cmd); - vstash_command(c, 0/*queuejump*/, opcallback, completed, v, cmd, ap); + vstash_command(c, 0/*queuejump*/, opcallback, completed, v, -1, 0, cmd, ap); + va_end(ap); + /* Give the state machine a kick, since we might be in state_idle */ + disorder_eclient_polled(c, 0); + return 0; +} + +static int simple_body(disorder_eclient *c, + operation_callback *opcallback, + void (*completed)(), + void *v, + int nbody, + char **body, + const char *cmd, ...) { + va_list ap; + + va_start(ap, cmd); + vstash_command(c, 0/*queuejump*/, opcallback, completed, v, nbody, body, cmd, ap); va_end(ap); /* Give the state machine a kick, since we might be in state_idle */ disorder_eclient_polled(c, 0); @@@ -1183,7 -1124,7 +1183,7 @@@ int disorder_eclient_moveafter(disorder for(n = 0; n < nids; ++n) vector_append(&vec, (char *)ids[n]); stash_command_vector(c, 0/*queuejump*/, no_response_opcallback, completed, v, - vec.nvec, vec.vec); + -1, 0, vec.nvec, vec.vec); disorder_eclient_polled(c, 0); return 0; } @@@ -1479,123 -1420,6 +1479,123 @@@ int disorder_eclient_adopt(disorder_ecl "adopt", id, (char *)0); } +/** @brief Get the list of playlists + * @param c Client + * @param completed Called with list of playlists + * @param v Passed to @p completed + * + * The playlist list is not sorted in any particular order. + */ +int disorder_eclient_playlists(disorder_eclient *c, + disorder_eclient_list_response *completed, + void *v) { + return simple(c, list_response_opcallback, (void (*)())completed, v, + "playlists", (char *)0); +} + +/** @brief Delete a playlist + * @param c Client + * @param completed Called on completion + * @param playlist Playlist to delete + * @param v Passed to @p completed + */ +int disorder_eclient_playlist_delete(disorder_eclient *c, + disorder_eclient_no_response *completed, + const char *playlist, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "playlist-delete", playlist, (char *)0); +} + +/** @brief Lock a playlist + * @param c Client + * @param completed Called on completion + * @param playlist Playlist to lock + * @param v Passed to @p completed + */ +int disorder_eclient_playlist_lock(disorder_eclient *c, + disorder_eclient_no_response *completed, + const char *playlist, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "playlist-lock", playlist, (char *)0); +} + +/** @brief Unlock the locked a playlist + * @param c Client + * @param completed Called on completion + * @param v Passed to @p completed + */ +int disorder_eclient_playlist_unlock(disorder_eclient *c, + disorder_eclient_no_response *completed, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "playlist-unlock", (char *)0); +} + +/** @brief Set a playlist's sharing + * @param c Client + * @param completed Called on completion + * @param playlist Playlist to modify + * @param sharing @c "public" or @c "private" + * @param v Passed to @p completed + */ +int disorder_eclient_playlist_set_share(disorder_eclient *c, + disorder_eclient_no_response *completed, + const char *playlist, + const char *sharing, + void *v) { + return simple(c, no_response_opcallback, (void (*)())completed, v, + "playlist-set-share", playlist, sharing, (char *)0); +} + +/** @brief Get a playlist's sharing + * @param c Client + * @param completed Called with sharing status + * @param playlist Playlist to inspect + * @param v Passed to @p completed + */ +int disorder_eclient_playlist_get_share(disorder_eclient *c, + disorder_eclient_string_response *completed, + const char *playlist, + void *v) { + return simple(c, string_response_opcallback, (void (*)())completed, v, + "playlist-get-share", playlist, (char *)0); +} + +/** @brief Set a playlist + * @param c Client + * @param completed Called on completion + * @param playlist Playlist to modify + * @param tracks List of tracks + * @param ntracks Number of tracks + * @param v Passed to @p completed + */ +int disorder_eclient_playlist_set(disorder_eclient *c, + disorder_eclient_no_response *completed, + const char *playlist, + char **tracks, + int ntracks, + void *v) { + return simple_body(c, no_response_opcallback, (void (*)())completed, v, + ntracks, tracks, + "playlist-set", playlist, (char *)0); +} + +/** @brief Get a playlist's contents + * @param c Client + * @param completed Called with playlist contents + * @param playlist Playlist to inspect + * @param v Passed to @p completed + */ +int disorder_eclient_playlist_get(disorder_eclient *c, + disorder_eclient_list_response *completed, + const char *playlist, + void *v) { + return simple(c, list_response_opcallback, (void (*)())completed, v, + "playlist-get", playlist, (char *)0); +} + /* Log clients ***************************************************************/ /** @brief Monitor the server log @@@ -1620,7 -1444,7 +1620,7 @@@ int disorder_eclient_log(disorder_eclie if(c->log_callbacks->state) c->log_callbacks->state(c->log_v, c->statebits); stash_command(c, 0/*queuejump*/, log_opcallback, 0/*completed*/, v, - "log", (char *)0); + -1, 0, "log", (char *)0); disorder_eclient_polled(c, 0); return 0; } @@@ -1788,27 -1612,6 +1788,27 @@@ static void logentry_rights_changed(dis } } +static void logentry_playlist_created(disorder_eclient *c, + int attribute((unused)) nvec, + char **vec) { + if(c->log_callbacks->playlist_created) + c->log_callbacks->playlist_created(c->log_v, vec[0], vec[1]); +} + +static void logentry_playlist_deleted(disorder_eclient *c, + int attribute((unused)) nvec, + char **vec) { + if(c->log_callbacks->playlist_deleted) + c->log_callbacks->playlist_deleted(c->log_v, vec[0]); +} + +static void logentry_playlist_modified(disorder_eclient *c, + int attribute((unused)) nvec, + char **vec) { + if(c->log_callbacks->playlist_modified) + c->log_callbacks->playlist_modified(c->log_v, vec[0], vec[1]); +} + static const struct { unsigned long bit; const char *enable; diff --combined lib/trackdb.c index 5536b73,6984b21..d724472 --- a/lib/trackdb.c +++ b/lib/trackdb.c @@@ -157,13 -157,6 +157,13 @@@ DB *trackdb_scheduledb */ DB *trackdb_usersdb; +/** @brief The playlists database + * - Keys are playlist names + * - Values are encoded key-value pairs + * - Data is user data and cannot be reconstructed + */ +DB *trackdb_playlistsdb; + static pid_t db_deadlock_pid = -1; /* deadlock manager PID */ static pid_t rescan_pid = -1; /* rescanner PID */ static int initialized, opened; /* state */ @@@ -479,7 -472,6 +479,7 @@@ void trackdb_open(int flags) trackdb_noticeddb = open_db("noticed.db", DB_DUPSORT, DB_BTREE, dbflags, 0666); trackdb_scheduledb = open_db("schedule.db", 0, DB_HASH, dbflags, 0666); + trackdb_playlistsdb = open_db("playlists.db", 0, DB_HASH, dbflags, 0666); if(!trackdb_existing_database && !(flags & TRACKDB_READ_ONLY)) { /* Stash the database version */ char buf[32]; @@@ -511,7 -503,6 +511,7 @@@ void trackdb_close(void) CLOSE("noticed.db", trackdb_noticeddb); CLOSE("schedule.db", trackdb_scheduledb); CLOSE("users.db", trackdb_usersdb); + CLOSE("playlists.db", trackdb_playlistsdb); D(("closed databases")); } @@@ -1051,7 -1042,7 +1051,7 @@@ int trackdb_notice_tid(const char *trac /* this is a real track */ t_changed += kvp_set(&t, "_alias_for", 0); t_changed += kvp_set(&t, "_path", path); - time(&now); + xtime(&now); if(ret == DB_NOTFOUND) { /* It's a new track; record the time */ byte_xasprintf(¬iced, "%lld", (long long)now); @@@ -2562,10 -2553,8 +2562,10 @@@ static int trusted(const char *user) * Currently we only allow the letters and digits in ASCII. We could be more * liberal than this but it is a nice simple test. It is critical that * semicolons are never allowed. + * + * NB also used by playlist_parse_name() to validate playlist names! */ -static int valid_username(const char *user) { +int valid_username(const char *user) { if(!*user) return 0; while(*user) { @@@ -2609,7 -2598,7 +2609,7 @@@ static int create_user(const char *user kvp_set(&k, "email", email); if(confirmation) kvp_set(&k, "confirmation", confirmation); - snprintf(s, sizeof s, "%jd", (intmax_t)time(0)); + snprintf(s, sizeof s, "%jd", (intmax_t)xtime(0)); kvp_set(&k, "created", s); return trackdb_putdata(trackdb_usersdb, user, k, tid, flags); } diff --combined server/server.c index ce45a1d,feaac7e..781c71a --- a/server/server.c +++ b/server/server.c @@@ -44,34 -44,6 +44,34 @@@ struct listener int pf; }; +struct conn; + +/** @brief Signature for line reader callback + * @param c Connection + * @param line Line + * @return 0 if incomplete, 1 if complete + * + * @p line is 0-terminated and excludes the newline. It points into the + * input buffer so will become invalid shortly. + */ +typedef int line_reader_type(struct conn *c, + char *line); + +/** @brief Signature for with-body command callbacks + * @param c Connection + * @param body List of body lines + * @param nbody Number of body lines + * @param u As passed to fetch_body() + * @return 0 to suspend input, 1 if complete + * + * The body strings are allocated (so survive indefinitely) and don't include + * newlines. + */ +typedef int body_callback_type(struct conn *c, + char **body, + int nbody, + void *u); + /** @brief One client connection */ struct conn { /** @brief Read commands from here */ @@@ -105,18 -77,6 +105,18 @@@ struct conn *next; /** @brief True if pending rescan had 'wait' set */ int rescan_wait; + /** @brief Playlist that this connection locks */ + const char *locked_playlist; + /** @brief When that playlist was locked */ + time_t locked_when; + /** @brief Line reader function */ + line_reader_type *line_reader; + /** @brief Called when command body has been read */ + body_callback_type *body_callback; + /** @brief Passed to @c body_callback */ + void *body_u; + /** @brief Accumulating body */ + struct vector body[1]; }; /** @brief Linked list of connections */ @@@ -128,15 -88,6 +128,15 @@@ static int reader_callback(ev_source *e size_t bytes, int eof, void *u); +static int c_playlist_set_body(struct conn *c, + char **body, + int nbody, + void *u); +static int fetch_body(struct conn *c, + body_callback_type body_callback, + void *u); +static int body_line(struct conn *c, char *line); +static int command(struct conn *c, char *line); static const char *noyes[] = { "no", "yes" }; @@@ -590,13 -541,13 +590,13 @@@ static int c_queue(struct conn *c queue_fix_sofar(playing); if((l = trackdb_get(playing->track, "_length")) && (length = atol(l))) { - time(&when); + xtime(&when); when += length - playing->sofar + config->gap; } } else /* Nothing is playing but playing is enabled, so whatever is * first in the queue can be expected to start immediately. */ - time(&when); + xtime(&when); } for(q = qhead.next; q != &qhead; q = q->next) { /* fill in estimated start time */ @@@ -943,7 -894,7 +943,7 @@@ static void logclient(const char *msg, return; } sink_printf(ev_writer_sink(c->w), "%"PRIxMAX" %s\n", - (uintmax_t)time(0), msg); + (uintmax_t)xtime(0), msg); } static int c_log(struct conn *c, @@@ -953,7 -904,7 +953,7 @@@ sink_writes(ev_writer_sink(c->w), "254 OK\n"); /* pump out initial state */ - time(&now); + xtime(&now); sink_printf(ev_writer_sink(c->w), "%"PRIxMAX" state %s\n", (uintmax_t)now, playing_is_enabled() ? "enable_play" : "disable_play"); @@@ -1079,25 -1030,21 +1079,25 @@@ static int c_resolve(struct conn *c return 1; } -static int c_tags(struct conn *c, - char attribute((unused)) **vec, - int attribute((unused)) nvec) { - char **tags = trackdb_alltags(); - - sink_printf(ev_writer_sink(c->w), "253 Tag list follows\n"); - while(*tags) { +static int list_response(struct conn *c, + const char *reply, + char **list) { + sink_printf(ev_writer_sink(c->w), "253 %s\n", reply); + while(*list) { sink_printf(ev_writer_sink(c->w), "%s%s\n", - **tags == '.' ? "." : "", *tags); - ++tags; + **list == '.' ? "." : "", *list); + ++list; } sink_writes(ev_writer_sink(c->w), ".\n"); return 1; /* completed */ } +static int c_tags(struct conn *c, + char attribute((unused)) **vec, + int attribute((unused)) nvec) { + return list_response(c, "Tag list follows", trackdb_alltags()); +} + static int c_set_global(struct conn *c, char **vec, int attribute((unused)) nvec) { @@@ -1315,7 -1262,7 +1315,7 @@@ static int c_edituser(struct conn *c if(d->lo) sink_printf(ev_writer_sink(d->w), "%"PRIxMAX" rights_changed %s\n", - (uintmax_t)time(0), + (uintmax_t)xtime(0), quoteutf8(new_rights)); } } @@@ -1367,7 -1314,17 +1367,7 @@@ static int c_userinfo(struct conn *c static int c_users(struct conn *c, char attribute((unused)) **vec, int attribute((unused)) nvec) { - /* TODO de-dupe with c_tags */ - char **users = trackdb_listusers(); - - sink_writes(ev_writer_sink(c->w), "253 User list follows\n"); - while(*users) { - sink_printf(ev_writer_sink(c->w), "%s%s\n", - **users == '.' ? "." : "", *users); - ++users; - } - sink_writes(ev_writer_sink(c->w), ".\n"); - return 1; /* completed */ + return list_response(c, "User list follows", trackdb_listusers()); } static int c_register(struct conn *c, @@@ -1486,7 -1443,7 +1486,7 @@@ static int c_reminder(struct conn *c if(!last_reminder) last_reminder = hash_new(sizeof (time_t)); last = hash_find(last_reminder, vec[0]); - time(&now); + xtime(&now); if(last && now < *last + config->reminder_interval) { error(0, "sent a password reminder to '%s' too recently", vec[0]); sink_writes(ev_writer_sink(c->w), "550 Cannot send a reminder email\n"); @@@ -1642,152 -1599,6 +1642,152 @@@ static int c_adopt(struct conn *c return 1; } +static int playlist_response(struct conn *c, + int err) { + switch(err) { + case 0: + assert(!"cannot cope with success"); + case EACCES: + sink_writes(ev_writer_sink(c->w), "550 Access denied\n"); + break; + case EINVAL: + sink_writes(ev_writer_sink(c->w), "550 Invalid playlist name\n"); + break; + case ENOENT: + sink_writes(ev_writer_sink(c->w), "555 No such playlist\n"); + break; + default: + sink_writes(ev_writer_sink(c->w), "550 Error accessing playlist\n"); + break; + } + return 1; +} + +static int c_playlist_get(struct conn *c, + char **vec, + int attribute((unused)) nvec) { + char **tracks; + int err; + + if(!(err = trackdb_playlist_get(vec[0], c->who, &tracks, 0, 0))) + return list_response(c, "Playlist contents follows", tracks); + else + return playlist_response(c, err); +} + +static int c_playlist_set(struct conn *c, + char **vec, + int attribute((unused)) nvec) { + return fetch_body(c, c_playlist_set_body, vec[0]); +} + +static int c_playlist_set_body(struct conn *c, + char **body, + int nbody, + void *u) { + const char *playlist = u; + int err; + + if(!c->locked_playlist + || strcmp(playlist, c->locked_playlist)) { + sink_writes(ev_writer_sink(c->w), "550 Playlist is not locked\n"); + return 1; + } + if(!(err = trackdb_playlist_set(playlist, c->who, + body, nbody, 0))) { + sink_printf(ev_writer_sink(c->w), "250 OK\n"); + return 1; + } else + return playlist_response(c, err); +} + +static int c_playlist_get_share(struct conn *c, + char **vec, + int attribute((unused)) nvec) { + char *share; + int err; + + if(!(err = trackdb_playlist_get(vec[0], c->who, 0, 0, &share))) { + sink_printf(ev_writer_sink(c->w), "252 %s\n", quoteutf8(share)); + return 1; + } else + return playlist_response(c, err); +} + +static int c_playlist_set_share(struct conn *c, + char **vec, + int attribute((unused)) nvec) { + int err; + + if(!(err = trackdb_playlist_set(vec[0], c->who, 0, 0, vec[1]))) { + sink_printf(ev_writer_sink(c->w), "250 OK\n"); + return 1; + } else + return playlist_response(c, err); +} + +static int c_playlists(struct conn *c, + char attribute((unused)) **vec, + int attribute((unused)) nvec) { + char **p; + + trackdb_playlist_list(c->who, &p, 0); + return list_response(c, "List of playlists follows", p); +} + +static int c_playlist_delete(struct conn *c, + char **vec, + int attribute((unused)) nvec) { + int err; + + if(!(err = trackdb_playlist_delete(vec[0], c->who))) { + sink_writes(ev_writer_sink(c->w), "250 OK\n"); + return 1; + } else + return playlist_response(c, err); +} + +static int c_playlist_lock(struct conn *c, + char **vec, + int attribute((unused)) nvec) { + int err; + struct conn *cc; + + /* Check we're allowed to modify this playlist */ + if((err = trackdb_playlist_set(vec[0], c->who, 0, 0, 0))) + return playlist_response(c, err); + /* If we hold a lock don't allow a new one */ + if(c->locked_playlist) { + sink_writes(ev_writer_sink(c->w), "550 Already holding a lock\n"); + return 1; + } + /* See if some other connection locks the same playlist */ + for(cc = connections; cc; cc = cc->next) + if(cc->locked_playlist && !strcmp(cc->locked_playlist, vec[0])) + break; + if(cc) { + /* TODO: implement config->playlist_lock_timeout */ + sink_writes(ev_writer_sink(c->w), "550 Already locked\n"); + return 1; + } + c->locked_playlist = xstrdup(vec[0]); + time(&c->locked_when); + sink_writes(ev_writer_sink(c->w), "250 Acquired lock\n"); + return 1; +} + +static int c_playlist_unlock(struct conn *c, + char attribute((unused)) **vec, + int attribute((unused)) nvec) { + if(!c->locked_playlist) { + sink_writes(ev_writer_sink(c->w), "550 Not holding a lock\n"); + return 1; + } + c->locked_playlist = 0; + sink_writes(ev_writer_sink(c->w), "250 Released lock\n"); + return 1; +} + static const struct command { /** @brief Command name */ const char *name; @@@ -1834,14 -1645,6 +1834,14 @@@ { "pause", 0, 0, c_pause, RIGHT_PAUSE }, { "play", 1, 1, c_play, RIGHT_PLAY }, { "playing", 0, 0, c_playing, RIGHT_READ }, + { "playlist-delete", 1, 1, c_playlist_delete, RIGHT_PLAY }, + { "playlist-get", 1, 1, c_playlist_get, RIGHT_READ }, + { "playlist-get-share", 1, 1, c_playlist_get_share, RIGHT_READ }, + { "playlist-lock", 1, 1, c_playlist_lock, RIGHT_PLAY }, + { "playlist-set", 1, 1, c_playlist_set, RIGHT_PLAY }, + { "playlist-set-share", 2, 2, c_playlist_set_share, RIGHT_PLAY }, + { "playlist-unlock", 0, 0, c_playlist_unlock, RIGHT_PLAY }, + { "playlists", 0, 0, c_playlists, RIGHT_READ }, { "prefs", 1, 1, c_prefs, RIGHT_READ }, { "queue", 0, 0, c_queue, RIGHT_READ }, { "random-disable", 0, 0, c_random_disable, RIGHT_GLOBAL_PREFS }, @@@ -1877,58 -1680,13 +1877,58 @@@ { "volume", 0, 2, c_volume, RIGHT_READ|RIGHT_VOLUME } }; +/** @brief Fetch a command body + * @param c Connection + * @param body_callback Called with body + * @param u Passed to body_callback + * @return 1 + */ +static int fetch_body(struct conn *c, + body_callback_type body_callback, + void *u) { + assert(c->line_reader == command); + c->line_reader = body_line; + c->body_callback = body_callback; + c->body_u = u; + vector_init(c->body); + return 1; +} + +/** @brief @ref line_reader_type callback for command body lines + * @param c Connection + * @param line Line + * @return 1 if complete, 0 if incomplete + * + * Called from reader_callback(). + */ +static int body_line(struct conn *c, + char *line) { + if(*line == '.') { + ++line; + if(!*line) { + /* That's the lot */ + c->line_reader = command; + vector_terminate(c->body); + return c->body_callback(c, c->body->vec, c->body->nvec, c->body_u); + } + } + vector_append(c->body, xstrdup(line)); + return 1; /* completed */ +} + static void command_error(const char *msg, void *u) { struct conn *c = u; sink_printf(ev_writer_sink(c->w), "500 parse error: %s\n", msg); } -/* process a command. Return 1 if complete, 0 if incomplete. */ +/** @brief @ref line_reader_type callback for commands + * @param c Connection + * @param line Line + * @return 1 if complete, 0 if incomplete + * + * Called from reader_callback(). + */ static int command(struct conn *c, char *line) { char **vec; int nvec, n; @@@ -1999,7 -1757,7 +1999,7 @@@ static int reader_callback(ev_source at while((eol = memchr(ptr, '\n', bytes))) { *eol++ = 0; ev_reader_consume(reader, eol - (char *)ptr); - complete = command(c, ptr); + complete = c->line_reader(c, ptr); /* usually command() */ bytes -= (eol - (char *)ptr); ptr = eol; if(!complete) { @@@ -2062,7 -1820,6 +2062,7 @@@ static int listen_callback(ev_source *e c->reader = reader_callback; c->l = l; c->rights = 0; + c->line_reader = command; connections = c; gcry_randomize(c->nonce, sizeof c->nonce, GCRY_STRONG_RANDOM); sink_printf(ev_writer_sink(c->w), "231 %d %s %s\n",