Files
Teaspeak-Server/server/src/music/MusicPlaylist.cpp
T
2019-07-17 19:37:18 +02:00

589 lines
19 KiB
C++

#include <linked_helper.h>
#include <json/json.h>
#include "MusicPlaylist.h"
#include "src/TSServer.h"
#include "src/client/ConnectedClient.h"
#include <log/LogUtils.h>
#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<ts::music::MusicBotManager> &manager, const std::shared_ptr<ts::Properties> &properties, const std::shared_ptr<permission::PermissionManager>& permissions) : _properties(properties), _permissions(permissions), manager(manager) { }
Playlist::~Playlist() {
this->destory_tree();
}
void Playlist::set_self_ref(const std::shared_ptr<ts::music::Playlist> &ref) {
assert(&*ref == this);
this->_self = ref;
}
std::shared_ptr<server::TSServer> 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<PlaylistEntry> Playlist::playlist_end(const std::unique_lock<std::shared_mutex>& lock) {
assert(lock.owns_lock());
auto current = this->playlist_head;
while(current && current->next_song)
current = current->next_song;
return current;
}
std::shared_ptr<PlaylistEntry> Playlist::playlist_find(const std::unique_lock<std::shared_mutex> &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<std::shared_mutex> &lock, const shared_ptr<PlaylistEntry> &entry, shared_ptr<PlaylistEntry> 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<std::shared_mutex>& lock, const shared_ptr<PlaylistEntry> &entry, std::shared_ptr<PlaylistEntry> 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<std::shared_mutex>& lock, const std::shared_ptr<ts::music::PlaylistEntry> &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<ts::music::PlaylistEntryInfo> &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<ts::music::PlaylistEntryInfo> &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<shared_ptr<PlaylistEntryInfo>> 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<ts::music::PlaylistEntryInfo> &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<std::shared_ptr<PlaylistEntryInfo>> Playlist::load_entries() {
std::deque<std::shared_ptr<PlaylistEntryInfo>> 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<PlaylistEntryInfo>();
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<PlaylistId, size_t> 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<std::shared_ptr<PlaylistEntryInfo>> _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<shared_ptr<PlaylistEntryInfo>> entries) {
this->playlist_head = nullptr;
if(entries.empty())
return true;
unique_lock list_lock(this->playlist_lock);
auto find_entry = [&](SongId id) -> shared_ptr<PlaylistEntryInfo> {
for(const auto& entry : entries)
if(entry->id == id)
return entry;
return nullptr;
};
deque<shared_ptr<linked::entry>> l_entries;
for(const auto& entry : entries)
l_entries.push_back(move(linked::create_entry(0, entry->id, entry->previous_song_id)));
deque<string> errors;
auto head = linked::build_chain(l_entries, errors);
shared_ptr<PlaylistEntry> current_tail = nullptr;
while(head) {
auto entry = make_shared<PlaylistEntry>();
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<std::shared_ptr<PlaylistEntryInfo>> Playlist::list_songs() {
deque<shared_ptr<PlaylistEntryInfo>> 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<PlaylistEntryInfo> 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<PlaylistEntryInfo> Playlist::add_song(const std::shared_ptr<ts::server::ConnectedClient> &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<PlaylistEntryInfo> Playlist::add_song(ClientDbId client, const std::string &url, const std::string& url_loader, ts::SongId order) {
auto entry = make_shared<PlaylistEntryInfo>();
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<PlaylistEntry>();
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<ts::music::PlaylistEntryInfo> &entry) {
if(entry->load_future) return; /* song has been already loaded or parsed */
entry->load_start = system_clock::now();
entry->load_future = make_unique<PlaylistEntryInfo::load_future_t>();
weak_ptr weak_self = this->ref<Playlist>();
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<Playlist> 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<std::string, std::string> metadata;
};
struct UrlPlaylistInfo : public UrlInfo {
std::deque<std::shared_ptr<UrlSongInfo>> entries;
};
*/
using UrlType = ::music::UrlType;
using UrlSongInfo = ::music::UrlSongInfo;
using UrlPlaylistInfo = ::music::UrlPlaylistInfo;
void Playlist::execute_async_load(const std::shared_ptr<ts::music::PlaylistEntryInfo> &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<UrlSongInfo>();
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<UrlPlaylistInfo>(result);
} else if(result->type == UrlType::TYPE_STREAM || result->type == UrlType::TYPE_VIDEO) {
auto casted = static_pointer_cast<UrlSongInfo>(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<UrlPlaylistInfo>(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;
}
}
}