From d3ee2023139b96cddfeac77ccf18ac78a1e61537 Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Sun, 1 Mar 2020 15:46:03 +0100 Subject: [PATCH] Some updates --- git-teaspeak | 2 +- .../ConnectedClientTextCommandHandler.cpp | 4 +- server/src/client/command_handler/music.cpp | 61 ++++- .../channel_replay/ChannelProvider.cpp | 150 ++++++++--- server/src/lincense/LicenseService.cpp | 2 +- server/src/music/MusicPlaylist.cpp | 245 +++++++++--------- server/src/music/MusicPlaylist.h | 49 ++-- server/src/music/PlayablePlaylist.cpp | 41 +-- 8 files changed, 352 insertions(+), 202 deletions(-) diff --git a/git-teaspeak b/git-teaspeak index 65760b9..9a26231 160000 --- a/git-teaspeak +++ b/git-teaspeak @@ -1 +1 @@ -Subproject commit 65760b9e3cd5647091c7a38a0427c23bffaa815e +Subproject commit 9a26231c1f6c44f799419f87a3cac37a55425bdb diff --git a/server/src/client/ConnectedClientTextCommandHandler.cpp b/server/src/client/ConnectedClientTextCommandHandler.cpp index 99636fd..2760304 100644 --- a/server/src/client/ConnectedClientTextCommandHandler.cpp +++ b/server/src/client/ConnectedClientTextCommandHandler.cpp @@ -397,8 +397,8 @@ bool ConnectedClient::handle_text_command( } } - string data = "\"" + song->url + "\" added by " + invoker; - if(song->id == current_song) + string data = "\"" + song->original_url + "\" added by " + invoker; + if(song->song_id == current_song) data = "[color=orange]" + data + "[/color]"; send_message(bot, " - " + data); } diff --git a/server/src/client/command_handler/music.cpp b/server/src/client/command_handler/music.cpp index b7739a7..1b8e161 100644 --- a/server/src/client/command_handler/music.cpp +++ b/server/src/client/command_handler/music.cpp @@ -42,6 +42,7 @@ #include #include #include +#include namespace fs = std::experimental::filesystem; using namespace std::chrono; @@ -818,15 +819,33 @@ command_result ConnectedClient::handleCommandPlaylistSongList(ts::Command &cmd) Command notify(this->notify_response_command("notifyplaylistsonglist")); notify["playlist_id"] = playlist->playlist_id(); + auto extract_metadata = cmd.hasParm("extract-metadata"); + size_t index = 0; for(const auto& song : songs) { - notify[index]["song_id"] = song->id; + notify[index]["song_id"] = song->song_id; notify[index]["song_invoker"] = song->invoker; notify[index]["song_previous_song_id"] = song->previous_song_id; - notify[index]["song_url"] = song->url; + notify[index]["song_url"] = song->original_url; notify[index]["song_url_loader"] = song->url_loader; - notify[index]["song_loaded"] = song->loaded; - notify[index]["song_metadata"] = song->metadata; + notify[index]["song_loaded"] = song->metadata.is_loaded(); + notify[index]["song_metadata"] = song->metadata.json_string; + + if(extract_metadata) { + auto metadata = song->metadata.loaded_data; + if(extract_metadata && song->metadata.is_loaded() && metadata) { + notify[index]["song_metadata_title"] = metadata->title; + notify[index]["song_metadata_description"] = metadata->description; + notify[index]["song_metadata_url"] = metadata->url; + notify[index]["song_metadata_length"] = metadata->length.count(); + if(auto thumbnail = static_pointer_cast<::music::ThumbnailUrl>(metadata->thumbnail); thumbnail && thumbnail->type() == ::music::THUMBNAIL_URL) { + notify[index]["song_metadata_thumbnail_url"] = thumbnail->url(); + } else { + notify[index]["song_metadata_thumbnail_url"] = "none"; + } + } + } + index++; } this->sendCommand(notify); @@ -862,18 +881,30 @@ command_result ConnectedClient::handleCommandPlaylistSongAdd(ts::Command &cmd) { if(auto perr = playlist->client_has_permissions(this->ref(), permission::i_playlist_song_needed_add_power, permission::i_playlist_song_add_power); perr) return command_result{perr}; - if(!cmd[0].has("invoker")) - cmd["invoker"] = ""; + if(cmd[0].has("invoker")) + cmd["type"] = ""; + else if(!cmd[0].has("type")) + cmd["type"] = ""; + if(!cmd[0].has("previous")) { auto songs = playlist->list_songs(); if(songs.empty()) cmd["previous"] = "0"; else - cmd["previous"] = songs.back()->id; + cmd["previous"] = songs.back()->song_id; } + auto& type = cmd[0]["type"]; + std::string loader_string{""}; + if((type.castable() && type.as() == 0) || type.as() == "yt") { + loader_string = "YouTube"; + } else if((type.castable() && type.as() == 1) || type.as() == "ffmpeg") { + loader_string = "FFMpeg"; + } else if((type.castable() && type.as() == 2) || type.as() == "channel") { + loader_string = "ChannelProvider"; + } - auto song = playlist->add_song(_this.lock(), cmd["url"], cmd["invoker"], cmd["previous"]); + auto song = playlist->add_song(_this.lock(), cmd["url"], loader_string, cmd["previous"]); if(!song) return command_result{error::vs_critical}; return command_result{error::ok}; @@ -927,6 +958,7 @@ command_result ConnectedClient::handleCommandPlaylistSongRemove(ts::Command &cmd /****** legacy ******/ command_result ConnectedClient::handleCommandMusicBotQueueList(Command& cmd) { +#if false CMD_REQ_SERVER; CMD_RESET_IDLE; CMD_CHK_AND_INC_FLOOD_POINTS(25); @@ -947,7 +979,7 @@ command_result ConnectedClient::handleCommandMusicBotQueueList(Command& cmd) { auto songs = playlist->list_songs(); int begin_index{0}; for(auto it = songs.begin(); it != songs.end(); it++) - if((*it)->id == playlist->currently_playing()) + if((*it)->song_id == playlist->currently_playing()) break; else begin_index--; @@ -957,12 +989,12 @@ command_result ConnectedClient::handleCommandMusicBotQueueList(Command& cmd) { notify = Command(this->notify_response_command("notifymusicqueueentry")); auto song = *it; - notify[command_index]["song_id"] = song->id; - notify[command_index]["song_url"] = song->url; + notify[command_index]["song_id"] = song->song_id; + notify[command_index]["song_url"] = song->original_url; notify[command_index]["song_invoker"] = song->invoker; - notify[command_index]["song_loaded"] = song->loaded; + notify[command_index]["song_loaded"] = song->metadata.is_loaded(); - auto entry = song->load_future ? song->load_future->getValue({}) : nullptr; + auto entry = song->load_future && song->load_future->succeeded() ? song->load_future->getValue({}) : nullptr; if(entry) { notify[command_index]["song_loaded"] = true; if(entry->type != ::music::TYPE_STREAM && entry->type != ::music::TYPE_VIDEO) @@ -999,8 +1031,9 @@ command_result ConnectedClient::handleCommandMusicBotQueueList(Command& cmd) { notify["bot_id"] = bot->getClientDatabaseId(); this->sendCommand(notify); } - return command_result{error::ok}; +#endif + return command_result{error::not_implemented}; //FIXME } command_result ConnectedClient::handleCommandMusicBotQueueAdd(Command& cmd) { diff --git a/server/src/client/music/internal_provider/channel_replay/ChannelProvider.cpp b/server/src/client/music/internal_provider/channel_replay/ChannelProvider.cpp index 7dcb9d6..8b7bbff 100644 --- a/server/src/client/music/internal_provider/channel_replay/ChannelProvider.cpp +++ b/server/src/client/music/internal_provider/channel_replay/ChannelProvider.cpp @@ -1,9 +1,7 @@ -#include #include "ChannelProvider.h" #include "../../MusicClient.h" #include "../../../../InstanceHandler.h" #include "../../../../server/file/FileServer.h" -#include "../../../../channel/ServerChannel.h" #include "../../../../../../music/providers/ffmpeg/FFMpegProvider.h" @@ -16,10 +14,10 @@ using namespace ts::server; struct AudioFileInfo { std::string absolute_path; + std::string title; std::string description; }; -extern ts::server::InstanceHandler* serverInstance; threads::Future> ChannelProvider::createPlayer(const std::string &url, void*, void* ptr_server) { auto server = ((VirtualServer*) ptr_server)->ref(); threads::Future> future; @@ -70,8 +68,8 @@ threads::Future> ChannelProvider::createPl auto info = make_shared(); info->absolute_path = file->path + "/" + file->name; - info->description = file->name; - + info->title = file->name; /* fallback */ + info->description = "File from channel " + channel->name() + " at " + path; /* fallback */ auto ffmpeg = ::music::manager::resolveProvider("FFMpeg", ""); if(!ffmpeg) { future.executionFailed("missing ffmpeg"); @@ -84,16 +82,20 @@ threads::Future> ChannelProvider::createPl future.executionFailed("failed to allocate memory"); return; } + memset(ffmpeg_data, 0, sizeof(::music::FFMpegData::FileReplay)); - ffmpeg_data->version = ::music::FFMpegData::CURRENT_VERSION; + ffmpeg_data->version = 2; ffmpeg_data->type = ::music::FFMpegData::REPLAY_FILE; ffmpeg_data->_free = ::free; ffmpeg_data->file_path = (char*) malloc(info->absolute_path.length() + 1); ffmpeg_data->file_path[info->absolute_path.length()] = '\0'; + ffmpeg_data->file_title = (char*) malloc(info->title.length() + 1); + ffmpeg_data->file_title[info->title.length()] = '\0'; ffmpeg_data->file_description = (char*) malloc(info->description.length() + 1); ffmpeg_data->file_description[info->description.length()] = '\0'; + memcpy(ffmpeg_data->file_title, info->title.data(), info->title.length()); memcpy(ffmpeg_data->file_path, info->absolute_path.data(), info->absolute_path.length()); memcpy(ffmpeg_data->file_description, info->description.data(), info->description.length()); @@ -101,8 +103,9 @@ threads::Future> ChannelProvider::createPl p.wait(); if(p.failed()) future.executionFailed(p.errorMegssage()); - else + else { future.executionSucceed(*p.get()); + } } return; } catch(std::exception& ex) { @@ -135,38 +138,17 @@ vector ChannelProvider::availableProtocols() { } std::shared_ptr<::music::manager::PlayerProvider> ChannelProvider::create_provider() { - /* - std::shared_ptr config = make_shared(); - - { - auto config_path = fs::u8path("providers/config_channel.ini"); - music::log::log(music::log::debug, "[YT-DL] Using config file located at " + config_path.string()); - if(fs::exists(config_path)) { - INIReader ini_reader(config_path.string()); - - if(ini_reader.ParseError()) { - music::log::log(music::log::err, "[YT-DL] Could not parse config! Using default values"); - } else { - config->youtubedl_command = ini_reader.Get("general", "youtubedl_command", config->youtubedl_command); - config->commands.version = ini_reader.Get("commands", "version", config->commands.version); - config->commands.query_video = ini_reader.Get("commands", "query_video", config->commands.query_video); - music::log::log(music::log::info, "[YT-DL] Config successfully loaded"); - } - } else { - music::log::log(music::log::debug, "[YT-DL] Missing configuration file. Using default values"); - } - } - */ - return std::shared_ptr(new ChannelProvider(), [](ChannelProvider* provider){ if(!provider) return; delete provider; }); } +#if 0 threads::Future> ChannelProvider::query_info(const std::string &string, void *pVoid, void *pVoid1) { auto info = make_shared(); + info->type = UrlType::TYPE_VIDEO; info->url = string; info->title = ""; @@ -177,3 +159,111 @@ threads::Future> ChannelProvider::query_info(const std::stri result.executionSucceed(info); return result; } +#endif + +threads::Future> ChannelProvider::query_info(const std::string &url, void*, void* ptr_server) { + auto server = ((VirtualServer*) ptr_server)->ref(); + threads::Future> future; + + if(server) { + std::thread([future, server, url, ptr_server]{ + auto f_server = serverInstance->getFileServer(); + if(!f_server) { + future.executionFailed("instance missing file server"); + return; + } + + Command command(""); + try { + command = Command::parse(pipes::buffer_view{url.data(), url.length()}, false); + } catch(std::exception& ex) { + future.executionFailed("failed to parse data"); + return; + } + + try { + auto channel_id = command["cid"].as(); + auto name = command["name"].string(); + auto path = command["path"].string(); + + auto channel = server->getChannelTree()->findChannel(channel_id); + if(!channel) { + future.executionFailed("could not find target channel"); + return; + } + + auto directory = f_server->resolveDirectory(server, channel, path); + if(!directory) { + future.executionFailed("invalid file path"); + return; + } + + auto file = f_server->findFile(name, directory); + if(!file) { + future.executionFailed("could not find file"); + return; + } + + if(file->type != file::FileType::FILE) { + future.executionFailed("file isnt a file"); + return; + } + + auto info = make_shared(); + info->absolute_path = file->path + "/" + file->name; + info->title = file->name; /* fallback */ + info->description = "File from channel " + channel->name() + " at " + file->path; /* fallback */ + + auto ffmpeg = ::music::manager::resolveProvider("FFMpeg", ""); + if(!ffmpeg) { + future.executionFailed("missing ffmpeg"); + return; + } + + { + auto ffmpeg_data = (::music::FFMpegData::FileReplay*) malloc(sizeof(::music::FFMpegData::FileReplay)); + if(!ffmpeg_data) { + future.executionFailed("failed to allocate memory"); + return; + } + memset(ffmpeg_data, 0, sizeof(::music::FFMpegData::FileReplay)); + + ffmpeg_data->version = 2; + ffmpeg_data->type = ::music::FFMpegData::REPLAY_FILE; + ffmpeg_data->_free = ::free; + + ffmpeg_data->file_path = (char*) malloc(info->absolute_path.length() + 1); + ffmpeg_data->file_path[info->absolute_path.length()] = '\0'; + ffmpeg_data->file_title = (char*) malloc(info->title.length() + 1); + ffmpeg_data->file_title[info->title.length()] = '\0'; + ffmpeg_data->file_description = (char*) malloc(info->description.length() + 1); + ffmpeg_data->file_description[info->description.length()] = '\0'; + + memcpy(ffmpeg_data->file_title, info->title.data(), info->title.length()); + memcpy(ffmpeg_data->file_path, info->absolute_path.data(), info->absolute_path.length()); + memcpy(ffmpeg_data->file_description, info->description.data(), info->description.length()); + + auto p = ffmpeg->query_info("", ffmpeg_data, ptr_server); + if(!p.wait(std::chrono::system_clock::now() + std::chrono::seconds{30})) + future.executionFailed("ffmpeg load timeout"); + else if(p.failed()) + future.executionFailed(p.errorMegssage()); + else { + auto result = *p.get(); + result->url = url; + future.executionSucceed(result); + } + } + return; + } catch(std::exception& ex) { + future.executionFailed("failed to process data"); + return; + } + }).detach(); + } else { + future.executionFailed("invalid bot"); + } + + + return future; +} \ No newline at end of file diff --git a/server/src/lincense/LicenseService.cpp b/server/src/lincense/LicenseService.cpp index 4258afc..029d71e 100644 --- a/server/src/lincense/LicenseService.cpp +++ b/server/src/lincense/LicenseService.cpp @@ -11,7 +11,7 @@ #include "../../../cmake-build-debug-wsl/license/LicenseRequest.pb.h" #include "src/InstanceHandler.h" -//#define DO_LOCAL_REQUEST +#define DO_LOCAL_REQUEST using namespace ts::server::license; LicenseService::LicenseService() { diff --git a/server/src/music/MusicPlaylist.cpp b/server/src/music/MusicPlaylist.cpp index 4e19f00..2787502 100644 --- a/server/src/music/MusicPlaylist.cpp +++ b/server/src/music/MusicPlaylist.cpp @@ -4,6 +4,8 @@ #include "src/VirtualServer.h" #include "src/client/ConnectedClient.h" #include + +#include #include "teaspeak/MusicPlayer.h" using namespace ts; @@ -11,8 +13,8 @@ using namespace ts::music; using namespace std; using namespace std::chrono; -Playlist::Playlist(const std::shared_ptr &manager, const std::shared_ptr &properties, std::shared_ptr permissions) : - PlaylistPermissions{std::move(permissions)}, _properties(properties), manager(manager) { } +Playlist::Playlist(const std::shared_ptr &manager, std::shared_ptr properties, std::shared_ptr permissions) : + PlaylistPermissions{std::move(permissions)}, _properties{std::move(properties)}, manager{manager} { } Playlist::~Playlist() { this->destroy_tree(); @@ -59,7 +61,7 @@ std::shared_ptr Playlist::playlist_find(const std::unique_lockentry); - if(current->entry->id == id) + if(current->entry->song_id == id) return current; current = current->next_song; @@ -145,19 +147,19 @@ bool Playlist::sql_add(const std::shared_ptr &entr auto sql_handle = this->get_sql(); if(!sql_handle) return false; - entry->id = ++current_id; + entry->song_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{":song_id", entry->song_id}, variable{":order_id", entry->previous_song_id}, variable{":invoker_dbid", entry->invoker}, - variable{":url", entry->url}, + variable{":url", entry->original_url}, variable{":url_loader", entry->url_loader}, - variable{":loaded", entry->loaded}, - variable{":metadata", entry->metadata} + variable{":loaded", entry->metadata.load_state == LoadState::REQUIRES_PARSE || entry->metadata.load_state == LoadState::LOADED}, + variable{":metadata", entry->metadata.json_string} ).execute(); LOG_SQL_CMD(sql_result); @@ -171,13 +173,13 @@ bool Playlist::sql_apply_changes(const std::shared_ptrget_server_id()}, variable{":playlist_id", this->playlist_id()}, - variable{":song_id", entry->id}, + variable{":song_id", entry->song_id}, variable{":order_id", entry->previous_song_id}, variable{":invoker_dbid", entry->invoker}, - variable{":url", entry->url}, + variable{":url", entry->original_url}, variable{":url_loader", entry->url_loader}, - variable{":loaded", entry->loaded}, - variable{":metadata", entry->metadata} + variable{":loaded", entry->metadata.load_state == LoadState::REQUIRES_PARSE || entry->metadata.load_state == LoadState::LOADED}, + variable{":metadata", entry->metadata.json_string} ).executeLater().waitAndGetLater(LOG_SQL_CMD, {-1, "failed future"}); return true; @@ -211,7 +213,7 @@ bool Playlist::sql_remove(const std::shared_ptr &e 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} + variable{":song_id", entry->song_id} ).executeLater().waitAndGetLater(LOG_SQL_CMD, {-1, "failed future"}); return true; @@ -231,34 +233,34 @@ std::deque> Playlist::load_entries() { for(int index = 0; index < length; index++) { try { if(columns[index] == "song_id") - entry->id = (SongId) stoll(values[index]); + entry->song_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]; + entry->original_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; + entry->metadata.load_state = (values[index].length() > 0 && stoll(values[index]) == 1) ? LoadState::REQUIRES_PARSE : LoadState::REQUIRES_QUERY; else if(columns[index] == "metadata") - entry->metadata = values[index]; + entry->metadata.json_string = 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; + if(entry->song_id > this->current_id) + this->current_id = entry->song_id; result.push_back(move(entry)); }); LOG_SQL_CMD(sql_result); map count; for(const auto& entry : result) - ++count[entry->id]; + ++count[entry->song_id]; for(const auto& entry : count) { if(entry.second <= 1) continue; @@ -276,7 +278,7 @@ std::deque> Playlist::load_entries() { std::deque> _result; for(const auto& entry : result) - if(count[entry->id] <= 1) { + if(count[entry->song_id] <= 1) { this->enqueue_load(entry); _result.push_back(entry); } @@ -294,14 +296,14 @@ bool Playlist::build_tree(deque> entries) { unique_lock list_lock(this->playlist_lock); auto find_entry = [&](SongId id) -> shared_ptr { for(const auto& entry : entries) - if(entry->id == id) + if(entry->song_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))); + l_entries.push_back(move(linked::create_entry(0, entry->song_id, entry->previous_song_id))); deque errors; auto head = linked::build_chain(l_entries, errors); @@ -372,7 +374,7 @@ 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) + if(head->entry->song_id == id) return head->entry; head = head->next_song; @@ -391,7 +393,7 @@ std::shared_ptr Playlist::add_song(ClientDbId client, const s entry->previous_song_id = order; entry->invoker = client; - entry->url = url; + entry->original_url = url; entry->url_loader = url_loader; if(!this->sql_add(entry)) return nullptr; @@ -402,15 +404,15 @@ std::shared_ptr Playlist::add_song(ClientDbId client, const s 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; + entry->previous_song_id = end ? end->entry->song_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) { + if(order_entry ? order_entry->entry->song_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; + entry->previous_song_id = list_entry->previous_song ? list_entry->previous_song->entry->song_id : 0; list_lock.unlock(); this->sql_apply_changes(entry); @@ -455,15 +457,15 @@ bool Playlist::reorder_song(ts::SongId song_id, ts::SongId order_id) { void Playlist::enqueue_load(const std::shared_ptr &entry) { assert(entry); - if(entry->load_future) return; /* song has been already loaded or parsed */ + std::lock_guard song_lock_lock{entry->metadata.load_lock}; + if(!entry->metadata.requires_load()) + return; - 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); + entry->metadata.load_begin = system_clock::now(); + std::weak_ptr weak_self = this->ref(); + std::weak_ptr weak_entry{entry}; + logTrace(this->get_server_id(), "[PlayList] Enqueueing song {} for loading", entry->song_id); MusicBotManager::load_music.execute([weak_self, weak_entry] { shared_ptr self = weak_self.lock(); auto entry = weak_entry.lock(); @@ -474,7 +476,6 @@ void Playlist::enqueue_load(const std::shared_ptr } #define FORCE_LOAD \ -entry->loaded = false; \ goto load_entry /* @@ -491,28 +492,41 @@ goto load_entry */ using UrlType = ::music::UrlType; -using UrlSongInfo = ::music::UrlSongInfo; using UrlPlaylistInfo = ::music::UrlPlaylistInfo; +using UrlSongInfo = ::music::UrlSongInfo; + +#define load_finished(state, error, result) \ +do { \ + { \ + std::lock_guard slock{entry->metadata.load_lock}; \ + entry->metadata.load_state = state; \ + entry->metadata.loaded_data = result; \ + entry->metadata.load_error = error; \ + entry->metadata.loaded_cv.notify_all(); \ + } \ + this->notify_song_loaded(entry);\ +} while(0) void Playlist::execute_async_load(const std::shared_ptr &entry) { - if(entry->loaded) { /* we need to parse the metadata */ + //TODO: Song might get removed while being loaded. Need better handling specially with the notified etc. + if(entry->metadata.load_state == LoadState::REQUIRES_PARSE) { /* we need to parse the metadata */ Json::Value data; Json::CharReaderBuilder rbuilder; std::string errs; - istringstream jsonStream(entry->metadata); + istringstream jsonStream(entry->metadata.json_string); 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); + logError(this->get_server_id(), "[PlayList] Failed to parse loaded metadata for song {}. Metadata data!", entry->song_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); + logError(this->get_server_id(), "[PlayList] Failed to parse loaded metadata for song {}. Metadata has an invalid key 'type'. Query data again!", entry->song_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); + logError(this->get_server_id(), "[PlayList] Failed to parse loaded metadata for song {}. Metadata has an invalid key 'url'. Query data again!", entry->song_id); FORCE_LOAD; } @@ -531,85 +545,77 @@ void Playlist::execute_async_load(const std::shared_ptrtype = type; info->description = data["description"].asString(); - entry->load_future->executionSucceed(info); + load_finished(LoadState::LOADED, "", info); + return; } } 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"); - this->notify_song_loaded(entry); - return; - } + auto provider = ::music::manager::resolveProvider(entry->url_loader, entry->original_url); + if(!provider) { + load_finished(LoadState::LOAD_ERROR, "failed to load info provider", nullptr); + 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? */ + auto info_future = provider->query_info(entry->original_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["length"] = chrono::ceil(casted->length).count(); - root["title"] = casted->title; - root["description"] = casted->description; - if(casted->thumbnail) { - if(auto thump = dynamic_pointer_cast<::music::ThumbnailUrl>(casted->thumbnail); thump) - root["thumbnail"] = thump->url(); - } - 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(info_future.succeeded() && info_future.get()) { + auto result = *info_future.get(); + /* create the json string */ + { + Json::Value root; + root["type"] = result->type; + root["url"] = result->url; 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; + 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["length"] = chrono::ceil(casted->length).count(); + root["title"] = casted->title; + root["description"] = casted->description; + if(casted->thumbnail) { + if(auto thump = dynamic_pointer_cast<::music::ThumbnailUrl>(casted->thumbnail); thump) + root["thumbnail"] = thump->url(); } - - this->delete_song(entry->id); /* delete playlist song entry because it has been resolved compleatly */ + for(const auto& meta : casted->metadata) + root["metadata"][meta.first] = meta.second; } - entry->load_future->executionSucceed(result); - entry->loaded = true; - /* we've already deleted this entry if this is a playlist entry */ - if(result->type != UrlType::TYPE_PLAYLIST) { - this->sql_apply_changes(entry); - this->notify_song_loaded(entry); + Json::StreamWriterBuilder builder; + builder["indentation"] = ""; // If you want whitespace-less output + entry->metadata.json_string = 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->song_id, playlist->entries.size()); + + SongId previous_id = entry->song_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->song_id; } - } else { - entry->load_future->executionFailed("failed to load info: " + info_future.errorMegssage()); - this->notify_song_loaded(entry); + + this->delete_song(entry->song_id); /* delete playlist song entry because it has been resolved completely */ return; } + + load_finished(LoadState::LOADED, "", static_pointer_cast(result)); + this->sql_apply_changes(entry); + } else { + load_finished(LoadState::LOAD_ERROR, info_future.failed() ? "failed to query info: " + info_future.errorMegssage() : "info load timeout", nullptr); + return; } } @@ -634,15 +640,16 @@ inline bool write_song(const std::shared_ptr &entry, command_ return true; } - builder.put_unchecked(index, "song_id", entry->id); + builder.put_unchecked(index, "song_id", entry->song_id); builder.put_unchecked(index, "song_previous_song_id", entry->previous_song_id); - builder.put_unchecked(index, "song_url", entry->url); + builder.put_unchecked(index, "song_url", entry->original_url); builder.put_unchecked(index, "song_url_loader", entry->url_loader); - builder.put_unchecked(index, "song_loaded", entry->loaded); - builder.put_unchecked(index, "song_invoker", entry->invoker); - builder.put_unchecked(index, "song_metadata", entry->metadata); + + std::lock_guard llock{entry->metadata.load_lock}; + builder.put_unchecked(index, "song_loaded", entry->metadata.is_loaded()); + builder.put_unchecked(index, "song_metadata", entry->metadata.json_string); return true; } @@ -672,7 +679,7 @@ bool Playlist::notify_song_remove(const std::shared_ptr &entr command_builder result{"notifyplaylistsongremove"}; result.put_unchecked(0, "playlist_id", this->playlist_id()); - result.put_unchecked(0, "song_id", entry->id); + result.put_unchecked(0, "song_id", entry->song_id); this->broadcast_notify(result); return true; @@ -682,7 +689,7 @@ bool Playlist::notify_song_reorder(const std::shared_ptr &ent command_builder result{"notifyplaylistsongreorder"}; result.put_unchecked(0, "playlist_id", this->playlist_id()); - result.put_unchecked(0, "song_id", entry->id); + result.put_unchecked(0, "song_id", entry->song_id); result.put_unchecked(0, "song_previous_song_id", entry->previous_song_id); this->broadcast_notify(result); @@ -693,14 +700,16 @@ bool Playlist::notify_song_loaded(const std::shared_ptr &entr command_builder result{"notifyplaylistsongloaded"}; result.put_unchecked(0, "playlist_id", this->playlist_id()); - result.put_unchecked(0, "song_id", entry->id); + result.put_unchecked(0, "song_id", entry->song_id); - if(entry->load_future && entry->load_future->failed()) { + + std::lock_guard llock{entry->metadata.load_lock}; + if(entry->metadata.has_failed_to_load()) { result.put_unchecked(0, "success", "0"); - result.put_unchecked(0, "load_error_msg", entry->load_future->errorMegssage()); + result.put_unchecked(0, "load_error_msg", entry->metadata.load_error); } else { result.put_unchecked(0, "success", "1"); - result.put_unchecked(0, "song_metadata", entry->metadata); + result.put_unchecked(0, "song_metadata", entry->metadata.json_string); } this->broadcast_notify(result); diff --git a/server/src/music/MusicPlaylist.h b/server/src/music/MusicPlaylist.h index b861535..7b9f006 100644 --- a/server/src/music/MusicPlaylist.h +++ b/server/src/music/MusicPlaylist.h @@ -21,24 +21,41 @@ namespace ts { namespace music { class Playlist; + enum struct LoadState { + REQUIRES_QUERY, /* requires general song query */ + REQUIRES_PARSE, /* requires a parse of the given json string */ + + LOADED, /* metadata has been loaded */ + LOAD_ERROR + }; + struct PlaylistEntryInfo { - typedef threads::Future> load_future_t; - SongId previous_song_id = 0; + /* static part available all the time */ + SongId previous_song_id{0}; + SongId song_id{0}; - ClientDbId invoker = 0; - SongId id = 0; - - std::string url; - std::string url_loader; - std::string metadata; /* only present when loaded */ - bool loaded = false; - - std::chrono::system_clock::time_point load_start; - std::unique_ptr load_future; + ClientDbId invoker{0}; + std::string original_url{}; + std::string url_loader{}; + /* dynamic part only available after the song has been loaded successfully */ struct { - bool successfully_loaded; - } data; + std::mutex load_lock{}; + LoadState load_state{false}; + + std::string json_string{}; + std::condition_variable loaded_cv{}; + + std::shared_ptr<::music::UrlSongInfo> loaded_data{nullptr}; /* only set when successfully loaded */ + std::string load_error{}; + + std::chrono::system_clock::time_point load_begin{}; + + [[nodiscard]] inline bool requires_load() const { return this->load_state == LoadState::REQUIRES_PARSE ||this->load_state == LoadState::REQUIRES_QUERY; } + [[nodiscard]] inline bool is_loading() const { return (this->load_begin.time_since_epoch().count() > 0) && (this->load_state == LoadState::REQUIRES_PARSE ||this->load_state == LoadState::REQUIRES_QUERY); } + [[nodiscard]] inline bool is_loaded() const { return this->load_state == LoadState::LOADED; } + [[nodiscard]] inline bool has_failed_to_load() const { return this->load_state == LoadState::LOAD_ERROR; } + } metadata; }; class PlaylistEntry { @@ -57,7 +74,7 @@ namespace ts { assert(this->entry); this->previous_song = song; - this->entry->previous_song_id = song ? song->entry->id : 0; + this->entry->previous_song_id = song ? song->entry->song_id : 0; this->modified = true; } }; @@ -73,7 +90,7 @@ namespace ts { }; }; - Playlist(const std::shared_ptr& /* manager */, const std::shared_ptr& /* properties */, std::shared_ptr /* permissions */); + Playlist(const std::shared_ptr& /* manager */, std::shared_ptr /* properties */, std::shared_ptr /* permissions */); virtual ~Playlist(); virtual void load_songs(); diff --git a/server/src/music/PlayablePlaylist.cpp b/server/src/music/PlayablePlaylist.cpp index da63e32..aede933 100644 --- a/server/src/music/PlayablePlaylist.cpp +++ b/server/src/music/PlayablePlaylist.cpp @@ -2,6 +2,7 @@ #include "src/VirtualServer.h" #include "src/client/music/MusicClient.h" #include "src/client/ConnectedClient.h" +#include "MusicPlaylist.h" #include using namespace ts; @@ -39,7 +40,7 @@ void PlayablePlaylist::previous() { auto entry = this->playlist_previous_entry(); if(entry) this->enqueue_load(entry); - this->properties()[property::PLAYLIST_CURRENT_SONG_ID] = entry ? entry->id : 0; + this->properties()[property::PLAYLIST_CURRENT_SONG_ID] = entry ? entry->song_id : 0; this->current_loading_entry = entry; } @@ -48,7 +49,7 @@ void PlayablePlaylist::next() { auto entry = this->playlist_next_entry(); if(entry) this->enqueue_load(entry); - this->properties()[property::PLAYLIST_CURRENT_SONG_ID] = entry ? entry->id : 0; + this->properties()[property::PLAYLIST_CURRENT_SONG_ID] = entry ? entry->song_id : 0; this->current_loading_entry = entry; } @@ -58,20 +59,19 @@ std::shared_ptr PlayablePlaylist::current_song(bool& aw unique_lock lock(this->currently_playing_lock); auto entry = this->current_loading_entry.lock(); - auto id = entry ? entry->id : 0; + auto id = entry ? entry->song_id : 0; if(this->properties()[property::PLAYLIST_CURRENT_SONG_ID].as() != id) /* should not happen */ this->properties()[property::PLAYLIST_CURRENT_SONG_ID] = id; - if(!entry) return nullptr; - if(!entry->load_future) return nullptr; /* skip this entry */ + if(!entry || entry->metadata.requires_load()) return nullptr; - if(entry->load_future->state() == threads::FutureState::WORKING) { - if(entry->load_start + seconds(30) < system_clock::now()) { - this->delete_song(entry->id); /* delete song because we cant load it */ + if(entry->metadata.is_loading()) { + if(entry->metadata.load_begin + seconds(30) < system_clock::now()) { + this->delete_song(entry->song_id); /* delete song because we cant load it */ return PlayableSong::create({ - entry->id, - entry->url, + entry->song_id, + entry->original_url, entry->invoker }, MusicClient::failedLoader("Song failed to load")); } @@ -80,28 +80,29 @@ std::shared_ptr PlayablePlaylist::current_song(bool& aw return nullptr; } - if(!entry->load_future->succeeded() || !entry->load_future->getValue(nullptr)) { - this->delete_song(entry->id); /* acquired playlist */ + if(!entry->metadata.is_loaded()) { + assert(entry->metadata.has_failed_to_load()); + this->delete_song(entry->song_id); /* acquired playlist */ /* return promise */ return PlayableSong::create({ - entry->id, - entry->url, + entry->song_id, + entry->original_url, entry->invoker - }, MusicClient::failedLoader(entry->load_future->errorMegssage())); + }, MusicClient::failedLoader(entry->metadata.load_error)); } - auto result = entry->load_future->getValue(nullptr); + auto result = entry->metadata.loaded_data; assert(result); /* previous tested */ if(result->type != ::music::UrlType::TYPE_VIDEO && result->type != ::music::UrlType::TYPE_STREAM) { /* we cant replay that kind of entry. Skipping it */ - debugMessage(this->get_server_id(), "[PlayList] Tried to replay an invalid entry type (SongID: {}, Type: {})! Skipping song", entry->id, result->type); + debugMessage(this->get_server_id(), "[PlayList] Tried to replay an invalid entry type (SongID: {}, Type: {})! Skipping song", entry->song_id, result->type); return nullptr; } return PlayableSong::create({ - entry->id, + entry->song_id, result->url, entry->invoker }, MusicClient::providerLoader(entry->url_loader)); @@ -154,7 +155,7 @@ std::shared_ptr PlayablePlaylist::playlist_next_entry() { } playlist_lock.unlock(); if(current_song && this->properties()[property::PLAYLIST_FLAG_DELETE_PLAYED].as()) { - this->delete_song(current_song->entry->id); + this->delete_song(current_song->entry->song_id); } return result; } @@ -197,7 +198,7 @@ bool PlayablePlaylist::set_current_song(SongId song_id) { this->current_loading_entry = current_song->entry; if(current_song) { this->enqueue_load(current_song->entry); - auto id = current_song ? current_song->entry->id : 0; + auto id = current_song ? current_song->entry->song_id : 0; if(this->properties()[property::PLAYLIST_CURRENT_SONG_ID].as() != id) this->properties()[property::PLAYLIST_CURRENT_SONG_ID] = id; }