Merge branch 'feature-webstreams'
This commit is contained in:
commit
3b0410f560
@ -93,6 +93,20 @@
|
|||||||
<span>×</span>
|
<span>×</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 class="modal-body">
|
||||||
<div id="albums_without_id_area">
|
<div id="albums_without_id_area">
|
||||||
@ -173,7 +187,7 @@ update_playlist = function(data) {
|
|||||||
tr = $('<tr>').data('track', i);
|
tr = $('<tr>').data('track', i);
|
||||||
tr.append($('<td>').html(i + 1));
|
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.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);
|
$('#track_list').append(tr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,12 +203,10 @@ update_playlist = function(data) {
|
|||||||
$('#button_track_prev').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
|
$('#button_track_prev').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$('#album').html(data.title);
|
||||||
var file = data.files[data.current_track];
|
var file = data.files[data.current_track];
|
||||||
if (file) {
|
if (file) {
|
||||||
file = file.substr(1);
|
$('#track').html(file.title);
|
||||||
$('#album').html(file.substr(0, file.indexOf('/')));
|
|
||||||
file = file.substr(file.indexOf('/')+1);
|
|
||||||
$('#track').html(file.substr(0, file.lastIndexOf('.')));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,6 +261,7 @@ process_ws_message = function(event) {
|
|||||||
for (var i=0; i<data.length; i++) {
|
for (var i=0; i<data.length; i++) {
|
||||||
var json = JSON.parse(data[i]);
|
var json = JSON.parse(data[i]);
|
||||||
console.log(json);
|
console.log(json);
|
||||||
|
if (json === null) continue;
|
||||||
switch(json["_type"]) {
|
switch(json["_type"]) {
|
||||||
case "position": update_position(json); break;
|
case "position": update_position(json); break;
|
||||||
case "player": update_player(json); break;
|
case "player": update_player(json); break;
|
||||||
@ -275,13 +288,35 @@ $(function() {
|
|||||||
$('#button_settings').click(function(e) { $('#settingsModal').modal('show'); });
|
$('#button_settings').click(function(e) { $('#settingsModal').modal('show'); });
|
||||||
$('#button_reset_vs1053').click(function(e) { ws.send("reset_vs1053"); $('#settingsModal').modal('hide'); });
|
$('#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_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) {
|
$('#button_add_mapping').click(function(e) {
|
||||||
$('#settingsModal').modal('hide');
|
$('#settingsModal').modal('hide');
|
||||||
$('#openModal').modal('show');
|
$('#openModal').modal('show');
|
||||||
$('.add_mapping_button').show();
|
$('.add_mapping_button').show();
|
||||||
|
$('#button_url_open').hide();
|
||||||
|
$('#button_url_add_mapping').show();
|
||||||
play_on_click = false;
|
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>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include <HTTPClient.h>
|
#include "http_client_wrapper.h"
|
||||||
|
|
||||||
class DataSource {
|
class DataSource {
|
||||||
private:
|
private:
|
||||||
@ -11,7 +11,7 @@ public:
|
|||||||
DataSource() {};
|
DataSource() {};
|
||||||
virtual ~DataSource() {};
|
virtual ~DataSource() {};
|
||||||
virtual size_t read(uint8_t* buf, size_t len) = 0;
|
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 size_t position() = 0;
|
||||||
virtual void seek(size_t position) = 0;
|
virtual void seek(size_t position) = 0;
|
||||||
virtual size_t size() = 0;
|
virtual size_t size() = 0;
|
||||||
@ -27,7 +27,7 @@ public:
|
|||||||
SDDataSource(String file);
|
SDDataSource(String file);
|
||||||
~SDDataSource();
|
~SDDataSource();
|
||||||
size_t read(uint8_t* buf, size_t len);
|
size_t read(uint8_t* buf, size_t len);
|
||||||
uint8_t read();
|
int read();
|
||||||
size_t position();
|
size_t position();
|
||||||
void seek(size_t position);
|
void seek(size_t position);
|
||||||
size_t size();
|
size_t size();
|
||||||
@ -39,17 +39,16 @@ public:
|
|||||||
class HTTPSDataSource : public DataSource {
|
class HTTPSDataSource : public DataSource {
|
||||||
private:
|
private:
|
||||||
WiFiClient* _stream = NULL;
|
WiFiClient* _stream = NULL;
|
||||||
HTTPClient* _http = NULL;
|
HTTPClientWrapper* _http = NULL;
|
||||||
uint32_t _length;
|
|
||||||
uint32_t _position;
|
uint32_t _position;
|
||||||
public:
|
public:
|
||||||
HTTPSDataSource(String url, uint32_t offset=0);
|
HTTPSDataSource(String url, uint32_t offset=0);
|
||||||
~HTTPSDataSource();
|
~HTTPSDataSource();
|
||||||
size_t read(uint8_t* buf, size_t len);
|
size_t read(uint8_t* buf, size_t len);
|
||||||
uint8_t read();
|
int read();
|
||||||
size_t position();
|
size_t position();
|
||||||
void seek(size_t position);
|
void seek(size_t position);
|
||||||
size_t size();
|
size_t size();
|
||||||
void close();
|
void close();
|
||||||
bool usable();
|
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 <Arduino.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
|
#include "http_client_wrapper.h"
|
||||||
|
|
||||||
|
struct PlaylistEntry {
|
||||||
|
String filename;
|
||||||
|
String title;
|
||||||
|
|
||||||
|
bool operator<(PlaylistEntry p) { return title < p.title; }
|
||||||
|
};
|
||||||
|
|
||||||
class Playlist {
|
class Playlist {
|
||||||
private:
|
private:
|
||||||
@ -9,10 +17,17 @@ private:
|
|||||||
uint32_t _current_track = 0;
|
uint32_t _current_track = 0;
|
||||||
bool _started = false;
|
bool _started = false;
|
||||||
bool _shuffled = 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:
|
public:
|
||||||
Playlist(String path, bool is_url=false);
|
Playlist(String path);
|
||||||
void start();
|
void start();
|
||||||
|
uint16_t get_file_count();
|
||||||
bool has_track_next();
|
bool has_track_next();
|
||||||
bool has_track_prev();
|
bool has_track_prev();
|
||||||
bool track_next();
|
bool track_next();
|
||||||
|
@ -17,6 +17,7 @@ build_flags=!./build_version.sh
|
|||||||
lib_deps = MFRC522
|
lib_deps = MFRC522
|
||||||
https://github.com/me-no-dev/ESPAsyncWebServer.git
|
https://github.com/me-no-dev/ESPAsyncWebServer.git
|
||||||
ArduinoJSON
|
ArduinoJSON
|
||||||
|
6691 ; TinyXML
|
||||||
upload_port = /dev/cu.SLAB_USBtoUART
|
upload_port = /dev/cu.SLAB_USBtoUART
|
||||||
monitor_speed = 74480
|
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(String file) { _file = SD.open(file, "r"); }
|
||||||
SDDataSource::~SDDataSource() { if (_file) _file.close(); }
|
SDDataSource::~SDDataSource() { if (_file) _file.close(); }
|
||||||
size_t SDDataSource::read(uint8_t* buf, size_t len) { return _file.read(buf, len); }
|
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(); }
|
size_t SDDataSource::position() { return _file.position(); }
|
||||||
void SDDataSource::seek(size_t position) { _file.seek(position); }
|
void SDDataSource::seek(size_t position) { _file.seek(position); }
|
||||||
size_t SDDataSource::size() { return _file.size(); }
|
size_t SDDataSource::size() { return _file.size(); }
|
||||||
@ -39,55 +39,19 @@ void SDDataSource::skip_id3_tag() {
|
|||||||
|
|
||||||
////////////// HTTPSDataSource //////////////
|
////////////// HTTPSDataSource //////////////
|
||||||
HTTPSDataSource::HTTPSDataSource(String url, uint32_t offset) {
|
HTTPSDataSource::HTTPSDataSource(String url, uint32_t offset) {
|
||||||
uint8_t tries_left = 5;
|
_http = new HTTPClientWrapper();
|
||||||
int status;
|
if (!_http->get(url, offset)) return;
|
||||||
do {
|
_position = 0;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HTTPSDataSource::~HTTPSDataSource() {
|
HTTPSDataSource::~HTTPSDataSource() {
|
||||||
if (_stream) _stream->stop();
|
_http->close();
|
||||||
_http->end();
|
|
||||||
delete _stream;
|
|
||||||
delete _http;
|
delete _http;
|
||||||
}
|
}
|
||||||
bool HTTPSDataSource::usable() { return _http && _stream; }
|
bool HTTPSDataSource::usable() { return _http; }
|
||||||
size_t HTTPSDataSource::read(uint8_t* buf, size_t len) { size_t result = _stream->read(buf, len); _position += result; return result; }
|
size_t HTTPSDataSource::read(uint8_t* buf, size_t len) { size_t result = _http->read(buf, len); _position += result; return result; }
|
||||||
uint8_t HTTPSDataSource::read() { _position++; return _stream->read(); }
|
int HTTPSDataSource::read() { int b = _http->read(); if (b>=0) _position++; return b; }
|
||||||
size_t HTTPSDataSource::position() { return _position; }
|
size_t HTTPSDataSource::position() { return _position; }
|
||||||
void HTTPSDataSource::seek(size_t position) { return; /* TODO */ }
|
void HTTPSDataSource::seek(size_t position) { return; /* TODO */ }
|
||||||
size_t HTTPSDataSource::size() { return _length; }
|
size_t HTTPSDataSource::size() { return _http->getSize(); }
|
||||||
void HTTPSDataSource::close() { _stream->stop(); }
|
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) {
|
} else if (type==WS_EVT_DATA) {
|
||||||
AwsFrameInfo* info = (AwsFrameInfo*) arg;
|
AwsFrameInfo* info = (AwsFrameInfo*) arg;
|
||||||
if (info->final && info->index==0 && info->len==len && info->opcode==WS_TEXT) {
|
if (info->final && info->index==0 && info->len==len && info->opcode==WS_TEXT) {
|
||||||
|
data[len]='\0';
|
||||||
DEBUG("Received ws message: %s\n", (char*)data);
|
DEBUG("Received ws message: %s\n", (char*)data);
|
||||||
_controller->queue_command((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;
|
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() {
|
void setup() {
|
||||||
delay(500);
|
delay(500);
|
||||||
Serial.begin(74880);
|
Serial.begin(74880);
|
||||||
@ -58,15 +71,35 @@ void setup() {
|
|||||||
controller = new Controller(player, pm);
|
controller = new Controller(player, pm);
|
||||||
INFO("Player and controller initialized.\n");
|
INFO("Player and controller initialized.\n");
|
||||||
|
|
||||||
DEBUG("Connecting to wifi \"%s\"...\n", WIFI_SSID);
|
bool connected = false;
|
||||||
WiFi.mode(WIFI_AP_STA);
|
INFO("Connecting to WiFi...\n");
|
||||||
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
SPIMaster::select_sd();
|
||||||
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
|
if (SD.exists("/_wifis.txt")) {
|
||||||
ERROR("Could not connect to Wifi. Rebooting.");
|
DEBUG("Reading /_wifis.txt\n");
|
||||||
delay(1000);
|
File f = SD.open("/_wifis.txt", "r");
|
||||||
ESP.restart();
|
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");
|
MDNS.begin("esmp3");
|
||||||
|
|
||||||
|
@ -533,6 +533,7 @@ bool Player::play() {
|
|||||||
if (_state == sleeping || _state == recording) _wakeup();
|
if (_state == sleeping || _state == recording) _wakeup();
|
||||||
if (_state != idle) return false;
|
if (_state != idle) return false;
|
||||||
if (_current_playlist == NULL) return false;
|
if (_current_playlist == NULL) return false;
|
||||||
|
if (_current_playlist->get_file_count()==0) return false;
|
||||||
_current_playlist->start();
|
_current_playlist->start();
|
||||||
String file = _current_playlist->get_current_file();
|
String file = _current_playlist->get_current_file();
|
||||||
uint32_t position = _current_playlist->get_position();
|
uint32_t position = _current_playlist->get_position();
|
||||||
@ -547,7 +548,7 @@ void Player::_play_file(String file, uint32_t file_offset) {
|
|||||||
_spi->select_sd();
|
_spi->select_sd();
|
||||||
if (file.startsWith("/")) {
|
if (file.startsWith("/")) {
|
||||||
_file = new SDDataSource(file);
|
_file = new SDDataSource(file);
|
||||||
} else if (file.startsWith("https://")) {
|
} else if (file.startsWith("http")) {
|
||||||
_file = new HTTPSDataSource(file);
|
_file = new HTTPSDataSource(file);
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
|
205
src/playlist.cpp
205
src/playlist.cpp
@ -4,13 +4,18 @@
|
|||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
|
#include <TinyXML.h>
|
||||||
|
|
||||||
Playlist::Playlist(String path, bool is_url) {
|
Playlist::Playlist(String path) {
|
||||||
if (is_url) {
|
if (path.startsWith("/")) {
|
||||||
_files.push_back(path);
|
_add_path(path);
|
||||||
return;
|
} 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();
|
SPIMaster::select_sd();
|
||||||
TRACE("Examining folder %s...\n", path.c_str());
|
TRACE("Examining folder %s...\n", path.c_str());
|
||||||
if (!path.startsWith("/")) path = String("/") + path;
|
if (!path.startsWith("/")) path = String("/") + path;
|
||||||
@ -19,6 +24,11 @@ Playlist::Playlist(String path, bool is_url) {
|
|||||||
SPIMaster::select_sd(false);
|
SPIMaster::select_sd(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_title = path.substring(1);
|
||||||
|
int idx = _title.indexOf('/');
|
||||||
|
if (idx>0) {
|
||||||
|
_title.remove(idx);
|
||||||
|
}
|
||||||
File dir = SD.open(path);
|
File dir = SD.open(path);
|
||||||
File entry;
|
File entry;
|
||||||
while (entry = dir.openNextFile()) {
|
while (entry = dir.openNextFile()) {
|
||||||
@ -33,7 +43,8 @@ Playlist::Playlist(String path, bool is_url) {
|
|||||||
ext.equals(".mp4") ||
|
ext.equals(".mp4") ||
|
||||||
ext.equals(".mpa"))) {
|
ext.equals(".mpa"))) {
|
||||||
TRACE(" Adding entry %s\n", entry.name());
|
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;
|
bool non_ascii_chars = false;
|
||||||
for(int i=0; i<filename.length(); i++) {
|
for(int i=0; i<filename.length(); i++) {
|
||||||
char c = filename.charAt(i);
|
char c = filename.charAt(i);
|
||||||
@ -55,6 +66,175 @@ Playlist::Playlist(String path, bool is_url) {
|
|||||||
std::sort(_files.begin(), _files.end());
|
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() {
|
void Playlist::start() {
|
||||||
_started = true;
|
_started = true;
|
||||||
}
|
}
|
||||||
@ -104,7 +284,7 @@ void Playlist::shuffle(uint8_t random_offset) {
|
|||||||
int j = random(random_offset, _files.size()-1);
|
int j = random(random_offset, _files.size()-1);
|
||||||
if (i!=j) {
|
if (i!=j) {
|
||||||
TRACE(" Swapping elements %d and %d.\n", i, j);
|
TRACE(" Swapping elements %d and %d.\n", i, j);
|
||||||
String temp = _files[i];
|
PlaylistEntry temp = _files[i];
|
||||||
_files[i] = _files[j];
|
_files[i] = _files[j];
|
||||||
_files[j] = temp;
|
_files[j] = temp;
|
||||||
}
|
}
|
||||||
@ -131,7 +311,7 @@ void Playlist::reset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String Playlist::get_current_file() {
|
String Playlist::get_current_file() {
|
||||||
return _files[_current_track];
|
return _files[_current_track].filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t Playlist::get_position() {
|
uint32_t Playlist::get_position() {
|
||||||
@ -148,15 +328,18 @@ bool Playlist::is_fresh() {
|
|||||||
|
|
||||||
void Playlist::dump() {
|
void Playlist::dump() {
|
||||||
for (int i=0; i<_files.size(); i++) {
|
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) {
|
void Playlist::json(JsonObject json) {
|
||||||
json["_type"] = "playlist";
|
json["_type"] = "playlist";
|
||||||
|
json["title"] = _title;
|
||||||
JsonArray files = json.createNestedArray("files");
|
JsonArray files = json.createNestedArray("files");
|
||||||
for (String file: _files) {
|
for (PlaylistEntry entry: _files) {
|
||||||
files.add(file);
|
JsonObject o = files.createNestedObject();
|
||||||
|
o["filename"] = entry.filename;
|
||||||
|
o["title"] = entry.title;
|
||||||
}
|
}
|
||||||
json["current_track"] = _current_track;
|
json["current_track"] = _current_track;
|
||||||
json["has_track_next"] = has_track_next();
|
json["has_track_next"] = has_track_next();
|
||||||
|
@ -39,16 +39,18 @@ void PlaylistManager::scan_files() {
|
|||||||
String folder = data.substring(eq + 1);
|
String folder = data.substring(eq + 1);
|
||||||
TRACE(" Adding mapping: %s=>%s\n", rfid_id.c_str(), folder.c_str());
|
TRACE(" Adding mapping: %s=>%s\n", rfid_id.c_str(), folder.c_str());
|
||||||
_map[rfid_id] = folder;
|
_map[rfid_id] = folder;
|
||||||
|
|
||||||
bool found=false;
|
if (folder.charAt(0)=='/') {
|
||||||
for (String f: folders) {
|
bool found=false;
|
||||||
if (f.equals(folder)) {
|
for (String f: folders) {
|
||||||
found = true;
|
if (f.equals(folder)) {
|
||||||
break;
|
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