C++程序  |  855行  |  24.8 KB

/**
 * \File playlist-spl.c
 *
 * Playlist_t to Samsung (.spl) and back conversion functions.
 *
 * Copyright (C) 2008 Alistair Boyle <alistair.js.boyle@gmail.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include <config.h>

#include <stdio.h>
#include <stdlib.h> // mkstmp()
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#ifdef HAVE_SYS_UIO_H
#include <sys/uio.h>
#endif
#include <fcntl.h>

#include <string.h>

#include "libmtp.h"
#include "libusb-glue.h"
#include "ptp.h"
#include "unicode.h"

#include "playlist-spl.h"

// set this to 1 to add lots of messy debug output to the playlist code
#define DEBUG_ENABLED 0

// debug macro
// d = indenting depth
#define IF_DEBUG() if(DEBUG_ENABLED) {\
                     printf("%s:%u:%s(): ", __FILE__, __LINE__, __func__); \
                   } \
                   if(DEBUG_ENABLED)

// Internal singly linked list of strings
// used to hold .spl playlist in memory
typedef struct text_struct {
  char* text; // String
  struct text_struct *next; // Link to next line, NULL if end of list
} text_t;


/**
 * Forward declarations of local (static) functions.
 */
static text_t* read_into_spl_text_t(LIBMTP_mtpdevice_t *device, const int fd);
static void write_from_spl_text_t(LIBMTP_mtpdevice_t *device, const int fd, text_t* p);
static void free_spl_text_t(text_t* p);
static void print_spl_text_t(text_t* p);
static uint32_t trackno_spl_text_t(text_t* p);
static void tracks_from_spl_text_t(text_t* p, uint32_t* tracks, LIBMTP_folder_t* folders, LIBMTP_file_t* files);
static void spl_text_t_from_tracks(text_t** p, uint32_t* tracks, const uint32_t trackno, const uint32_t ver_major, const uint32_t ver_minor, char* dnse, LIBMTP_folder_t* folders, LIBMTP_file_t* files);

static uint32_t discover_id_from_filepath(const char* s, LIBMTP_folder_t* folders, LIBMTP_file_t* files); // TODO add file/dir cached args
static void discover_filepath_from_id(char** p, uint32_t track, LIBMTP_folder_t* folders, LIBMTP_file_t* files);
static void find_folder_name(LIBMTP_folder_t* folders, uint32_t* id, char** name);
static uint32_t find_folder_id(LIBMTP_folder_t* folders, uint32_t parent, char* name);

static void append_text_t(text_t** t, char* s);




/**
 * Decides if the indicated object index is an .spl playlist.
 *
 * @param oi object we are deciding on
 * @return 1 if this is a Samsung .spl object, 0 otherwise
 */
int is_spl_playlist(PTPObjectInfo *oi)
{
  return (oi->ObjectFormat == PTP_OFC_Undefined) &&
         (strlen(oi->Filename) > 4) &&
         (strcmp((oi->Filename + strlen(oi->Filename) -4), ".spl") == 0);
}

#ifndef HAVE_MKSTEMP
# ifdef __WIN32__
#  include <fcntl.h>
#  define mkstemp(_pattern) _open(_mktemp(_pattern), _O_CREAT | _O_SHORT_LIVED | _O_EXCL)
# else
#  error Missing mkstemp() function.
# endif
#endif

/**
 * Take an object ID, a .spl playlist on the MTP device,
 * and convert it to a playlist_t object.
 *
 * @param device mtp device pointer
 * @param oi object we are reading
 * @param id .spl playlist id on MTP device
 * @param pl the LIBMTP_playlist_t pointer to be filled with info from id
 */

void spl_to_playlist_t(LIBMTP_mtpdevice_t* device, PTPObjectInfo *oi,
                       const uint32_t id, LIBMTP_playlist_t * const pl)
{
  // Fill in playlist metadata
  // Use the Filename as the playlist name, dropping the ".spl" extension
  pl->name = malloc(sizeof(char)*(strlen(oi->Filename) -4 +1));
  memcpy(pl->name, oi->Filename, strlen(oi->Filename) -4);
  // Set terminating character
  pl->name[strlen(oi->Filename) - 4] = 0;
  pl->playlist_id = id;
  pl->parent_id = oi->ParentObject;
  pl->storage_id = oi->StorageID;
  pl->tracks = NULL;
  pl->no_tracks = 0;

  IF_DEBUG() printf("pl->name='%s'\n",pl->name);

  // open a temporary file
  char tmpname[] = "/tmp/mtp-spl2pl-XXXXXX";
  int fd = mkstemp(tmpname);
  if(fd < 0) {
    printf("failed to make temp file for %s.spl -> %s, errno=%s\n", pl->name, tmpname, strerror(errno));
    return;
  }
  // make sure the file will be deleted afterwards
  if(unlink(tmpname) < 0)
    printf("failed to delete temp file for %s.spl -> %s, errno=%s\n", pl->name, tmpname, strerror(errno));
  int ret = LIBMTP_Get_File_To_File_Descriptor(device, pl->playlist_id, fd, NULL, NULL, NULL);
  if( ret < 0 ) {
    // FIXME     add_ptp_error_to_errorstack(device, ret, "LIBMTP_Get_Playlist: Could not get .spl playlist file.");
    close(fd);
    printf("FIXME closed\n");
  }

  text_t* p = read_into_spl_text_t(device, fd);
  close(fd);

  // FIXME cache these somewhere else so we don't keep calling this!
  LIBMTP_folder_t *folders;
  LIBMTP_file_t *files;
  folders = LIBMTP_Get_Folder_List(device);
  files = LIBMTP_Get_Filelisting_With_Callback(device, NULL, NULL);

  // convert the playlist listing to track ids
  pl->no_tracks = trackno_spl_text_t(p);
  IF_DEBUG() printf("%u track%s found\n", pl->no_tracks, pl->no_tracks==1?"":"s");
  pl->tracks = malloc(sizeof(uint32_t)*(pl->no_tracks));
  tracks_from_spl_text_t(p, pl->tracks, folders, files);

  free_spl_text_t(p);

  // debug: add a break since this is the top level function call
  IF_DEBUG() printf("------------\n\n");
}


/**
 * Push a playlist_t onto the device after converting it to a .spl format
 *
 * @param device mtp device pointer
 * @param pl the LIBMTP_playlist_t to convert (pl->playlist_id will be updated
 *           with the newly created object's id)
 * @return 0 on success, any other value means failure.
 */
int playlist_t_to_spl(LIBMTP_mtpdevice_t *device,
                      LIBMTP_playlist_t * const pl)
{
  text_t* t;
  LIBMTP_folder_t *folders;
  LIBMTP_file_t *files;
  folders = LIBMTP_Get_Folder_List(device);
  files = LIBMTP_Get_Filelisting_With_Callback(device, NULL, NULL);

  char tmpname[] = "/tmp/mtp-spl2pl-XXXXXX"; // must be a var since mkstemp modifies it

  IF_DEBUG() printf("pl->name='%s'\n",pl->name);

  // open a file descriptor
  int fd = mkstemp(tmpname);
  if(fd < 0) {
    printf("failed to make temp file for %s.spl -> %s, errno=%s\n", pl->name, tmpname, strerror(errno));
    return -1;
  }
  // make sure the file will be deleted afterwards
  if(unlink(tmpname) < 0)
    printf("failed to delete temp file for %s.spl -> %s, errno=%s\n", pl->name, tmpname, strerror(errno));

  // decide on which version of the .spl format to use
  uint32_t ver_major;
  uint32_t ver_minor = 0;
  PTP_USB *ptp_usb = (PTP_USB*) device->usbinfo;
  if(FLAG_PLAYLIST_SPL_V2(ptp_usb)) ver_major = 2;
  else ver_major = 1; // FLAG_PLAYLIST_SPL_V1()

  IF_DEBUG() printf("%u track%s\n", pl->no_tracks, pl->no_tracks==1?"":"s");
  IF_DEBUG() printf(".spl version %d.%02d\n", ver_major, ver_minor);

  // create the text for the playlist
  spl_text_t_from_tracks(&t, pl->tracks, pl->no_tracks, ver_major, ver_minor, NULL, folders, files);
  write_from_spl_text_t(device, fd, t);
  free_spl_text_t(t); // done with the text

  // create the file object for storing
  LIBMTP_file_t* f = malloc(sizeof(LIBMTP_file_t));
  f->item_id = 0;
  f->parent_id = pl->parent_id;
  f->storage_id = pl->storage_id;
  f->filename = malloc(sizeof(char)*(strlen(pl->name)+5));
  strcpy(f->filename, pl->name);
  strcat(f->filename, ".spl"); // append suffix
  f->filesize = lseek(fd, 0, SEEK_CUR); // file desc is currently at end of file
  f->filetype = LIBMTP_FILETYPE_UNKNOWN;
  f->next = NULL;

  IF_DEBUG() printf("%s is %dB\n", f->filename, (int)f->filesize);

  // push the playlist to the device
  lseek(fd, 0, SEEK_SET); // reset file desc. to start of file
  int ret = LIBMTP_Send_File_From_File_Descriptor(device, fd, f, NULL, NULL);
  pl->playlist_id = f->item_id;
  free(f->filename);
  free(f);

  // release the memory when we're done with it
  close(fd);
  // debug: add a break since this is the top level function call
  IF_DEBUG() printf("------------\n\n");

  return ret;
}



/**
 * Update a playlist on the device. If only the playlist's name is being
 * changed the pl->playlist_id will likely remain the same. An updated track
 * list will result in the old playlist being replaced (ie: new playlist_id).
 * NOTE: Other playlist metadata aside from playlist name and tracks are
 * ignored.
 *
 * @param device mtp device pointer
 * @param new the LIBMTP_playlist_t to convert (pl->playlist_id will be updated
 *           with the newly created object's id)
 * @return 0 on success, any other value means failure.
 */
int update_spl_playlist(LIBMTP_mtpdevice_t *device,
			  LIBMTP_playlist_t * const newlist)
{
  IF_DEBUG() printf("pl->name='%s'\n",newlist->name);

  // read in the playlist of interest
  LIBMTP_playlist_t * old = LIBMTP_Get_Playlist(device, newlist->playlist_id);
  
  // check to see if we found it
  if (!old)
    return -1;

  // check if the playlists match
  int delta = 0;
  int i;
  if(old->no_tracks != newlist->no_tracks)
    delta++;
  for(i=0;i<newlist->no_tracks && delta==0;i++) {
    if(old->tracks[i] != newlist->tracks[i])
      delta++;
  }

  // if not, kill the playlist and replace it
  if(delta) {
    IF_DEBUG() printf("new tracks detected:\n");
    IF_DEBUG() printf("delete old playlist and build a new one\n");
    IF_DEBUG() printf(" NOTE: new playlist_id will result!\n");
    if(LIBMTP_Delete_Object(device, old->playlist_id) != 0)
      return -1;

    IF_DEBUG() {
      if(strcmp(old->name,newlist->name) == 0)
        printf("name unchanged\n");
      else
        printf("name is changing too -> %s\n",newlist->name);
    }

    return LIBMTP_Create_New_Playlist(device, newlist);
  }


  // update the name only
  if(strcmp(old->name,newlist->name) != 0) {
    IF_DEBUG() printf("ONLY name is changing -> %s\n",newlist->name);
    IF_DEBUG() printf("playlist_id will remain unchanged\n");
    char* s = malloc(sizeof(char)*(strlen(newlist->name)+5));
    strcpy(s, newlist->name);
    strcat(s,".spl"); // FIXME check for success
    int ret = LIBMTP_Set_Playlist_Name(device, newlist, s);
    free(s);
    return ret;
  }

  IF_DEBUG() printf("no change\n");
  return 0; // nothing to be done, success
}


/**
 * Load a file descriptor into a string.
 *
 * @param device a pointer to the current device.
 *               (needed for ucs2->utf8 charset conversion)
 * @param fd the file descriptor to load
 * @return text_t* a linked list of lines of text, id is left blank, NULL if nothing read in
 */
static text_t* read_into_spl_text_t(LIBMTP_mtpdevice_t *device, const int fd)
{
  // set MAXREAD to match STRING_BUFFER_LENGTH in unicode.h conversion function
  const size_t MAXREAD = 1024*2;
  char t[MAXREAD];
  // upto 3 bytes per utf8 character, 2 bytes per ucs2 character,
  // +1 for '\0' at end of string
  const size_t WSIZE = MAXREAD/2*3+1;
  char w[WSIZE];
  char* it = t; // iterator on t
  char* iw = w;
  ssize_t rdcnt;
  off_t offcnt;
  text_t* head = NULL;
  text_t* tail = NULL;
  int eof = 0;

  // reset file descriptor (fd) to start of file
  offcnt = lseek(fd, 0, SEEK_SET);

  while(!eof) {
    // find the current offset in the file
    // to allow us to determine how many bytes we read if we hit the EOF
    // where returned rdcnt=0 from read()
    offcnt = lseek(fd, 0, SEEK_CUR);
    // read to refill buffer
    // (there might be data left from an incomplete last string in t,
    // hence start filling at it)
    it = t; // set ptr to start of buffer
    rdcnt = read(fd, it, sizeof(char)*MAXREAD);
    if(rdcnt < 0)
      printf("load_spl_fd read err %s\n", strerror(errno));
    else if(rdcnt == 0) { // for EOF, fix rdcnt
      if(it-t == MAXREAD)
        printf("error -- buffer too small to read in .spl playlist entry\n");

      rdcnt = lseek(fd, 0, SEEK_CUR) - offcnt;
      eof = 1;
    }

    IF_DEBUG() printf("read buff= {%dB new, %dB old/left-over}%s\n",(int)rdcnt, (int)(iw-w), eof?", EOF":"");

    // while more input bytes
    char* it_end = t + rdcnt;
    while(it < it_end) {
      // copy byte, unless EOL (then replace with end-of-string \0)
      if(*it == '\r' || *it == '\n')
        *iw = '\0';
      else
        *iw = *it;

      it++;
      iw++;

      // EOL -- store it
      if( (iw-w) >= 2 && // we must have at least two bytes
          *(iw-1) == '\0' && *(iw-2) == '\0' && // 0x0000 is end-of-string
          // but it must be aligned such that we have an {odd,even} set of
          // bytes since we are expecting to consume bytes two-at-a-time
          !((iw-w)%2) ) {

        // drop empty lines
        //  ... cast as a string of 2 byte characters
        if(ucs2_strlen((uint16_t*)w) == 0) {
          iw = w;
          continue;
        }

        // create a new node in the list
        if(head == NULL) {
          head = malloc(sizeof(text_t));
          tail = head;
        }
        else {
          tail->next = malloc(sizeof(text_t));
          tail = tail->next;
        }
        // fill in the data for the node
        //  ... cast as a string of 2 byte characters
        tail->text = utf16_to_utf8(device, (uint16_t*) w);
        iw = w; // start again

        IF_DEBUG() printf("line: %s\n", tail->text);
      }

      // prevent buffer overflow
      if(iw >= w + WSIZE) {
        // if we ever see this error its BAD:
        //   we are dropping all the processed bytes for this line and
        //   proceeding on as if everything is okay, probably losing a track
        //   from the playlist
        printf("ERROR %s:%u:%s(): buffer overflow! .spl line too long @ %zuB\n",
               __FILE__, __LINE__, __func__, WSIZE);
        iw = w; // reset buffer
      }
    }

    // if the last thing we did was save our line, then we finished working
    // on the input buffer and we can start fresh
    // otherwise we need to save our partial work, if we're not quiting (eof).
    // there is nothing special we need to do, to achieve this since the
    // partially completed string will sit in 'w' until we return to complete
    // the line

  }

  // set the next pointer at the end
  // if there is any list
  if(head != NULL)
    tail->next = NULL;

  // return the head of the list (NULL if no list)
  return head;
}


/**
 * Write a .spl text file to a file in preparation for pushing it
 * to the device.
 *
 * @param fd file descriptor to write to
 * @param p the text to output one line per string in the linked list
 * @see playlist_t_to_spl()
 */
static void write_from_spl_text_t(LIBMTP_mtpdevice_t *device,
                                  const int fd,
                                  text_t* p) {
  ssize_t ret;
  // write out BOM for utf16/ucs2 (byte order mark)
  ret = write(fd,"\xff\xfe",2);
  while(p != NULL) {
    char *const t = (char*) utf8_to_utf16(device, p->text);
    // note: 2 bytes per ucs2 character
    const size_t len = ucs2_strlen((uint16_t*)t)*sizeof(uint16_t);
    int i;

    IF_DEBUG() {
      printf("\nutf8=%s ",p->text);
      for(i=0;i<strlen(p->text);i++)
        printf("%02x ", p->text[i] & 0xff);
      printf("\n");
      printf("ucs2=");
      for(i=0;i<ucs2_strlen((uint16_t*)t)*sizeof(uint16_t);i++)
        printf("%02x ", t[i] & 0xff);
      printf("\n");
    }

    // write: utf8 -> utf16
    ret += write(fd, t, len);

    // release the converted string
    free(t);

    // check for failures
    if(ret < 0)
      printf("write spl file failed: %s\n", strerror(errno));
    else if(ret != len +2)
      printf("write spl file wrong number of bytes ret=%d len=%d '%s'\n", (int)ret, (int)len, p->text);

    // write carriage return, line feed in ucs2
    ret = write(fd, "\r\0\n\0", 4);
    if(ret < 0)
      printf("write spl file failed: %s\n", strerror(errno));
    else if(ret != 4)
      printf("failed to write the correct number of bytes '\\n'!\n");

    // fake out count (first time through has two extra bytes from BOM)
    ret = 2;

    // advance to the next line
    p = p->next;
  }
}

/**
 * Destroy a linked-list of strings.
 *
 * @param p the list to destroy
 * @see spl_to_playlist_t()
 * @see playlist_t_to_spl()
 */
static void free_spl_text_t(text_t* p)
{
  text_t* d;
  while(p != NULL) {
    d = p;
    free(p->text);
    p = p->next;
    free(d);
  }
}

/**
 * Print a linked-list of strings to stdout.
 *
 * @param p the list to print
 */
static void print_spl_text_t(text_t* p)
{
  while(p != NULL) {
    printf("%s\n",p->text);
    p = p->next;
  }
}

/**
 * Count the number of tracks in this playlist. A track will be counted as
 * such if the line starts with a leading slash.
 *
 * @param p the text to search
 * @return number of tracks in the playlist
 * @see spl_to_playlist_t()
 */
static uint32_t trackno_spl_text_t(text_t* p) {
  uint32_t c = 0;
  while(p != NULL) {
    if(p->text[0] == '\\' ) c++;
    p = p->next;
  }

  return c;
}

/**
 * Find the track ids for this playlist's files.
 * (ie: \Music\song.mp3 -> 12345)
 *
 * @param p the text to search
 * @param tracks returned list of track id's for the playlist_t, must be large
 *               enough to accomodate all the tracks as reported by
 *               trackno_spl_text_t()
 * @param folders the folders list for the device
 * @param fiels the files list for the device
 * @see spl_to_playlist_t()
 */
static void tracks_from_spl_text_t(text_t* p,
                                   uint32_t* tracks,
                                   LIBMTP_folder_t* folders,
                                   LIBMTP_file_t* files)
{
  uint32_t c = 0;
  while(p != NULL) {
    if(p->text[0] == '\\' ) {
      tracks[c] = discover_id_from_filepath(p->text, folders, files);
      IF_DEBUG()
        printf("track %d = %s (%u)\n", c+1, p->text, tracks[c]);
      c++;
    }
    p = p->next;
  }
}


/**
 * Find the track names (including path) for this playlist's track ids.
 * (ie: 12345 -> \Music\song.mp3)
 *
 * @param p the text to search
 * @param tracks list of track id's to look up
 * @param folders the folders list for the device
 * @param fiels the files list for the device
 * @see playlist_t_to_spl()
 */
static void spl_text_t_from_tracks(text_t** p,
                                   uint32_t* tracks,
                                   const uint32_t trackno,
                                   const uint32_t ver_major,
                                   const uint32_t ver_minor,
                                   char* dnse,
                                   LIBMTP_folder_t* folders,
                                   LIBMTP_file_t* files)
{

  // HEADER
  text_t* c = NULL;
  append_text_t(&c, "SPL PLAYLIST");
  *p = c; // save the top of the list!

  char vs[14]; // "VERSION 2.00\0"
  sprintf(vs,"VERSION %d.%02d",ver_major,ver_minor);

  append_text_t(&c, vs);
  append_text_t(&c, "");

  // TRACKS
  int i;
  char* f;
  for(i=0;i<trackno;i++) {
    discover_filepath_from_id(&f, tracks[i], folders, files);

    if(f != NULL) {
      append_text_t(&c, f);
      IF_DEBUG()
        printf("track %d = %s (%u)\n", i+1, f, tracks[i]);
    }
    else
      printf("failed to find filepath for track=%d\n", tracks[i]);
  }

  // FOOTER
  append_text_t(&c, "");
  append_text_t(&c, "END PLAYLIST");
  if(ver_major == 2) {
    append_text_t(&c, "");
    append_text_t(&c, "myDNSe DATA");
    if(dnse != NULL) {
      append_text_t(&c, dnse);
    }
    else {
      append_text_t(&c, "");
      append_text_t(&c, "");
    }
    append_text_t(&c, "END myDNSe");
  }

  c->next = NULL;

  // debug
  IF_DEBUG() {
    printf(".spl playlist:\n");
    print_spl_text_t(*p);
  }
}


/**
 * Find the track names (including path) given a fileid
 * (ie: 12345 -> \Music\song.mp3)
 *
 * @param p returns the file path (ie: \Music\song.mp3),
 *          (*p) == NULL if the look up fails
 * @param track track id to look up
 * @param folders the folders list for the device
 * @param files the files list for the device
 * @see spl_text_t_from_tracks()
 */

// returns p = NULL on failure, else the filepath to the track including track name, allocated as a correct length string
static void discover_filepath_from_id(char** p,
                                      uint32_t track,
                                      LIBMTP_folder_t* folders,
                                      LIBMTP_file_t* files)
{
  // fill in a string from the right side since we don't know the root till the end
  const int M = 1024;
  char w[M];
  char* iw = w + M; // iterator on w

  // in case of failure return NULL string
  *p = NULL;


  // find the right file
  while(files != NULL && files->item_id != track) {
    files = files->next;
  }
  // if we didn't find a matching file, abort
  if(files == NULL)
    return;

  // stuff the filename into our string
  // FIXME: check for string overflow before it occurs
  iw = iw - (strlen(files->filename) +1); // leave room for '\0' at the end
  strcpy(iw,files->filename);

  // next follow the directories to the root
  // prepending folders to the path as we go
  uint32_t id = files->parent_id;
  char* f = NULL;
  while(id != 0) {
    find_folder_name(folders, &id, &f);
    if(f == NULL) return; // fail if the next part of the path couldn't be found
    iw = iw - (strlen(f) +1);
    // FIXME: check for string overflow before it occurs
    strcpy(iw, f);
    iw[strlen(f)] = '\\';
    free(f);
  }

  // prepend a slash
  iw--;
  iw[0] = '\\';

  // now allocate a string of the right length to be returned
  *p = strdup(iw);
}


/**
 * Find the track id given a track's name (including path)
 * (ie: \Music\song.mp3 -> 12345)
 *
 * @param s file path to look up (ie: \Music\song.mp3),
 *          (*p) == NULL if the look up fails
 * @param folders the folders list for the device
 * @param files the files list for the device
 * @return track id, 0 means failure
 * @see tracks_from_spl_text_t()
 */
static uint32_t discover_id_from_filepath(const char* s, LIBMTP_folder_t* folders, LIBMTP_file_t* files)
{
  // abort if this isn't a path
  if(s[0] != '\\')
    return 0;

  int i;
  uint32_t id = 0;
  char* sc = strdup(s);
  char* sci = sc +1; // iterator
  // skip leading slash in path

  // convert all \ to \0
  size_t len = strlen(s);
  for(i=0;i<len;i++) {
    if(sc[i] == '\\') {
      sc[i] = '\0';
    }
  }

  // now for each part of the string, find the id
  while(sci != sc + len +1) {
    // if its the last part of the string, its the filename
    if(sci + strlen(sci) == sc + len) {

      while(files != NULL) {
        // check parent matches id and name matches sci
        if( (files->parent_id == id) &&
            (strcmp(files->filename, sci) == 0) ) { // found it!
          id = files->item_id;
          break;
        }
        files = files->next;
      }
    }
    else { // otherwise its part of the directory path
      id = find_folder_id(folders, id, sci);
    }

    // move to next folder/file
    sci += strlen(sci) +1;
  }

  // release our copied string
  free(sc);

  // FIXME check that we actually have a file

  return id;
}



/**
 * Find the folder name given the folder's id.
 *
 * @param folders the folders list for the device
 * @param id the folder_id to look up, returns the folder's parent folder_id
 * @param name returns the name of the folder or NULL on failure
 * @see discover_filepath_from_id()
 */
static void find_folder_name(LIBMTP_folder_t* folders, uint32_t* id, char** name)
{

  // FIXME this function is exactly LIBMTP_Find_Folder

  LIBMTP_folder_t* f = LIBMTP_Find_Folder(folders, *id);
  if(f == NULL) {
    *name = NULL;
  }
  else { // found it!
    *name = strdup(f->name);
    *id = f->parent_id;
  }
}


/**
 * Find the folder id given the folder's name and parent id.
 *
 * @param folders the folders list for the device
 * @param parent the folder's parent's id
 * @param name the name of the folder
 * @return the folder_id or 0 on failure
 * @see discover_filepath_from_id()
 */
static uint32_t find_folder_id(LIBMTP_folder_t* folders, uint32_t parent, char* name) {

  if(folders == NULL)
    return 0;

  // found it!
  else if( (folders->parent_id == parent) &&
           (strcmp(folders->name, name) == 0) )
    return folders->folder_id;

  // no luck so far, search both siblings and children
  else {
    uint32_t id = 0;

    if(folders->sibling != NULL)
      id = find_folder_id(folders->sibling, parent, name);
    if( (id == 0) && (folders->child != NULL) )
      id = find_folder_id(folders->child, parent, name);

    return id;
  }
}


/**
 * Append a string to a linked-list of strings.
 *
 * @param t the list-of-strings, returns with the added string
 * @param s the string to append
 * @see spl_text_t_from_tracks()
 */
static void append_text_t(text_t** t, char* s)
{
  if(*t == NULL) {
    *t = malloc(sizeof(text_t));
  }
  else {
    (*t)->next = malloc(sizeof(text_t));
    (*t) = (*t)->next;
  }
  (*t)->text = strdup(s);
}