| /*************************************************************************** |
| * __________ __ ___. |
| * Open \______ \ ____ ____ | | _\_ |__ _______ ___ |
| * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / |
| * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < |
| * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ |
| * \/ \/ \/ \/ \/ |
| * $Id$ |
| * |
| * Copyright (C) 2002 by wavey@wavey.org |
| * |
| * This program is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU General Public License |
| * as published by the Free Software Foundation; either version 2 |
| * of the License, or (at your option) any later version. |
| * |
| * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY |
| * KIND, either express or implied. |
| * |
| ****************************************************************************/ |
| |
| /* |
| Dynamic playlist design (based on design originally proposed by ricII) |
| |
| There are two files associated with a dynamic playlist: |
| 1. Playlist file : This file contains the initial songs in the playlist. |
| The file is created by the user and stored on the hard |
| drive. NOTE: If we are playing the contents of a |
| directory, there will be no playlist file. |
| 2. Control file : This file is automatically created when a playlist is |
| started and contains all the commands done to it. |
| |
| The first non-comment line in a control file must begin with |
| "P:VERSION:DIR:FILE" where VERSION is the playlist control file version, |
| DIR is the directory where the playlist is located and FILE is the |
| playlist filename. For dirplay, FILE will be empty. An empty playlist |
| will have both entries as null. |
| |
| Control file commands: |
| a. Add track (A:<position>:<last position>:<path to track>) |
| - Insert a track at the specified position in the current |
| playlist. Last position is used to specify where last insertion |
| occurred. |
| b. Queue track (Q:<position>:<last position>:<path to track>) |
| - Queue a track at the specified position in the current |
| playlist. Queued tracks differ from added tracks in that they |
| are deleted from the playlist as soon as they are played and |
| they are not saved to disk as part of the playlist. |
| c. Delete track (D:<position>) |
| - Delete track from specified position in the current playlist. |
| d. Shuffle playlist (S:<seed>:<index>) |
| - Shuffle entire playlist with specified seed. The index |
| identifies the first index in the newly shuffled playlist |
| (needed for repeat mode). |
| e. Unshuffle playlist (U:<index>) |
| - Unshuffle entire playlist. The index identifies the first index |
| in the newly unshuffled playlist. |
| f. Reset last insert position (R) |
| - Needed so that insertions work properly after resume |
| |
| Resume: |
| The only resume info that needs to be saved is the current index in the |
| playlist and the position in the track. When resuming, all the commands |
| in the control file will be reapplied so that the playlist indices are |
| exactly the same as before shutdown. To avoid unnecessary disk |
| accesses, the shuffle mode settings are also saved in settings and only |
| flushed to disk when required. |
| */ |
| |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <ctype.h> |
| #include "playlist.h" |
| #include "ata_idle_notify.h" |
| #include "file.h" |
| #include "action.h" |
| #include "dir.h" |
| #include "sprintf.h" |
| #include "debug.h" |
| #include "audio.h" |
| #include "lcd.h" |
| #include "kernel.h" |
| #include "settings.h" |
| #include "status.h" |
| #include "applimits.h" |
| #include "screens.h" |
| #include "buffer.h" |
| #include "misc.h" |
| #include "button.h" |
| #include "filetree.h" |
| #include "abrepeat.h" |
| #include "thread.h" |
| #include "usb.h" |
| #include "filetypes.h" |
| #ifdef HAVE_LCD_BITMAP |
| #include "icons.h" |
| #endif |
| |
| #include "lang.h" |
| #include "talk.h" |
| #include "splash.h" |
| #include "rbunicode.h" |
| #include "root_menu.h" |
| |
| #define PLAYLIST_CONTROL_FILE ROCKBOX_DIR "/.playlist_control" |
| #define PLAYLIST_CONTROL_FILE_VERSION 2 |
| |
| /* |
| Each playlist index has a flag associated with it which identifies what |
| type of track it is. These flags are stored in the 4 high order bits of |
| the index. |
| |
| NOTE: This limits the playlist file size to a max of 256M. |
| |
| Bits 31-30: |
| 00 = Playlist track |
| 01 = Track was prepended into playlist |
| 10 = Track was inserted into playlist |
| 11 = Track was appended into playlist |
| Bit 29: |
| 0 = Added track |
| 1 = Queued track |
| Bit 28: |
| 0 = Track entry is valid |
| 1 = Track does not exist on disk and should be skipped |
| */ |
| #define PLAYLIST_SEEK_MASK 0x0FFFFFFF |
| #define PLAYLIST_INSERT_TYPE_MASK 0xC0000000 |
| #define PLAYLIST_QUEUE_MASK 0x20000000 |
| |
| #define PLAYLIST_INSERT_TYPE_PREPEND 0x40000000 |
| #define PLAYLIST_INSERT_TYPE_INSERT 0x80000000 |
| #define PLAYLIST_INSERT_TYPE_APPEND 0xC0000000 |
| |
| #define PLAYLIST_QUEUED 0x20000000 |
| #define PLAYLIST_SKIPPED 0x10000000 |
| |
| #define PLAYLIST_DISPLAY_COUNT 10 |
| |
| struct directory_search_context { |
| struct playlist_info* playlist; |
| int position; |
| bool queue; |
| int count; |
| }; |
| |
| static struct playlist_info current_playlist; |
| static char now_playing[MAX_PATH+1]; |
| |
| static void empty_playlist(struct playlist_info* playlist, bool resume); |
| static void new_playlist(struct playlist_info* playlist, const char *dir, |
| const char *file); |
| static void create_control(struct playlist_info* playlist); |
| static int check_control(struct playlist_info* playlist); |
| static int recreate_control(struct playlist_info* playlist); |
| static void update_playlist_filename(struct playlist_info* playlist, |
| const char *dir, const char *file); |
| static int add_indices_to_playlist(struct playlist_info* playlist, |
| char* buffer, size_t buflen); |
| static int add_track_to_playlist(struct playlist_info* playlist, |
| const char *filename, int position, |
| bool queue, int seek_pos); |
| static int directory_search_callback(char* filename, void* context); |
| static int remove_track_from_playlist(struct playlist_info* playlist, |
| int position, bool write); |
| static int randomise_playlist(struct playlist_info* playlist, |
| unsigned int seed, bool start_current, |
| bool write); |
| static int sort_playlist(struct playlist_info* playlist, bool start_current, |
| bool write); |
| static int get_next_index(const struct playlist_info* playlist, int steps, |
| int repeat_mode); |
| static void find_and_set_playlist_index(struct playlist_info* playlist, |
| unsigned int seek); |
| static int compare(const void* p1, const void* p2); |
| static int get_filename(struct playlist_info* playlist, int index, int seek, |
| bool control_file, char *buf, int buf_length); |
| static int get_next_directory(char *dir); |
| static int get_next_dir(char *dir, bool is_forward, bool recursion); |
| static int get_previous_directory(char *dir); |
| static int check_subdir_for_music(char *dir, char *subdir); |
| static int format_track_path(char *dest, char *src, int buf_length, int max, |
| const char *dir); |
| static void display_playlist_count(int count, const unsigned char *fmt, |
| bool final); |
| static void display_buffer_full(void); |
| static int flush_cached_control(struct playlist_info* playlist); |
| static int update_control(struct playlist_info* playlist, |
| enum playlist_command command, int i1, int i2, |
| const char* s1, const char* s2, void* data); |
| static void sync_control(struct playlist_info* playlist, bool force); |
| static int rotate_index(const struct playlist_info* playlist, int index); |
| |
| #ifdef HAVE_DIRCACHE |
| #define PLAYLIST_LOAD_POINTERS 1 |
| |
| static struct event_queue playlist_queue; |
| static long playlist_stack[(DEFAULT_STACK_SIZE + 0x800)/sizeof(long)]; |
| static const char playlist_thread_name[] = "playlist cachectrl"; |
| #endif |
| |
| #define BOM "\xef\xbb\xbf" |
| #define BOM_SIZE 3 |
| |
| /* Check if the filename suggests M3U or M3U8 format. */ |
| static bool is_m3u8(const char* filename) |
| { |
| int len = strlen(filename); |
| |
| /* Default to M3U8 unless explicitly told otherwise. */ |
| return !(len > 4 && strcasecmp(&filename[len - 4], ".m3u") == 0); |
| } |
| |
| /* Check if a strings starts with an UTF-8 byte-order mark. */ |
| static bool is_utf8_bom(const char* str, int len) |
| { |
| return len >= BOM_SIZE && memcmp(str, BOM, BOM_SIZE) == 0; |
| } |
| |
| /* Convert a filename in an M3U playlist to UTF-8. |
| * |
| * buf - the filename to convert; can contain more than one line from the |
| * playlist. |
| * buf_len - amount of buf that is used. |
| * buf_max - total size of buf. |
| * temp - temporary conversion buffer, at least buf_max bytes. |
| * |
| * Returns the length of the converted filename. |
| */ |
| static int convert_m3u(char* buf, int buf_len, int buf_max, char* temp) |
| { |
| int i = 0; |
| char* dest; |
| |
| /* Locate EOL. */ |
| while ((buf[i] != '\n') && (buf[i] != '\r') && (i < buf_len)) |
| { |
| i++; |
| } |
| |
| /* Work back killing white space. */ |
| while ((i > 0) && isspace(buf[i - 1])) |
| { |
| i--; |
| } |
| |
| buf_len = i; |
| dest = temp; |
| |
| /* Convert char by char, so as to not overflow temp (iso_decode should |
| * preferably handle this). No more than 4 bytes should be generated for |
| * each input char. |
| */ |
| for (i = 0; i < buf_len && dest < (temp + buf_max - 4); i++) |
| { |
| dest = iso_decode(&buf[i], dest, -1, 1); |
| } |
| |
| *dest = 0; |
| strcpy(buf, temp); |
| return dest - temp; |
| } |
| |
| /* |
| * remove any files and indices associated with the playlist |
| */ |
| static void empty_playlist(struct playlist_info* playlist, bool resume) |
| { |
| playlist->filename[0] = '\0'; |
| playlist->utf8 = true; |
| |
| if(playlist->fd >= 0) |
| /* If there is an already open playlist, close it. */ |
| close(playlist->fd); |
| playlist->fd = -1; |
| |
| if(playlist->control_fd >= 0) |
| close(playlist->control_fd); |
| playlist->control_fd = -1; |
| playlist->control_created = false; |
| |
| playlist->in_ram = false; |
| |
| if (playlist->buffer) |
| playlist->buffer[0] = 0; |
| |
| playlist->buffer_end_pos = 0; |
| |
| playlist->index = 0; |
| playlist->first_index = 0; |
| playlist->amount = 0; |
| playlist->last_insert_pos = -1; |
| playlist->seed = 0; |
| playlist->shuffle_modified = false; |
| playlist->deleted = false; |
| playlist->num_inserted_tracks = 0; |
| playlist->started = false; |
| |
| playlist->num_cached = 0; |
| playlist->pending_control_sync = false; |
| |
| if (!resume && playlist->current) |
| { |
| /* start with fresh playlist control file when starting new |
| playlist */ |
| create_control(playlist); |
| |
| /* Reset resume settings */ |
| global_status.resume_first_index = 0; |
| global_status.resume_seed = -1; |
| } |
| } |
| |
| /* |
| * Initialize a new playlist for viewing/editing/playing. dir is the |
| * directory where the playlist is located and file is the filename. |
| */ |
| static void new_playlist(struct playlist_info* playlist, const char *dir, |
| const char *file) |
| { |
| const char *fileused = file; |
| const char *dirused = dir; |
| empty_playlist(playlist, false); |
| |
| if (!fileused) |
| { |
| fileused = ""; |
| |
| if (dirused && playlist->current) /* !current cannot be in_ram */ |
| playlist->in_ram = true; |
| else |
| dirused = ""; /* empty playlist */ |
| } |
| |
| update_playlist_filename(playlist, dirused, fileused); |
| |
| if (playlist->control_fd >= 0) |
| { |
| update_control(playlist, PLAYLIST_COMMAND_PLAYLIST, |
| PLAYLIST_CONTROL_FILE_VERSION, -1, dirused, fileused, NULL); |
| sync_control(playlist, false); |
| } |
| } |
| |
| /* |
| * create control file for playlist |
| */ |
| static void create_control(struct playlist_info* playlist) |
| { |
| playlist->control_fd = open(playlist->control_filename, |
| O_CREAT|O_RDWR|O_TRUNC); |
| if (playlist->control_fd < 0) |
| { |
| if (check_rockboxdir()) |
| { |
| cond_talk_ids_fq(LANG_PLAYLIST_CONTROL_ACCESS_ERROR); |
| gui_syncsplash(HZ*2, (unsigned char *)"%s (%d)", |
| str(LANG_PLAYLIST_CONTROL_ACCESS_ERROR), |
| playlist->control_fd); |
| } |
| playlist->control_created = false; |
| } |
| else |
| { |
| playlist->control_created = true; |
| } |
| } |
| |
| /* |
| * validate the control file. This may include creating/initializing it if |
| * necessary; |
| */ |
| static int check_control(struct playlist_info* playlist) |
| { |
| if (!playlist->control_created) |
| { |
| create_control(playlist); |
| |
| if (playlist->control_fd >= 0) |
| { |
| char* dir = playlist->filename; |
| char* file = playlist->filename+playlist->dirlen; |
| char c = playlist->filename[playlist->dirlen-1]; |
| |
| playlist->filename[playlist->dirlen-1] = '\0'; |
| |
| update_control(playlist, PLAYLIST_COMMAND_PLAYLIST, |
| PLAYLIST_CONTROL_FILE_VERSION, -1, dir, file, NULL); |
| sync_control(playlist, false); |
| playlist->filename[playlist->dirlen-1] = c; |
| } |
| } |
| |
| if (playlist->control_fd < 0) |
| return -1; |
| |
| return 0; |
| } |
| |
| /* |
| * recreate the control file based on current playlist entries |
| */ |
| static int recreate_control(struct playlist_info* playlist) |
| { |
| char temp_file[MAX_PATH+1]; |
| int temp_fd = -1; |
| int i; |
| int result = 0; |
| |
| if(playlist->control_fd >= 0) |
| { |
| char* dir = playlist->filename; |
| char* file = playlist->filename+playlist->dirlen; |
| char c = playlist->filename[playlist->dirlen-1]; |
| |
| close(playlist->control_fd); |
| |
| snprintf(temp_file, sizeof(temp_file), "%s_temp", |
| playlist->control_filename); |
| |
| if (rename(playlist->control_filename, temp_file) < 0) |
| return -1; |
| |
| temp_fd = open(temp_file, O_RDONLY); |
| if (temp_fd < 0) |
| return -1; |
| |
| playlist->control_fd = open(playlist->control_filename, |
| O_CREAT|O_RDWR|O_TRUNC); |
| if (playlist->control_fd < 0) |
| return -1; |
| |
| playlist->filename[playlist->dirlen-1] = '\0'; |
| |
| /* cannot call update_control() because of mutex */ |
| result = fdprintf(playlist->control_fd, "P:%d:%s:%s\n", |
| PLAYLIST_CONTROL_FILE_VERSION, dir, file); |
| |
| playlist->filename[playlist->dirlen-1] = c; |
| |
| if (result < 0) |
| { |
| close(temp_fd); |
| return result; |
| } |
| } |
| |
| playlist->seed = 0; |
| playlist->shuffle_modified = false; |
| playlist->deleted = false; |
| playlist->num_inserted_tracks = 0; |
| |
| if (playlist->current) |
| { |
| global_status.resume_seed = -1; |
| status_save(); |
| } |
| |
| for (i=0; i<playlist->amount; i++) |
| { |
| if (playlist->indices[i] & PLAYLIST_INSERT_TYPE_MASK) |
| { |
| bool queue = playlist->indices[i] & PLAYLIST_QUEUE_MASK; |
| char inserted_file[MAX_PATH+1]; |
| |
| lseek(temp_fd, playlist->indices[i] & PLAYLIST_SEEK_MASK, |
| SEEK_SET); |
| read_line(temp_fd, inserted_file, sizeof(inserted_file)); |
| |
| result = fdprintf(playlist->control_fd, "%c:%d:%d:", |
| queue?'Q':'A', i, playlist->last_insert_pos); |
| if (result > 0) |
| { |
| /* save the position in file where name is written */ |
| int seek_pos = lseek(playlist->control_fd, 0, SEEK_CUR); |
| |
| result = fdprintf(playlist->control_fd, "%s\n", |
| inserted_file); |
| |
| playlist->indices[i] = |
| (playlist->indices[i] & ~PLAYLIST_SEEK_MASK) | seek_pos; |
| } |
| |
| if (result < 0) |
| break; |
| |
| playlist->num_inserted_tracks++; |
| } |
| } |
| |
| close(temp_fd); |
| remove(temp_file); |
| fsync(playlist->control_fd); |
| |
| if (result < 0) |
| return result; |
| |
| return 0; |
| } |
| |
| /* |
| * store directory and name of playlist file |
| */ |
| static void update_playlist_filename(struct playlist_info* playlist, |
| const char *dir, const char *file) |
| { |
| char *sep=""; |
| int dirlen = strlen(dir); |
| |
| playlist->utf8 = is_m3u8(file); |
| |
| /* If the dir does not end in trailing slash, we use a separator. |
| Otherwise we don't. */ |
| if('/' != dir[dirlen-1]) |
| { |
| sep="/"; |
| dirlen++; |
| } |
| |
| playlist->dirlen = dirlen; |
| |
| snprintf(playlist->filename, sizeof(playlist->filename), |
| "%s%s%s", dir, sep, file); |
| } |
| |
| /* |
| * calculate track offsets within a playlist file |
| */ |
| static int add_indices_to_playlist(struct playlist_info* playlist, |
| char* buffer, size_t buflen) |
| { |
| unsigned int nread; |
| unsigned int i = 0; |
| unsigned int count = 0; |
| bool store_index; |
| unsigned char *p; |
| int result = 0; |
| |
| if(-1 == playlist->fd) |
| playlist->fd = open(playlist->filename, O_RDONLY); |
| if(playlist->fd < 0) |
| return -1; /* failure */ |
| |
| gui_syncsplash(0, ID2P(LANG_WAIT)); |
| |
| if (!buffer) |
| { |
| /* use mp3 buffer for maximum load speed */ |
| audio_stop(); |
| #if CONFIG_CODEC != SWCODEC |
| talk_buffer_steal(); /* we use the mp3 buffer, need to tell */ |
| buflen = (audiobufend - audiobuf); |
| buffer = (char *)audiobuf; |
| #else |
| buffer = (char *)audio_get_buffer(false, &buflen); |
| #endif |
| } |
| |
| store_index = true; |
| |
| while(1) |
| { |
| nread = read(playlist->fd, buffer, buflen); |
| /* Terminate on EOF */ |
| if(nread <= 0) |
| break; |
| |
| p = (unsigned char *)buffer; |
| |
| /* utf8 BOM at beginning of file? */ |
| if(i == 0 && is_utf8_bom(p, nread)) { |
| nread -= BOM_SIZE; |
| p += BOM_SIZE; |
| i += BOM_SIZE; |
| playlist->utf8 = true; /* Override any earlier indication. */ |
| } |
| |
| for(count=0; count < nread; count++,p++) { |
| |
| /* Are we on a new line? */ |
| if((*p == '\n') || (*p == '\r')) |
| { |
| store_index = true; |
| } |
| else if(store_index) |
| { |
| store_index = false; |
| |
| if(*p != '#') |
| { |
| if ( playlist->amount >= playlist->max_playlist_size ) { |
| display_buffer_full(); |
| result = -1; |
| goto exit; |
| } |
| |
| /* Store a new entry */ |
| playlist->indices[ playlist->amount ] = i+count; |
| #ifdef HAVE_DIRCACHE |
| if (playlist->filenames) |
| playlist->filenames[ playlist->amount ] = NULL; |
| #endif |
| playlist->amount++; |
| } |
| } |
| } |
| |
| i+= count; |
| } |
| |
| exit: |
| #ifdef HAVE_DIRCACHE |
| queue_post(&playlist_queue, PLAYLIST_LOAD_POINTERS, 0); |
| #endif |
| |
| return result; |
| } |
| |
| /* |
| * Utility function to create a new playlist, fill it with the next or |
| * previous directory, shuffle it if needed, and start playback. |
| * If play_last is true and direction zero or negative, start playing |
| * the last file in the directory, otherwise start playing the first. |
| */ |
| static int create_and_play_dir(int direction, bool play_last) |
| { |
| char dir[MAX_PATH + 1]; |
| int res; |
| int index = -1; |
| |
| if(direction > 0) |
| res = get_next_directory(dir); |
| else |
| res = get_previous_directory(dir); |
| |
| if (!res) |
| { |
| if (playlist_create(dir, NULL) != -1) |
| { |
| ft_build_playlist(tree_get_context(), 0); |
| |
| if (global_settings.playlist_shuffle) |
| playlist_shuffle(current_tick, -1); |
| |
| if (play_last && direction <= 0) |
| index = current_playlist.amount - 1; |
| else |
| index = 0; |
| |
| #if (CONFIG_CODEC != SWCODEC) |
| playlist_start(index, 0); |
| #endif |
| } |
| |
| /* we've overwritten the dircache when getting the next/previous dir, |
| so the tree browser context will need to be reloaded */ |
| reload_directory(); |
| } |
| |
| return index; |
| } |
| |
| /* |
| * Removes all tracks, from the playlist, leaving the presently playing |
| * track queued. |
| */ |
| int playlist_remove_all_tracks(struct playlist_info *playlist) |
| { |
| int result; |
| |
| if (playlist == NULL) |
| playlist = ¤t_playlist; |
| |
| while (playlist->index > 0) |
| if ((result = remove_track_from_playlist(playlist, 0, true)) < 0) |
| return result; |
| |
| while (playlist->amount > 1) |
| if ((result = remove_track_from_playlist(playlist, 1, true)) < 0) |
| return result; |
| |
| if (playlist->amount == 1) { |
| playlist->indices[0] |= PLAYLIST_QUEUED; |
| } |
| |
| return 0; |
| } |
| |
| |
| /* |
| * Add track to playlist at specified position. There are five special |
| * positions that can be specified: |
| * PLAYLIST_PREPEND - Add track at beginning of playlist |
| * PLAYLIST_INSERT - Add track after current song. NOTE: If |
| * there are already inserted tracks then track |
| * is added to the end of the insertion list |
| * PLAYLIST_INSERT_FIRST - Add track immediately after current song, no |
| * matter what other tracks have been inserted |
| * PLAYLIST_INSERT_LAST - Add track to end of playlist |
| * PLAYLIST_INSERT_SHUFFLED - Add track at some random point between the |
| * current playing track and end of playlist |
| * PLAYLIST_REPLACE - Erase current playlist, Cue the current track |
| * and inster this track at the end. |
| */ |
| static int add_track_to_playlist(struct playlist_info* playlist, |
| const char *filename, int position, |
| bool queue, int seek_pos) |
| { |
| int insert_position, orig_position; |
| unsigned long flags = PLAYLIST_INSERT_TYPE_INSERT; |
| int i; |
| |
| insert_position = orig_position = position; |
| |
| if (playlist->amount >= playlist->max_playlist_size) |
| { |
| display_buffer_full(); |
| return -1; |
| } |
| |
| switch (position) |
| { |
| case PLAYLIST_PREPEND: |
| position = insert_position = playlist->first_index; |
| break; |
| case PLAYLIST_INSERT: |
| /* if there are already inserted tracks then add track to end of |
| insertion list else add after current playing track */ |
| if (playlist->last_insert_pos >= 0 && |
| playlist->last_insert_pos < playlist->amount && |
| (playlist->indices[playlist->last_insert_pos]& |
| PLAYLIST_INSERT_TYPE_MASK) == PLAYLIST_INSERT_TYPE_INSERT) |
| position = insert_position = playlist->last_insert_pos+1; |
| else if (playlist->amount > 0) |
| position = insert_position = playlist->index + 1; |
| else |
| position = insert_position = 0; |
| |
| if (playlist->started) |
| playlist->last_insert_pos = position; |
| break; |
| case PLAYLIST_INSERT_FIRST: |
| if (playlist->amount > 0) |
| position = insert_position = playlist->index + 1; |
| else |
| position = insert_position = 0; |
| |
| if (playlist->last_insert_pos < 0 && playlist->started) |
| playlist->last_insert_pos = position; |
| break; |
| case PLAYLIST_INSERT_LAST: |
| if (playlist->first_index > 0) |
| position = insert_position = playlist->first_index; |
| else |
| position = insert_position = playlist->amount; |
| break; |
| case PLAYLIST_INSERT_SHUFFLED: |
| { |
| if (playlist->started) |
| { |
| int offset; |
| int n = playlist->amount - |
| rotate_index(playlist, playlist->index); |
| |
| if (n > 0) |
| offset = rand() % n; |
| else |
| offset = 0; |
| |
| position = playlist->index + offset + 1; |
| if (position >= playlist->amount) |
| position -= playlist->amount; |
| |
| insert_position = position; |
| } |
| else |
| position = insert_position = (rand() % (playlist->amount+1)); |
| break; |
| } |
| case PLAYLIST_REPLACE: |
| if (playlist_remove_all_tracks(playlist) < 0) |
| return -1; |
| |
| position = insert_position = playlist->index + 1; |
| break; |
| } |
| |
| if (queue) |
| flags |= PLAYLIST_QUEUED; |
| |
| /* shift indices so that track can be added */ |
| for (i=playlist->amount; i>insert_position; i--) |
| { |
| playlist->indices[i] = playlist->indices[i-1]; |
| #ifdef HAVE_DIRCACHE |
| if (playlist->filenames) |
| playlist->filenames[i] = playlist->filenames[i-1]; |
| #endif |
| } |
| |
| /* update stored indices if needed */ |
| if (playlist->amount > 0 && insert_position <= playlist->index && |
| playlist->started) |
| playlist->index++; |
| |
| if (playlist->amount > 0 && insert_position <= playlist->first_index && |
| orig_position != PLAYLIST_PREPEND && playlist->started) |
| { |
| playlist->first_index++; |
| |
| if (seek_pos < 0 && playlist->current) |
| { |
| global_status.resume_first_index = playlist->first_index; |
| status_save(); |
| } |
| } |
| |
| if (insert_position < playlist->last_insert_pos || |
| (insert_position == playlist->last_insert_pos && position < 0)) |
| playlist->last_insert_pos++; |
| |
| if (seek_pos < 0 && playlist->control_fd >= 0) |
| { |
| int result = update_control(playlist, |
| (queue?PLAYLIST_COMMAND_QUEUE:PLAYLIST_COMMAND_ADD), position, |
| playlist->last_insert_pos, filename, NULL, &seek_pos); |
| |
| if (result < 0) |
| return result; |
| } |
| |
| playlist->indices[insert_position] = flags | seek_pos; |
| |
| #ifdef HAVE_DIRCACHE |
| if (playlist->filenames) |
| playlist->filenames[insert_position] = NULL; |
| #endif |
| |
| playlist->amount++; |
| playlist->num_inserted_tracks++; |
| |
| return insert_position; |
| } |
| |
| /* |
| * Callback for playlist_directory_tracksearch to insert track into |
| * playlist. |
| */ |
| static int directory_search_callback(char* filename, void* context) |
| { |
| struct directory_search_context* c = |
| (struct directory_search_context*) context; |
| int insert_pos; |
| |
| insert_pos = add_track_to_playlist(c->playlist, filename, c->position, |
| c->queue, -1); |
| |
| if (insert_pos < 0) |
| return -1; |
| |
| (c->count)++; |
| |
| /* Make sure tracks are inserted in correct order if user requests |
| INSERT_FIRST */ |
| if (c->position == PLAYLIST_INSERT_FIRST || c->position >= 0) |
| c->position = insert_pos + 1; |
| |
| if (((c->count)%PLAYLIST_DISPLAY_COUNT) == 0) |
| { |
| unsigned char* count_str; |
| |
| if (c->queue) |
| count_str = ID2P(LANG_PLAYLIST_QUEUE_COUNT); |
| else |
| count_str = ID2P(LANG_PLAYLIST_INSERT_COUNT); |
| |
| display_playlist_count(c->count, count_str, false); |
| |
| if ((c->count) == PLAYLIST_DISPLAY_COUNT && |
| (audio_status() & AUDIO_STATUS_PLAY) && |
| c->playlist->started) |
| audio_flush_and_reload_tracks(); |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * remove track at specified position |
| */ |
| static int remove_track_from_playlist(struct playlist_info* playlist, |
| int position, bool write) |
| { |
| int i; |
| bool inserted; |
| |
| if (playlist->amount <= 0) |
| return -1; |
| |
| inserted = playlist->indices[position] & PLAYLIST_INSERT_TYPE_MASK; |
| |
| /* shift indices now that track has been removed */ |
| for (i=position; i<playlist->amount; i++) |
| { |
| playlist->indices[i] = playlist->indices[i+1]; |
| #ifdef HAVE_DIRCACHE |
| if (playlist->filenames) |
| playlist->filenames[i] = playlist->filenames[i+1]; |
| #endif |
| } |
| |
| playlist->amount--; |
| |
| if (inserted) |
| playlist->num_inserted_tracks--; |
| else |
| playlist->deleted = true; |
| |
| /* update stored indices if needed */ |
| if (position < playlist->index) |
| playlist->index--; |
| |
| if (position < playlist->first_index) |
| { |
| playlist->first_index--; |
| |
| if (write) |
| { |
| global_status.resume_first_index = playlist->first_index; |
| status_save(); |
| } |
| } |
| |
| if (position <= playlist->last_insert_pos) |
| playlist->last_insert_pos--; |
| |
| if (write && playlist->control_fd >= 0) |
| { |
| int result = update_control(playlist, PLAYLIST_COMMAND_DELETE, |
| position, -1, NULL, NULL, NULL); |
| |
| if (result < 0) |
| return result; |
| |
| sync_control(playlist, false); |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * randomly rearrange the array of indices for the playlist. If start_current |
| * is true then update the index to the new index of the current playing track |
| */ |
| static int randomise_playlist(struct playlist_info* playlist, |
| unsigned int seed, bool start_current, |
| bool write) |
| { |
| int count; |
| int candidate; |
| long store; |
| unsigned int current = playlist->indices[playlist->index]; |
| |
| /* seed 0 is used to identify sorted playlist for resume purposes */ |
| if (seed == 0) |
| seed = 1; |
| |
| /* seed with the given seed */ |
| srand(seed); |
| |
| /* randomise entire indices list */ |
| for(count = playlist->amount - 1; count >= 0; count--) |
| { |
| /* the rand is from 0 to RAND_MAX, so adjust to our value range */ |
| candidate = rand() % (count + 1); |
| |
| /* now swap the values at the 'count' and 'candidate' positions */ |
| store = playlist->indices[candidate]; |
| playlist->indices[candidate] = playlist->indices[count]; |
| playlist->indices[count] = store; |
| #ifdef HAVE_DIRCACHE |
| if (playlist->filenames) |
| { |
| store = (long)playlist->filenames[candidate]; |
| playlist->filenames[candidate] = playlist->filenames[count]; |
| playlist->filenames[count] = (struct dircache_entry *)store; |
| } |
| #endif |
| } |
| |
| if (start_current) |
| find_and_set_playlist_index(playlist, current); |
| |
| /* indices have been moved so last insert position is no longer valid */ |
| playlist->last_insert_pos = -1; |
| |
| playlist->seed = seed; |
| if (playlist->num_inserted_tracks > 0 || playlist->deleted) |
| playlist->shuffle_modified = true; |
| |
| if (write) |
| { |
| update_control(playlist, PLAYLIST_COMMAND_SHUFFLE, seed, |
| playlist->first_index, NULL, NULL, NULL); |
| global_status.resume_seed = seed; |
| status_save(); |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * Sort the array of indices for the playlist. If start_current is true then |
| * set the index to the new index of the current song. |
| */ |
| static int sort_playlist(struct playlist_info* playlist, bool start_current, |
| bool write) |
| { |
| unsigned int current = playlist->indices[playlist->index]; |
| |
| if (playlist->amount > 0) |
| qsort(playlist->indices, playlist->amount, |
| sizeof(playlist->indices[0]), compare); |
| |
| #ifdef HAVE_DIRCACHE |
| /** We need to re-check the song names from disk because qsort can't |
| * sort two arrays at once :/ |
| * FIXME: Please implement a better way to do this. */ |
| memset(playlist->filenames, 0, playlist->max_playlist_size * sizeof(int)); |
| queue_post(&playlist_queue, PLAYLIST_LOAD_POINTERS, 0); |
| #endif |
| |
| if (start_current) |
| find_and_set_playlist_index(playlist, current); |
| |
| /* indices have been moved so last insert position is no longer valid */ |
| playlist->last_insert_pos = -1; |
| |
| if (!playlist->num_inserted_tracks && !playlist->deleted) |
| playlist->shuffle_modified = false; |
| if (write && playlist->control_fd >= 0) |
| { |
| update_control(playlist, PLAYLIST_COMMAND_UNSHUFFLE, |
| playlist->first_index, -1, NULL, NULL, NULL); |
| global_status.resume_seed = 0; |
| status_save(); |
| } |
| |
| return 0; |
| } |
| |
| /* Calculate how many steps we have to really step when skipping entries |
| * marked as bad. |
| */ |
| static int calculate_step_count(const struct playlist_info *playlist, int steps) |
| { |
| int i, count, direction; |
| int index; |
| int stepped_count = 0; |
| |
| if (steps < 0) |
| { |
| direction = -1; |
| count = -steps; |
| } |
| else |
| { |
| direction = 1; |
| count = steps; |
| } |
| |
| index = playlist->index; |
| i = 0; |
| do { |
| /* Boundary check */ |
| if (index < 0) |
| index += playlist->amount; |
| if (index >= playlist->amount) |
| index -= playlist->amount; |
| |
| /* Check if we found a bad entry. */ |
| if (playlist->indices[index] & PLAYLIST_SKIPPED) |
| { |
| steps += direction; |
| /* Are all entries bad? */ |
| if (stepped_count++ > playlist->amount) |
| break ; |
| } |
| else |
| i++; |
| |
| index += direction; |
| } while (i <= count); |
| |
| return steps; |
| } |
| |
| /* Marks the index of the track to be skipped that is "steps" away from |
| * current playing track. |
| */ |
| void playlist_skip_entry(struct playlist_info *playlist, int steps) |
| { |
| int index; |
| |
| if (playlist == NULL) |
| playlist = ¤t_playlist; |
| |
| /* need to account for already skipped tracks */ |
| steps = calculate_step_count(playlist, steps); |
| |
| index = playlist->index + steps; |
| if (index < 0) |
| index += playlist->amount; |
| else if (index >= playlist->amount) |
| index -= playlist->amount; |
| |
| playlist->indices[index] |= PLAYLIST_SKIPPED; |
| } |
| |
| /* |
| * returns the index of the track that is "steps" away from current playing |
| * track. |
| */ |
| static int get_next_index(const struct playlist_info* playlist, int steps, |
| int repeat_mode) |
| { |
| int current_index = playlist->index; |
| int next_index = -1; |
| |
| if (playlist->amount <= 0) |
| return -1; |
| |
| if (repeat_mode == -1) |
| repeat_mode = global_settings.repeat_mode; |
| |
| if (repeat_mode == REPEAT_SHUFFLE && playlist->amount <= 1) |
| repeat_mode = REPEAT_ALL; |
| |
| steps = calculate_step_count(playlist, steps); |
| switch (repeat_mode) |
| { |
| case REPEAT_SHUFFLE: |
| /* Treat repeat shuffle just like repeat off. At end of playlist, |
| play will be resumed in playlist_next() */ |
| case REPEAT_OFF: |
| { |
| current_index = rotate_index(playlist, current_index); |
| next_index = current_index+steps; |
| if ((next_index < 0) || (next_index >= playlist->amount)) |
| next_index = -1; |
| else |
| next_index = (next_index+playlist->first_index) % |
| playlist->amount; |
| |
| break; |
| } |
| |
| case REPEAT_ONE: |
| #ifdef AB_REPEAT_ENABLE |
| case REPEAT_AB: |
| #endif |
| next_index = current_index; |
| break; |
| |
| case REPEAT_ALL: |
| default: |
| { |
| next_index = (current_index+steps) % playlist->amount; |
| while (next_index < 0) |
| next_index += playlist->amount; |
| |
| if (steps >= playlist->amount) |
| { |
| int i, index; |
| |
| index = next_index; |
| next_index = -1; |
| |
| /* second time around so skip the queued files */ |
| for (i=0; i<playlist->amount; i++) |
| { |
| if (playlist->indices[index] & PLAYLIST_QUEUE_MASK) |
| index = (index+1) % playlist->amount; |
| else |
| { |
| next_index = index; |
| break; |
| } |
| } |
| } |
| break; |
| } |
| } |
| |
| /* No luck if the whole playlist was bad. */ |
| if (playlist->indices[next_index] & PLAYLIST_SKIPPED) |
| return -1; |
| |
| return next_index; |
| } |
| |
| /* |
| * Search for the seek track and set appropriate indices. Used after shuffle |
| * to make sure the current index is still pointing to correct track. |
| */ |
| static void find_and_set_playlist_index(struct playlist_info* playlist, |
| unsigned int seek) |
| { |
| int i; |
| |
| /* Set the index to the current song */ |
| for (i=0; i<playlist->amount; i++) |
| { |
| if (playlist->indices[i] == seek) |
| { |
| playlist->index = playlist->first_index = i; |
| |
| if (playlist->current) |
| { |
| global_status.resume_first_index = i; |
| status_save(); |
| } |
| |
| break; |
| } |
| } |
| } |
| |
| /* |
| * used to sort track indices. Sort order is as follows: |
| * 1. Prepended tracks (in prepend order) |
| * 2. Playlist/directory tracks (in playlist order) |
| * 3. Inserted/Appended tracks (in insert order) |
| */ |
| static int compare(const void* p1, const void* p2) |
| { |
| unsigned long* e1 = (unsigned long*) p1; |
| unsigned long* e2 = (unsigned long*) p2; |
| unsigned long flags1 = *e1 & PLAYLIST_INSERT_TYPE_MASK; |
| unsigned long flags2 = *e2 & PLAYLIST_INSERT_TYPE_MASK; |
| |
| if (flags1 == flags2) |
| return (*e1 & PLAYLIST_SEEK_MASK) - (*e2 & PLAYLIST_SEEK_MASK); |
| else if (flags1 == PLAYLIST_INSERT_TYPE_PREPEND || |
| flags2 == PLAYLIST_INSERT_TYPE_APPEND) |
| return -1; |
| else if (flags1 == PLAYLIST_INSERT_TYPE_APPEND || |
| flags2 == PLAYLIST_INSERT_TYPE_PREPEND) |
| return 1; |
| else if (flags1 && flags2) |
| return (*e1 & PLAYLIST_SEEK_MASK) - (*e2 & PLAYLIST_SEEK_MASK); |
| else |
| return *e1 - *e2; |
| } |
| |
| #ifdef HAVE_DIRCACHE |
| /** |
| * Thread to update filename pointers to dircache on background |
| * without affecting playlist load up performance. This thread also flushes |
| * any pending control commands when the disk spins up. |
| */ |
| static bool playlist_flush_callback(void) |
| { |
| struct playlist_info *playlist; |
| playlist = ¤t_playlist; |
| if (playlist->control_fd >= 0) |
| { |
| if (playlist->num_cached > 0) |
| { |
| mutex_lock(&playlist->control_mutex); |
| flush_cached_control(playlist); |
| mutex_unlock(&playlist->control_mutex); |
| } |
| sync_control(playlist, true); |
| } |
| return true; |
| } |
| |
| static void playlist_thread(void) |
| { |
| struct queue_event ev; |
| bool dirty_pointers = false; |
| static char tmp[MAX_PATH+1]; |
| |
| struct playlist_info *playlist; |
| int index; |
| int seek; |
| bool control_file; |
| |
| int sleep_time = 5; |
| |
| #ifndef HAVE_FLASH_STORAGE |
| if (global_settings.disk_spindown > 1 && |
| global_settings.disk_spindown <= 5) |
| sleep_time = global_settings.disk_spindown - 1; |
| #endif |
| |
| while (1) |
| { |
| queue_wait_w_tmo(&playlist_queue, &ev, HZ*sleep_time); |
| |
| switch (ev.id) |
| { |
| case PLAYLIST_LOAD_POINTERS: |
| dirty_pointers = true; |
| break ; |
| |
| /* Start the background scanning after either the disk spindown |
| timeout or 5s, whichever is less */ |
| case SYS_TIMEOUT: |
| playlist = ¤t_playlist; |
| if (playlist->control_fd >= 0) |
| { |
| if (playlist->num_cached > 0) |
| register_ata_idle_func(playlist_flush_callback); |
| } |
| |
| if (!dirty_pointers) |
| break ; |
| |
| if (!dircache_is_enabled() || !playlist->filenames |
| || playlist->amount <= 0) |
| break ; |
| |
| #ifdef HAVE_ADJUSTABLE_CPU_FREQ |
| cpu_boost(true); |
| #endif |
| for (index = 0; index < playlist->amount |
| && queue_empty(&playlist_queue); index++) |
| { |
| /* Process only pointers that are not already loaded. */ |
| if (playlist->filenames[index]) |
| continue ; |
| |
| control_file = playlist->indices[index] & PLAYLIST_INSERT_TYPE_MASK; |
| seek = playlist->indices[index] & PLAYLIST_SEEK_MASK; |
| |
| /* Load the filename from playlist file. */ |
| if (get_filename(playlist, index, seek, control_file, tmp, |
| sizeof(tmp)) < 0) |
| break ; |
| |
| /* Set the dircache entry pointer. */ |
| playlist->filenames[index] = dircache_get_entry_ptr(tmp); |
| |
| /* And be on background so user doesn't notice any delays. */ |
| yield(); |
| } |
| |
| #ifdef HAVE_ADJUSTABLE_CPU_FREQ |
| cpu_boost(false); |
| #endif |
| dirty_pointers = false; |
| break ; |
| |
| #ifndef SIMULATOR |
| case SYS_USB_CONNECTED: |
| usb_acknowledge(SYS_USB_CONNECTED_ACK); |
| usb_wait_for_disconnect(&playlist_queue); |
| break ; |
| #endif |
| } |
| } |
| } |
| #endif |
| |
| /* |
| * gets pathname for track at seek index |
| */ |
| static int get_filename(struct playlist_info* playlist, int index, int seek, |
| bool control_file, char *buf, int buf_length) |
| { |
| int fd; |
| int max = -1; |
| char tmp_buf[MAX_PATH+1]; |
| char dir_buf[MAX_PATH+1]; |
| bool utf8 = playlist->utf8; |
| |
| if (buf_length > MAX_PATH+1) |
| buf_length = MAX_PATH+1; |
| |
| #ifdef HAVE_DIRCACHE |
| if (dircache_is_enabled() && playlist->filenames) |
| { |
| if (playlist->filenames[index] != NULL) |
| { |
| dircache_copy_path(playlist->filenames[index], tmp_buf, sizeof(tmp_buf)-1); |
| max = strlen(tmp_buf) + 1; |
| } |
| } |
| #else |
| (void)index; |
| #endif |
| |
| if (playlist->in_ram && !control_file && max < 0) |
| { |
| strncpy(tmp_buf, &playlist->buffer[seek], sizeof(tmp_buf)); |
| tmp_buf[MAX_PATH] = '\0'; |
| max = strlen(tmp_buf) + 1; |
| } |
| else if (max < 0) |
| { |
| mutex_lock(&playlist->control_mutex); |
| |
| if (control_file) |
| { |
| fd = playlist->control_fd; |
| utf8 = true; |
| } |
| else |
| { |
| if(-1 == playlist->fd) |
| playlist->fd = open(playlist->filename, O_RDONLY); |
| |
| fd = playlist->fd; |
| } |
| |
| if(-1 != fd) |
| { |
| |
| if (lseek(fd, seek, SEEK_SET) != seek) |
| max = -1; |
| else |
| { |
| max = read(fd, tmp_buf, MIN((size_t) buf_length, sizeof(tmp_buf))); |
| |
| if ((max > 0) && !utf8) |
| { |
| /* Use dir_buf as a temporary buffer. Note that dir_buf must |
| * be as large as tmp_buf. |
| */ |
| max = convert_m3u(tmp_buf, max, sizeof(tmp_buf), dir_buf); |
| } |
| } |
| } |
| |
| mutex_unlock(&playlist->control_mutex); |
| |
| if (max < 0) |
| { |
| if (control_file) |
| gui_syncsplash(HZ*2, ID2P(LANG_PLAYLIST_CONTROL_ACCESS_ERROR)); |
| else |
| gui_syncsplash(HZ*2, ID2P(LANG_PLAYLIST_ACCESS_ERROR)); |
| |
| return max; |
| } |
| } |
| |
| strncpy(dir_buf, playlist->filename, playlist->dirlen-1); |
| dir_buf[playlist->dirlen-1] = 0; |
| |
| return (format_track_path(buf, tmp_buf, buf_length, max, dir_buf)); |
| } |
| |
| static int get_next_directory(char *dir){ |
| return get_next_dir(dir,true,false); |
| } |
| |
| static int get_previous_directory(char *dir){ |
| return get_next_dir(dir,false,false); |
| } |
| |
| /* |
| * search through all the directories (starting with the current) to find |
| * one that has tracks to play |
| */ |
| static int get_next_dir(char *dir, bool is_forward, bool recursion) |
| { |
| struct playlist_info* playlist = ¤t_playlist; |
| int result = -1; |
| int sort_dir = global_settings.sort_dir; |
| char *start_dir = NULL; |
| bool exit = false; |
| struct tree_context* tc = tree_get_context(); |
| int dirfilter = *(tc->dirfilter); |
| |
| if (global_settings.next_folder == FOLDER_ADVANCE_RANDOM) |
| { |
| int fd = open(ROCKBOX_DIR "/folder_advance_list.dat",O_RDONLY); |
| char buffer[MAX_PATH]; |
| int folder_count = 0,i; |
| srand(current_tick); |
| *(tc->dirfilter) = SHOW_MUSIC; |
| if (fd >= 0) |
| { |
| read(fd,&folder_count,sizeof(int)); |
| while (!exit) |
| { |
| i = rand()%folder_count; |
| lseek(fd,sizeof(int) + (MAX_PATH*i),SEEK_SET); |
| read(fd,buffer,MAX_PATH); |
| if (check_subdir_for_music(buffer,"") ==0) |
| exit = true; |
| } |
| strcpy(dir,buffer); |
| close(fd); |
| *(tc->dirfilter) = dirfilter; |
| reload_directory(); |
| return 0; |
| } |
| } |
| /* not random folder advance */ |
| if (recursion){ |
| /* start with root */ |
| dir[0] = '\0'; |
| } |
| else{ |
| /* start with current directory */ |
| strncpy(dir, playlist->filename, playlist->dirlen-1); |
| dir[playlist->dirlen-1] = '\0'; |
| } |
| |
| /* use the tree browser dircache to load files */ |
| *(tc->dirfilter) = SHOW_ALL; |
| |
| /* sort in another direction if previous dir is requested */ |
| if(!is_forward){ |
| if ((global_settings.sort_dir == 0) || (global_settings.sort_dir == 3)) |
| global_settings.sort_dir = 4; |
| else if (global_settings.sort_dir == 1) |
| global_settings.sort_dir = 2; |
| else if (global_settings.sort_dir == 2) |
| global_settings.sort_dir = 1; |
| else if (global_settings.sort_dir == 4) |
| global_settings.sort_dir = 0; |
| } |
| |
| while (!exit) |
| { |
| struct entry *files; |
| int num_files = 0; |
| int i; |
| |
| if (ft_load(tc, (dir[0]=='\0')?"/":dir) < 0) |
| { |
| gui_syncsplash(HZ*2, ID2P(LANG_PLAYLIST_DIRECTORY_ACCESS_ERROR)); |
| exit = true; |
| result = -1; |
| break; |
| } |
| |
| files = (struct entry*) tc->dircache; |
| num_files = tc->filesindir; |
| |
| for (i=0; i<num_files; i++) |
| { |
| /* user abort */ |
| if (action_userabort(TIMEOUT_NOBLOCK)) |
| { |
| result = -1; |
| exit = true; |
| break; |
| } |
| |
| if (files[i].attr & ATTR_DIRECTORY) |
| { |
| if (!start_dir) |
| { |
| result = check_subdir_for_music(dir, files[i].name); |
| if (result != -1) |
| { |
| exit = true; |
| break; |
| } |
| } |
| else if (!strcmp(start_dir, files[i].name)) |
| start_dir = NULL; |
| } |
| } |
| |
| if (!exit) |
| { |
| /* move down to parent directory. current directory name is |
| stored as the starting point for the search in parent */ |
| start_dir = strrchr(dir, '/'); |
| if (start_dir) |
| { |
| *start_dir = '\0'; |
| start_dir++; |
| } |
| else |
| break; |
| } |
| } |
| |
| /* restore dirfilter & sort_dir */ |
| *(tc->dirfilter) = dirfilter; |
| global_settings.sort_dir = sort_dir; |
| |
| /* special case if nothing found: try start searching again from root */ |
| if (result == -1 && !recursion){ |
| result = get_next_dir(dir,is_forward, true); |
| } |
| |
| return result; |
| } |
| |
| /* |
| * Checks if there are any music files in the dir or any of its |
| * subdirectories. May be called recursively. |
| */ |
| static int check_subdir_for_music(char *dir, char *subdir) |
| { |
| int result = -1; |
| int dirlen = strlen(dir); |
| int num_files = 0; |
| int i; |
| struct entry *files; |
| bool has_music = false; |
| bool has_subdir = false; |
| struct tree_context* tc = tree_get_context(); |
| |
| snprintf(dir+dirlen, MAX_PATH-dirlen, "/%s", subdir); |
| |
| if (ft_load(tc, dir) < 0) |
| { |
| gui_syncsplash(HZ*2, ID2P(LANG_PLAYLIST_DIRECTORY_ACCESS_ERROR)); |
| return -2; |
| } |
| |
| files = (struct entry*) tc->dircache; |
| num_files = tc->filesindir; |
| |
| for (i=0; i<num_files; i++) |
| { |
| if (files[i].attr & ATTR_DIRECTORY) |
| has_subdir = true; |
| else if ((files[i].attr & FILE_ATTR_MASK) == FILE_ATTR_AUDIO) |
| { |
| has_music = true; |
| break; |
| } |
| } |
| |
| if (has_music) |
| return 0; |
| |
| if (has_subdir) |
| { |
| for (i=0; i<num_files; i++) |
| { |
| if (action_userabort(TIMEOUT_NOBLOCK)) |
| { |
| result = -2; |
| break; |
| } |
| |
| if (files[i].attr & ATTR_DIRECTORY) |
| { |
| result = check_subdir_for_music(dir, files[i].name); |
| if (!result) |
| break; |
| } |
| } |
| } |
| |
| if (result < 0) |
| { |
| if (dirlen) |
| { |
| dir[dirlen] = '\0'; |
| } |
| else |
| { |
| strcpy(dir, "/"); |
| } |
| |
| /* we now need to reload our current directory */ |
| if(ft_load(tc, dir) < 0) |
| gui_syncsplash(HZ*2, |
| ID2P(LANG_PLAYLIST_DIRECTORY_ACCESS_ERROR)); |
| } |
| |
| return result; |
| } |
| |
| /* |
| * Returns absolute path of track |
| */ |
| static int format_track_path(char *dest, char *src, int buf_length, int max, |
| const char *dir) |
| { |
| int i = 0; |
| int j; |
| char *temp_ptr; |
| |
| /* Zero-terminate the file name */ |
| while((src[i] != '\n') && |
| (src[i] != '\r') && |
| (i < max)) |
| i++; |
| |
| /* Now work back killing white space */ |
| while((src[i-1] == ' ') || |
| (src[i-1] == '\t')) |
| i--; |
| |
| src[i]=0; |
| |
| /* replace backslashes with forward slashes */ |
| for ( j=0; j<i; j++ ) |
| if ( src[j] == '\\' ) |
| src[j] = '/'; |
| |
| if('/' == src[0]) |
| { |
| strncpy(dest, src, buf_length); |
| } |
| else |
| { |
| /* handle dos style drive letter */ |
| if (':' == src[1]) |
| strncpy(dest, &src[2], buf_length); |
| else if (!strncmp(src, "../", 3)) |
| { |
| /* handle relative paths */ |
| i=3; |
| while(!strncmp(&src[i], "../", 3)) |
| i += 3; |
| for (j=0; j<i/3; j++) { |
| temp_ptr = strrchr(dir, '/'); |
| if (temp_ptr) |
| *temp_ptr = '\0'; |
| else |
| break; |
| } |
| snprintf(dest, buf_length, "%s/%s", dir, &src[i]); |
| } |
| else if ( '.' == src[0] && '/' == src[1] ) { |
| snprintf(dest, buf_length, "%s/%s", dir, &src[2]); |
| } |
| else { |
| snprintf(dest, buf_length, "%s/%s", dir, src); |
| } |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * Display splash message showing progress of playlist/directory insertion or |
| * save. |
| */ |
| static void display_playlist_count(int count, const unsigned char *fmt, |
| bool final) |
| { |
| static long talked_tick = 0; |
| long id = P2ID(fmt); |
| if(global_settings.talk_menu && id>=0) |
| { |
| if(final || (count && (talked_tick == 0 |
| || TIME_AFTER(current_tick, talked_tick+5*HZ)))) |
| { |
| talked_tick = current_tick; |
| talk_number(count, false); |
| talk_id(id, true); |
| } |
| } |
| fmt = P2STR(fmt); |
| |
| gui_syncsplash(0, fmt, count, str(LANG_OFF_ABORT)); |
| } |
| |
| /* |
| * Display buffer full message |
| */ |
| static void display_buffer_full(void) |
| { |
| gui_syncsplash(HZ*2, ID2P(LANG_PLAYLIST_BUFFER_FULL)); |
| } |
| |
| /* |
| * Flush any cached control commands to disk. Called when playlist is being |
| * modified. Returns 0 on success and -1 on failure. |
| */ |
| static int flush_cached_control(struct playlist_info* playlist) |
| { |
| int result = 0; |
| int i; |
| |
| if (!playlist->num_cached) |
| return 0; |
| |
| lseek(playlist->control_fd, 0, SEEK_END); |
| |
| for (i=0; i<playlist->num_cached; i++) |
| { |
| struct playlist_control_cache* cache = |
| &(playlist->control_cache[i]); |
| |
| switch (cache->command) |
| { |
| case PLAYLIST_COMMAND_PLAYLIST: |
| result = fdprintf(playlist->control_fd, "P:%d:%s:%s\n", |
| cache->i1, cache->s1, cache->s2); |
| break; |
| case PLAYLIST_COMMAND_ADD: |
| case PLAYLIST_COMMAND_QUEUE: |
| result = fdprintf(playlist->control_fd, "%c:%d:%d:", |
| (cache->command == PLAYLIST_COMMAND_ADD)?'A':'Q', |
| cache->i1, cache->i2); |
| if (result > 0) |
| { |
| /* save the position in file where name is written */ |
| int* seek_pos = (int *)cache->data; |
| *seek_pos = lseek(playlist->control_fd, 0, SEEK_CUR); |
| result = fdprintf(playlist->control_fd, "%s\n", |
| cache->s1); |
| } |
| break; |
| case PLAYLIST_COMMAND_DELETE: |
| result = fdprintf(playlist->control_fd, "D:%d\n", cache->i1); |
| break; |
| case PLAYLIST_COMMAND_SHUFFLE: |
| result = fdprintf(playlist->control_fd, "S:%d:%d\n", |
| cache->i1, cache->i2); |
| break; |
| case PLAYLIST_COMMAND_UNSHUFFLE: |
| result = fdprintf(playlist->control_fd, "U:%d\n", cache->i1); |
| break; |
| case PLAYLIST_COMMAND_RESET: |
| result = fdprintf(playlist->control_fd, "R\n"); |
| break; |
| default: |
| break; |
| } |
| |
| if (result <= 0) |
| break; |
| } |
| |
| if (result > 0) |
| { |
| if (global_status.resume_seed >= 0) |
| { |
| global_status.resume_seed = -1; |
| status_save(); |
| } |
| |
| playlist->num_cached = 0; |
| playlist->pending_control_sync = true; |
| |
| result = 0; |
| } |
| else |
| { |
| result = -1; |
| gui_syncsplash(HZ*2, ID2P(LANG_PLAYLIST_CONTROL_UPDATE_ERROR)); |
| } |
| |
| return result; |
| } |
| |
| /* |
| * Update control data with new command. Depending on the command, it may be |
| * cached or flushed to disk. |
| */ |
| static int update_control(struct playlist_info* playlist, |
| enum playlist_command command, int i1, int i2, |
| const char* s1, const char* s2, void* data) |
| { |
| int result = 0; |
| struct playlist_control_cache* cache; |
| bool flush = false; |
| |
| mutex_lock(&playlist->control_mutex); |
| |
| cache = &(playlist->control_cache[playlist->num_cached++]); |
| |
| cache->command = command; |
| cache->i1 = i1; |
| cache->i2 = i2; |
| cache->s1 = s1; |
| cache->s2 = s2; |
| cache->data = data; |
| |
| switch (command) |
| { |
| case PLAYLIST_COMMAND_PLAYLIST: |
| case PLAYLIST_COMMAND_ADD: |
| case PLAYLIST_COMMAND_QUEUE: |
| #ifndef HAVE_DIRCACHE |
| case PLAYLIST_COMMAND_DELETE: |
| case PLAYLIST_COMMAND_RESET: |
| #endif |
| flush = true; |
| break; |
| case PLAYLIST_COMMAND_SHUFFLE: |
| case PLAYLIST_COMMAND_UNSHUFFLE: |
| default: |
| /* only flush when needed */ |
| break; |
| } |
| |
| if (flush || playlist->num_cached == PLAYLIST_MAX_CACHE) |
| result = flush_cached_control(playlist); |
| |
| mutex_unlock(&playlist->control_mutex); |
| |
| return result; |
| } |
| |
| /* |
| * sync control file to disk |
| */ |
| static void sync_control(struct playlist_info* playlist, bool force) |
| { |
| #ifdef HAVE_DIRCACHE |
| if (playlist->started && force) |
| #else |
| (void) force; |
| |
| if (playlist->started) |
| #endif |
| { |
| if (playlist->pending_control_sync) |
| { |
| mutex_lock(&playlist->control_mutex); |
| fsync(playlist->control_fd); |
| playlist->pending_control_sync = false; |
| mutex_unlock(&playlist->control_mutex); |
| } |
| } |
| } |
| |
| /* |
| * Rotate indices such that first_index is index 0 |
| */ |
| static int rotate_index(const struct playlist_info* playlist, int index) |
| { |
| index -= playlist->first_index; |
| if (index < 0) |
| index += playlist->amount; |
| |
| return index; |
| } |
| |
| /* |
| * Initialize playlist entries at startup |
| */ |
| void playlist_init(void) |
| { |
| struct playlist_info* playlist = ¤t_playlist; |
| |
| playlist->current = true; |
| snprintf(playlist->control_filename, sizeof(playlist->control_filename), |
| "%s", PLAYLIST_CONTROL_FILE); |
| playlist->fd = -1; |
| playlist->control_fd = -1; |
| playlist->max_playlist_size = global_settings.max_files_in_playlist; |
| playlist->indices = buffer_alloc( |
| playlist->max_playlist_size * sizeof(int)); |
| playlist->buffer_size = |
| AVERAGE_FILENAME_LENGTH * global_settings.max_files_in_dir; |
| playlist->buffer = buffer_alloc(playlist->buffer_size); |
| mutex_init(&playlist->control_mutex); |
| empty_playlist(playlist, true); |
| |
| #ifdef HAVE_DIRCACHE |
| playlist->filenames = buffer_alloc( |
| playlist->max_playlist_size * sizeof(int)); |
| memset(playlist->filenames, 0, |
| playlist->max_playlist_size * sizeof(int)); |
| create_thread(playlist_thread, playlist_stack, sizeof(playlist_stack), |
| 0, playlist_thread_name IF_PRIO(, PRIORITY_BACKGROUND) |
| IF_COP(, CPU)); |
| queue_init(&playlist_queue, true); |
| #endif |
| } |
| |
| /* |
| * Clean playlist at shutdown |
| */ |
| void playlist_shutdown(void) |
| { |
| struct playlist_info* playlist = ¤t_playlist; |
| |
| if (playlist->control_fd >= 0) |
| { |
| mutex_lock(&playlist->control_mutex); |
| |
| if (playlist->num_cached > 0) |
| flush_cached_control(playlist); |
| |
| close(playlist->control_fd); |
| |
| mutex_unlock(&playlist->control_mutex); |
| } |
| } |
| |
| /* |
| * Create new playlist |
| */ |
| int playlist_create(const char *dir, const char *file) |
| { |
| struct playlist_info* playlist = ¤t_playlist; |
| |
| new_playlist(playlist, dir, file); |
| |
| if (file) |
| /* load the playlist file */ |
| add_indices_to_playlist(playlist, NULL, 0); |
| |
| return 0; |
| } |
| |
| #define PLAYLIST_COMMAND_SIZE (MAX_PATH+12) |
| |
| /* |
| * Restore the playlist state based on control file commands. Called to |
| * resume playback after shutdown. |
| */ |
| int playlist_resume(void) |
| { |
| struct playlist_info* playlist = ¤t_playlist; |
| char *buffer; |
| size_t buflen; |
| int nread; |
| int total_read = 0; |
| int control_file_size = 0; |
| bool first = true; |
| bool sorted = true; |
| |
| /* use mp3 buffer for maximum load speed */ |
| #if CONFIG_CODEC != SWCODEC |
| talk_buffer_steal(); /* we use the mp3 buffer, need to tell */ |
| buflen = (audiobufend - audiobuf); |
| buffer = (char *)audiobuf; |
| #else |
| buffer = (char *)audio_get_buffer(false, &buflen); |
| #endif |
| |
| empty_playlist(playlist, true); |
| |
| gui_syncsplash(0, ID2P(LANG_WAIT)); |
| playlist->control_fd = open(playlist->control_filename, O_RDWR); |
| if (playlist->control_fd < 0) |
| { |
| gui_syncsplash(HZ*2, ID2P(LANG_PLAYLIST_CONTROL_ACCESS_ERROR)); |
| return -1; |
| } |
| playlist->control_created = true; |
| |
| control_file_size = filesize(playlist->control_fd); |
| if (control_file_size <= 0) |
| { |
| gui_syncsplash(HZ*2, ID2P(LANG_PLAYLIST_CONTROL_ACCESS_ERROR)); |
| return -1; |
| } |
| |
| /* read a small amount first to get the header */ |
| nread = read(playlist->control_fd, buffer, |
| PLAYLIST_COMMAND_SIZE<buflen?PLAYLIST_COMMAND_SIZE:buflen); |
| if(nread <= 0) |
| { |
| gui_syncsplash(HZ*2, ID2P(LANG_PLAYLIST_CONTROL_ACCESS_ERROR)); |
| return -1; |
| } |
| |
| playlist->started = true; |
| |
| while (1) |
| { |
| int result = 0; |
| int count; |
| enum playlist_command current_command = PLAYLIST_COMMAND_COMMENT; |
| int last_newline = 0; |
| int str_count = -1; |
| bool newline = true; |
| bool exit_loop = false; |
| char *p = buffer; |
| char *str1 = NULL; |
| char *str2 = NULL; |
| char *str3 = NULL; |
| unsigned long last_tick = current_tick; |
| |
| for(count=0; count<nread && !exit_loop; count++,p++) |
| { |
| /* So a splash while we are loading. */ |
| if (current_tick - last_tick > HZ/4) |
| { |
| gui_syncsplash(0, str(LANG_LOADING_PERCENT), |
| (total_read+count)*100/control_file_size, |
| str(LANG_OFF_ABORT)); |
| if (action_userabort(TIMEOUT_NOBLOCK)) |
| { |
| /* FIXME: |
| * Not sure how to implement this, somebody more familiar |
| * with the code, please fix this. */ |
| } |
| last_tick = current_tick; |
| } |
| |
| /* Are we on a new line? */ |
| if((*p == '\n') || (*p == '\r')) |
| { |
| *p = '\0'; |
| |
| /* save last_newline in case we need to load more data */ |
| last_newline = count; |
| |
| switch (current_command) |
| { |
| case PLAYLIST_COMMAND_PLAYLIST: |
| { |
| /* str1=version str2=dir str3=file */ |
| int version; |
| |
| if (!str1) |
| { |
| result = -1; |
| exit_loop = true; |
| break; |
| } |
| |
| if (!str2) |
| str2 = ""; |
| |
| if (!str3) |
| str3 = ""; |
| |
| version = atoi(str1); |
| |
| if (version != PLAYLIST_CONTROL_FILE_VERSION) |
| return -1; |
| |
| update_playlist_filename(playlist, str2, str3); |
| |
| if (str3[0] != '\0') |
| { |
| /* NOTE: add_indices_to_playlist() overwrites the |
| audiobuf so we need to reload control file |
| data */ |
| add_indices_to_playlist(playlist, NULL, 0); |
| } |
| else if (str2[0] != '\0') |
| { |
| playlist->in_ram = true; |
| resume_directory(str2); |
| } |
| |
| /* load the rest of the data */ |
| first = false; |
| exit_loop = true; |
| |
| break; |
| } |
| case PLAYLIST_COMMAND_ADD: |
| case PLAYLIST_COMMAND_QUEUE: |
| { |
| /* str1=position str2=last_position str3=file */ |
| int position, last_position; |
| bool queue; |
| |
| if (!str1 || !str2 || !str3) |
| { |
| result = -1; |
| exit_loop = true; |
| break; |
| } |
| |
| position = atoi(str1); |
| last_position = atoi(str2); |
| |
| queue = (current_command == PLAYLIST_COMMAND_ADD)? |
| false:true; |
| |
| /* seek position is based on str3's position in |
| buffer */ |
| if (add_track_to_playlist(playlist, str3, position, |
| queue, total_read+(str3-buffer)) < 0) |
| return -1; |
| |
| playlist->last_insert_pos = last_position; |
| |
| break; |
| } |
| case PLAYLIST_COMMAND_DELETE: |
| { |
| /* str1=position */ |
| int position; |
| |
| if (!str1) |
| { |
| result = -1; |
| exit_loop = true; |
| break; |
| } |
| |
| position = atoi(str1); |
| |
| if (remove_track_from_playlist(playlist, position, |
| false) < 0) |
| return -1; |
| |
| break; |
| } |
| case PLAYLIST_COMMAND_SHUFFLE: |
| { |
| /* str1=seed str2=first_index */ |
| int seed; |
| |
| if (!str1 || !str2) |
| { |
| result = -1; |
| exit_loop = true; |
| break; |
| } |
| |
| if (!sorted) |
| { |
| /* Always sort list before shuffling */ |
| sort_playlist(playlist, false, false); |
| } |
| |
| seed = atoi(str1); |
| playlist->first_index = atoi(str2); |
| |
| if (randomise_playlist(playlist, seed, false, |
| false) < 0) |
| return -1; |
| |
| sorted = false; |
| break; |
| } |
| case PLAYLIST_COMMAND_UNSHUFFLE: |
| { |
| /* str1=first_index */ |
| if (!str1) |
| { |
| result = -1; |
| exit_loop = true; |
| break; |
| } |
| |
| playlist->first_index = atoi(str1); |
| |
| if (sort_playlist(playlist, false, false) < 0) |
| return -1; |
| |
| sorted = true; |
| break; |
| } |
| case PLAYLIST_COMMAND_RESET: |
| { |
| playlist->last_insert_pos = -1; |
| break; |
| } |
| case PLAYLIST_COMMAND_COMMENT: |
| default: |
| break; |
| } |
| |
| newline = true; |
| |
| /* to ignore any extra newlines */ |
| current_command = PLAYLIST_COMMAND_COMMENT; |
| } |
| else if(newline) |
| { |
| newline = false; |
| |
| /* first non-comment line must always specify playlist */ |
| if (first && *p != 'P' && *p != '#') |
| { |
| result = -1; |
| exit_loop = true; |
| break; |
| } |
| |
| switch (*p) |
| { |
| case 'P': |
| /* playlist can only be specified once */ |
| if (!first) |
| { |
| result = -1; |
| exit_loop = true; |
| break; |
| } |
| |
| current_command = PLAYLIST_COMMAND_PLAYLIST; |
| break; |
| case 'A': |
| current_command = PLAYLIST_COMMAND_ADD; |
| break; |
| case 'Q': |
| current_command = PLAYLIST_COMMAND_QUEUE; |
| break; |
| case 'D': |
| current_command = PLAYLIST_COMMAND_DELETE; |
| break; |
| case 'S': |
| current_command = PLAYLIST_COMMAND_SHUFFLE; |
| break; |
| case 'U': |
| current_command = PLAYLIST_COMMAND_UNSHUFFLE; |
| break; |
| case 'R': |
| current_command = PLAYLIST_COMMAND_RESET; |
| break; |
| case '#': |
| current_command = PLAYLIST_COMMAND_COMMENT; |
| break; |
| default: |
| result = -1; |
| exit_loop = true; |
| break; |
| } |
| |
| str_count = -1; |
| str1 = NULL; |
| str2 = NULL; |
| str3 = NULL; |
| } |
| else if(current_command != PLAYLIST_COMMAND_COMMENT) |
| { |
| /* all control file strings are separated with a colon. |
| Replace the colon with 0 to get proper strings that can be |
| used by commands above */ |
| if (*p == ':') |
| { |
| *p = '\0'; |
| str_count++; |
| |
| if ((count+1) < nread) |
| { |
| switch (str_count) |
| { |
| case 0: |
| str1 = p+1; |
| break; |
| case 1: |
| str2 = p+1; |
| break; |
| case 2: |
| str3 = p+1; |
| break; |
| default: |
| /* allow last string to contain colons */ |
| *p = ':'; |
| break; |
| } |
| } |
| } |
| } |
| } |
| |
| if (result < 0) |
| { |
| gui_syncsplash(HZ*2, ID2P(LANG_PLAYLIST_CONTROL_INVALID)); |
| return result; |
| } |
| |
| if (!newline || (exit_loop && count<nread)) |
| { |
| if ((total_read + count) >= control_file_size) |
| { |
| /* no newline at end of control file */ |
| gui_syncsplash(HZ*2, ID2P(LANG_PLAYLIST_CONTROL_INVALID)); |
| return -1; |
| } |
| |
| /* We didn't end on a newline or we exited loop prematurely. |
| Either way, re-read the remainder. */ |
| count = last_newline; |
| lseek(playlist->control_fd, total_read+count, SEEK_SET); |
| } |
| |
| total_read += count; |
| |
| if (first) |
| /* still looking for header */ |
| nread = read(playlist->control_fd, buffer, |
| PLAYLIST_COMMAND_SIZE<buflen?PLAYLIST_COMMAND_SIZE:buflen); |
| else |
| nread = read(playlist->control_fd, buffer, buflen); |
| |
| /* Terminate on EOF */ |
| if(nread <= 0) |
| { |
| if (global_status.resume_seed >= 0) |
| { |
| /* Apply shuffle command saved in settings */ |
| if (global_status.resume_seed == 0) |
| sort_playlist(playlist, false, true); |
| else |
| { |
| if (!sorted) |
| sort_playlist(playlist, false, false); |
| |
| randomise_playlist(playlist, global_status.resume_seed, |
| false, true); |
| } |
| } |
| |
| playlist->first_index = global_status.resume_first_index; |
| break; |
| } |
| } |
| |
| #ifdef HAVE_DIRCACHE |
| queue_post(&playlist_queue, PLAYLIST_LOAD_POINTERS, 0); |
| #endif |
| |
| return 0; |
| } |
| |
| /* |
| * Add track to in_ram playlist. Used when playing directories. |
| */ |
| int playlist_add(const char *filename) |
| { |
| struct playlist_info* playlist = ¤t_playlist; |
| int len = strlen(filename); |
| |
| if((len+1 > playlist->buffer_size - playlist->buffer_end_pos) || |
| (playlist->amount >= playlist->max_playlist_size)) |
| { |
| display_buffer_full(); |
| return -1; |
| } |
| |
| playlist->indices[playlist->amount] = playlist->buffer_end_pos; |
| #ifdef HAVE_DIRCACHE |
| playlist->filenames[playlist->amount] = NULL; |
| #endif |
| playlist->amount++; |
| |
| strcpy(&playlist->buffer[playlist->buffer_end_pos], filename); |
| playlist->buffer_end_pos += len; |
| playlist->buffer[playlist->buffer_end_pos++] = '\0'; |
| |
| return 0; |
| } |
| |
| /* shuffle newly created playlist using random seed. */ |
| int playlist_shuffle(int random_seed, int start_index) |
| { |
| struct playlist_info* playlist = ¤t_playlist; |
| |
| unsigned int seek_pos = 0; |
| bool start_current = false; |
| |
| if (start_index >= 0 && global_settings.play_selected) |
| { |
| /* store the seek position before the shuffle */ |
| seek_pos = playlist->indices[start_index]; |
| playlist->index = global_status.resume_first_index = |
| playlist->first_index = start_index; |
| start_current = true; |
| } |
| |
| randomise_playlist(playlist, random_seed, start_current, true); |
| |
| return playlist->index; |
| } |
| |
| /* start playing current playlist at specified index/offset */ |
| int playlist_start(int start_index, int offset) |
| { |
| struct playlist_info* playlist = ¤t_playlist; |
| |
| /* Cancel FM radio selection as previous music. For cases where we start |
| playback without going to the WPS, such as playlist insert.. or |
| playlist catalog. */ |
| previous_music_is_wps(); |
| |
| playlist->index = start_index; |
| |
| #if CONFIG_CODEC != SWCODEC |
| talk_buffer_steal(); /* will use the mp3 buffer */ |
| #endif |
| |
| playlist->started = true; |
| sync_control(playlist, false); |
| audio_play(offset); |
| |
| return 0; |
| } |
| |
| /* Returns false if 'steps' is out of bounds, else true */ |
| bool playlist_check(int steps) |
| { |
| struct playlist_info* playlist = ¤t_playlist; |
| |
| /* always allow folder navigation */ |
| if (global_settings.next_folder && playlist->in_ram) |
| return true; |
| |
| int index = get_next_index(playlist, steps, -1); |
| |
| if (index < 0 && steps >= 0 && global_settings.repeat_mode == REPEAT_SHUFFLE) |
| index = get_next_index(playlist, steps, REPEAT_ALL); |
| |
| return (index >= 0); |
| } |
| |
| /* get trackname of track that is "steps" away from current playing track. |
| NULL is used to identify end of playlist */ |
| char* playlist_peek(int steps) |
| { |
| struct playlist_info* playlist = ¤t_playlist; |
| int seek; |
| char *temp_ptr; |
| int index; |
| bool control_file; |
| |
| index = get_next_index(playlist, steps, -1); |
| if (index < 0) |
| return NULL; |
| |
| control_file = playlist->indices[index] & PLAYLIST_INSERT_TYPE_MASK; |
| seek = playlist->indices[index] & PLAYLIST_SEEK_MASK; |
| |
| if (get_filename(playlist, index, seek, control_file, now_playing, |
| MAX_PATH+1) < 0) |
| return NULL; |
| |
| temp_ptr = now_playing; |
| |
| if (!playlist->in_ram || control_file) |
| { |
| /* remove bogus dirs from beginning of path |
| (workaround for buggy playlist creation tools) */ |
| while (temp_ptr) |
| { |
| if (file_exists(temp_ptr)) |
| break; |
| |
| temp_ptr = strchr(temp_ptr+1, '/'); |
| } |
| |
| if (!temp_ptr) |
| { |
| /* Even though this is an invalid file, we still need to pass a |
| file name to the caller because NULL is used to indicate end |
| of playlist */ |
| return now_playing; |
| } |
| } |
| |
| return temp_ptr; |
| } |
| |
| /* |
| * Update indices as track has changed |
| */ |
| int playlist_next(int steps) |
| { |
| struct playlist_info* playlist = ¤t_playlist; |
| int index; |
| |
| if ( (steps > 0) |
| #ifdef AB_REPEAT_ENABLE |
| && (global_settings.repeat_mode != REPEAT_AB) |
| #endif |
| && (global_settings.repeat_mode != REPEAT_ONE) ) |
| { |
| int i, j; |
| |
| /* We need to delete all the queued songs */ |
| for (i=0, j=steps; i<j; i++) |
| { |
| index = get_next_index(playlist, i, -1); |
| |
| if (playlist->indices[index] & PLAYLIST_QUEUE_MASK) |
| { |
| remove_track_from_playlist(playlist, index, true); |
| steps--; /* one less track */ |
| } |
| } |
| } |
| |
| index = get_next_index(playlist, steps, -1); |
| |
| if (index < 0) |
| { |
| /* end of playlist... or is it */ |
| if (global_settings.repeat_mode == REPEAT_SHUFFLE && |
| playlist->amount > 1) |
| { |
| /* Repeat shuffle mode. Re-shuffle playlist and resume play */ |
| playlist->first_index = global_status.resume_first_index = 0; |
| sort_playlist(playlist, false, false); |
| randomise_playlist(playlist, current_tick, false, true); |
| #if CONFIG_CODEC != SWCODEC |
| playlist_start(0, 0); |
| #endif |
| playlist->index = 0; |
| index = 0; |
| } |
| else if (playlist->in_ram && global_settings.next_folder) |
| { |
| index = create_and_play_dir(steps, true); |
| |
| if (index >= 0) |
| { |
| playlist->index = index; |
| } |
| } |
| |
| return index; |
| } |
| |
| playlist->index = index; |
| |
| if (playlist->last_insert_pos >= 0 && steps > 0) |
| { |
| /* check to see if we've gone beyond the last inserted track */ |
| int cur = rotate_index(playlist, index); |
| int last_pos = rotate_index(playlist, playlist->last_insert_pos); |
| |
| if (cur > last_pos) |
| { |
| /* reset last inserted track */ |
| playlist->last_insert_pos = -1; |
| |
| if (playlist->control_fd >= 0) |
| { |
| int result = update_control(playlist, PLAYLIST_COMMAND_RESET, |
| -1, -1, NULL, NULL, NULL); |
| |
| if (result < 0) |
| return result; |
| |
| sync_control(playlist, false); |
| } |
| } |
| } |
| |
| return index; |
| } |
| |
| /* try playing next or previous folder */ |
| bool playlist_next_dir(int direction) |
| { |
| /* not to mess up real playlists */ |
| if(!current_playlist.in_ram) |
| return false; |
| |
| return create_and_play_dir(direction, false) >= 0; |
| } |
| |
| /* Get resume info for current playing song. If return value is -1 then |
| settings shouldn't be saved. */ |
| int playlist_get_resume_info(int *resume_index) |
| { |
| struct playlist_info* playlist = ¤t_playlist; |
| |
| *resume_index = playlist->index; |
| |
| return 0; |
| } |
| |
| /* Update resume info for current playing song. Returns -1 on error. */ |
| int playlist_update_resume_info(const struct mp3entry* id3) |
| { |
| struct playlist_info* playlist = ¤t_playlist; |
| |
| if (id3) |
| { |
| if (global_status.resume_index != playlist->index || |
| global_status.resume_offset != id3->offset) |
| { |
| global_status.resume_index = playlist->index; |
| global_status.resume_offset = id3->offset; |
| status_save(); |
| } |
| } |
| else |
| { |
| global_status.resume_index = -1; |
| global_status.resume_offset = -1; |
| status_save(); |
| } |
| |
| return 0; |
| } |
| |
| /* Returns index of current playing track for display purposes. This value |
| should not be used for resume purposes as it doesn't represent the actual |
| index into the playlist */ |
| int playlist_get_display_index(void) |
| { |
| struct playlist_info* playlist = ¤t_playlist; |
| |
| /* first_index should always be index 0 for display purposes */ |
| int index = rotate_index(playlist, playlist->index); |
| |
| return (index+1); |
| } |
| |
| /* returns number of tracks in current playlist */ |
| int playlist_amount(void) |
| { |
| return playlist_amount_ex(NULL); |
| } |
| |
| /* |
| * Create a new playlist If playlist is not NULL then we're loading a |
| * playlist off disk for viewing/editing. The index_buffer is used to store |
| * playlist indices (required for and only used if !current playlist). The |
| * temp_buffer (if not NULL) is used as a scratchpad when loading indices. |
| */ |
| int playlist_create_ex(struct playlist_info* playlist, |
| const char* dir, const char* file, |
| void* index_buffer, int index_buffer_size, |
| void* temp_buffer, int temp_buffer_size) |
| { |
| if (!playlist) |
| playlist = ¤t_playlist; |
| else |
| { |
| /* Initialize playlist structure */ |
| int r = rand() % 10; |
| playlist->current = false; |
| |
| /* Use random name for control file */ |
| snprintf(playlist->control_filename, sizeof(playlist->control_filename), |
| "%s.%d", PLAYLIST_CONTROL_FILE, r); |
| playlist->fd = -1; |
| playlist->control_fd = -1; |
| |
| if (index_buffer) |
| { |
| int num_indices = index_buffer_size / sizeof(int); |
| |
| #ifdef HAVE_DIRCACHE |
| num_indices /= 2; |
| #endif |
| if (num_indices > global_settings.max_files_in_playlist) |
| num_indices = global_settings.max_files_in_playlist; |
| |
| playlist->max_playlist_size = num_indices; |
| playlist->indices = index_buffer; |
| #ifdef HAVE_DIRCACHE |
| playlist->filenames = (const struct dircache_entry **) |
| &playlist->indices[num_indices]; |
| #endif |
| } |
| else |
| { |
| playlist->max_playlist_size = current_playlist.max_playlist_size; |
| playlist->indices = current_playlist.indices; |
| #ifdef HAVE_DIRCACHE |
| playlist->filenames = current_playlist.filenames; |
| #endif |
| } |
| |
| playlist->buffer_size = 0; |
| playlist->buffer = NULL; |
| mutex_init(&playlist->control_mutex); |
| } |
| |
| new_playlist(playlist, dir, file); |
| |
| if (file) |
| /* load the playlist file */ |
| add_indices_to_playlist(playlist, temp_buffer, temp_buffer_size); |
| |
| return 0; |
| } |
| |
| /* |
| * Set the specified playlist as the current. |
| * NOTE: You will get undefined behaviour if something is already playing so |
| * remember to stop before calling this. Also, this call will |
| * effectively close your playlist, making it unusable. |
| */ |
| int playlist_set_current(struct playlist_info* playlist) |
| { |
| if (!playlist || (check_control(playlist) < 0)) |
| return -1; |
| |
| empty_playlist(¤t_playlist, false); |
| |
| strncpy(current_playlist.filename, playlist->filename, |
| sizeof(current_playlist.filename)); |
| |
| current_playlist.utf8 = playlist->utf8; |
| current_playlist.fd = playlist->fd; |
| |
| close(playlist->control_fd); |
| close(current_playlist.control_fd); |
| remove(current_playlist.control_filename); |
| if (rename(playlist->control_filename, |
| current_playlist.control_filename) < 0) |
| return -1; |
| current_playlist.control_fd = open(current_playlist.control_filename, |
| O_RDWR); |
| if (current_playlist.control_fd < 0) |
| return -1; |
| current_playlist.control_created = true; |
| |
| current_playlist.dirlen = playlist->dirlen; |
| |
| if (playlist->indices && playlist->indices != current_playlist.indices) |
| { |
| memcpy(current_playlist.indices, playlist->indices, |
| playlist->max_playlist_size*sizeof(int)); |
| #ifdef HAVE_DIRCACHE |
| memcpy(current_playlist.filenames, playlist->filenames, |
| playlist->
|