/* This file is part of Clementine. Copyright 2012, Andreas Muttscheller Clementine is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Clementine is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Clementine. If not, see . */ #include "outgoingdatacreator.h" #include #include "networkremote.h" #include "core/logging.h" #include "core/timeconstants.h" #include "core/utilities.h" #include "library/librarybackend.h" #include #include #include "core/database.h" const quint32 OutgoingDataCreator::kFileChunkSize = 100000; // in Bytes OutgoingDataCreator::OutgoingDataCreator(Application* app) : app_(app), aww_(false), ultimate_reader_(new UltimateLyricsReader(this)), fetcher_(new SongInfoFetcher(this)) { // Create Keep Alive Timer keep_alive_timer_ = new QTimer(this); connect(keep_alive_timer_, SIGNAL(timeout()), this, SLOT(SendKeepAlive())); keep_alive_timeout_ = 10000; } OutgoingDataCreator::~OutgoingDataCreator() {} void OutgoingDataCreator::SetClients(QList* clients) { clients_ = clients; // After we got some clients, start the keep alive timer // Default: every 10 seconds keep_alive_timer_->start(keep_alive_timeout_); // Create the song position timer track_position_timer_ = new QTimer(this); connect(track_position_timer_, SIGNAL(timeout()), this, SLOT(UpdateTrackPosition())); // Parse the ultimate lyrics xml file ultimate_reader_->SetThread(this->thread()); provider_list_ = ultimate_reader_->Parse(":lyrics/ultimate_providers.xml"); // Set up the lyrics parser connect(fetcher_, SIGNAL(ResultReady(int, SongInfoFetcher::Result)), SLOT(SendLyrics(int, SongInfoFetcher::Result))); for (SongInfoProvider* provider : provider_list_) { fetcher_->AddProvider(provider); } CheckEnabledProviders(); } void OutgoingDataCreator::CheckEnabledProviders() { QSettings s; s.beginGroup(SongInfoView::kSettingsGroup); // Put the providers in the right order QList ordered_providers; QVariantList default_order; default_order << "lyrics.wikia.com" << "lyricstime.com" << "lyricsreg.com" << "lyricsmania.com" << "metrolyrics.com" << "azlyrics.com" << "songlyrics.com" << "elyrics.net" << "lyricsdownload.com" << "lyrics.com" << "lyricsbay.com" << "directlyrics.com" << "loudson.gs" << "teksty.org" << "tekstowo.pl (Polish translations)" << "vagalume.uol.com.br" << "vagalume.uol.com.br (Portuguese translations)" << "darklyrics.com"; QVariant saved_order = s.value("search_order", default_order); for (const QVariant& name : saved_order.toList()) { SongInfoProvider* provider = ProviderByName(name.toString()); if (provider) ordered_providers << provider; } // Enable all the providers in the list and rank them int relevance = 100; for (SongInfoProvider* provider : ordered_providers) { provider->set_enabled(true); qobject_cast(provider)->set_relevance(relevance--); } // Any lyric providers we don't have in ordered_providers are considered // disabled for (SongInfoProvider* provider : fetcher_->providers()) { if (qobject_cast(provider) && !ordered_providers.contains(provider)) { provider->set_enabled(false); } } } SongInfoProvider* OutgoingDataCreator::ProviderByName(const QString& name) const { for (SongInfoProvider* provider : fetcher_->providers()) { if (UltimateLyricsProvider* lyrics = qobject_cast(provider)) { if (lyrics->name() == name) return provider; } } return nullptr; } void OutgoingDataCreator::SendDataToClients(pb::remote::Message* msg) { // Check if we have clients to send data to if (clients_->empty()) { return; } for (RemoteClient* client : *clients_) { // Do not send data to downloaders if (client->isDownloader()) { if (client->State() != QTcpSocket::ConnectedState) { clients_->removeAt(clients_->indexOf(client)); download_queue_.remove(client); delete client; } continue; } // Check if the client is still active if (client->State() == QTcpSocket::ConnectedState) { client->SendData(msg); } else { clients_->removeAt(clients_->indexOf(client)); delete client; } } } void OutgoingDataCreator::SendClementineInfo() { // Create the general message and set the message type pb::remote::Message msg; msg.set_type(pb::remote::INFO); // Now add the message specific data pb::remote::ResponseClementineInfo* info = msg.mutable_response_clementine_info(); SetEngineState(info); QString version = QString("%1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion()); info->set_version(version.toAscii()); SendDataToClients(&msg); } void OutgoingDataCreator::SetEngineState( pb::remote::ResponseClementineInfo* msg) { switch (app_->player()->GetState()) { case Engine::Idle: msg->set_state(pb::remote::Idle); break; case Engine::Error: case Engine::Empty: msg->set_state(pb::remote::Empty); break; case Engine::Playing: msg->set_state(pb::remote::Playing); break; case Engine::Paused: msg->set_state(pb::remote::Paused); break; } } void OutgoingDataCreator::SendAllPlaylists() { // Get all Playlists QList app_playlists = app_->playlist_manager()->GetAllPlaylists(); int active_playlist = app_->playlist_manager()->active_id(); // Create message pb::remote::Message msg; msg.set_type(pb::remote::PLAYLISTS); pb::remote::ResponsePlaylists* playlists = msg.mutable_response_playlists(); // Get all playlists, even ones that are hidden in the UI. for (const PlaylistBackend::Playlist& p : app_->playlist_backend()->GetAllPlaylists()) { bool playlist_open = app_->playlist_manager()->IsPlaylistOpen(p.id); int item_count = playlist_open ? app_playlists.at(p.id)->rowCount() : 0; // Create a new playlist pb::remote::Playlist* playlist = playlists->add_playlist(); playlist->set_name(DataCommaSizeFromQString(p.name)); playlist->set_id(p.id); playlist->set_active((p.id == active_playlist)); playlist->set_item_count(item_count); playlist->set_closed(!playlist_open); } SendDataToClients(&msg); } void OutgoingDataCreator::SendAllActivePlaylists() { // Get all Playlists QList app_playlists = app_->playlist_manager()->GetAllPlaylists(); int active_playlist = app_->playlist_manager()->active_id(); // Create message pb::remote::Message msg; msg.set_type(pb::remote::PLAYLISTS); pb::remote::ResponsePlaylists* playlists = msg.mutable_response_playlists(); QListIterator it(app_playlists); while (it.hasNext()) { // Get the next Playlist Playlist* p = it.next(); QString playlist_name = app_->playlist_manager()->GetPlaylistName(p->id()); // Create a new playlist pb::remote::Playlist* playlist = playlists->add_playlist(); playlist->set_name(DataCommaSizeFromQString(playlist_name)); playlist->set_id(p->id()); playlist->set_active((p->id() == active_playlist)); playlist->set_item_count(p->rowCount()); playlist->set_closed(false); } SendDataToClients(&msg); } void OutgoingDataCreator::ActiveChanged(Playlist* playlist) { // Send the tracks of the active playlist SendPlaylistSongs(playlist->id()); // Send the changed message after sending the playlist songs pb::remote::Message msg; msg.set_type(pb::remote::ACTIVE_PLAYLIST_CHANGED); msg.mutable_response_active_changed()->set_id(playlist->id()); SendDataToClients(&msg); } void OutgoingDataCreator::PlaylistAdded(int id, const QString& name, bool favorite) { SendAllActivePlaylists(); } void OutgoingDataCreator::PlaylistDeleted(int id) { SendAllActivePlaylists(); } void OutgoingDataCreator::PlaylistClosed(int id) { SendAllActivePlaylists(); } void OutgoingDataCreator::PlaylistRenamed(int id, const QString& new_name) { SendAllActivePlaylists(); } void OutgoingDataCreator::SendFirstData(bool send_playlist_songs) { // First Send the current song PlaylistItemPtr item = app_->player()->GetCurrentItem(); if (!item) { qLog(Info) << "No current item found!"; } CurrentSongChanged(current_song_, current_uri_, current_image_); // then the current volume VolumeChanged(app_->player()->GetVolume()); // Check if we need to start the track position timer if (!track_position_timer_->isActive() && app_->player()->engine()->state() == Engine::Playing) { track_position_timer_->start(1000); } // And the current track position UpdateTrackPosition(); // And the current playlists SendAllActivePlaylists(); // Send the tracks of the active playlist if (send_playlist_songs) { SendPlaylistSongs(app_->playlist_manager()->active_id()); } // Send the current random and repeat mode SendShuffleMode(app_->playlist_manager()->sequence()->shuffle_mode()); SendRepeatMode(app_->playlist_manager()->sequence()->repeat_mode()); // We send all first data pb::remote::Message msg; msg.set_type(pb::remote::FIRST_DATA_SENT_COMPLETE); SendDataToClients(&msg); } void OutgoingDataCreator::CurrentSongChanged(const Song& song, const QString& uri, const QImage& img) { current_song_ = song; current_uri_ = uri; if (!aww_) { current_image_ = img; } SendSongMetadata(); } void OutgoingDataCreator::SendSongMetadata() { // Create the message pb::remote::Message msg; msg.set_type(pb::remote::CURRENT_METAINFO); // If there is no song, create an empty node, otherwise fill it with data int i = app_->playlist_manager()->active()->current_row(); CreateSong(current_song_, current_image_, i, msg.mutable_response_current_metadata()->mutable_song_metadata()); SendDataToClients(&msg); } void OutgoingDataCreator::CreateSong(const Song& song, const QImage& art, const int index, pb::remote::SongMetadata* song_metadata) { if (song.is_valid()) { song_metadata->set_id(song.id()); song_metadata->set_index(index); song_metadata->set_title(DataCommaSizeFromQString(song.PrettyTitle())); song_metadata->set_artist(DataCommaSizeFromQString(song.artist())); song_metadata->set_album(DataCommaSizeFromQString(song.album())); song_metadata->set_albumartist( DataCommaSizeFromQString(song.albumartist())); song_metadata->set_length(song.length_nanosec() / kNsecPerSec); song_metadata->set_pretty_length( DataCommaSizeFromQString(song.PrettyLength())); song_metadata->set_genre(DataCommaSizeFromQString(song.genre())); song_metadata->set_pretty_year(DataCommaSizeFromQString(song.PrettyYear())); song_metadata->set_track(song.track()); song_metadata->set_disc(song.disc()); song_metadata->set_playcount(song.playcount()); song_metadata->set_is_local(song.url().scheme() == "file"); song_metadata->set_filename(DataCommaSizeFromQString(song.basefilename())); song_metadata->set_file_size(song.filesize()); song_metadata->set_rating(song.rating()); // Append coverart if (!art.isNull()) { QImage small; // Check if we resize the image if (art.width() > 1000 || art.height() > 1000) { small = art.scaled(1000, 1000, Qt::KeepAspectRatio); } else { small = art; } // Read the image in a buffer and compress it QByteArray data; QBuffer buf(&data); buf.open(QIODevice::WriteOnly); small.save(&buf, "JPG"); // Append the Data in the protocol buffer song_metadata->set_art(data.constData(), data.size()); } } } void OutgoingDataCreator::VolumeChanged(int volume) { // Create the message pb::remote::Message msg; msg.set_type(pb::remote::SET_VOLUME); msg.mutable_request_set_volume()->set_volume(volume); SendDataToClients(&msg); } void OutgoingDataCreator::SendPlaylistSongs(int id) { // Get the PlaylistQByteArray(data.data(), data.size() Playlist* playlist = app_->playlist_manager()->playlist(id); if (!playlist) { qLog(Info) << "Could not find playlist with id = " << id; return; } // Create the message and the playlist pb::remote::Message msg; msg.set_type(pb::remote::PLAYLIST_SONGS); // Create the Response message pb::remote::ResponsePlaylistSongs* pb_response_playlist_songs = msg.mutable_response_playlist_songs(); // Create a new playlist pb::remote::Playlist* pb_playlist = pb_response_playlist_songs->mutable_requested_playlist(); pb_playlist->set_id(id); // Send all songs int index = 0; SongList song_list = playlist->GetAllSongs(); QListIterator it(song_list); QImage null_img; while (it.hasNext()) { Song song = it.next(); pb::remote::SongMetadata* pb_song = pb_response_playlist_songs->add_songs(); CreateSong(song, null_img, index, pb_song); ++index; } SendDataToClients(&msg); } void OutgoingDataCreator::PlaylistChanged(Playlist* playlist) { // If a playlist changed, then send the new songs to the client SendPlaylistSongs(playlist->id()); } void OutgoingDataCreator::StateChanged(Engine::State state) { // Send state only if it changed // When selecting next song, StateChanged is emitted, but we already know // that we are playing if (state == last_state_) { return; } last_state_ = state; pb::remote::Message msg; switch (state) { case Engine::Playing: msg.set_type(pb::remote::PLAY); track_position_timer_->start(1000); break; case Engine::Paused: msg.set_type(pb::remote::PAUSE); track_position_timer_->stop(); break; case Engine::Empty: msg.set_type(pb::remote::STOP); // Empty is called when player stopped track_position_timer_->stop(); break; default: msg.set_type(pb::remote::STOP); track_position_timer_->stop(); break; }; SendDataToClients(&msg); } void OutgoingDataCreator::SendRepeatMode(PlaylistSequence::RepeatMode mode) { pb::remote::Message msg; msg.set_type(pb::remote::REPEAT); switch (mode) { case PlaylistSequence::Repeat_Off: msg.mutable_repeat()->set_repeat_mode(pb::remote::Repeat_Off); break; case PlaylistSequence::Repeat_Track: msg.mutable_repeat()->set_repeat_mode(pb::remote::Repeat_Track); break; case PlaylistSequence::Repeat_Album: msg.mutable_repeat()->set_repeat_mode(pb::remote::Repeat_Album); break; case PlaylistSequence::Repeat_Playlist: msg.mutable_repeat()->set_repeat_mode(pb::remote::Repeat_Playlist); break; } SendDataToClients(&msg); } void OutgoingDataCreator::SendShuffleMode(PlaylistSequence::ShuffleMode mode) { pb::remote::Message msg; msg.set_type(pb::remote::SHUFFLE); switch (mode) { case PlaylistSequence::Shuffle_Off: msg.mutable_shuffle()->set_shuffle_mode(pb::remote::Shuffle_Off); break; case PlaylistSequence::Shuffle_All: msg.mutable_shuffle()->set_shuffle_mode(pb::remote::Shuffle_All); break; case PlaylistSequence::Shuffle_InsideAlbum: msg.mutable_shuffle()->set_shuffle_mode(pb::remote::Shuffle_InsideAlbum); break; case PlaylistSequence::Shuffle_Albums: msg.mutable_shuffle()->set_shuffle_mode(pb::remote::Shuffle_Albums); break; } SendDataToClients(&msg); } void OutgoingDataCreator::SendKeepAlive() { pb::remote::Message msg; msg.set_type(pb::remote::KEEP_ALIVE); SendDataToClients(&msg); } void OutgoingDataCreator::UpdateTrackPosition() { pb::remote::Message msg; msg.set_type(pb::remote::UPDATE_TRACK_POSITION); int position = std::floor( float(app_->player()->engine()->position_nanosec()) / kNsecPerSec + 0.5); if (app_->player()->engine()->position_nanosec() > current_song_.length_nanosec()) position = last_track_position_; msg.mutable_response_update_track_position()->set_position(position); last_track_position_ = position; SendDataToClients(&msg); } void OutgoingDataCreator::DisconnectAllClients() { pb::remote::Message msg; msg.set_type(pb::remote::DISCONNECT); msg.mutable_response_disconnect()->set_reason_disconnect( pb::remote::Server_Shutdown); SendDataToClients(&msg); } void OutgoingDataCreator::GetLyrics() { fetcher_->FetchInfo(current_song_); } void OutgoingDataCreator::SendLyrics(int id, const SongInfoFetcher::Result& result) { pb::remote::Message msg; msg.set_type(pb::remote::LYRICS); pb::remote::ResponseLyrics* response = msg.mutable_response_lyrics(); for (const CollapsibleInfoPane::Data& data : result.info_) { // If the size is zero, do not send the provider UltimateLyricsLyric* editor = qobject_cast(data.content_object_); if (editor->toPlainText().length() == 0) continue; pb::remote::Lyric* lyric = response->mutable_lyrics()->Add(); lyric->set_id(DataCommaSizeFromQString(data.id_)); lyric->set_title(DataCommaSizeFromQString(data.title_)); lyric->set_content(DataCommaSizeFromQString(editor->toPlainText())); } SendDataToClients(&msg); results_.take(id); } void OutgoingDataCreator::SendSongs( const pb::remote::RequestDownloadSongs& request, RemoteClient* client) { if (!download_queue_.contains(client)) { download_queue_.insert(client, QQueue()); } switch (request.download_item()) { case pb::remote::CurrentItem: { DownloadItem item(current_song_, 1, 1); download_queue_[client].append(item); break; } case pb::remote::ItemAlbum: SendAlbum(client, current_song_); break; case pb::remote::APlaylist: SendPlaylist(client, request.playlist_id()); break; case pb::remote::Urls: SendUrls(client, request); break; default: break; } // Send first file OfferNextSong(client); } void OutgoingDataCreator::OfferNextSong(RemoteClient* client) { if (!download_queue_.contains(client)) return; pb::remote::Message msg; if (download_queue_.value(client).isEmpty()) { // We sent all songs, tell the client the queue is empty msg.set_type(pb::remote::DOWNLOAD_QUEUE_EMPTY); } else { // Get the item and send the single song DownloadItem item = download_queue_[client].head(); msg.set_type(pb::remote::SONG_FILE_CHUNK); pb::remote::ResponseSongFileChunk* chunk = msg.mutable_response_song_file_chunk(); // Song offer is chunk no 0 chunk->set_chunk_count(0); chunk->set_chunk_number(0); chunk->set_file_count(item.song_count_); chunk->set_file_number(item.song_no_); CreateSong(item.song_, QImage(), -1, chunk->mutable_song_metadata()); } client->SendData(&msg); } void OutgoingDataCreator::ResponseSongOffer(RemoteClient* client, bool accepted) { if (!download_queue_.contains(client)) return; if (download_queue_.value(client).isEmpty()) return; // Get the item and send the single song DownloadItem item = download_queue_[client].dequeue(); if (accepted) SendSingleSong(client, item.song_, item.song_no_, item.song_count_); // And offer the next song OfferNextSong(client); } void OutgoingDataCreator::SendSingleSong(RemoteClient* client, const Song& song, int song_no, int song_count) { // Only local files!!! if (!(song.url().scheme() == "file")) return; // Open the file QFile file(song.url().toLocalFile()); // Get sha1 for file QByteArray sha1 = Utilities::Sha1File(file).toHex(); qLog(Debug) << "sha1 for file" << song.url().toLocalFile() << "=" << sha1; file.open(QIODevice::ReadOnly); QByteArray data; pb::remote::Message msg; pb::remote::ResponseSongFileChunk* chunk = msg.mutable_response_song_file_chunk(); msg.set_type(pb::remote::SONG_FILE_CHUNK); QImage null_image; // Calculate the number of chunks int chunk_count = qRound((file.size() / kFileChunkSize) + 0.5); int chunk_number = 1; while (!file.atEnd()) { // Read file chunk data = file.read(kFileChunkSize); // Set chunk data chunk->set_chunk_count(chunk_count); chunk->set_chunk_number(chunk_number); chunk->set_file_count(song_count); chunk->set_file_number(song_no); chunk->set_size(file.size()); chunk->set_data(data.data(), data.size()); chunk->set_file_hash(sha1.data(), sha1.size()); // On the first chunk send the metadata, so the client knows // what file it receives. if (chunk_number == 1) { int i = app_->playlist_manager()->active()->current_row(); CreateSong( song, null_image, i, msg.mutable_response_song_file_chunk()->mutable_song_metadata()); } // Send data directly to the client client->SendData(&msg); // Clear working data chunk->Clear(); data.clear(); chunk_number++; } file.close(); } void OutgoingDataCreator::SendAlbum(RemoteClient* client, const Song& song) { // No streams! if (song.url().scheme() != "file") return; SongList album = app_->library_backend()->GetSongsByAlbum(song.album()); for (Song s : album) { DownloadItem item(s, album.indexOf(s) + 1, album.size()); download_queue_[client].append(item); } } void OutgoingDataCreator::SendPlaylist(RemoteClient* client, int playlist_id) { Playlist* playlist = app_->playlist_manager()->playlist(playlist_id); if (!playlist) { qLog(Info) << "Could not find playlist with id = " << playlist_id; return; } SongList song_list = playlist->GetAllSongs(); // Count the local songs int count = 0; for (Song s : song_list) { if (s.url().scheme() == "file") { count++; } } for (Song s : song_list) { // Only local files! if (s.url().scheme() == "file") { DownloadItem item(s, song_list.indexOf(s) + 1, count); download_queue_[client].append(item); } } } void OutgoingDataCreator::SendUrls(RemoteClient *client, const pb::remote::RequestDownloadSongs &request) { SongList song_list; // First gather all valid songs for (auto it = request.urls().begin(); it != request.urls().end(); ++it) { std::string s = *it; QUrl url = QUrl(QStringFromStdString(s)); Song song = app_->library_backend()->GetSongByUrl(url); if (song.is_valid() && song.url().scheme() == "file") { song_list.append(song); } } // Then send them to Clementine Remote for (Song s : song_list) { DownloadItem item(s, song_list.indexOf(s) + 1, song_list.count()); download_queue_[client].append(item); } } void OutgoingDataCreator::SendLibrary(RemoteClient* client) { // Get a temporary file name QString temp_file_name = Utilities::GetTemporaryFileName(); // Attach this file to the database Database::AttachedDatabase adb(temp_file_name, "", true); QSqlDatabase db(app_->database()->Connect()); app_->database()->AttachDatabaseOnDbConnection("songs_export", adb, db); // Copy the content of the song table to this temporary database QSqlQuery q(QString( "create table songs_export.songs as SELECT * FROM songs " "where unavailable = 0;"), db); if (app_->database()->CheckErrors(q)) return; // Detach the database app_->database()->DetachDatabase("songs_export"); // Open the file QFile file(temp_file_name); // Get the sha1 hash QByteArray sha1 = Utilities::Sha1File(file).toHex(); qLog(Debug) << "Library sha1" << sha1; file.open(QIODevice::ReadOnly); QByteArray data; pb::remote::Message msg; pb::remote::ResponseLibraryChunk* chunk = msg.mutable_response_library_chunk(); msg.set_type(pb::remote::LIBRARY_CHUNK); // Calculate the number of chunks int chunk_count = qRound((file.size() / kFileChunkSize) + 0.5); int chunk_number = 1; while (!file.atEnd()) { // Read file chunk data = file.read(kFileChunkSize); // Set chunk data chunk->set_chunk_count(chunk_count); chunk->set_chunk_number(chunk_number); chunk->set_size(file.size()); chunk->set_data(data.data(), data.size()); chunk->set_file_hash(sha1.data(), sha1.size()); // Send data directly to the client client->SendData(&msg); // Clear working data chunk->Clear(); data.clear(); chunk_number++; } // Remove temporary file file.remove(); } void OutgoingDataCreator::EnableKittens(bool aww) { aww_ = aww; } void OutgoingDataCreator::SendKitten(const QImage& kitten) { if (aww_) { current_image_ = kitten; SendSongMetadata(); } }