Merge branch 'feature-webstreams'
This commit is contained in:
commit
3b0410f560
@ -93,6 +93,20 @@
|
||||
<span>×</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);
|
||||
}
|
||||
|
||||
@ -189,12 +203,10 @@ update_playlist = function(data) {
|
||||
$('#button_track_prev').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
|
||||
}
|
||||
|
||||
$('#album').html(data.title);
|
||||
var file = data.files[data.current_track];
|
||||
if (file) {
|
||||
file = file.substr(1);
|
||||
$('#album').html(file.substr(0, file.indexOf('/')));
|
||||
file = file.substr(file.indexOf('/')+1);
|
||||
$('#track').html(file.substr(0, file.lastIndexOf('.')));
|
||||
$('#track').html(file.title);
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,6 +261,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 +288,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>
|
||||
|
@ -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();
|
||||
};
|
||||
};
|
||||
|
37
include/http_client_wrapper.h
Normal file
37
include/http_client_wrapper.h
Normal 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();
|
||||
};
|
@ -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;
|
||||
String _title = "";
|
||||
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);
|
||||
Playlist(String path);
|
||||
void start();
|
||||
uint16_t get_file_count();
|
||||
bool has_track_next();
|
||||
bool has_track_prev();
|
||||
bool track_next();
|
||||
|
@ -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
|
||||
|
@ -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
210
src/http_client_wrapper.cpp
Normal 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");
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
49
src/main.cpp
49
src/main.cpp
@ -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");
|
||||
|
||||
|
@ -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;
|
||||
|
205
src/playlist.cpp
205
src/playlist.cpp
@ -4,13 +4,18 @@
|
||||
#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
|
||||
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;
|
||||
@ -19,6 +24,11 @@ Playlist::Playlist(String path, bool is_url) {
|
||||
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()) {
|
||||
@ -33,7 +43,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({.filename=entry.name(), .title=title});
|
||||
bool non_ascii_chars = false;
|
||||
for(int i=0; i<filename.length(); i++) {
|
||||
char c = filename.charAt(i);
|
||||
@ -55,6 +66,175 @@ 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({.filename=url, .title=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);
|
||||
if (_title.length()==0) _title=title;
|
||||
}
|
||||
} else if (line.startsWith("http")) {
|
||||
if (title.length()==0) title = line;
|
||||
_files.push_back({.filename=line, .title=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({.filename=url, .title=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 +284,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 +311,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 +328,18 @@ 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";
|
||||
json["title"] = _title;
|
||||
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();
|
||||
|
@ -39,16 +39,18 @@ void PlaylistManager::scan_files() {
|
||||
String folder = data.substring(eq + 1);
|
||||
TRACE(" Adding mapping: %s=>%s\n", rfid_id.c_str(), folder.c_str());
|
||||
_map[rfid_id] = folder;
|
||||
|
||||
bool found=false;
|
||||
for (String f: folders) {
|
||||
if (f.equals(folder)) {
|
||||
found = true;
|
||||
break;
|
||||
|
||||
if (folder.charAt(0)=='/') {
|
||||
bool found=false;
|
||||
for (String f: folders) {
|
||||
if (f.equals(folder)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
INFO("WARNING: Found mapping for RFID id %s which maps to non-existing folder %s!\n", rfid_id.c_str(), folder.c_str());
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
INFO("WARNING: Found mapping for RFID id %s which maps to non-existing folder %s!\n", rfid_id.c_str(), folder.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user