From 65118fbc4273f111a0908f3f41bde62633b23eac Mon Sep 17 00:00:00 2001 From: Fabian Schlenz Date: Fri, 29 Nov 2019 17:41:16 +0100 Subject: [PATCH] Play position in stuff like podcasts can now be permanently persisted. --- include/playlist.h | 12 +++++++ include/playlist_manager.h | 1 + src/player.cpp | 3 +- src/playlist.cpp | 42 ++++++++++++++++++---- src/playlist_manager.cpp | 72 ++++++++++++++++++++++++++++++++++++-- 5 files changed, 121 insertions(+), 9 deletions(-) diff --git a/include/playlist.h b/include/playlist.h index d352b37..38e1f97 100644 --- a/include/playlist.h +++ b/include/playlist.h @@ -4,9 +4,16 @@ #include #include "http_client_wrapper.h" +enum PlaylistPersistence { + PERSIST_NONE, + PERSIST_TEMPORARY, + PERSIST_PERMANENTLY +}; + struct PlaylistEntry { String filename; String title; + String id; bool operator<(PlaylistEntry p) { return title < p.title; } }; @@ -19,12 +26,14 @@ private: bool _shuffled = false; std::vector _files; String _title = ""; + String _path; 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: + PlaylistPersistence persistence = PERSIST_TEMPORARY; Playlist(String path); void start(); uint16_t get_file_count(); @@ -34,9 +43,12 @@ public: bool track_prev(); void track_restart(); bool set_track(uint8_t track); + void set_track_by_id(String id); void reset(); + String path(); bool is_empty(); bool get_current_file(String* dst); + String get_current_track_id(); uint32_t get_position(); void set_position(uint32_t p); void shuffle(uint8_t random_offset=0); diff --git a/include/playlist_manager.h b/include/playlist_manager.h index 920728c..42c88a0 100644 --- a/include/playlist_manager.h +++ b/include/playlist_manager.h @@ -20,4 +20,5 @@ public: String json(); bool add_mapping(String id, String folder); String create_mapping_txt(); + void persist(Playlist* p); }; diff --git a/src/player.cpp b/src/player.cpp index d12a99c..2a9f337 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -613,7 +613,8 @@ void Player::stop(bool turn_speaker_off) { if (_state != playing) return; INFO("Stopping...\n"); _current_playlist->set_position(_current_play_position); - + _controller->pm->persist(_current_playlist); + _state = stopping; _stop_delay = 0; _write_control_register(SCI_MODE, _read_control_register(SCI_MODE) | SM_CANCEL); diff --git a/src/playlist.cpp b/src/playlist.cpp index c476a1f..a07a1b6 100644 --- a/src/playlist.cpp +++ b/src/playlist.cpp @@ -7,7 +7,9 @@ #include Playlist::Playlist(String path) { + _path = path; if (path.startsWith("/")) { + persistence = PERSIST_TEMPORARY; _add_path(path); } else if (path.startsWith("http")) { _examine_http_url(path); @@ -44,7 +46,7 @@ void Playlist::_add_path(String path) { ext.equals(".mpa"))) { TRACE(" Adding entry %s\n", entry.name()); String title = filename.substring(0, filename.length() - 4); - _files.push_back({.filename=entry.name(), .title=title}); + _files.push_back({.filename=entry.name(), .title=title, .id=String(_files.size())}); bool non_ascii_chars = false; for(int i=0; igetContentType(); 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}); + persistence = PERSIST_NONE; + _files.push_back({.filename=url, .title=url, .id="none"}); } else if (ct.startsWith("application/rss+xml")) { + persistence = PERSIST_PERMANENTLY; _parse_rss(http); } else if (ct.startsWith("application/pls+xml")) { + persistence = PERSIST_PERMANENTLY; _parse_pls(http); } else { ERROR("Unknown content type %s.\n", ct.c_str()); @@ -95,6 +101,7 @@ String xml_title = ""; String xml_album_title = ""; String xml_url = ""; String xml_enclosure_url = ""; +String xml_guid = ""; bool xml_enclosure_is_audio = false; void xmlcb(uint8_t status, char* tagName, uint16_t tagLen, char* data, uint16_t dataLen) { @@ -106,8 +113,11 @@ void xmlcb(uint8_t status, char* tagName, uint16_t tagLen, char* data, uint16_t } else if (tag.endsWith("/item") && (status & STATUS_START_TAG)) { xml_title = ""; xml_url = ""; + xml_guid = ""; } else if (tag.endsWith("/item/title") && (status & STATUS_TAG_TEXT)) { xml_title = String(data); + } else if (tag.endsWith("/item/guid") && (status & STATUS_TAG_TEXT)) { + xml_guid = data; //} else if (xml_last_tag.endsWith("/item/enclosure") && (status & STATUS_ATTR_TEXT)) { // DEBUG("tag: %s, data: %s\n", tag.c_str(), data); } else if (xml_last_tag.endsWith("/item/enclosure") && tag.equals("type") && (status & STATUS_ATTR_TEXT) && String(data).indexOf("audio/")>=0) { @@ -126,7 +136,7 @@ void xmlcb(uint8_t status, char* tagName, uint16_t tagLen, char* data, uint16_t } 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}); + xml_files_ptr->insert(xml_files_ptr->begin(), {.filename=xml_url, .title=xml_title, .id=xml_guid}); } } } @@ -180,7 +190,7 @@ void Playlist::_parse_m3u(HTTPClientWrapper* http) { } } else if (line.startsWith("http")) { if (title.length()==0) title = line; - _files.push_back({.filename=line, .title=title}); + _files.push_back({.filename=line, .title=title, .id="none"}); title = ""; } line = ""; @@ -222,7 +232,7 @@ void Playlist::_parse_pls(HTTPClientWrapper* http) { } if (title.length()>0 && url.length()>0) { - _files.push_back({.filename=url, .title=title}); + _files.push_back({.filename=url, .title=title, .id="none"}); last_index = -1; title = ""; url = ""; @@ -231,6 +241,10 @@ void Playlist::_parse_pls(HTTPClientWrapper* http) { // don't close http at the end } +String Playlist::path() { + return _path; +} + uint16_t Playlist::get_file_count() { return _files.size(); } @@ -274,6 +288,15 @@ bool Playlist::set_track(uint8_t track) { return false; } +void Playlist::set_track_by_id(String id) { + for (int i=0; i<_files.size(); i++) { + if (id.equals(_files[i].id)) { + set_track(i); + return; + } + } +} + void Playlist::track_restart() { _position = 0; } @@ -310,8 +333,15 @@ void Playlist::reset() { _started = false; } +String Playlist::get_current_track_id() { + if (_current_track > _files.size()) { + return ""; + } + return _files[_current_track].id; +} + bool Playlist::get_current_file(String* dst) { - if (_current_track < _files.size()) { + if (_current_track > _files.size()) { return false; } else { dst->concat(_files[_current_track].filename); diff --git a/src/playlist_manager.cpp b/src/playlist_manager.cpp index fa87022..5f3576c 100644 --- a/src/playlist_manager.cpp +++ b/src/playlist_manager.cpp @@ -104,10 +104,41 @@ Playlist* PlaylistManager::get_playlist_for_id(String id) { } Playlist* PlaylistManager::get_playlist_for_folder(String folder) { + Playlist* p; if (!_playlists.count(folder)) { - _playlists[folder] = new Playlist(folder); + p = new Playlist(folder); + _playlists[folder] = p; + if (p->persistence == PERSIST_PERMANENTLY) { + // TODO Load persistence from file + String search = folder; + search += "="; + SPIMaster::select_sd(); + if (SD.exists("/_positions.txt")) { + File f = SD.open("/_positions.txt", "r"); + while (true) { + String s = f.readStringUntil('\n'); + if (s.length()==0) break; + if (s.startsWith(search)) { + s = s.substring(search.length()); + int idx = s.indexOf(','); + String title_index = s.substring(0, idx); + uint32_t position = s.substring(idx+1).toInt(); + p->set_track_by_id(title_index); + p->set_position(position); + break; + } + } + f.close(); + } + SPIMaster::select_sd(false); + } + } else { + p = _playlists[folder]; + if (p->persistence == PERSIST_NONE) { + p->reset(); + } } - return _playlists[folder]; + return p; } void PlaylistManager::dump_ids() { @@ -164,3 +195,40 @@ String PlaylistManager::create_mapping_txt() { } return s; } + +void PlaylistManager::persist(Playlist* p) { + if (p->persistence != PERSIST_PERMANENTLY) return; + + String search = p->path(); + search += '='; + + bool old_file_existed = false; + + SPIMaster::select_sd(); + if (SD.exists("_positions.txt")) { + SD.rename("/_positions.txt", "/_positions.temp.txt"); + old_file_existed = true; + } + File dst = SD.open("/_positions.txt", "w"); + + if (old_file_existed) { + File src = SD.open("/_positions.temp.txt", "r"); + + while (true) { + String line = src.readStringUntil('\n'); + line.trim(); + if (line.startsWith(search)) continue; + dst.println(line); + } + + src.close(); + SD.remove("/_positions.temp.txt"); + } + + dst.print(search); + dst.print(p->get_current_track_id()); + dst.print(','); + dst.println(p->get_position()); + dst.close(); + SPIMaster::select_sd(false); +}