589 lines
19 KiB
C++
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;
|
|
}
|
|
}
|
|
} |