Compare commits
	
		
			5 Commits
		
	
	
		
			710b8a2cdc
			...
			feature-we
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8f19b990ff | |||
| 519ac0e3bd | |||
| 651843fb06 | |||
| fcbbdce118 | |||
| 6f8683ba9d | 
| @@ -94,6 +94,20 @@ | ||||
| 					</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"> | ||||
| 						<h6>Albums without RFID card</h6> | ||||
| @@ -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,14 +39,13 @@ 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(); | ||||
|   | ||||
							
								
								
									
										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 | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -40,6 +40,7 @@ void PlaylistManager::scan_files() { | ||||
| 				TRACE("  Adding mapping: %s=>%s\n", rfid_id.c_str(), folder.c_str()); | ||||
| 				_map[rfid_id] = folder; | ||||
| 				 | ||||
| 				if (folder.charAt(0)=='/') { | ||||
| 					bool found=false; | ||||
| 					for (String f: folders) { | ||||
| 						if (f.equals(folder)) { | ||||
| @@ -52,6 +53,7 @@ void PlaylistManager::scan_files() { | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		f.close(); | ||||
| 	} | ||||
| 	root.close(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user