WIP: Lots of streaming stuff

This commit is contained in:
Fabian Schlenz 2019-11-27 06:51:20 +01:00
parent 710b8a2cdc
commit 6f8683ba9d
11 changed files with 551 additions and 78 deletions

View File

@ -93,6 +93,20 @@
<span>&times;</span>
</button>
</div>
<h6>Open URL</h6>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="fa fa-link"></i>
</span>
</div>
<input type="text" class="form-control" id="input_url" />
<div class="input-group-append">
<button class="btn btn-primary" id="button_url_open">Go</button>
<button class="btn btn-danger" id="button_url_add_mapping" style="display: none;"><i class="fa fa-arrows-alt-h"></i></button>
</div>
</div>
<div class="modal-body">
<div id="albums_without_id_area">
@ -173,7 +187,7 @@ update_playlist = function(data) {
tr = $('<tr>').data('track', i);
tr.append($('<td>').html(i + 1));
tr.append($('<td>').html(data.current_track==i ? '<i class="fa fa-play"></i>' : ''));
tr.append($('<td>').html(data.files[i].substr(data.files[i].lastIndexOf('/')+1)));
tr.append($('<td>').html(data.files[i].substr(data.files[i].title)));
$('#track_list').append(tr);
}
@ -249,6 +263,7 @@ process_ws_message = function(event) {
for (var i=0; i<data.length; i++) {
var json = JSON.parse(data[i]);
console.log(json);
if (json === null) continue;
switch(json["_type"]) {
case "position": update_position(json); break;
case "player": update_player(json); break;
@ -275,13 +290,35 @@ $(function() {
$('#button_settings').click(function(e) { $('#settingsModal').modal('show'); });
$('#button_reset_vs1053').click(function(e) { ws.send("reset_vs1053"); $('#settingsModal').modal('hide'); });
$('#button_reboot').click(function(e) { ws.send("reboot"); $('#settingsModal').modal('hide'); });
$('#button_url_open').click(function(e) { ws.send("play " + $('#input_url').val()); $('#openModal').modal('hide');});
$('#button_add_mapping').click(function(e) {
$('#settingsModal').modal('hide');
$('#openModal').modal('show');
$('.add_mapping_button').show();
$('#button_url_open').hide();
$('#button_url_add_mapping').show();
play_on_click = false;
});
$('#openModal').on('click', '.add_mapping_button', function(e) {ws.send("add_mapping=" + $('#last_rfid_id').html() + "=" + $(e.target).parents('tr').data('folder')); $('#openModal').modal('hide'); $('.add_mapping_button').hide(); e.stopPropagation(); play_on_click=true; return false;});
$('#openModal').on('click', '.add_mapping_button', function(e) {
ws.send("add_mapping=" + $('#last_rfid_id').html() + "=" + $(e.target).parents('tr').data('folder'));
$('#openModal').modal('hide');
$('.add_mapping_button').hide();
$('#button_url_open').hide();
$('#button_url_add_mapping').show();
e.stopPropagation();
play_on_click=true;
return false;
});
$('#button_url_add_mapping').click(function(e) {
ws.send("add_mapping=" + $('#last_rfid_id').html() + "=" + $('#input_url').val());
$('#openModal').modal('hide');
$('.add_mapping_button').hide();
$('#button_url_open').hide();
$('#button_url_add_mapping').show();
e.stopPropagation();
play_on_click=true;
return false;
});
});
</script>
</html>

View File

@ -3,7 +3,7 @@
#include <Arduino.h>
#include <SD.h>
#include "config.h"
#include <HTTPClient.h>
#include "http_client_wrapper.h"
class DataSource {
private:
@ -11,7 +11,7 @@ public:
DataSource() {};
virtual ~DataSource() {};
virtual size_t read(uint8_t* buf, size_t len) = 0;
virtual uint8_t read() = 0;
virtual int read() = 0;
virtual size_t position() = 0;
virtual void seek(size_t position) = 0;
virtual size_t size() = 0;
@ -27,7 +27,7 @@ public:
SDDataSource(String file);
~SDDataSource();
size_t read(uint8_t* buf, size_t len);
uint8_t read();
int read();
size_t position();
void seek(size_t position);
size_t size();
@ -39,17 +39,16 @@ public:
class HTTPSDataSource : public DataSource {
private:
WiFiClient* _stream = NULL;
HTTPClient* _http = NULL;
uint32_t _length;
HTTPClientWrapper* _http = NULL;
uint32_t _position;
public:
HTTPSDataSource(String url, uint32_t offset=0);
~HTTPSDataSource();
size_t read(uint8_t* buf, size_t len);
uint8_t read();
int read();
size_t position();
void seek(size_t position);
size_t size();
void close();
bool usable();
};
};

View File

@ -0,0 +1,37 @@
#pragma once
#include <HTTPClient.h>
#include "config.h"
class HTTPClientWrapper {
private:
HTTPClient* _http;
uint8_t* _buffer;
uint16_t _buffer_size;
uint16_t _buffer_length;
uint16_t _buffer_position;
uint32_t _chunk_length;
bool _connected = false;
String _content_type;
uint32_t _length;
bool _request(String method, String url, uint32_t offset=0, uint8_t redirection_count=0);
WiFiClient* _stream;
bool _is_chunked;
void _read_next_chunk_header(bool first);
uint16_t _fill_buffer();
public:
HTTPClientWrapper();
~HTTPClientWrapper();
bool get(String url, uint32_t offset=0, uint8_t redirection_count=0);
bool head(String url, uint32_t offset=0, uint8_t redirection_count=0);
String getContentType();
String getString();
int read();
uint32_t read(uint8_t* dst, uint32_t len);
void close();
uint32_t getSize();
String readUntil(String sep);
String readLine();
};

View File

@ -2,6 +2,14 @@
#include <Arduino.h>
#include <vector>
#include <ArduinoJson.h>
#include "http_client_wrapper.h"
struct PlaylistEntry {
String filename;
String title;
bool operator<(PlaylistEntry p) { return title < p.title; }
};
class Playlist {
private:
@ -9,10 +17,17 @@ private:
uint32_t _current_track = 0;
bool _started = false;
bool _shuffled = false;
std::vector<String> _files;
std::vector<PlaylistEntry> _files;
void _add_path(String path);
void _examine_http_url(String url);
void _parse_rss(HTTPClientWrapper* http);
void _parse_m3u(HTTPClientWrapper* http);
void _parse_pls(HTTPClientWrapper* http);
public:
Playlist(String path, bool is_url=false);
String title;
Playlist(String path);
void start();
uint16_t get_file_count();
bool has_track_next();
bool has_track_prev();
bool track_next();

View File

@ -17,6 +17,7 @@ build_flags=!./build_version.sh
lib_deps = MFRC522
https://github.com/me-no-dev/ESPAsyncWebServer.git
ArduinoJSON
6691 ; TinyXML
upload_port = /dev/cu.SLAB_USBtoUART
monitor_speed = 74480
;monitor_port = /dev/cu.wchusbserial1420
;monitor_port = /dev/cu.wchusbserial1420

View File

@ -4,7 +4,7 @@
SDDataSource::SDDataSource(String file) { _file = SD.open(file, "r"); }
SDDataSource::~SDDataSource() { if (_file) _file.close(); }
size_t SDDataSource::read(uint8_t* buf, size_t len) { return _file.read(buf, len); }
uint8_t SDDataSource::read() { return _file.read(); }
int SDDataSource::read() { return _file.read(); }
size_t SDDataSource::position() { return _file.position(); }
void SDDataSource::seek(size_t position) { _file.seek(position); }
size_t SDDataSource::size() { return _file.size(); }
@ -39,55 +39,19 @@ void SDDataSource::skip_id3_tag() {
////////////// HTTPSDataSource //////////////
HTTPSDataSource::HTTPSDataSource(String url, uint32_t offset) {
uint8_t tries_left = 5;
int status;
do {
if (tries_left == 0) {
ERROR("Redirection loop? Cancelling!\n");
return;
}
tries_left--;
DEBUG("Connecting to %s...\n", url.c_str());
if (_http) delete _http;
_http = new HTTPClient();
_http->setUserAgent("PodBox 0.1");
const char* headers[] = {"Location"};
_http->collectHeaders(headers, 1);
bool result = _http->begin(url);
DEBUG("HTTP->begin result: %d\n", result);
if (!result) return;
status = _http->GET();
DEBUG("Status code: %d\n", status);
if (status == HTTP_CODE_FOUND || status==HTTP_CODE_MOVED_PERMANENTLY || status==HTTP_CODE_TEMPORARY_REDIRECT) {
if (_http->hasHeader("Location")) {
url = _http->header("Location");
} else {
ERROR("Got redirection HTTP code, but could not find Location header.\n");
for(int i=0; i<_http->headers(); i++) {
DEBUG(" Header: %s=%s\n", _http->headerName(i).c_str(), _http->header(i).c_str());
}
return;
}
} else if (status != HTTP_CODE_OK) {
DEBUG("Unexpected HTTP return code. Cancelling.\n");
return;
}
} while (status != HTTP_CODE_OK);
_length = _http->getSize();
DEBUG("Content-Length: %d\n", _length);
_stream = _http->getStreamPtr();
_http = new HTTPClientWrapper();
if (!_http->get(url, offset)) return;
_position = 0;
}
HTTPSDataSource::~HTTPSDataSource() {
if (_stream) _stream->stop();
_http->end();
delete _stream;
_http->close();
delete _http;
}
bool HTTPSDataSource::usable() { return _http && _stream; }
size_t HTTPSDataSource::read(uint8_t* buf, size_t len) { size_t result = _stream->read(buf, len); _position += result; return result; }
uint8_t HTTPSDataSource::read() { _position++; return _stream->read(); }
bool HTTPSDataSource::usable() { return _http; }
size_t HTTPSDataSource::read(uint8_t* buf, size_t len) { size_t result = _http->read(buf, len); _position += result; return result; }
int HTTPSDataSource::read() { int b = _http->read(); if (b>=0) _position++; return b; }
size_t HTTPSDataSource::position() { return _position; }
void HTTPSDataSource::seek(size_t position) { return; /* TODO */ }
size_t HTTPSDataSource::size() { return _length; }
void HTTPSDataSource::close() { _stream->stop(); }
size_t HTTPSDataSource::size() { return _http->getSize(); }
void HTTPSDataSource::close() { _http->close(); }

210
src/http_client_wrapper.cpp Normal file
View File

@ -0,0 +1,210 @@
#include "http_client_wrapper.h"
#include <WiFiClientSecure.h>
HTTPClientWrapper::HTTPClientWrapper() {
_buffer = new uint8_t[512];
_buffer_size = 512;
}
HTTPClientWrapper::~HTTPClientWrapper() {
if (_http) {
_http->end();
delete _http;
}
delete _buffer;
}
void HTTPClientWrapper::close() {
_http->end();
_connected = false;
}
bool HTTPClientWrapper::get(String url, uint32_t offset, uint8_t redirection_count) { return _request("GET", url, offset, redirection_count); }
bool HTTPClientWrapper::head(String url, uint32_t offset, uint8_t redirection_count) { return _request("HEAD", url, offset, redirection_count); }
bool HTTPClientWrapper::_request(String method, String url, uint32_t offset, uint8_t redirection_count) {
if (redirection_count>=5) return false;
//if (_http) delete _http;
DEBUG("%s %s\n", method.c_str(), url.c_str());
_http = new HTTPClient();
_http->setUserAgent("PodBox/0.1");
if (offset) {
String s = "bytes=";
s += offset;
s += "-";
_http->addHeader("Range: ", s);
}
const char* headers[] = {"Location", "Content-Type", "Transfer-Encoding"};
_http->collectHeaders(headers, 3);
bool result;
/*if (url.startsWith("https:")) {
BearSSL::WiFiClientSecure* client = new BearSSL::WiFiClientSecure();
client->setInsecure();
result = _http->begin(*client, url);
} else {
result = _http->begin(url);
}*/
result = _http->begin(url);
TRACE("HTTP->begin result: %d\n", result);
if (!result) return false;
int status = _http->sendRequest(method.c_str());
TRACE("HTTP Status code: %d\n", status);
if (status == HTTP_CODE_FOUND || status==HTTP_CODE_MOVED_PERMANENTLY || status==HTTP_CODE_TEMPORARY_REDIRECT) {
if (_http->hasHeader("Location")) {
url = _http->header("Location");
_http->end();
delete _http;
_http = NULL;
return _request(method, url, offset, redirection_count+1);
} else {
ERROR("Got redirection HTTP code, but no Location header.\n");
delete _http;
_http = NULL;
return false;
}
} else if (status != HTTP_CODE_OK) {
DEBUG("Unexpected HTTP return code %d. Cancelling.\n", status);
return false;
}
_connected = true;
_length = _http->getSize() + offset;
if (_http->hasHeader("Content-Type")) {
_content_type = _http->header("Content-Type");
} else {
_content_type = "";
}
_is_chunked = (_http->hasHeader("Transfer-Encoding")) && (_http->header("Transfer-Encoding").indexOf("chunked")!=-1);
_stream = _http->getStreamPtr();
if (_is_chunked) {
_read_next_chunk_header(true);
}
return true;
}
void HTTPClientWrapper::_read_next_chunk_header(bool first) {
if (!_connected) {
_chunk_length = 0;
return;
}
if (!first) {
// read() returns an error if no bytes is available right at this moment.
// So we wait until 2 bytes are available or the connection times out.
while (_stream->connected() && !_stream->available()) { delay(1); }
int c1 = _stream->read();
while (_stream->connected() && !_stream->available()) { delay(1); }
int c2 = _stream->read();
if (c1==-1 || c2==-1) {
ERROR("Connection timeout.\n");
DEBUG("_stream.connected() returns %d\n", _stream->connected());
_chunk_length = 0;
_connected = false;
return;
} else if (c1!='\r' || c2!='\n') {
ERROR("Invalid chunk border found. Found: 0x%02X 0x%02X\n", c1, c2);
_chunk_length = 0;
_connected = false;
return;
}
}
String chunk_header = _stream->readStringUntil('\n');
chunk_header.trim();
_chunk_length = strtol(chunk_header.c_str(), NULL, 16);
if (_chunk_length == 0) {
_connected = false;
TRACE("Empty chunk found -> EOF reached.\n");
} else {
TRACE("Chunk found. Length: %d\n", _chunk_length);
}
}
uint16_t HTTPClientWrapper::_fill_buffer() {
if (!_connected) {
_buffer_position = 0;
_buffer_length = 0;
return 0;
}
uint16_t bytes_to_fill = _buffer_size;
uint16_t bytes_filled = 0;
while (bytes_to_fill > 0) {
uint16_t bytes_to_request = bytes_to_fill;
if (_is_chunked && _chunk_length < bytes_to_fill) bytes_to_request = _chunk_length;
TRACE("fill_buffer loop. _is_chunked: %d, _chunk_length: %d, _buffer_size: %d, bytes_filled: %d, bytes_to_fill: %d, bytes_to_request: %d", _is_chunked, _chunk_length, _buffer_size, bytes_filled, bytes_to_fill, bytes_to_request);
uint16_t result = _stream->readBytes(_buffer + bytes_filled, bytes_to_request);
TRACE(", result: %d\n", result);
bytes_filled += result;
bytes_to_fill -= result;
if (_is_chunked) {
_chunk_length -= result;
if (_chunk_length == 0) _read_next_chunk_header(false);
}
if (result == 0) {
_connected = false;
break;
}
}
_buffer_position = 0;
_buffer_length = bytes_filled;
TRACE("Buffer filled. _buffer_length: %d\n", _buffer_length);
return bytes_filled;
}
String HTTPClientWrapper::getContentType() {
return _content_type;
}
int HTTPClientWrapper::read() {
if (_buffer_position >= _buffer_length) _fill_buffer();
if (_buffer_position >= _buffer_length) return -1;
return _buffer[_buffer_position++];
}
uint32_t HTTPClientWrapper::read(uint8_t* dst, uint32_t len) {
TRACE("Reading %d bytes...\n", len);
uint32_t bytes_filled = 0;
while (1) {
if (_buffer_position >= _buffer_length) _fill_buffer();
if (_buffer_position >= _buffer_length) break;
uint32_t bytes_to_fill = len;
if (bytes_to_fill > _buffer_length - _buffer_position) bytes_to_fill = _buffer_length - _buffer_position;
TRACE("read_loop: _buffer_length=%d, _buffer_position=%d, len=%d, bytes_to_fill=%d\n", _buffer_length, _buffer_position, len, bytes_to_fill);
memcpy(dst + bytes_filled, _buffer + _buffer_position, bytes_to_fill);
_buffer_position += bytes_to_fill;
bytes_filled += bytes_to_fill;
len -= bytes_to_fill;
if (bytes_to_fill==0 || len==0) break;
}
return bytes_filled;
}
uint32_t HTTPClientWrapper::getSize() {return _length; }
String HTTPClientWrapper::readUntil(String sep) {
String result = "";
while(true) {
int i = read();
if (i==-1) break;
char c = i;
if (sep.indexOf(c)!=-1) {
// separator
if (result.length()>0) break;
} else {
result.concat(c);
}
}
return result;
}
String HTTPClientWrapper::readLine() {
return readUntil("\n\r");
}

View File

@ -136,6 +136,7 @@ void HTTPServer::_onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client
} else if (type==WS_EVT_DATA) {
AwsFrameInfo* info = (AwsFrameInfo*) arg;
if (info->final && info->index==0 && info->len==len && info->opcode==WS_TEXT) {
data[len]='\0';
DEBUG("Received ws message: %s\n", (char*)data);
_controller->queue_command((char*)data);
}

View File

@ -18,6 +18,19 @@ HTTPServer* http_server;
uint8_t SPIMaster::state = 0;
bool connect_to_wifi(String ssid, String pass) {
TRACE("Connecting to wifi \"%s\"...\n", ssid.c_str());
WiFi.mode(WIFI_AP_STA);
WiFi.begin(ssid.c_str(), pass.c_str());
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
DEBUG("Could not connect to wifi \"%s\".\n", ssid.c_str());
return false;
} else {
INFO("Connected to \"%s\". IP address: %s\n", ssid.c_str(), WiFi.localIP().toString().c_str());
}
return true;
}
void setup() {
delay(500);
Serial.begin(74880);
@ -58,15 +71,35 @@ void setup() {
controller = new Controller(player, pm);
INFO("Player and controller initialized.\n");
DEBUG("Connecting to wifi \"%s\"...\n", WIFI_SSID);
WiFi.mode(WIFI_AP_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
ERROR("Could not connect to Wifi. Rebooting.");
delay(1000);
ESP.restart();
bool connected = false;
INFO("Connecting to WiFi...\n");
SPIMaster::select_sd();
if (SD.exists("/_wifis.txt")) {
DEBUG("Reading /_wifis.txt\n");
File f = SD.open("/_wifis.txt", "r");
while (String line = f.readStringUntil('\n')) {
if (line.length()==0 || line.startsWith("#") || line.indexOf('=')==-1) {
continue;
}
String ssid = line.substring(0, line.indexOf('='));
String pass = line.substring(line.indexOf('=')+1);
connected = connect_to_wifi(ssid, pass);
if (connected) break;
}
f.close();
} else {
File f = SD.open("/_wifis.txt", "w");
f.print("# WiFi definitions. Syntax: <SSID>=<PASS>. Lines starting with # are ignored. Example:\n# My WiFi=VerySecretPassword\n");
f.close();
}
SPIMaster::select_sd(false);
if (!connected) {
DEBUG("Trying hardcoded WiFi data...\n");
connected = connect_to_wifi(WIFI_SSID, WIFI_PASS);
}
if (!connected) {
INFO("No WiFi connection!\n");
}
INFO("WiFi connected. IP address: %s\n", WiFi.localIP().toString().c_str());
MDNS.begin("esmp3");

View File

@ -533,6 +533,7 @@ bool Player::play() {
if (_state == sleeping || _state == recording) _wakeup();
if (_state != idle) return false;
if (_current_playlist == NULL) return false;
if (_current_playlist->get_file_count()==0) return false;
_current_playlist->start();
String file = _current_playlist->get_current_file();
uint32_t position = _current_playlist->get_position();
@ -547,7 +548,7 @@ void Player::_play_file(String file, uint32_t file_offset) {
_spi->select_sd();
if (file.startsWith("/")) {
_file = new SDDataSource(file);
} else if (file.startsWith("https://")) {
} else if (file.startsWith("http")) {
_file = new HTTPSDataSource(file);
} else {
return;

View File

@ -4,13 +4,17 @@
#include <SD.h>
#include <algorithm>
#include <ArduinoJson.h>
#include <TinyXML.h>
Playlist::Playlist(String path, bool is_url) {
if (is_url) {
_files.push_back(path);
return;
Playlist::Playlist(String path) {
if (path.startsWith("/")) {
_add_path(path);
} else if (path.startsWith("http")) {
_examine_http_url(path);
}
// Add files to _files
}
void Playlist::_add_path(String path) {
SPIMaster::select_sd();
TRACE("Examining folder %s...\n", path.c_str());
if (!path.startsWith("/")) path = String("/") + path;
@ -33,7 +37,8 @@ Playlist::Playlist(String path, bool is_url) {
ext.equals(".mp4") ||
ext.equals(".mpa"))) {
TRACE(" Adding entry %s\n", entry.name());
_files.push_back(entry.name());
String title = filename.substring(0, filename.length() - 4);
_files.push_back({entry.name(), title});
bool non_ascii_chars = false;
for(int i=0; i<filename.length(); i++) {
char c = filename.charAt(i);
@ -55,6 +60,174 @@ Playlist::Playlist(String path, bool is_url) {
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/")) {
_files.push_back({url, url});
} else if (ct.startsWith("application/rss+xml")) {
_parse_rss(http);
} else if (ct.startsWith("application/pls+xml")) {
_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 = "";
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 = "";
} else if (tag.endsWith("/item/title") && (status & STATUS_TAG_TEXT)) {
xml_title = String(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) {
DEBUG("Adding playlist entry: '%s' => '%s'\n", xml_title.c_str(), xml_url.c_str());
xml_files_ptr->push_back({xml_url, xml_title});
}
}
}
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);
}
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);
}
} else if (line.startsWith("http")) {
if (title.length()==0) title = line;
_files.push_back({line, title});
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({url, title});
last_index = -1;
title = "";
url = "";
}
}
// don't close http at the end
}
uint16_t Playlist::get_file_count() {
return _files.size();
}
void Playlist::start() {
_started = true;
}
@ -104,7 +277,7 @@ void Playlist::shuffle(uint8_t random_offset) {
int j = random(random_offset, _files.size()-1);
if (i!=j) {
TRACE(" Swapping elements %d and %d.\n", i, j);
String temp = _files[i];
PlaylistEntry temp = _files[i];
_files[i] = _files[j];
_files[j] = temp;
}
@ -131,7 +304,7 @@ void Playlist::reset() {
}
String Playlist::get_current_file() {
return _files[_current_track];
return _files[_current_track].filename;
}
uint32_t Playlist::get_position() {
@ -148,15 +321,17 @@ bool Playlist::is_fresh() {
void Playlist::dump() {
for (int i=0; i<_files.size(); i++) {
DEBUG(" %02d %2s %s\n", i+1, (i==_current_track) ? "->" : "", _files[i].c_str());
DEBUG(" %02d %2s %s\n", i+1, (i==_current_track) ? "->" : "", _files[i].filename.c_str());
}
}
void Playlist::json(JsonObject json) {
json["_type"] = "playlist";
JsonArray files = json.createNestedArray("files");
for (String file: _files) {
files.add(file);
for (PlaylistEntry entry: _files) {
JsonObject o = files.createNestedObject();
o["filename"] = entry.filename;
o["title"] = entry.title;
}
json["current_track"] = _current_track;
json["has_track_next"] = has_track_next();