mod_musicindex  1.4.1
playlist.c
Go to the documentation of this file.
1 /*
2  * playlist.c
3  * mod_musicindex
4  *
5  * $Id: playlist.c 1022 2012-10-15 21:33:12Z varenet $
6  *
7  * Created by Thibaut VARENE on Thu Mar 20 2003.
8  * Copyright (c) 2003-2004 Regis BOUDIN
9  * Copyright (c) 2003-2009,2012 Thibaut VARENE
10  *
11  * This program is free software; you can redistribute it and/or modify
12  * it under the terms of the GNU Lesser General Public License version 2.1,
13  * as published by the Free Software Foundation.
14  *
15  */
16 
35 #include "playlist.h"
36 #include "sort.h"
37 
38 #include "playlist-flac.h"
39 #include "playlist-vorbis.h"
40 #include "playlist-mp3.h"
41 #include "playlist-mp4.h"
42 
43 #include <stdio.h> /* fops */
44 #ifdef HAVE_UNISTD_H
45 #include <unistd.h> /* access */
46 #endif
47 #ifdef HAVE_SYS_TYPES_H
48 #include <sys/types.h>
49 #endif
50 #ifdef HAVE_DIRENT_H
51 #include <dirent.h> /* opendir */
52 #endif
53 
65 const struct ftype const filetype[] = {
66  { "MP3", "audio/x-mp3" },
67  { "Vorbis", "audio/x-ogg" },
68  { "FLAC", "audio/flac" },
69  { "MP4/AAC", "audio/mp4" }
70 };
71 
72 
83 static inline mu_ent *make_no_entry(request_rec *r, apr_pool_t *pool, FILE *const in,
84  const char *const filename)
85 {
86  fclose(in);
87  return NULL;
88 }
89 
100 static mu_ent *make_cache_entry(request_rec *r, apr_pool_t *pool, FILE *const in,
101  const char *const filename)
102 {
103  const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
104 
105  if (conf->cache && likely(conf->cache->make_entry)) {
106  mu_ent *result = conf->cache->make_entry(r, pool, in, filename);
107  if (result) {
108  result->flags |= EF_INCACHE;
109  return result;
110  }
111  }
112 
113  return NULL;
114 }
115 
131 static const make_entry_ptr const make_entry[] = {
133 #ifdef ENABLE_CODEC_MP3
135 #endif
136 #ifdef ENABLE_CODEC_MP4
138 #endif
139 #ifdef ENABLE_CODEC_FLAC
141 #endif
142 #ifdef ENABLE_CODEC_VORBIS
144 #endif
145  make_no_entry, /* MUST be last before NULL */
146  NULL
147 };
148 
150 typedef struct mu_dir {
151  void *dir;
152  unsigned char handled;
153 } mu_dir;
154 
171 static inline void
172 musicindex_opendir(request_rec *r, mu_dir *dir, mu_pack *const pack,
173  const mu_ent_names * const names, unsigned long soptions)
174 {
175  const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
176 
177  /* It's all we need to initialise. Don't add a call to memset */
178  dir->dir = NULL;
179 
180  /* A cache is configured, try to read directly data from it */
181  if (conf->cache && conf->cache->opendir)
182  dir->dir = conf->cache->opendir(r, pack, names->filename, names->uri, soptions);
183 
184  /* Cache data could not be used, fall back to the old opendir way */
185  if (dir->dir == NULL) {
186  dir->dir = opendir(names->filename);
187  dir->handled = 0;
188  }
189  else {
190  dir->handled = 1;
191  }
192 }
193 
203 static inline const char *musicindex_readdir(request_rec *r, mu_dir *dir)
204 {
205  const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
206 
207  if (dir->handled == 0 ) {
208  struct dirent *dstruct = NULL;
209 
210  dstruct = readdir((DIR *)(dir->dir));
211 
212  if (NULL == dstruct)
213  return NULL;
214  else
215  return dstruct->d_name;
216  }
217  else {
218  /* not all cache subsys can perform whole directory read */
219  if (conf->cache && conf->cache->readdir)
220  return conf->cache->readdir(dir->dir);
221  }
222  return NULL;
223 }
224 
231 static inline void musicindex_closedir(request_rec *r, mu_dir *dir)
232 {
233  const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
234 
235  /* A cache is configured, try to read directly data from it */
236  if (dir->handled == 0)
237  closedir((DIR *)(dir->dir));
238  else
239  if (conf->cache->closedir)
240  conf->cache->closedir(dir->dir);
241 }
242 
253 static inline short
254 go_through_directory(request_rec *r, const unsigned short local_options)
255 {
256  const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
257 
258  /* before dealing with a directory, sanity checks */
259  /* First, is the module enabled ? */
260  if (!(local_options & MI_ACTIVE))
261  return FALSE;
262 
263  /* playall... is stream allowed ? */
264  if (((conf->options & MI_STREAMALL) == MI_STREAMALL)
265  && !(local_options & MI_ALLOWSTREAM))
266  return FALSE;
267 
268  /* Searching... Is searching allowed ? */
269  if ((conf->search) && !(local_options & MI_ALLOWSEARCH))
270  return FALSE;
271 
272  /* Everything is fine, go on */
273  return TRUE;
274 }
275 
307 void make_music_entry(request_rec *r, apr_pool_t *pool, mu_pack *const pack,
308  mu_ent_names *names, unsigned long soptions)
309 {
310  const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
311  mu_ent *p = NULL;
312  register unsigned short i;
313  char *uri;
314 
315  /* place us at the end of the string */
316  uri = r->parsed_uri.path + strlen(r->parsed_uri.path);
317 
318  while (*(--uri) != '/')
319  continue;
320 
321  if (*(++uri) == '.') /* we don't want "invisible" files to show */
322  return;
323 
324  if (access(r->filename, R_OK) != 0)
325  return;
326 
327  if (!names) {
328  if (unlikely((strlen(r->filename) >= MAX_STRING) || (strlen(r->parsed_uri.path) >= MAX_STRING))) {
329  mi_rerror("DBG: MAX_STRING overflow on %s and/or %s - file ignored",
330  r->filename, r->parsed_uri.path);
331  return;
332  }
333  names = apr_palloc(pool, sizeof(mu_ent_names));
334  strcpy(names->filename, r->filename);
335  strcpy(names->uri, r->parsed_uri.path);
336  /* remove trailing '/' if any */
337  if (names->filename[strlen(names->filename)-1] == '/')
338  names->filename[strlen(names->filename)-1] = '\0';
339  }
340 
341  if ((ap_is_directory(pool, names->filename))) {
342  request_rec *subreq;
343  int local_options = 0;
344 
345  uri = names->uri + strlen(names->uri) - 1;
346  if (*uri++ != '/')
347  *uri++ = '/';
348  *uri = '\0';
349 
350  if (conf->dir_per_line > 0) {
351  /* pretty listing of directories with action items */
352  subreq = ap_sub_req_lookup_uri(names->uri, r, NULL);
353 
354  /* soptions contains the value passed to make_music_entry
355  local_options contains the configuration for the subdirectory we're looking at
356  conf->options contains the configuration for the directory that received the request, possibly modified by treat_{post,get}_args() etc */
357  soptions &= ~(MI_ALLOWFETCH|MI_ALLOWRSS);
358 
359  if (subreq != NULL) {
360  local_options = ((mu_config *)ap_get_module_config(subreq->per_dir_config, &musicindex_module))->options;
361  int local_rss = ((mu_config *)ap_get_module_config(subreq->per_dir_config, &musicindex_module))->rss_items;
362  ap_destroy_sub_req(subreq);
363 
364  soptions |= (local_options & MI_ALLOWFETCH);
365 
366  if (local_rss > 0)
367  soptions |= MI_ALLOWRSS;
368  }
369  }
370 
371  if (soptions & MI_RECURSIVE) {
372  /* We were asked to recurse, let's dig in */
373  mu_dir dir_handler;
374  const char * restrict filename;
375  unsigned short fn_max = MAX_STRING;
376  unsigned short uri_max = MAX_STRING;
377  char * restrict fn = names->filename + strlen(names->filename);
378 
379  if (conf->dir_per_line < 0) {
380  subreq = ap_sub_req_lookup_uri(names->uri, r, NULL);
381 
382  soptions &= ~(MI_ALLOWFETCH|MI_ALLOWRSS);
383 
384  if (subreq != NULL) {
385  local_options = ((mu_config *)ap_get_module_config(subreq->per_dir_config, &musicindex_module))->options;
386  ap_destroy_sub_req(subreq);
387 
388  soptions |= (local_options & MI_ALLOWFETCH);
389  }
390  }
391 
392  /* We remove the recursive bit at this point. It will be reinstated
393  if it's present in conf->options, e.g. when searching */
394  soptions &= ((~(MI_RECURSIVE)) | (conf->options));
395 
396  if (go_through_directory(r, local_options) == FALSE)
397  return;
398 
399  /* Try opening the directory */
400  musicindex_opendir(r, &dir_handler, pack, names, soptions);
401  if (NULL == dir_handler.dir)
402  return;
403 
404  /* up to this point, names->filename doesn't have trailing '/' */
405 
406  /* make sure we insert a trailing slash at this point */
407  *fn++ = '/';
408  *fn = '\0';
409 
410  /* DEV NOTE: keep in mind that uri and fn both points to
411  the end of their "names->*" counterpart */
412  fn_max -= strlen(names->filename);
413  uri_max -= strlen(names->uri);
414 
415  /* if a cache backend can provide full listings (during opendir()) it will return NULL
416  * here. Otherwise, it will provide filenames "a la readdir()" for each file that's
417  * missing in cache */
418  while ((filename = musicindex_readdir(r, &dir_handler))) {
419  /* Before doing some strcpy, check we want the file and there is enough space... */
420  if ((filename[0] == '.') || (strlen(filename) >= fn_max) || (strlen(filename) >= uri_max))
421  continue;
422  strcpy(fn, filename);
423  strcpy(uri, filename);
424  make_music_entry(r, pool, pack, names, soptions);
425  }
426  musicindex_closedir(r, &dir_handler);
427  return;
428  }
429  else if (!(conf->options & MI_STREAM) && !(conf->options & MI_RSS) && !(conf->options & MI_TARBALL)) {
430  if (!(p = NEW_ENT(pool)))
431  return;
432  p->filetype = -1;
433  }
434  }
435  else { /* The file was not a directory */
436  FILE *in = fopen(names->filename, "r");
437 
438  if (in == NULL)
439  return;
440 
441  /* This is the end of adventures for the array of function pointers.
442  We call them sequentially, until a function has recognised
443  it (p!=head), or until we reach the end of the array. In either
444  case, the file will be closed (this is the magic loop) */
445  for (i=0; (!p) && make_entry[i]; i++)
446  p = make_entry[i](r, pool, in, names->filename);
447 
448  }
449 
450  /* The file was not recognised, don't go any further */
451  if (!p)
452  return;
453 
454  /* OK, now we have an entry, fill in the missing bits */
455 
456  p->next = pack->head; /* Add it to the linked list */
457 
458  p->uri = apr_pstrdup(pool, names->uri); /* The absolute URI will be useful at some point */
459  p->filename = p->file = p->uri; /* Add the relative filename */
460 
461  /* We remove the path prefix if any. We have to treat files differently
462  * than directories because we can access a given file from various
463  * parsed_uri.path whereas directories will always be shown from their
464  * parent directory (XXX assert?) */
465  if ((p->filetype >= 0) && strrchr(p->filename, '/'))
466  p->filename = (strrchr(p->filename, '/') + 1);
467  else if ((p->filetype < 0))
468  p->filename += strlen(r->parsed_uri.path);
469 
470  /* convert options into flags - We overwrite whatever was in cache,
471  as it is an abuse to use these EF_ALLOW flags anyway: nothing like that
472  will be cached, as the setting may change while the cache info may still
473  be valid. */
474  P_FLAGS_OPTIONS(p, soptions);
475  if ((soptions & MI_CUSTOM) == 0)
476  p->file += strlen(r->parsed_uri.path); /* offset the path before the filename relative to request current dir, except for custom playlists */
477  if (conf->options & MI_TARBALL) /* if the user asked for a tarball, we're going to need the absolute path to access the file */
478  p->filename = apr_pstrdup(pool, names->filename);
479 
480  /* In the case of directories, we want to have a pretty printed version
481  * of the directory name, for display and sorting reasons (trailing '/'
482  * in the real uri would screw alphabetical sorting). Doing the work here
483  * also saves additional work in html.c. Also on can now assert that
484  * p->title always exist (sort.c) */
485  if (!(p->title)) {
486 #ifdef NO_TITLE_STRIP
487  p->title = p->filename;
488 #else
489  /* Copy the name removing file extension (if any) and changing '_' to ' ' */
490  char *const restrict stitle = apr_pstrndup(pool, p->filename,
491  ((p->filetype >= 0) && strrchr(p->filename, '.')) ? /* only strip ext if !dir */
492  (strrchr(p->filename, '.') - p->filename) : /* <=> strlen(p->file) - strlen(strrchr(p->file, '.')) */
493  strlen(p->filename));
494 
495  for (i=0; stitle[i]; i++)
496  if (stitle[i] == '_')
497  stitle[i] = ' ';
498 
499  /* Remove trailing '/' (directories) */
500  if (stitle[--i] == '/')
501  stitle[i] = '\0';
502 
503  p->title = stitle;
504 #endif /* NO_TITLE_STRIP */
505  }
506 
507  /* We put that here for cache backends which need to "see" directories;
508  * and it's faster to retrieve p->title than to process it again. */
509  if (conf->cache && (!(p->flags & EF_INCACHE)) && likely((!(conf->options & MI_QUICKPL)) && conf->cache->write))
510  conf->cache->write(r, p, names->filename);
511 
512  /* Entry is a directory, don't go further */
513  if (p->filetype < 0) {
514  pack->dirnb++;
515  pack->head = p;
516  return;
517  }
518 
519  /* Here we go if we have an ongoing search request, and the file we're
520  * looking at wasn't found in the cache. We then fall back to a basic strcasestr()
521  * check, returning only the entries matching the search criteria */
522  if (unlikely((conf->search) && ((soptions & MI_CUSTOM) == 0))) {
523  const char *const file = names->uri + strlen(r->parsed_uri.path);
524  if ((ap_strcasestr(file, conf->search)) ||
525  (p->artist && (ap_strcasestr(p->artist, conf->search))) ||
526  (p->album && (ap_strcasestr(p->album, conf->search))) ||
527  (p->title && (ap_strcasestr(p->title, conf->search))) );
528  /* do nothing if there's a match */
529  else
530  return; /* skip if there isn't */
531  }
532 
533  /* sum the total size and number of files in the pack */
534  pack->fsize += p->size;
535  pack->filenb++;
536  pack->head = p;
537  return;
538 }
539 
556 void build_custom_list(request_rec *r, mu_pack *const pack)
557 {
558  const mu_config *const conf = (mu_config *)ap_get_module_config(r->per_dir_config, &musicindex_module);
559  request_rec *subreq;
560  mu_ent_names names;
561  const char *args;
562  mu_ent *mobile_ent = NULL, *custom = NULL, *result = NULL;
563  char *p, *decodeduri = NULL;
564  short direct = 0;
565 
566  if (conf->custom_list == NULL)
567  return;
568 
569  args = conf->custom_list;
570 
571 
572  if (strncmp(args, "playlist=", 9) == 0)
573  args += 9; /* cookie */
574  else if (strncmp(args, "file=", 5) == 0)
575  direct = 1; /* direct request (Play Selected) */
576 
577  while ((*args != '\0') && (*args != ';')) {
578  int local_options;
579  p = ap_getword(r->pool, &args, '&');
580  if (direct && (!strncmp(p, "file=", 5))) {
581  p+=5;
582  ap_unescape_url(p);
583  }
584  else if (direct)
585  continue;
586 
587  decodeduri = (char *)realloc(decodeduri, 1 + apr_base64_decode_len(p));
588  if (!decodeduri)
589  return;
590 
591  apr_base64_decode(decodeduri, p);
592  /* decodeduri is unescaped. We need to escape it before
593  passing a new request to the server */
594  p = ap_escape_uri(r->pool, decodeduri);
595 
596  /* we do that to avoid nasty accesses if somebody crafts a uri */
597  subreq = ap_sub_req_lookup_uri(p, r, NULL);
598 
599  if (subreq == NULL)
600  continue;
601 
602  strcpy(names.uri, subreq->parsed_uri.path);
603  strcpy(names.filename, subreq->filename);
604 
605  local_options = ((mu_config *)ap_get_module_config(subreq->per_dir_config, &musicindex_module))->options;
606 
607  ap_destroy_sub_req(subreq);
608 
609  /* adds entries bottom-up */
610  make_music_entry(r, r->pool, pack, &names,
611  (MI_CUSTOM | (local_options & MI_ALLOWFETCH)));
612  }
613  free(decodeduri);
614 
615  /* reorder the list top-down. Deconstify because we do something nasty */
616  custom = (mu_ent *)pack->head;
617  for (result = NULL; custom; custom = mobile_ent) {
618  mobile_ent = (mu_ent *)custom->next;
619  custom->next = result;
620  result = custom;
621  }
622  pack->fhead = result;
623 
624  return;
625 }