text/plain
•
12.58 KB
•
527 lines
#include "playlist.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "audio_player.h"
#include "bookmark.h"
#include "lvgl.h"
#include "screens/files_screen.h"
#include "storage.h"
#include "ui.h"
#define PLAYLIST_BUF_SIZE 4096
playlist_t playlist;
// --- Playback control state (moved from now_playing_screen) ---
static char pl_dir[STORAGE_MAX_PATH];
static char pl_files[MAX_AUDIOBOOK_FILES][STORAGE_MAX_NAME];
static int pl_file_count;
static int pl_current_file_index;
static uint32_t pl_bookmark_ms;
static bool pl_bookmark_applied;
void playlist_load(void) {
memset(&playlist, 0, sizeof(playlist));
storage_file_t f = storage_open(PLAYLIST_FILE, "r");
if (!f) {
return;
}
char buf[PLAYLIST_BUF_SIZE];
size_t n = storage_read(f, buf, sizeof(buf) - 1);
storage_close(f);
buf[n] = '\0';
// Parse line by line
char* line = buf;
while (line && *line) {
char* eol = strchr(line, '\n');
size_t line_len = eol ? (size_t)(eol - line) : strlen(line);
// Null-terminate line temporarily
char saved = line[line_len];
line[line_len] = '\0';
// Parse index = N
if (strncmp(line, "index", 5) == 0) {
char* eq = strchr(line, '=');
if (eq) {
playlist.index = atoi(eq + 1);
}
}
// Parse type = "music" or type = "audiobook"
else if (strncmp(line, "type", 4) == 0 &&
playlist.count < PLAYLIST_MAX_ENTRIES) {
char* eq = strchr(line, '=');
if (eq) {
char* q1 = strchr(eq, '"');
if (q1) {
q1++;
if (strncmp(q1, "audiobook", 9) == 0) {
playlist.entries[playlist.count].type =
PLAYLIST_AUDIOBOOK;
} else {
playlist.entries[playlist.count].type = PLAYLIST_MUSIC;
}
}
}
}
// Parse path = "/some/path"
else if (strncmp(line, "path", 4) == 0 &&
playlist.count < PLAYLIST_MAX_ENTRIES) {
char* eq = strchr(line, '=');
if (eq) {
char* q1 = strchr(eq, '"');
if (q1) {
q1++;
char* q2 = strchr(q1, '"');
if (q2) {
size_t plen = (size_t)(q2 - q1);
if (plen < STORAGE_MAX_PATH) {
memcpy(playlist.entries[playlist.count].path, q1,
plen);
playlist.entries[playlist.count].path[plen] = '\0';
playlist.count++;
}
}
}
}
}
line[line_len] = saved;
if (!eol) {
break;
}
line = eol + 1;
}
// Clamp index
if (playlist.count > 0) {
if (playlist.index < 0) {
playlist.index = 0;
}
if (playlist.index >= playlist.count) {
playlist.index = playlist.count - 1;
}
} else {
playlist.index = 0;
}
}
void playlist_save(void) {
storage_mkdir(DATA_DIR);
char buf[PLAYLIST_BUF_SIZE];
int pos = 0;
pos += snprintf(buf + pos, sizeof(buf) - (size_t)pos,
"[playlist]\nindex = %d\n\n", playlist.index);
for (int i = 0; i < playlist.count && pos < (int)sizeof(buf) - 128; i++) {
const char* type_str = playlist.entries[i].type == PLAYLIST_AUDIOBOOK
? "audiobook"
: "music";
pos += snprintf(buf + pos, sizeof(buf) - (size_t)pos,
"[[entry]]\ntype = \"%s\"\npath = \"%s\"\n\n", type_str,
playlist.entries[i].path);
}
storage_file_t f = storage_open(PLAYLIST_FILE, "w");
if (f) {
storage_write(f, buf, (size_t)pos);
storage_close(f);
}
}
void playlist_add(const char* path, playlist_type_t type) {
if (playlist.count >= PLAYLIST_MAX_ENTRIES) {
return;
}
playlist.entries[playlist.count].type = type;
strncpy(playlist.entries[playlist.count].path, path, STORAGE_MAX_PATH - 1);
playlist.entries[playlist.count].path[STORAGE_MAX_PATH - 1] = '\0';
playlist.count++;
}
void playlist_clear(void) {
memset(&playlist, 0, sizeof(playlist));
}
playlist_entry_t* playlist_current(void) {
if (playlist.count == 0) {
return NULL;
}
if (playlist.index < 0 || playlist.index >= playlist.count) {
return NULL;
}
return &playlist.entries[playlist.index];
}
bool playlist_next(void) {
if (playlist.index + 1 >= playlist.count) {
return false;
}
playlist.index++;
return true;
}
bool playlist_prev(void) {
if (playlist.index <= 0) {
return false;
}
playlist.index--;
return true;
}
bool playlist_is_empty(void) {
return playlist.count == 0;
}
int playlist_count(void) {
return playlist.count;
}
void playlist_set_index(int i) {
if (i >= 0 && i < playlist.count) {
playlist.index = i;
}
}
void playlist_tick(void) {
static uint32_t last_slot = 0;
playlist_entry_t* entry = playlist_current();
if (!entry || entry->type != PLAYLIST_AUDIOBOOK) {
return;
}
if (audio_player_get_state() != AUDIO_STATE_PLAYING) {
return;
}
uint32_t slot = lv_tick_get() / PLAYLIST_BOOKMARK_INTERVAL_MS;
if (slot == last_slot) {
return;
}
last_slot = slot;
uint32_t pos = audio_player_get_position_ms();
if (pos > 0 && app_state.last_played_path[0]) {
bookmark_save(app_state.last_played_path, pos);
}
// Also save chapter bookmark for directory audiobooks
if (app_state.audiobook_dir[0]) {
const char* filename = strrchr(app_state.last_played_path, '/');
if (filename) {
bookmark_save_string(app_state.audiobook_dir, filename + 1);
}
}
}
// --- High-level playback control ---
static int filename_cmp(const void* a, const void* b) {
return strcmp((const char*)a, (const char*)b);
}
static void load_directory_files(void) {
storage_entry_t* entries =
malloc(MAX_AUDIOBOOK_FILES * sizeof(storage_entry_t));
if (!entries) {
return;
}
int count = storage_list_dir(pl_dir, entries, MAX_AUDIOBOOK_FILES);
pl_file_count = 0;
for (int i = 0; i < count && pl_file_count < MAX_AUDIOBOOK_FILES; i++) {
if (entries[i].type == STORAGE_TYPE_FILE &&
is_audio_file(entries[i].name)) {
strncpy(pl_files[pl_file_count], entries[i].name,
STORAGE_MAX_NAME - 1);
pl_files[pl_file_count][STORAGE_MAX_NAME - 1] = '\0';
pl_file_count++;
}
}
free(entries);
qsort(pl_files, (size_t)pl_file_count, STORAGE_MAX_NAME, filename_cmp);
}
static int find_file_index(const char* filename) {
for (int i = 0; i < pl_file_count; i++) {
if (strcmp(pl_files[i], filename) == 0) {
return i;
}
}
return -1;
}
void playlist_save_bookmarks(void) {
uint32_t pos = audio_player_get_position_ms();
if (pos > 0 && app_state.last_played_path[0]) {
bookmark_save(app_state.last_played_path, pos);
}
if (pl_dir[0] && pl_current_file_index >= 0 &&
pl_current_file_index < pl_file_count) {
bookmark_save_string(pl_dir, pl_files[pl_current_file_index]);
}
}
static bool play_next_chapter(void) {
int next = pl_current_file_index + 1;
if (next >= pl_file_count) {
return false;
}
// Remove completed file's bookmark
if (app_state.last_played_path[0]) {
bookmark_remove(app_state.last_played_path);
}
char file_path[STORAGE_MAX_PATH + STORAGE_MAX_NAME + 2];
snprintf(file_path, sizeof(file_path), "%s/%s", pl_dir, pl_files[next]);
if (!audio_player_play(file_path)) {
return false;
}
pl_current_file_index = next;
strncpy(app_state.last_played_path, file_path,
sizeof(app_state.last_played_path));
app_state.last_played_path[sizeof(app_state.last_played_path) - 1] = '\0';
bookmark_save_string(pl_dir, pl_files[next]);
pl_bookmark_ms = bookmark_load(app_state.last_played_path);
pl_bookmark_applied = false;
return true;
}
bool playlist_start(void) {
playlist_entry_t* entry = playlist_current();
if (!entry) {
return false;
}
audio_player_stop();
pl_bookmark_applied = false;
pl_bookmark_ms = 0;
pl_dir[0] = '\0';
pl_file_count = 0;
pl_current_file_index = 0;
if (entry->type == PLAYLIST_AUDIOBOOK) {
// Check if path is a directory by trying to list it
storage_entry_t test_entry;
int count = storage_list_dir(entry->path, &test_entry, 1);
bool is_dir = (count >= 0);
if (is_dir) {
// Directory audiobook mode
strncpy(pl_dir, entry->path, sizeof(pl_dir));
pl_dir[sizeof(pl_dir) - 1] = '\0';
strncpy(app_state.audiobook_dir, entry->path,
sizeof(app_state.audiobook_dir));
app_state.audiobook_dir[sizeof(app_state.audiobook_dir) - 1] = '\0';
load_directory_files();
// Find resume file from bookmark, fall back to first file
char resume_file[STORAGE_MAX_NAME] = {0};
bookmark_load_string(entry->path, resume_file, sizeof(resume_file));
char file_path[STORAGE_MAX_PATH + STORAGE_MAX_NAME + 2];
if (resume_file[0]) {
pl_current_file_index = find_file_index(resume_file);
if (pl_current_file_index < 0) {
pl_current_file_index = 0;
}
} else {
if (pl_file_count == 0) {
return false;
}
pl_current_file_index = 0;
}
char fname[STORAGE_MAX_NAME];
strncpy(fname, pl_files[pl_current_file_index], sizeof(fname) - 1);
fname[sizeof(fname) - 1] = '\0';
snprintf(file_path, sizeof(file_path), "%s/%s", entry->path, fname);
pl_bookmark_ms = bookmark_load(file_path);
if (!audio_player_play(file_path)) {
return false;
}
strncpy(app_state.last_played_path, file_path,
sizeof(app_state.last_played_path));
app_state.last_played_path[sizeof(app_state.last_played_path) - 1] =
'\0';
} else {
// Single file audiobook
app_state.audiobook_dir[0] = '\0';
pl_bookmark_ms = bookmark_load(entry->path);
if (!audio_player_play(entry->path)) {
return false;
}
strncpy(app_state.last_played_path, entry->path,
sizeof(app_state.last_played_path));
app_state.last_played_path[sizeof(app_state.last_played_path) - 1] =
'\0';
}
} else {
// Music: simple playback, no bookmarks
app_state.audiobook_dir[0] = '\0';
if (!audio_player_play(entry->path)) {
return false;
}
strncpy(app_state.last_played_path, entry->path,
sizeof(app_state.last_played_path));
app_state.last_played_path[sizeof(app_state.last_played_path) - 1] =
'\0';
}
playlist_save();
return true;
}
bool playlist_play_next(void) {
playlist_entry_t* entry = playlist_current();
if (entry && entry->type == PLAYLIST_AUDIOBOOK) {
playlist_save_bookmarks();
}
if (playlist_next()) {
return playlist_start();
}
return false;
}
bool playlist_play_prev(void) {
playlist_entry_t* entry = playlist_current();
bool is_audiobook = entry && entry->type == PLAYLIST_AUDIOBOOK;
if (!is_audiobook) {
// Music: if more than 10 seconds in, restart current song
uint32_t pos = audio_player_get_position_ms();
if (pos > PREV_RESTART_THRESHOLD_MS) {
audio_player_seek_ms(0);
return true;
}
}
if (is_audiobook) {
playlist_save_bookmarks();
}
if (playlist_prev()) {
return playlist_start();
} else if (!is_audiobook) {
// Already at first music entry, just restart
audio_player_seek_ms(0);
return true;
}
return false;
}
bool playlist_check_advance(void) {
audio_state_t astate = audio_player_get_state();
if (astate != AUDIO_STATE_STOPPED || !pl_bookmark_applied) {
return false;
}
// Try next chapter in audiobook directory first
if (pl_dir[0] && play_next_chapter()) {
return false;
}
// Remove completed bookmarks
if (app_state.last_played_path[0]) {
bookmark_remove(app_state.last_played_path);
}
if (pl_dir[0]) {
bookmark_remove(pl_dir);
}
// Try next playlist entry
if (playlist_next()) {
playlist_start();
return false;
}
// End of playlist
playlist_clear();
playlist_save();
return true;
}
void playlist_apply_bookmark(void) {
if (pl_bookmark_applied || pl_bookmark_ms == 0) {
return;
}
if (audio_player_get_state() != AUDIO_STATE_PLAYING) {
return;
}
pl_bookmark_applied = true;
audio_player_seek_ms(pl_bookmark_ms);
}
bool playlist_get_chapter_info(int* current, int* total) {
if (!pl_dir[0] || pl_file_count == 0) {
return false;
}
*current = pl_current_file_index + 1;
*total = pl_file_count;
return true;
}
uint32_t playlist_get_bookmark_ms(void) {
return pl_bookmark_ms;
}
bool playlist_is_bookmark_applied(void) {
return pl_bookmark_applied;
}
// Sync internal state to match already-playing audio (called when screen opens
// while audio is already playing, so we didn't go through playlist_start).
void playlist_sync_state(void) {
pl_bookmark_applied = true;
pl_bookmark_ms = 0;
pl_dir[0] = '\0';
pl_file_count = 0;
pl_current_file_index = 0;
playlist_entry_t* entry = playlist_current();
if (!entry || entry->type != PLAYLIST_AUDIOBOOK) {
return;
}
storage_entry_t test_entry;
int count = storage_list_dir(entry->path, &test_entry, 1);
if (count < 0) {
return;
}
// Directory audiobook — rebuild file list and find current chapter
strncpy(pl_dir, entry->path, sizeof(pl_dir));
pl_dir[sizeof(pl_dir) - 1] = '\0';
load_directory_files();
const char* filename = strrchr(app_state.last_played_path, '/');
filename = filename ? filename + 1 : app_state.last_played_path;
pl_current_file_index = find_file_index(filename);
if (pl_current_file_index < 0) {
pl_current_file_index = 0;
}
}