Started rewrite for using an I2S amplifier with less unnecessary features.
This commit is contained in:
402
src/playlist.cpp
402
src/playlist.cpp
@ -1,399 +1,9 @@
|
||||
#include <playlist.h>
|
||||
#include "spi_master.h"
|
||||
#include "config.h"
|
||||
#include <SD.h>
|
||||
#include <algorithm>
|
||||
#include <ArduinoJson.h>
|
||||
#include <TinyXML.h>
|
||||
#include "main.h"
|
||||
#include "playlist.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_file(String filename) {
|
||||
files.push_back(filename);
|
||||
}
|
||||
|
||||
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 (trace_enabled) {
|
||||
if (status & STATUS_START_TAG) {
|
||||
TRACE("Start of tag: %s\n", tagName);
|
||||
} else if (status & STATUS_END_TAG) {
|
||||
TRACE("End of tag: %s\n", tagName);
|
||||
}
|
||||
}
|
||||
|
||||
if (tag.equals("/rss/channel/title") && (status & STATUS_TAG_TEXT)) {
|
||||
xml_album_title = data;
|
||||
} else if (tag.endsWith("/title") && (status & STATUS_TAG_TEXT)) {
|
||||
xml_title = String(data);
|
||||
} else if (tag.endsWith("/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("/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("/enclosure") && tag.equals("url") && (status & STATUS_ATTR_TEXT)) {
|
||||
DEBUG("found url\n");
|
||||
xml_enclosure_url = String(data);
|
||||
} else if (tag.endsWith("/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 || status & STATUS_START_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});
|
||||
}
|
||||
xml_title = "";
|
||||
xml_url = "";
|
||||
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>=0 && 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();
|
||||
}
|
||||
void Playlist::sort() {
|
||||
std::sort(files.begin(), files.end());
|
||||
}
|
Reference in New Issue
Block a user