esmp3/src/playlist.cpp

392 lines
9.9 KiB
C++

#include <playlist.h>
#include "spi_master.h"
#include "config.h"
#include <SD.h>
#include <algorithm>
#include <ArduinoJson.h>
#include <TinyXML.h>
Playlist::Playlist(String path) {
_path = path;
if (path.startsWith("/")) {
persistence = PERSIST_TEMPORARY;
_add_path(path);
} else if (path.startsWith("http")) {
_examine_http_url(path);
}
if (_title.length()==0) _title=path;
}
void Playlist::_add_path(String path) {
SPIMaster::select_sd();
TRACE("Examining folder %s...\n", path.c_str());
if (!path.startsWith("/")) path = String("/") + path;
if (!SD.exists(path)) {
DEBUG("Could not open path '%s'.\n", path.c_str());
SPIMaster::select_sd(false);
return;
}
_title = path.substring(1);
int idx = _title.indexOf('/');
if (idx>0) {
_title.remove(idx);
}
File dir = SD.open(path);
File entry;
while (entry = dir.openNextFile()) {
String filename = entry.name();
filename = filename.substring(path.length() + 1);
String ext = filename.substring(filename.length() - 4);
if (!entry.isDirectory() &&
!filename.startsWith(".") &&
( ext.equals(".mp3") ||
ext.equals(".ogg") ||
ext.equals(".wma") ||
ext.equals(".mp4") ||
ext.equals(".mpa"))) {
TRACE(" Adding entry %s\n", entry.name());
String title = filename.substring(0, filename.length() - 4);
_files.push_back({.filename=entry.name(), .title=title, .id=String(_files.size())});
bool non_ascii_chars = false;
for(int i=0; i<filename.length(); i++) {
char c = filename.charAt(i);
if (c < 0x20 || c >= 0x7F) {
non_ascii_chars = true;
break;
}
}
if (non_ascii_chars) {
ERROR("WARNING: File '%s' contains non-ascii chars!\n", filename.c_str());
}
} else {
TRACE(" Ignoring entry %s\n", filename.c_str());
}
entry.close();
}
dir.close();
SPIMaster::select_sd(false);
std::sort(_files.begin(), _files.end());
}
void Playlist::_examine_http_url(String url) {
HTTPClientWrapper* http = new HTTPClientWrapper();
if (!http->get(url)) {
DEBUG("Could not GET %s.\n", url.c_str());
return;
}
String ct = http->getContentType();
DEBUG("Content-Type is %s.\n", ct.c_str());
if (ct.startsWith("audio/x-mpegurl")) {
_parse_m3u(http);
} else if (ct.startsWith("audio/")) {
persistence = PERSIST_NONE;
_files.push_back({.filename=url, .title=url, .id="none"});
} else if (ct.startsWith("application/rss+xml")) {
persistence = PERSIST_PERMANENTLY;
_parse_rss(http);
} else if (ct.startsWith("application/pls+xml")) {
persistence = PERSIST_PERMANENTLY;
_parse_pls(http);
} else {
ERROR("Unknown content type %s.\n", ct.c_str());
}
http->close();
delete http;
}
std::vector<PlaylistEntry>* xml_files_ptr = NULL;
String xml_last_tag = "";
String xml_title = "";
String xml_album_title = "";
String xml_url = "";
String xml_enclosure_url = "";
String xml_guid = "";
bool xml_enclosure_is_audio = false;
void xmlcb(uint8_t status, char* tagName, uint16_t tagLen, char* data, uint16_t dataLen) {
String tag(tagName);
if (status & STATUS_START_TAG) xml_last_tag = tag;
if (tag.equals("/rss/channel/title") && (status & STATUS_TAG_TEXT)) {
xml_album_title = data;
} else if (tag.endsWith("/item") && (status & STATUS_START_TAG)) {
xml_title = "";
xml_url = "";
xml_guid = "";
} else if (tag.endsWith("/item/title") && (status & STATUS_TAG_TEXT)) {
xml_title = String(data);
} else if (tag.endsWith("/item/guid") && (status & STATUS_TAG_TEXT)) {
xml_guid = data;
//} else if (xml_last_tag.endsWith("/item/enclosure") && (status & STATUS_ATTR_TEXT)) {
// DEBUG("tag: %s, data: %s\n", tag.c_str(), data);
} else if (xml_last_tag.endsWith("/item/enclosure") && tag.equals("type") && (status & STATUS_ATTR_TEXT) && String(data).indexOf("audio/")>=0) {
DEBUG("enclosure is audio\n");
xml_enclosure_is_audio = true;
} else if (xml_last_tag.endsWith("/item/enclosure") && tag.equals("url") && (status & STATUS_ATTR_TEXT)) {
DEBUG("found url\n");
xml_enclosure_url = String(data);
} else if (tag.endsWith("/item/enclosure") && (status & STATUS_END_TAG)) {
DEBUG("end of enclosure. xml_enclosure_is_audio: %d, xml_enclosure_url: %s\n", xml_enclosure_is_audio, xml_enclosure_url.c_str());
if (xml_enclosure_is_audio && xml_enclosure_url.length()>0) {
xml_url = xml_enclosure_url;
}
xml_enclosure_is_audio = false;
xml_enclosure_url = "";
} else if (tag.endsWith("/item") && (status & STATUS_END_TAG)) {
if (xml_title.length()>0 && xml_url.length()>0) {
if (xml_files_ptr->size() > 20) return;
DEBUG("Adding playlist entry: '%s' => '%s'\n", xml_title.c_str(), xml_url.c_str());
xml_files_ptr->insert(xml_files_ptr->begin(), {.filename=xml_url, .title=xml_title, .id=xml_guid});
}
}
}
void Playlist::_parse_rss(HTTPClientWrapper* http) {
DEBUG("RSS parser running.\n");
// http is already initialized
int i;
TinyXML xml;
uint8_t* buffer = new uint8_t[150];
xml.init(buffer, 150, &xmlcb);
xml_files_ptr = &_files;
xml_title = "";
xml_album_title = "";
xml_url = "";
xml_enclosure_is_audio = false;
xml_enclosure_url = "";
while ((i = http->read()) >= 0) {
xml.processChar(i);
}
_current_track = _files.size()-1;
xml_files_ptr = NULL;
if (xml_album_title.length()>0) {
_title = xml_album_title;
}
xml_album_title = "";
delete buffer;
// don't close http at the end
DEBUG("RSS parser finished.\n");
}
void Playlist::_parse_m3u(HTTPClientWrapper* http) {
// http is already initialized
String line = "";
String title = "";
int i;
do {
i = http->read();
char c = i;
if (i>=-1 && c!='\r' && c!='\n') {
line += c;
} else {
if (line.equals("#EXTM3U")) {
// Do nothing
} else if (line.startsWith("#EXTINF")) {
int idx = line.indexOf(",");
if (idx>4) {
// Get the title
title = line.substring(idx+1);
if (_title.length()==0) _title=title;
}
} else if (line.startsWith("http")) {
if (title.length()==0) title = line;
_files.push_back({.filename=line, .title=title, .id="none"});
title = "";
}
line = "";
}
} while (i>=0);
// don't close http at the end
}
void Playlist::_parse_pls(HTTPClientWrapper* http) {
// http is already initialized
String line;
String title = "";
String url = "";
int last_index = -1;
int index;
while(true) {
line = http->readLine();
if (line.startsWith("Title")) {
uint8_t eq_idx = line.indexOf('=');
if (eq_idx==-1) continue;
index = line.substring(5, eq_idx-4).toInt();
title = line.substring(eq_idx+1);
if (index != last_index) {
url = "";
last_index = index;
}
} else if (line.startsWith("File")) {
uint8_t eq_idx = line.indexOf('=');
if (eq_idx==-1) continue;
index = line.substring(5, eq_idx-4).toInt();
url = line.substring(eq_idx+1);
if (index != last_index) {
title = "";
last_index = index;
}
}
if (title.length()>0 && url.length()>0) {
_files.push_back({.filename=url, .title=title, .id="none"});
last_index = -1;
title = "";
url = "";
}
}
// don't close http at the end
}
String Playlist::path() {
return _path;
}
uint16_t Playlist::get_file_count() {
return _files.size();
}
void Playlist::start() {
_started = true;
}
bool Playlist::has_track_prev() {
return _current_track > 0;
}
bool Playlist::has_track_next() {
return _current_track < _files.size()-1;
}
bool Playlist::track_prev() {
if (_current_track > 0) {
_current_track--;
_position = 0;
return true;
}
return false;
}
bool Playlist::track_next() {
if (_current_track < _files.size()-1) {
_current_track++;
_position = 0;
return true;
}
return false;
}
bool Playlist::set_track(uint8_t track) {
if (track < _files.size()) {
_current_track = track;
_position = 0;
return true;
}
return false;
}
void Playlist::set_track_by_id(String id) {
for (int i=0; i<_files.size(); i++) {
if (id.equals(_files[i].id)) {
set_track(i);
return;
}
}
}
void Playlist::track_restart() {
_position = 0;
}
void Playlist::shuffle(uint8_t random_offset) {
DEBUG("Shuffling the playlist with an offset of %d...\n", random_offset);
for (int i=random_offset; i<_files.size(); i++) {
int j = random(random_offset, _files.size()-1);
if (i!=j) {
TRACE(" Swapping elements %d and %d.\n", i, j);
PlaylistEntry temp = _files[i];
_files[i] = _files[j];
_files[j] = temp;
}
}
_shuffled = true;
TRACE("Done.\n");
}
void Playlist::advent_shuffle(uint8_t day) {
TRACE("advent_shuffle running...\n");
// Not enough songs till the current day? Play all songs in the default order.
if (day > _files.size()) {
return;
}
// We are in the "different playlist every day" mode. So we don't persist it in order to not miss changes.
persistence = PERSIST_NONE;
_files.insert(_files.begin(), _files[day - 1]);
_files.erase(_files.begin() + day, _files.end());
}
void Playlist::reset() {
std::sort(_files.begin(), _files.end());
_current_track = 0;
_position = 0;
_shuffled = false;
_started = false;
}
String Playlist::get_current_track_id() {
if (_current_track > _files.size()) {
return "";
}
return _files[_current_track].id;
}
bool Playlist::get_current_file(String* dst) {
if (_current_track > _files.size()) {
return false;
} else {
dst->concat(_files[_current_track].filename);
return true;
}
}
uint32_t Playlist::get_position() {
return _position;
}
void Playlist::set_position(uint32_t p) {
_position = p;
}
bool Playlist::is_fresh() {
return !_shuffled && !_started && _position==0 && _current_track==0;
}
void Playlist::dump() {
for (int i=0; i<_files.size(); i++) {
DEBUG(" %02d %2s %s\n", i+1, (i==_current_track) ? "->" : "", _files[i].filename.c_str());
}
}
void Playlist::json(JsonObject json) {
json["_type"] = "playlist";
json["title"] = _title;
JsonArray files = json.createNestedArray("files");
for (PlaylistEntry entry: _files) {
JsonObject o = files.createNestedObject();
o["filename"] = entry.filename;
o["title"] = entry.title;
o["id"] = entry.id;
}
json["current_track"] = _current_track;
json["has_track_next"] = has_track_next();
json["has_track_prev"] = has_track_prev();
}