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);
/** @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) {
--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"));
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();
}
actual_playing_track = q;
queue_playing_changed();
- time(&last_playing);
+ xtime(&last_playing);
}
/** @brief Schedule an update to the queue
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 },
.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,
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);
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 */
operation_callback *opcallback,
void (*completed)(),
void *v,
+ int nbody,
+ char **body,
const char *cmd,
...);
static void log_opcallback(disorder_eclient *c, struct operation *op);
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 ********************************************************************/
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),
/* 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.
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. */
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;
/* 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;
}
return;
}
stash_command(c, 1/*queuejump*/, authuser_opcallback, 0/*completed*/, 0/*v*/,
+ -1/*nbody*/, 0/*body*/,
"user", quoteutf8(config->username), quoteutf8(res),
(char *)0);
}
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);
}
operation_callback *opcallback,
void (*completed)(),
void *v,
+ int nbody,
+ char **body,
int ncmd,
char **cmd) {
struct operation *op = xmalloc(sizeof *op);
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;
operation_callback *opcallback,
void (*completed)(),
void *v,
+ int nbody,
+ char **body,
const char *cmd, va_list ap) {
char *arg;
struct vector vec;
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,
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);
}
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);
}
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);
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;
}
"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
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;
}
}
}
+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;
*/
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 */
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];
CLOSE("noticed.db", trackdb_noticeddb);
CLOSE("schedule.db", trackdb_scheduledb);
CLOSE("users.db", trackdb_usersdb);
+ CLOSE("playlists.db", trackdb_playlistsdb);
D(("closed databases"));
}
/* 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);
* 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) {
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);
}
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 */
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 */
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" };
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 */
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,
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");
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) {
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));
}
}
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,
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");
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;
{ "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 },
{ "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;
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) {
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",