#include #include #include "MusicPlaylist.h" #include "src/TSServer.h" #include "src/client/ConnectedClient.h" #include #include "src/client/music/internal_provider/channel_replay/ChannelProvider.h" #include "MusicPlayer.h" using namespace ts; using namespace ts::music; using namespace std; using namespace std::chrono; Playlist::Playlist(const std::shared_ptr &manager, const std::shared_ptr &properties, const std::shared_ptr& permissions) : _properties(properties), _permissions(permissions), manager(manager) { } Playlist::~Playlist() { this->destory_tree(); } void Playlist::set_self_ref(const std::shared_ptr &ref) { assert(&*ref == this); this->_self = ref; } std::shared_ptr Playlist::get_server() { auto handle = this->ref_handle(); if(!handle) return nullptr; return handle->ref_server(); } ServerId Playlist::get_server_id() { auto server = this->get_server(); if(!server) return 0; return server->getServerId(); } sql::SqlManager* Playlist::get_sql() { auto server = this->get_server(); if(!server) return nullptr; return server->getSql(); } std::shared_ptr Playlist::playlist_end(const std::unique_lock& lock) { assert(lock.owns_lock()); auto current = this->playlist_head; while(current && current->next_song) current = current->next_song; return current; } std::shared_ptr Playlist::playlist_find(const std::unique_lock &lock, ts::SongId id) { assert(lock.owns_lock()); auto current = this->playlist_head; while(current) { assert(current->entry); if(current->entry->id == id) return current; current = current->next_song; } return nullptr; } bool Playlist::playlist_insert(const std::unique_lock &lock, const shared_ptr &entry, shared_ptr previous, bool link_only) { assert(lock.owns_lock()); entry->next_song = nullptr; if(!this->playlist_head) { this->playlist_head = entry; previous = nullptr; } else { if(previous) { if(previous->next_song) { assert(previous->next_song->previous_song == previous); entry->next_song = previous->next_song; if(link_only) previous->next_song->previous_song = entry; else previous->next_song->set_previous_song(entry); } previous->next_song = entry; } else { if(link_only) this->playlist_head->previous_song = entry; else this->playlist_head->set_previous_song(entry); entry->next_song = this->playlist_head; this->playlist_head = entry; } } if(link_only) entry->previous_song = previous; else entry->set_previous_song(previous); return true; } bool Playlist::playlist_reorder(const std::unique_lock& lock, const shared_ptr &entry, std::shared_ptr previous) { assert(lock.owns_lock()); if(!this->playlist_remove(lock, entry)) return false; return this->playlist_insert(lock, entry, std::move(previous)); } bool Playlist::playlist_remove(const std::unique_lock& lock, const std::shared_ptr &entry) { assert(lock.owns_lock()); if(entry == this->playlist_head) { this->playlist_head = entry->next_song; if(this->playlist_head) this->playlist_head->set_previous_song(nullptr); } else { assert(entry->previous_song); /* if its not head it must have a previous song */ entry->previous_song->next_song = entry->next_song; if(entry->next_song) entry->next_song->set_previous_song(entry->previous_song); } /* release references */ entry->previous_song = nullptr; entry->next_song = nullptr; return true; } void Playlist::load_songs() { if(!this->_songs_loaded) this->_songs_loaded = true; else return; auto entries = this->load_entries(); this->build_tree(std::move(entries)); } bool Playlist::sql_add(const std::shared_ptr &entry) { auto sql_handle = this->get_sql(); if(!sql_handle) return false; entry->id = ++current_id; //`serverId` INT NOT NULL, `playlist_id` INT, `song_id` INT, `order_id` INT, `invoker_dbid` INT, `url` TEXT auto sql_result = sql::command(sql_handle, "INSERT INTO `playlist_songs` (`serverId`, `playlist_id`, `song_id`, `order_id`, `invoker_dbid`, `url`, `url_loader`, `loaded`, `metadata`) VALUES (:server_id, :playlist_id, :song_id, :order_id, :invoker_dbid, :url, :url_loader, :loaded, :metadata)", variable{":server_id", this->get_server_id()}, variable{":playlist_id", this->playlist_id()}, variable{":song_id", entry->id}, variable{":order_id", entry->previous_song_id}, variable{":invoker_dbid", entry->invoker}, variable{":url", entry->url}, variable{":url_loader", entry->url_loader}, variable{":loaded", entry->loaded}, variable{":metadata", entry->metadata} ).execute(); LOG_SQL_CMD(sql_result); return !!sql_result; } bool Playlist::sql_apply_changes(const std::shared_ptr &entry) { auto sql_handle = this->get_sql(); if(!sql_handle) return false; sql::command(sql_handle, "UPDATE `playlist_songs` SET `order_id` = :order_id, `invoker_dbid` = :invoker_dbid, `url` = :url , `url_loader` = :url_loader, `loaded` = :loaded, `metadata` = :metadata WHERE `serverId` = :server_id AND `playlist_id` = :playlist_id AND `song_id` = :song_id", variable{":server_id", this->get_server_id()}, variable{":playlist_id", this->playlist_id()}, variable{":song_id", entry->id}, variable{":order_id", entry->previous_song_id}, variable{":invoker_dbid", entry->invoker}, variable{":url", entry->url}, variable{":url_loader", entry->url_loader}, variable{":loaded", entry->loaded}, variable{":metadata", entry->metadata} ).executeLater().waitAndGetLater(LOG_SQL_CMD, {-1, "failed future"}); return true; } bool Playlist::sql_flush_all_changes() { unique_lock list_lock(this->playlist_lock); deque> changed_entries; auto head = this->playlist_head; while(head) { if(head->modified) { changed_entries.push_back(head->entry); head->modified = false; continue; } head = head->next_song; } list_lock.unlock(); for(const auto& entry : changed_entries) this->sql_apply_changes(entry); //TODO Whats when we encounter an error? return true; } bool Playlist::sql_remove(const std::shared_ptr &entry) { auto sql_handle = this->get_sql(); if(!sql_handle) return false; sql::command(sql_handle, "DELETE FROM `playlist_songs` WHERE `serverId` = :server_id AND `playlist_id` = :playlist_id AND `song_id` = :song_id", variable{":server_id", this->get_server_id()}, variable{":playlist_id", this->playlist_id()}, variable{":song_id", entry->id} ).executeLater().waitAndGetLater(LOG_SQL_CMD, {-1, "failed future"}); return true; } std::deque> Playlist::load_entries() { std::deque> result; this->current_id = 0; auto sql_handle = this->get_sql(); assert(sql_handle); auto sql_result = sql::command(sql_handle, "SELECT `song_id`, `order_id`, `invoker_dbid`, `url`, `url_loader`, `loaded`, `metadata` FROM `playlist_songs` WHERE `serverId` = :server_id AND `playlist_id` = :playlist_id", variable{":server_id", this->get_server_id()}, variable{":playlist_id", this->playlist_id()}) .query([&](int length, string* values, string* columns) { auto entry = make_shared(); for(int index = 0; index < length; index++) { try { if(columns[index] == "song_id") entry->id = (SongId) stoll(values[index]); else if(columns[index] == "order_id") entry->previous_song_id = (SongId) stoll(values[index]); else if(columns[index] == "invoker_dbid") entry->invoker = (ClientDbId) stoll(values[index]); else if(columns[index] == "url") entry->url = values[index]; else if(columns[index] == "url_loader") entry->url_loader = values[index]; else if(columns[index] == "loaded") entry->loaded = values[index].length() > 0 && stoll(values[index]) == 1; else if(columns[index] == "metadata") entry->metadata = values[index]; } catch(const std::exception& ex) { logError(this->get_server_id(), "[PlayList] Failed to parse song entry property in playlist {}. Key: {}, Value: {}, Error: {}", this->playlist_id(), columns[index], values[index], ex.what()); return; } } if(entry->id > this->current_id) this->current_id = entry->id; result.push_back(move(entry)); }); LOG_SQL_CMD(sql_result); map count; for(const auto& entry : result) ++count[entry->id]; for(const auto& entry : count) { if(entry.second <= 1) continue; logError(this->get_server_id(), "[PlayList] Playlist {} contains {} times a song with ID {}. removing all!", this->playlist_id(), entry.second, entry.first); auto sql_command = sql::command(sql_handle, "DELETE FROM `playlist_songs` WHERE `serverId` = :server_id AND `playlist_id` = :playlist_id AND `song_id` = :song_id", variable{":server_id", this->get_server_id()}, variable{":playlist_id", this->playlist_id()}, variable{":song_id", entry.first} ).execute(); LOG_SQL_CMD(sql_command); } { std::deque> _result; for(const auto& entry : result) if(count[entry->id] <= 1) { this->enqueue_load(entry); _result.push_back(entry); } return _result; } } bool Playlist::build_tree(deque> entries) { this->playlist_head = nullptr; if(entries.empty()) return true; unique_lock list_lock(this->playlist_lock); auto find_entry = [&](SongId id) -> shared_ptr { for(const auto& entry : entries) if(entry->id == id) return entry; return nullptr; }; deque> l_entries; for(const auto& entry : entries) l_entries.push_back(move(linked::create_entry(0, entry->id, entry->previous_song_id))); deque errors; auto head = linked::build_chain(l_entries, errors); shared_ptr current_tail = nullptr; while(head) { auto entry = make_shared(); entry->entry = find_entry(head->entry_id); if(!current_tail) { /* initialization */ if(head->modified) entry->set_previous_song(nullptr); else entry->previous_song = nullptr; this->playlist_head = entry; } else { if(head->modified) entry->set_previous_song(current_tail); else entry->previous_song = current_tail; } if(current_tail) current_tail->next_song = entry; current_tail = entry; head = head->next; } return true; } void Playlist::destory_tree() { unique_lock list_lock(this->playlist_lock); auto element = this->playlist_head; while(element) { element->entry = nullptr; if(element->previous_song) { element->previous_song->next_song = nullptr; element->previous_song = nullptr; } element = element->next_song; } } std::deque> Playlist::list_songs() { deque> result; unique_lock list_lock(this->playlist_lock); auto head = this->playlist_head; while(head) { result.push_back(head->entry); head = head->next_song; } return result; } std::shared_ptr Playlist::find_song(ts::SongId id) { unique_lock list_lock(this->playlist_lock); auto head = this->playlist_head; while(head) { if(head->entry->id == id) return head->entry; head = head->next_song; } return nullptr; } std::shared_ptr Playlist::add_song(const std::shared_ptr &client, const std::string &url, const std::string& url_loader, ts::SongId order) { return this->add_song(client ? client->getClientDatabaseId() : 0, url, url_loader, order); } std::shared_ptr Playlist::add_song(ClientDbId client, const std::string &url, const std::string& url_loader, ts::SongId order) { auto entry = make_shared(); entry->previous_song_id = order; entry->invoker = client; entry->url = url; entry->url_loader = url_loader; if(!this->sql_add(entry)) return nullptr; auto list_entry = make_shared(); list_entry->entry = entry; unique_lock list_lock(this->playlist_lock); if(order == 0) { auto end = playlist_end(list_lock); entry->previous_song_id = end ? end->entry->id : 0; list_entry->modified = true; } auto order_entry = this->playlist_find(list_lock, entry->previous_song_id); this->playlist_insert(list_lock, list_entry, order_entry); if(order_entry ? order_entry->entry->id : 0 != order || list_entry->modified) { list_entry->modified = false; entry->previous_song_id = list_entry->previous_song ? list_entry->previous_song->entry->id : 0; this->sql_apply_changes(entry); } this->enqueue_load(entry); this->properties()[property::PLAYLIST_FLAG_FINISHED] = false; return entry; } bool Playlist::delete_song(ts::SongId id) { unique_lock list_lock(this->playlist_lock); auto song = this->playlist_find(list_lock, id); if(!song) return false; this->playlist_remove(list_lock, song); list_lock.unlock(); this->sql_remove(song->entry); return true; } bool Playlist::reorder_song(ts::SongId song_id, ts::SongId order_id) { unique_lock list_lock(this->playlist_lock); auto song = this->playlist_find(list_lock, song_id); auto order = this->playlist_find(list_lock, order_id); if(!song) return false; if(!order && order_id != 0) return false; if(!this->playlist_reorder(list_lock, song, order)) return false; list_lock.unlock(); this->sql_flush_all_changes(); return true; } void Playlist::enqueue_load(const std::shared_ptr &entry) { if(entry->load_future) return; /* song has been already loaded or parsed */ entry->load_start = system_clock::now(); entry->load_future = make_unique(); weak_ptr weak_self = this->ref(); weak_ptr weak_entry = entry; logTrace(this->get_server_id(), "[PlayList] Enqueueing song {} for loading", entry->id); MusicBotManager::load_music.execute([weak_self, weak_entry] { shared_ptr self = weak_self.lock(); auto entry = weak_entry.lock(); if(!self || !entry) return; /* we're old */ self->execute_async_load(entry); }); } #define FORCE_LOAD \ entry->loaded = false; \ goto load_entry /* struct UrlSongInfo : public UrlInfo { std::string title; std::string description; std::map metadata; }; struct UrlPlaylistInfo : public UrlInfo { std::deque> entries; }; */ using UrlType = ::music::UrlType; using UrlSongInfo = ::music::UrlSongInfo; using UrlPlaylistInfo = ::music::UrlPlaylistInfo; void Playlist::execute_async_load(const std::shared_ptr &entry) { if(entry->loaded) { /* we need to parse the metadata */ Json::Value data; Json::CharReaderBuilder rbuilder; std::string errs; istringstream jsonStream(entry->metadata); bool parsingSuccessful = Json::parseFromStream(rbuilder, jsonStream, &data, &errs); if (!parsingSuccessful) { logError(this->get_server_id(), "[PlayList] Failed to parse loaded metadata for song {}. Metadata data!", entry->id); FORCE_LOAD; } if(!data["type"].isUInt()) { logError(this->get_server_id(), "[PlayList] Failed to parse loaded metadata for song {}. Metadata has an invalid key 'type'. Query data again!", entry->id); FORCE_LOAD; } if(!data["url"].isString()) { logError(this->get_server_id(), "[PlayList] Failed to parse loaded metadata for song {}. Metadata has an invalid key 'url'. Query data again!", entry->id); FORCE_LOAD; } auto type = (UrlType) data["type"].asUInt(); if(type != UrlType::TYPE_VIDEO && type != UrlType::TYPE_STREAM) { /* we're currently not able to parse playlists */ FORCE_LOAD; } else { auto info = make_shared(); if(data["metadata"].isObject()) { for(const auto& key : data["metadata"]) info->metadata[key.asString()] = data["metadata"][key.asString()].asString(); } info->url = data["url"].asString(); info->title = data["title"].asString(); info->type = type; info->description = data["description"].asString(); entry->load_future->executionSucceed(info); } } load_entry: if(!entry->loaded) { auto provider = ::music::manager::resolveProvider(entry->url_loader, entry->url); if(!provider) { entry->load_future->executionFailed("failed to load info provider"); return; } auto info_future = provider->query_info(entry->url, nullptr, &*this->get_server()); info_future.waitAndGet(system_clock::now() + seconds(30)); /* TODO load timeout configurable? */ if(info_future.succeeded() && info_future.get()) { auto result = *info_future.get(); { Json::Value root; root["type"] = result->type; root["url"] = result->url; if(result->type == UrlType::TYPE_PLAYLIST) { auto casted = static_pointer_cast(result); } else if(result->type == UrlType::TYPE_STREAM || result->type == UrlType::TYPE_VIDEO) { auto casted = static_pointer_cast(result); root["title"] = casted->title; root["description"] = casted->description; for(const auto& meta : casted->metadata) { root["metadata"][meta.first] = meta.second; } } entry->metadata = root.toStyledString(); Json::StreamWriterBuilder builder; builder["indentation"] = ""; // If you want whitespace-less output entry->metadata = Json::writeString(builder, root); } if(result->type == UrlType::TYPE_PLAYLIST) { auto playlist = static_pointer_cast(result); debugMessage(this->get_server_id(), "[PlayList] Song {} shows up as a playlist. Inserting entries {}", entry->id, playlist->entries.size()); SongId previous_id = entry->id; size_t current_songs = this->list_songs().size(); auto max_songs = this->max_songs(); for(const auto& element : playlist->entries) { if(max_songs != -1 && max_songs < /* = (+1 because we delete the main playlist) */ current_songs) { logMessage(this->get_server_id(), "[PlayList][{}] Added playlist {} contains more songs than allowed by setting. Dropping the rest.", this->playlist_id(), element->url); break; } auto queued_element = this->add_song(entry->invoker, element->url, "", previous_id); if(!queued_element) continue; //TODO log insert fail? previous_id = queued_element->id; } this->delete_song(entry->id); /* delete playlist song entry because it has been resolved compleatly */ } entry->load_future->executionSucceed(result); entry->loaded = true; if(result->type != UrlType::TYPE_PLAYLIST) /* we've already deleted this entry if this is a playlist entry*/ this->sql_apply_changes(entry); } else { entry->load_future->executionFailed("failed to load info: " + info_future.errorMegssage()); return; } } }