#include #include "spi_master.h" #include "config.h" #include #include #include #include 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= 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* 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(); }