chiark / gitweb /
Add new 'playafter' command to protocol, eclient and python.
[disorder] / lib / trackdb-playlists.c
CommitLineData
2563dc1f
RK
1/*
2 * This file is part of DisOrder
3 * Copyright (C) 2008 Richard Kettlewell
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
18 * USA
19 */
20/** @file lib/trackdb-playlists.c
21 * @brief Track database playlist support
22 *
23 * This file implements reading and modification of playlists, including access
24 * control, but not locking or event logging (at least yet).
25 */
26#include "common.h"
27
28#include <errno.h>
29
30#include "trackdb-int.h"
31#include "mem.h"
32#include "log.h"
33#include "configuration.h"
34#include "vector.h"
5c102112 35#include "eventlog.h"
2563dc1f
RK
36
37static int trackdb_playlist_get_tid(const char *name,
38 const char *who,
39 char ***tracksp,
40 int *ntracksp,
41 char **sharep,
42 DB_TXN *tid);
43static int trackdb_playlist_set_tid(const char *name,
44 const char *who,
45 char **tracks,
46 int ntracks,
47 const char *share,
48 DB_TXN *tid);
49static int trackdb_playlist_list_tid(const char *who,
50 char ***playlistsp,
51 int *nplaylistsp,
52 DB_TXN *tid);
53static int trackdb_playlist_delete_tid(const char *name,
54 const char *who,
55 DB_TXN *tid);
56
57/** @brief Parse a playlist name
58 * @param name Playlist name
59 * @param ownerp Where to put owner, or NULL
60 * @param sharep Where to put default sharing, or NULL
61 * @return 0 on success, -1 on error
62 *
63 * Playlists take the form USER.PLAYLIST or just PLAYLIST. The PLAYLIST part
64 * is alphanumeric and nonempty. USER is a username (see valid_username()).
65 */
66int playlist_parse_name(const char *name,
67 char **ownerp,
68 char **sharep) {
69 const char *dot = strchr(name, '.'), *share;
70 char *owner;
71
72 if(dot) {
73 /* Owned playlist */
74 owner = xstrndup(name, dot - name);
75 if(!valid_username(owner))
76 return -1;
77 if(!valid_username(dot + 1))
78 return -1;
79 share = "private";
80 } else {
81 /* Shared playlist */
82 if(!valid_username(name))
83 return -1;
84 owner = 0;
97b379d2 85 share = "shared";
2563dc1f
RK
86 }
87 if(ownerp)
88 *ownerp = owner;
89 if(sharep)
90 *sharep = xstrdup(share);
91 return 0;
92}
93
94/** @brief Check read access rights
95 * @param name Playlist name
96 * @param who Who wants to read
97 * @param share Playlist share status
98 */
99static int playlist_may_read(const char *name,
100 const char *who,
101 const char *share) {
102 char *owner;
103
440af55d 104 if(playlist_parse_name(name, &owner, 0))
2563dc1f
RK
105 return 0;
106 /* Anyone can read shared playlists */
107 if(!owner)
108 return 1;
109 /* You can always read playlists you own */
110 if(!strcmp(owner, who))
111 return 1;
112 /* You can read public playlists */
113 if(!strcmp(share, "public"))
114 return 1;
115 /* Anything else is prohibited */
116 return 0;
117}
118
119/** @brief Check modify access rights
120 * @param name Playlist name
121 * @param who Who wants to modify
122 * @param share Playlist share status
123 */
124static int playlist_may_write(const char *name,
125 const char *who,
126 const char attribute((unused)) *share) {
127 char *owner;
128
440af55d 129 if(playlist_parse_name(name, &owner, 0))
2563dc1f
RK
130 return 0;
131 /* Anyone can modify shared playlists */
132 if(!owner)
133 return 1;
134 /* You can always modify playlists you own */
135 if(!strcmp(owner, who))
136 return 1;
137 /* Anything else is prohibited */
138 return 0;
139}
140
141/** @brief Get playlist data
142 * @param name Name of playlist
143 * @param who Who wants to know
144 * @param tracksp Where to put list of tracks, or NULL
145 * @param ntracksp Where to put count of tracks, or NULL
146 * @param sharep Where to put sharing type, or NULL
147 * @return 0 on success, non-0 on error
148 *
149 * Possible return values:
150 * - @c 0 on success
ddbf05c8 151 * - @c ENOENT if the playlist doesn't exist
2563dc1f
RK
152 * - @c EINVAL if the playlist name is invalid
153 * - @c EACCES if the playlist cannot be read by @p who
154 */
155int trackdb_playlist_get(const char *name,
156 const char *who,
157 char ***tracksp,
158 int *ntracksp,
159 char **sharep) {
160 int e;
161
440af55d 162 if(playlist_parse_name(name, 0, 0)) {
2563dc1f
RK
163 error(0, "invalid playlist name '%s'", name);
164 return EINVAL;
165 }
166 WITH_TRANSACTION(trackdb_playlist_get_tid(name, who,
167 tracksp, ntracksp, sharep,
168 tid));
ddbf05c8
RK
169 /* Don't expose libdb error codes too much */
170 if(e == DB_NOTFOUND)
171 e = ENOENT;
2563dc1f
RK
172 return e;
173}
174
175static int trackdb_playlist_get_tid(const char *name,
176 const char *who,
177 char ***tracksp,
178 int *ntracksp,
179 char **sharep,
180 DB_TXN *tid) {
181 struct kvp *k;
182 int e, ntracks;
183 const char *s;
184
185 if((e = trackdb_getdata(trackdb_playlistsdb, name, &k, tid)))
186 return e;
187 /* Get sharability */
188 if(!(s = kvp_get(k, "sharing"))) {
189 error(0, "playlist '%s' has no 'sharing' key", name);
190 s = "private";
191 }
192 /* Check the read is allowed */
193 if(!playlist_may_read(name, who, s))
194 return EACCES;
195 /* Return sharability */
196 if(sharep)
197 *sharep = xstrdup(s);
198 /* Get track count */
199 if(!(s = kvp_get(k, "count"))) {
200 error(0, "playlist '%s' has no 'count' key", name);
201 s = "0";
202 }
203 ntracks = atoi(s);
204 if(ntracks < 0) {
205 error(0, "playlist '%s' has negative count", name);
206 ntracks = 0;
207 }
208 /* Return track count */
209 if(ntracksp)
210 *ntracksp = ntracks;
211 if(tracksp) {
212 /* Get track list */
213 char **tracks = xcalloc(ntracks + 1, sizeof (char *));
214 char b[16];
215
216 for(int n = 0; n < ntracks; ++n) {
217 snprintf(b, sizeof b, "%d", n);
218 if(!(s = kvp_get(k, b))) {
219 error(0, "playlist '%s' lacks track %d", name, n);
220 s = "unknown";
221 }
222 tracks[n] = xstrdup(s);
223 }
224 tracks[ntracks] = 0;
225 /* Return track list */
226 *tracksp = tracks;
227 }
228 return 0;
229}
230
231/** @brief Modify or create a playlist
232 * @param name Playlist name
c0f52b2c 233 * @param who User modifying playlist
2563dc1f
RK
234 * @param tracks List of tracks to set, or NULL to leave alone
235 * @param ntracks Length of @p tracks
236 * @param share Sharing status, or NULL to leave alone
237 * @return 0 on success, non-0 on error
238 *
239 * If the playlist exists it is just modified.
240 *
241 * If the playlist does not exist it is created. The default set of tracks is
242 * none, and the default sharing is private (if it is an owned one) or shared
243 * (otherwise).
244 *
ddbf05c8
RK
245 * If neither @c tracks nor @c share are set then we only do an access check.
246 * The database is never modified (even to create the playlist) in this
247 * situation.
248 *
2563dc1f
RK
249 * Possible return values:
250 * - @c 0 on success
251 * - @c EINVAL if the playlist name is invalid
252 * - @c EACCES if the playlist cannot be modified by @p who
253 */
254int trackdb_playlist_set(const char *name,
255 const char *who,
256 char **tracks,
257 int ntracks,
258 const char *share) {
259 int e;
260 char *owner;
261
440af55d 262 if(playlist_parse_name(name, &owner, 0)) {
2563dc1f
RK
263 error(0, "invalid playlist name '%s'", name);
264 return EINVAL;
265 }
266 /* Check valid share types */
267 if(share) {
268 if(owner) {
269 /* Playlists with an owner must be public or private */
270 if(strcmp(share, "public")
271 && strcmp(share, "private")) {
272 error(0, "playlist '%s' must be public or private", name);
273 return EINVAL;
274 }
275 } else {
276 /* Playlists with no owner must be shared */
277 if(strcmp(share, "shared")) {
278 error(0, "playlist '%s' must be shared", name);
279 return EINVAL;
280 }
281 }
282 }
283 /* We've checked as much as we can for now, now go and attempt the change */
284 WITH_TRANSACTION(trackdb_playlist_set_tid(name, who, tracks, ntracks, share,
285 tid));
286 return e;
287}
288
289static int trackdb_playlist_set_tid(const char *name,
290 const char *who,
291 char **tracks,
292 int ntracks,
293 const char *share,
294 DB_TXN *tid) {
295 struct kvp *k;
296 int e;
297 const char *s;
5c102112 298 const char *event = "playlist_modified";
2563dc1f
RK
299
300 if((e = trackdb_getdata(trackdb_playlistsdb, name, &k, tid))
301 && e != DB_NOTFOUND)
302 return e;
303 /* If the playlist doesn't exist set some defaults */
304 if(e == DB_NOTFOUND) {
305 char *defshare, *owner;
306
307 if(playlist_parse_name(name, &owner, &defshare))
308 return EINVAL;
309 /* Can't create a non-shared playlist belonging to someone else. In fact
310 * this should be picked up by playlist_may_write() below but it's clearer
311 * to do it here. */
312 if(owner && strcmp(owner, who))
313 return EACCES;
314 k = 0;
315 kvp_set(&k, "count", 0);
316 kvp_set(&k, "sharing", defshare);
5c102112 317 event = "playlist_created";
2563dc1f
RK
318 }
319 /* Check that the modification is allowed */
320 if(!(s = kvp_get(k, "sharing"))) {
321 error(0, "playlist '%s' has no 'sharing' key", name);
322 s = "private";
323 }
324 if(!playlist_may_write(name, who, s))
325 return EACCES;
ddbf05c8
RK
326 /* If no change was requested then don't even create */
327 if(!share && !tracks)
328 return 0;
2563dc1f
RK
329 /* Set the new values */
330 if(share)
440af55d 331 kvp_set(&k, "sharing", share);
2563dc1f
RK
332 if(tracks) {
333 char b[16];
334 int oldcount, n;
335
336 /* Sanity check track count */
337 if(ntracks < 0 || ntracks > config->playlist_max) {
338 error(0, "invalid track count %d", ntracks);
339 return EINVAL;
340 }
341 /* Set the tracks */
342 for(n = 0; n < ntracks; ++n) {
343 snprintf(b, sizeof b, "%d", n);
344 kvp_set(&k, b, tracks[n]);
345 }
346 /* Get the old track count */
347 if((s = kvp_get(k, "count")))
348 oldcount = atoi(s);
349 else
350 oldcount = 0;
351 /* Delete old slots */
352 for(; n < oldcount; ++n) {
353 snprintf(b, sizeof b, "%d", n);
354 kvp_set(&k, b, NULL);
355 }
356 /* Set the new count */
357 snprintf(b, sizeof b, "%d", ntracks);
358 kvp_set(&k, "count", b);
359 }
360 /* Store the resulting record */
5c102112
RK
361 e = trackdb_putdata(trackdb_playlistsdb, name, k, tid, 0);
362 /* Log the event */
363 if(!e)
364 eventlog(event, name, kvp_get(k, "sharing"), (char *)0);
365 return e;
2563dc1f
RK
366}
367
368/** @brief Get a list of playlists
369 * @param who Who wants to know
370 * @param playlistsp Where to put list of playlists
371 * @param nplaylistsp Where to put count of playlists, or NULL
372 */
373void trackdb_playlist_list(const char *who,
374 char ***playlistsp,
375 int *nplaylistsp) {
376 int e;
377
378 WITH_TRANSACTION(trackdb_playlist_list_tid(who, playlistsp, nplaylistsp,
379 tid));
380}
381
382static int trackdb_playlist_list_tid(const char *who,
383 char ***playlistsp,
384 int *nplaylistsp,
385 DB_TXN *tid) {
386 struct vector v[1];
387 DBC *c;
388 DBT k[1], d[1];
389 int e;
390
391 vector_init(v);
392 c = trackdb_opencursor(trackdb_playlistsdb, tid);
393 memset(k, 0, sizeof k);
394 while(!(e = c->c_get(c, k, prepare_data(d), DB_NEXT))) {
395 char *name = xstrndup(k->data, k->size), *owner;
396 const char *share = kvp_get(kvp_urldecode(d->data, d->size),
440af55d 397 "sharing");
2563dc1f
RK
398
399 /* Extract owner; malformed names are skipped */
400 if(playlist_parse_name(name, &owner, 0)) {
401 error(0, "invalid playlist name '%s' found in database", name);
402 continue;
403 }
440af55d
RK
404 if(!share) {
405 error(0, "playlist '%s' has no 'sharing' key", name);
406 continue;
407 }
2563dc1f
RK
408 /* Always list public and shared playlists
409 * Only list private ones to their owner
410 * Don't list anything else
411 */
412 if(!strcmp(share, "public")
413 || !strcmp(share, "shared")
414 || (!strcmp(share, "private")
415 && owner && !strcmp(owner, who)))
416 vector_append(v, name);
417 }
440af55d 418 trackdb_closecursor(c);
2563dc1f
RK
419 switch(e) {
420 case DB_NOTFOUND:
421 break;
422 case DB_LOCK_DEADLOCK:
423 return e;
424 default:
425 fatal(0, "c->c_get: %s", db_strerror(e));
426 }
427 vector_terminate(v);
428 if(playlistsp)
429 *playlistsp = v->vec;
430 if(nplaylistsp)
431 *nplaylistsp = v->nvec;
432 return 0;
433}
434
435/** @brief Delete a playlist
436 * @param name Playlist name
437 * @param who Who is deleting it
438 * @return 0 on success, non-0 on error
439 *
440 * Possible return values:
441 * - @c 0 on success
442 * - @c EINVAL if the playlist name is invalid
443 * - @c EACCES if the playlist cannot be modified by @p who
ddbf05c8 444 * - @c ENOENT if the playlist doesn't exist
2563dc1f
RK
445 */
446int trackdb_playlist_delete(const char *name,
447 const char *who) {
448 int e;
449 char *owner;
450
440af55d 451 if(playlist_parse_name(name, &owner, 0)) {
2563dc1f
RK
452 error(0, "invalid playlist name '%s'", name);
453 return EINVAL;
454 }
455 /* We've checked as much as we can for now, now go and attempt the change */
456 WITH_TRANSACTION(trackdb_playlist_delete_tid(name, who, tid));
ddbf05c8
RK
457 if(e == DB_NOTFOUND)
458 e = ENOENT;
2563dc1f
RK
459 return e;
460}
461
462static int trackdb_playlist_delete_tid(const char *name,
463 const char *who,
464 DB_TXN *tid) {
465 struct kvp *k;
466 int e;
467 const char *s;
468
469 if((e = trackdb_getdata(trackdb_playlistsdb, name, &k, tid)))
470 return e;
471 /* Check that modification is allowed */
472 if(!(s = kvp_get(k, "sharing"))) {
473 error(0, "playlist '%s' has no 'sharing' key", name);
474 s = "private";
475 }
476 if(!playlist_may_write(name, who, s))
477 return EACCES;
478 /* Delete the playlist */
5c102112
RK
479 e = trackdb_delkey(trackdb_playlistsdb, name, tid);
480 if(!e)
481 eventlog("playlist_deleted", name, 0);
482 return e;
2563dc1f
RK
483}
484
485/*
486Local Variables:
487c-basic-offset:2
488comment-column:40
489fill-column:79
490indent-tabs-mode:nil
491End:
492*/