Merge branch 'external-tagreader'
This commit is contained in:
commit
626ce20ec0
|
@ -47,7 +47,7 @@ find_package(OpenGL REQUIRED)
|
|||
find_package(Boost REQUIRED)
|
||||
find_package(Gettext REQUIRED)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
find_package(Protobuf)
|
||||
find_package(Protobuf REQUIRED)
|
||||
|
||||
pkg_check_modules(TAGLIB REQUIRED taglib>=1.6)
|
||||
pkg_check_modules(QJSON REQUIRED QJson)
|
||||
|
@ -377,6 +377,9 @@ add_subdirectory(3rdparty/universalchardet)
|
|||
add_subdirectory(tests)
|
||||
add_subdirectory(dist)
|
||||
add_subdirectory(tools/ultimate_lyrics_parser)
|
||||
add_subdirectory(ext/libclementine-common)
|
||||
add_subdirectory(ext/libclementine-tagreader)
|
||||
add_subdirectory(ext/clementine-tagreader)
|
||||
|
||||
option(WITH_DEBIAN OFF)
|
||||
if(WITH_DEBIAN)
|
||||
|
@ -392,11 +395,11 @@ if(HAVE_BREAKPAD)
|
|||
endif(HAVE_BREAKPAD)
|
||||
|
||||
if(HAVE_SPOTIFY)
|
||||
add_subdirectory(spotifyblob/common)
|
||||
add_subdirectory(ext/libclementine-spotifyblob)
|
||||
endif(HAVE_SPOTIFY)
|
||||
|
||||
if(HAVE_SPOTIFY_BLOB)
|
||||
add_subdirectory(spotifyblob/blob)
|
||||
add_subdirectory(ext/clementine-spotifyblob)
|
||||
endif(HAVE_SPOTIFY_BLOB)
|
||||
|
||||
# This goes after everything else because KDE fucks everything else up with its
|
||||
|
|
|
@ -109,6 +109,7 @@ Section "Clementine" Clementine
|
|||
File "avformat-52.dll"
|
||||
File "avutil-50.dll"
|
||||
File "clementine.exe"
|
||||
File "clementine-tagreader.exe"
|
||||
File "clementine-spotifyblob.exe"
|
||||
File "clementine.ico"
|
||||
File "glew32.dll"
|
||||
|
@ -950,6 +951,7 @@ Section "Uninstall"
|
|||
Delete "$INSTDIR\avutil-50.dll"
|
||||
Delete "$INSTDIR\clementine.ico"
|
||||
Delete "$INSTDIR\clementine.exe"
|
||||
Delete "$INSTDIR\clementine-tagreader.exe"
|
||||
Delete "$INSTDIR\clementine-spotifyblob.exe"
|
||||
Delete "$INSTDIR\glew32.dll"
|
||||
Delete "$INSTDIR\intl.dll"
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
include_directories(${SPOTIFY_INCLUDE_DIRS})
|
||||
include_directories(${PROTOBUF_INCLUDE_DIRS})
|
||||
include_directories(${CMAKE_CURRENT_BINARY_DIR})
|
||||
include_directories(${CMAKE_CURRENT_BINARY_DIR}/../common)
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../common)
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
|
||||
include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-spotifyblob)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-spotifyblob)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-common)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src)
|
||||
|
||||
link_directories(${SPOTIFY_LIBRARY_DIRS})
|
||||
|
@ -14,8 +16,6 @@ set(SOURCES
|
|||
mediapipeline.cpp
|
||||
spotifyclient.cpp
|
||||
spotify_utilities.cpp
|
||||
|
||||
${CMAKE_SOURCE_DIR}/src/core/logging.cpp
|
||||
)
|
||||
|
||||
set(HEADERS
|
||||
|
@ -45,6 +45,7 @@ target_link_libraries(clementine-spotifyblob
|
|||
${GSTREAMER_BASE_LIBRARIES}
|
||||
${GSTREAMER_APP_LIBRARIES}
|
||||
clementine-spotifyblob-messages
|
||||
libclementine-common
|
||||
)
|
||||
|
||||
if(APPLE)
|
|
@ -23,7 +23,6 @@
|
|||
#include "mediapipeline.h"
|
||||
#include "spotifyclient.h"
|
||||
#include "spotifykey.h"
|
||||
#include "spotifymessagehandler.h"
|
||||
#include "spotifymessages.pb.h"
|
||||
#include "spotify_utilities.h"
|
||||
#include "core/logging.h"
|
||||
|
@ -39,12 +38,13 @@ const int SpotifyClient::kWaveHeaderSize = 44;
|
|||
|
||||
|
||||
SpotifyClient::SpotifyClient(QObject* parent)
|
||||
: QObject(parent),
|
||||
: AbstractMessageHandler<pb::spotify::Message>(NULL, parent),
|
||||
api_key_(QByteArray::fromBase64(kSpotifyApiKey)),
|
||||
protocol_socket_(new QTcpSocket(this)),
|
||||
handler_(new SpotifyMessageHandler(protocol_socket_, this)),
|
||||
session_(NULL),
|
||||
events_timer_(new QTimer(this)) {
|
||||
SetDevice(protocol_socket_);
|
||||
|
||||
memset(&spotify_callbacks_, 0, sizeof(spotify_callbacks_));
|
||||
memset(&spotify_config_, 0, sizeof(spotify_config_));
|
||||
memset(&playlistcontainer_callbacks_, 0, sizeof(playlistcontainer_callbacks_));
|
||||
|
@ -91,8 +91,6 @@ SpotifyClient::SpotifyClient(QObject* parent)
|
|||
events_timer_->setSingleShot(true);
|
||||
connect(events_timer_, SIGNAL(timeout()), SLOT(ProcessEvents()));
|
||||
|
||||
connect(handler_, SIGNAL(MessageArrived(spotify_pb::SpotifyMessage)),
|
||||
SLOT(HandleMessage(spotify_pb::SpotifyMessage)));
|
||||
connect(protocol_socket_, SIGNAL(disconnected()),
|
||||
QCoreApplication::instance(), SLOT(quit()));
|
||||
}
|
||||
|
@ -115,7 +113,7 @@ void SpotifyClient::Init(quint16 port) {
|
|||
void SpotifyClient::LoggedInCallback(sp_session* session, sp_error error) {
|
||||
SpotifyClient* me = reinterpret_cast<SpotifyClient*>(sp_session_userdata(session));
|
||||
const bool success = error == SP_ERROR_OK;
|
||||
spotify_pb::LoginResponse_Error error_code = spotify_pb::LoginResponse_Error_Other;
|
||||
pb::spotify::LoginResponse_Error error_code = pb::spotify::LoginResponse_Error_Other;
|
||||
|
||||
if (!success) {
|
||||
qLog(Warning) << "Failed to login" << sp_error_message(error);
|
||||
|
@ -123,16 +121,16 @@ void SpotifyClient::LoggedInCallback(sp_session* session, sp_error error) {
|
|||
|
||||
switch (error) {
|
||||
case SP_ERROR_BAD_USERNAME_OR_PASSWORD:
|
||||
error_code = spotify_pb::LoginResponse_Error_BadUsernameOrPassword;
|
||||
error_code = pb::spotify::LoginResponse_Error_BadUsernameOrPassword;
|
||||
break;
|
||||
case SP_ERROR_USER_BANNED:
|
||||
error_code = spotify_pb::LoginResponse_Error_UserBanned;
|
||||
error_code = pb::spotify::LoginResponse_Error_UserBanned;
|
||||
break;
|
||||
case SP_ERROR_USER_NEEDS_PREMIUM :
|
||||
error_code = spotify_pb::LoginResponse_Error_UserNeedsPremium;
|
||||
error_code = pb::spotify::LoginResponse_Error_UserNeedsPremium;
|
||||
break;
|
||||
default:
|
||||
error_code = spotify_pb::LoginResponse_Error_Other;
|
||||
error_code = pb::spotify::LoginResponse_Error_Other;
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -160,7 +158,7 @@ void SpotifyClient::LogMessageCallback(sp_session* session, const char* data) {
|
|||
qLog(Debug) << "libspotify:" << QString::fromUtf8(data).trimmed();
|
||||
}
|
||||
|
||||
void SpotifyClient::Search(const spotify_pb::SearchRequest& req) {
|
||||
void SpotifyClient::Search(const pb::spotify::SearchRequest& req) {
|
||||
sp_search* search = sp_search_create(
|
||||
session_, req.query().c_str(),
|
||||
0, req.limit(),
|
||||
|
@ -214,11 +212,11 @@ void SpotifyClient::SearchAlbumBrowseComplete(sp_albumbrowse* result, void* user
|
|||
|
||||
void SpotifyClient::SendSearchResponse(sp_search* result) {
|
||||
// Take the request out of the queue
|
||||
spotify_pb::SearchRequest req = pending_searches_.take(result);
|
||||
pb::spotify::SearchRequest req = pending_searches_.take(result);
|
||||
|
||||
// Prepare the response
|
||||
spotify_pb::SpotifyMessage message;
|
||||
spotify_pb::SearchResponse* response = message.mutable_search_response();
|
||||
pb::spotify::Message message;
|
||||
pb::spotify::SearchResponse* response = message.mutable_search_response();
|
||||
|
||||
*response->mutable_request() = req;
|
||||
|
||||
|
@ -227,7 +225,7 @@ void SpotifyClient::SendSearchResponse(sp_search* result) {
|
|||
if (error != SP_ERROR_OK) {
|
||||
response->set_error(sp_error_message(error));
|
||||
|
||||
handler_->SendMessage(message);
|
||||
SendMessage(message);
|
||||
sp_search_release(result);
|
||||
return;
|
||||
}
|
||||
|
@ -243,7 +241,7 @@ void SpotifyClient::SendSearchResponse(sp_search* result) {
|
|||
QList<sp_albumbrowse*> browses = pending_search_album_browses_.take(result);
|
||||
foreach (sp_albumbrowse* browse, browses) {
|
||||
sp_album* album = sp_albumbrowse_album(browse);
|
||||
spotify_pb::Album* msg = response->add_album();
|
||||
pb::spotify::Album* msg = response->add_album();
|
||||
|
||||
ConvertAlbum(album, msg->mutable_metadata());
|
||||
ConvertAlbumBrowse(browse, msg->mutable_metadata());
|
||||
|
@ -261,11 +259,11 @@ void SpotifyClient::SendSearchResponse(sp_search* result) {
|
|||
response->set_total_tracks(sp_search_total_tracks(result));
|
||||
response->set_did_you_mean(sp_search_did_you_mean(result));
|
||||
|
||||
handler_->SendMessage(message);
|
||||
SendMessage(message);
|
||||
sp_search_release(result);
|
||||
}
|
||||
|
||||
void SpotifyClient::HandleMessage(const spotify_pb::SpotifyMessage& message) {
|
||||
void SpotifyClient::MessageArrived(const pb::spotify::Message& message) {
|
||||
if (message.has_login_request()) {
|
||||
Login(message.login_request());
|
||||
} else if (message.has_load_playlist_request()) {
|
||||
|
@ -287,12 +285,12 @@ void SpotifyClient::HandleMessage(const spotify_pb::SpotifyMessage& message) {
|
|||
}
|
||||
}
|
||||
|
||||
void SpotifyClient::SetPlaybackSettings(const spotify_pb::PlaybackSettings& req) {
|
||||
void SpotifyClient::SetPlaybackSettings(const pb::spotify::PlaybackSettings& req) {
|
||||
sp_bitrate bitrate = SP_BITRATE_320k;
|
||||
switch (req.bitrate()) {
|
||||
case spotify_pb::Bitrate96k: bitrate = SP_BITRATE_96k; break;
|
||||
case spotify_pb::Bitrate160k: bitrate = SP_BITRATE_160k; break;
|
||||
case spotify_pb::Bitrate320k: bitrate = SP_BITRATE_320k; break;
|
||||
case pb::spotify::Bitrate96k: bitrate = SP_BITRATE_96k; break;
|
||||
case pb::spotify::Bitrate160k: bitrate = SP_BITRATE_160k; break;
|
||||
case pb::spotify::Bitrate320k: bitrate = SP_BITRATE_320k; break;
|
||||
}
|
||||
|
||||
qLog(Debug) << "Setting playback settings: bitrate"
|
||||
|
@ -303,11 +301,11 @@ void SpotifyClient::SetPlaybackSettings(const spotify_pb::PlaybackSettings& req)
|
|||
sp_session_set_volume_normalization(session_, req.volume_normalisation());
|
||||
}
|
||||
|
||||
void SpotifyClient::Login(const spotify_pb::LoginRequest& req) {
|
||||
void SpotifyClient::Login(const pb::spotify::LoginRequest& req) {
|
||||
sp_error error = sp_session_create(&spotify_config_, &session_);
|
||||
if (error != SP_ERROR_OK) {
|
||||
qLog(Warning) << "Failed to create session" << sp_error_message(error);
|
||||
SendLoginCompleted(false, sp_error_message(error), spotify_pb::LoginResponse_Error_Other);
|
||||
SendLoginCompleted(false, sp_error_message(error), pb::spotify::LoginResponse_Error_Other);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -318,7 +316,7 @@ void SpotifyClient::Login(const spotify_pb::LoginRequest& req) {
|
|||
if (error != SP_ERROR_OK) {
|
||||
qLog(Warning) << "Tried to relogin but no stored credentials";
|
||||
SendLoginCompleted(false, sp_error_message(error),
|
||||
spotify_pb::LoginResponse_Error_ReloginFailed);
|
||||
pb::spotify::LoginResponse_Error_ReloginFailed);
|
||||
}
|
||||
} else {
|
||||
sp_session_login(session_,
|
||||
|
@ -329,10 +327,10 @@ void SpotifyClient::Login(const spotify_pb::LoginRequest& req) {
|
|||
}
|
||||
|
||||
void SpotifyClient::SendLoginCompleted(bool success, const QString& error,
|
||||
spotify_pb::LoginResponse_Error error_code) {
|
||||
spotify_pb::SpotifyMessage message;
|
||||
pb::spotify::LoginResponse_Error error_code) {
|
||||
pb::spotify::Message message;
|
||||
|
||||
spotify_pb::LoginResponse* response = message.mutable_login_response();
|
||||
pb::spotify::LoginResponse* response = message.mutable_login_response();
|
||||
response->set_success(success);
|
||||
response->set_error(DataCommaSizeFromQString(error));
|
||||
|
||||
|
@ -340,7 +338,7 @@ void SpotifyClient::SendLoginCompleted(bool success, const QString& error,
|
|||
response->set_error_code(error_code);
|
||||
}
|
||||
|
||||
handler_->SendMessage(message);
|
||||
SendMessage(message);
|
||||
}
|
||||
|
||||
void SpotifyClient::PlaylistContainerLoadedCallback(sp_playlistcontainer* pc, void* userdata) {
|
||||
|
@ -380,8 +378,8 @@ void SpotifyClient::PlaylistRemovedCallback(sp_playlistcontainer* pc, sp_playlis
|
|||
}
|
||||
|
||||
void SpotifyClient::SendPlaylistList() {
|
||||
spotify_pb::SpotifyMessage message;
|
||||
spotify_pb::Playlists* response = message.mutable_playlists_updated();
|
||||
pb::spotify::Message message;
|
||||
pb::spotify::Playlists* response = message.mutable_playlists_updated();
|
||||
|
||||
sp_playlistcontainer* container = sp_session_playlistcontainer(session_);
|
||||
if (!container) {
|
||||
|
@ -408,7 +406,7 @@ void SpotifyClient::SendPlaylistList() {
|
|||
continue;
|
||||
}
|
||||
|
||||
spotify_pb::Playlists::Playlist* msg = response->add_playlist();
|
||||
pb::spotify::Playlists::Playlist* msg = response->add_playlist();
|
||||
msg->set_index(i);
|
||||
msg->set_name(sp_playlist_name(playlist));
|
||||
|
||||
|
@ -424,21 +422,21 @@ void SpotifyClient::SendPlaylistList() {
|
|||
}
|
||||
}
|
||||
|
||||
handler_->SendMessage(message);
|
||||
SendMessage(message);
|
||||
}
|
||||
|
||||
sp_playlist* SpotifyClient::GetPlaylist(spotify_pb::PlaylistType type, int user_index) {
|
||||
sp_playlist* SpotifyClient::GetPlaylist(pb::spotify::PlaylistType type, int user_index) {
|
||||
sp_playlist* playlist = NULL;
|
||||
switch (type) {
|
||||
case spotify_pb::Inbox:
|
||||
case pb::spotify::Inbox:
|
||||
playlist = sp_session_inbox_create(session_);
|
||||
break;
|
||||
|
||||
case spotify_pb::Starred:
|
||||
case pb::spotify::Starred:
|
||||
playlist = sp_session_starred_create(session_);
|
||||
break;
|
||||
|
||||
case spotify_pb::UserPlaylist: {
|
||||
case pb::spotify::UserPlaylist: {
|
||||
sp_playlistcontainer* pc = sp_session_playlistcontainer(session_);
|
||||
|
||||
if (pc && user_index <= sp_playlistcontainer_num_playlists(pc)) {
|
||||
|
@ -454,7 +452,7 @@ sp_playlist* SpotifyClient::GetPlaylist(spotify_pb::PlaylistType type, int user_
|
|||
return playlist;
|
||||
}
|
||||
|
||||
void SpotifyClient::LoadPlaylist(const spotify_pb::LoadPlaylistRequest& req) {
|
||||
void SpotifyClient::LoadPlaylist(const pb::spotify::LoadPlaylistRequest& req) {
|
||||
PendingLoadPlaylist pending_load;
|
||||
pending_load.request_ = req;
|
||||
pending_load.playlist_ = GetPlaylist(req.type(), req.user_playlist_index());
|
||||
|
@ -464,10 +462,10 @@ void SpotifyClient::LoadPlaylist(const spotify_pb::LoadPlaylistRequest& req) {
|
|||
if (!pending_load.playlist_) {
|
||||
qLog(Warning) << "Invalid playlist requested or not logged in";
|
||||
|
||||
spotify_pb::SpotifyMessage message;
|
||||
spotify_pb::LoadPlaylistResponse* response = message.mutable_load_playlist_response();
|
||||
pb::spotify::Message message;
|
||||
pb::spotify::LoadPlaylistResponse* response = message.mutable_load_playlist_response();
|
||||
*response->mutable_request() = req;
|
||||
handler_->SendMessage(message);
|
||||
SendMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -477,7 +475,7 @@ void SpotifyClient::LoadPlaylist(const spotify_pb::LoadPlaylistRequest& req) {
|
|||
PlaylistStateChangedForLoadPlaylist(pending_load.playlist_, this);
|
||||
}
|
||||
|
||||
void SpotifyClient::SyncPlaylist(const spotify_pb::SyncPlaylistRequest& req) {
|
||||
void SpotifyClient::SyncPlaylist(const pb::spotify::SyncPlaylistRequest& req) {
|
||||
sp_playlist* playlist = GetPlaylist(req.request().type(), req.request().user_playlist_index());
|
||||
|
||||
// The playlist should already be loaded.
|
||||
|
@ -528,12 +526,12 @@ void SpotifyClient::PlaylistStateChangedForLoadPlaylist(sp_playlist* pl, void* u
|
|||
}
|
||||
|
||||
// Everything is loaded so send the response protobuf and unref everything.
|
||||
spotify_pb::SpotifyMessage message;
|
||||
spotify_pb::LoadPlaylistResponse* response = message.mutable_load_playlist_response();
|
||||
pb::spotify::Message message;
|
||||
pb::spotify::LoadPlaylistResponse* response = message.mutable_load_playlist_response();
|
||||
|
||||
// For some reason, we receive the starred tracks in reverse order but not
|
||||
// other playlists.
|
||||
if (pending_load->request_.type() == spotify_pb::Starred) {
|
||||
if (pending_load->request_.type() == pb::spotify::Starred) {
|
||||
std::reverse(pending_load->tracks_.begin(),
|
||||
pending_load->tracks_.end());
|
||||
}
|
||||
|
@ -543,7 +541,7 @@ void SpotifyClient::PlaylistStateChangedForLoadPlaylist(sp_playlist* pl, void* u
|
|||
me->ConvertTrack(track, response->add_track());
|
||||
sp_track_release(track);
|
||||
}
|
||||
me->handler_->SendMessage(message);
|
||||
me->SendMessage(message);
|
||||
|
||||
// Unref the playlist and remove our callbacks
|
||||
sp_playlist_remove_callbacks(pl, &me->load_playlist_callbacks_, me);
|
||||
|
@ -559,7 +557,7 @@ void SpotifyClient::PlaylistStateChangedForGetPlaylists(sp_playlist* pl, void* u
|
|||
me->SendPlaylistList();
|
||||
}
|
||||
|
||||
void SpotifyClient::ConvertTrack(sp_track* track, spotify_pb::Track* pb) {
|
||||
void SpotifyClient::ConvertTrack(sp_track* track, pb::spotify::Track* pb) {
|
||||
sp_album* album = sp_track_album(track);
|
||||
|
||||
pb->set_starred(sp_track_is_starred(session_, track));
|
||||
|
@ -592,7 +590,7 @@ void SpotifyClient::ConvertTrack(sp_track* track, spotify_pb::Track* pb) {
|
|||
pb->set_uri(uri);
|
||||
}
|
||||
|
||||
void SpotifyClient::ConvertAlbum(sp_album* album, spotify_pb::Track* pb) {
|
||||
void SpotifyClient::ConvertAlbum(sp_album* album, pb::spotify::Track* pb) {
|
||||
pb->set_album(sp_album_name(album));
|
||||
pb->set_year(sp_album_year(album));
|
||||
pb->add_artist(sp_artist_name(sp_album_artist(album)));
|
||||
|
@ -622,7 +620,7 @@ void SpotifyClient::ConvertAlbum(sp_album* album, spotify_pb::Track* pb) {
|
|||
pb->set_uri(uri);
|
||||
}
|
||||
|
||||
void SpotifyClient::ConvertAlbumBrowse(sp_albumbrowse* browse, spotify_pb::Track* pb) {
|
||||
void SpotifyClient::ConvertAlbumBrowse(sp_albumbrowse* browse, pb::spotify::Track* pb) {
|
||||
pb->set_track(sp_albumbrowse_num_tracks(browse));
|
||||
}
|
||||
|
||||
|
@ -722,7 +720,7 @@ void SpotifyClient::OfflineStatusUpdatedCallback(sp_session* session) {
|
|||
|
||||
int download_progress = me->GetDownloadProgress(playlist);
|
||||
if (download_progress != -1) {
|
||||
me->SendDownloadProgress(spotify_pb::UserPlaylist, i, download_progress);
|
||||
me->SendDownloadProgress(pb::spotify::UserPlaylist, i, download_progress);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -731,7 +729,7 @@ void SpotifyClient::OfflineStatusUpdatedCallback(sp_session* session) {
|
|||
sp_playlist_release(inbox);
|
||||
|
||||
if (download_progress != -1) {
|
||||
me->SendDownloadProgress(spotify_pb::Inbox, -1, download_progress);
|
||||
me->SendDownloadProgress(pb::spotify::Inbox, -1, download_progress);
|
||||
}
|
||||
|
||||
sp_playlist* starred = sp_session_starred_create(session);
|
||||
|
@ -739,20 +737,20 @@ void SpotifyClient::OfflineStatusUpdatedCallback(sp_session* session) {
|
|||
sp_playlist_release(starred);
|
||||
|
||||
if (download_progress != -1) {
|
||||
me->SendDownloadProgress(spotify_pb::Starred, -1, download_progress);
|
||||
me->SendDownloadProgress(pb::spotify::Starred, -1, download_progress);
|
||||
}
|
||||
}
|
||||
|
||||
void SpotifyClient::SendDownloadProgress(
|
||||
spotify_pb::PlaylistType type, int index, int download_progress) {
|
||||
spotify_pb::SpotifyMessage message;
|
||||
spotify_pb::SyncPlaylistProgress* progress = message.mutable_sync_playlist_progress();
|
||||
pb::spotify::PlaylistType type, int index, int download_progress) {
|
||||
pb::spotify::Message message;
|
||||
pb::spotify::SyncPlaylistProgress* progress = message.mutable_sync_playlist_progress();
|
||||
progress->mutable_request()->set_type(type);
|
||||
if (index != -1) {
|
||||
progress->mutable_request()->set_user_playlist_index(index);
|
||||
}
|
||||
progress->set_sync_progress(download_progress);
|
||||
handler_->SendMessage(message);
|
||||
SendMessage(message);
|
||||
}
|
||||
|
||||
int SpotifyClient::GetDownloadProgress(sp_playlist* playlist) {
|
||||
|
@ -771,7 +769,7 @@ int SpotifyClient::GetDownloadProgress(sp_playlist* playlist) {
|
|||
return -1;
|
||||
}
|
||||
|
||||
void SpotifyClient::StartPlayback(const spotify_pb::PlaybackRequest& req) {
|
||||
void SpotifyClient::StartPlayback(const pb::spotify::PlaybackRequest& req) {
|
||||
// Get a link object from the URI
|
||||
sp_link* link = sp_link_create_from_string(req.track_uri().c_str());
|
||||
if (!link) {
|
||||
|
@ -835,11 +833,11 @@ void SpotifyClient::TryPlaybackAgain(const PendingPlaybackRequest& req) {
|
|||
}
|
||||
|
||||
void SpotifyClient::SendPlaybackError(const QString& error) {
|
||||
spotify_pb::SpotifyMessage message;
|
||||
spotify_pb::PlaybackError* msg = message.mutable_playback_error();
|
||||
pb::spotify::Message message;
|
||||
pb::spotify::PlaybackError* msg = message.mutable_playback_error();
|
||||
|
||||
msg->set_error(DataCommaSizeFromQString(error));
|
||||
handler_->SendMessage(message);
|
||||
SendMessage(message);
|
||||
}
|
||||
|
||||
void SpotifyClient::LoadImage(const QString& id_b64) {
|
||||
|
@ -849,10 +847,10 @@ void SpotifyClient::LoadImage(const QString& id_b64) {
|
|||
<< kSpotifyImageIDSize << "bytes):" << id_b64;
|
||||
|
||||
// Send an error response straight away
|
||||
spotify_pb::SpotifyMessage message;
|
||||
spotify_pb::ImageResponse* msg = message.mutable_image_response();
|
||||
pb::spotify::Message message;
|
||||
pb::spotify::ImageResponse* msg = message.mutable_image_response();
|
||||
msg->set_id(DataCommaSizeFromQString(id_b64));
|
||||
handler_->SendMessage(message);
|
||||
SendMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -898,13 +896,13 @@ void SpotifyClient::TryImageAgain(sp_image* image) {
|
|||
const void* data = sp_image_data(image, &size);
|
||||
|
||||
// Send the response
|
||||
spotify_pb::SpotifyMessage message;
|
||||
spotify_pb::ImageResponse* msg = message.mutable_image_response();
|
||||
pb::spotify::Message message;
|
||||
pb::spotify::ImageResponse* msg = message.mutable_image_response();
|
||||
msg->set_id(DataCommaSizeFromQString(req->id_b64_));
|
||||
if (data && size) {
|
||||
msg->set_data(data, size);
|
||||
}
|
||||
handler_->SendMessage(message);
|
||||
SendMessage(message);
|
||||
|
||||
// Free stuff
|
||||
image_callbacks_registered_[image] --;
|
||||
|
@ -951,8 +949,8 @@ void SpotifyClient::AlbumBrowseComplete(sp_albumbrowse* result, void* userdata)
|
|||
|
||||
QString uri = me->pending_album_browses_.take(result);
|
||||
|
||||
spotify_pb::SpotifyMessage message;
|
||||
spotify_pb::BrowseAlbumResponse* msg = message.mutable_browse_album_response();
|
||||
pb::spotify::Message message;
|
||||
pb::spotify::BrowseAlbumResponse* msg = message.mutable_browse_album_response();
|
||||
|
||||
msg->set_uri(DataCommaSizeFromQString(uri));
|
||||
|
||||
|
@ -961,6 +959,12 @@ void SpotifyClient::AlbumBrowseComplete(sp_albumbrowse* result, void* userdata)
|
|||
me->ConvertTrack(sp_albumbrowse_track(result, i), msg->add_track());
|
||||
}
|
||||
|
||||
me->handler_->SendMessage(message);
|
||||
me->SendMessage(message);
|
||||
sp_albumbrowse_release(result);
|
||||
}
|
||||
|
||||
void SpotifyClient::SocketClosed() {
|
||||
AbstractMessageHandler<pb::spotify::Message>::SocketClosed();
|
||||
|
||||
qApp->exit();
|
||||
}
|
|
@ -23,6 +23,7 @@
|
|||
#define SPOTIFYCLIENT_H
|
||||
|
||||
#include "spotifymessages.pb.h"
|
||||
#include "core/messagehandler.h"
|
||||
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
|
@ -34,9 +35,8 @@ class QTimer;
|
|||
|
||||
class MediaPipeline;
|
||||
class ResponseMessage;
|
||||
class SpotifyMessageHandler;
|
||||
|
||||
class SpotifyClient : public QObject {
|
||||
class SpotifyClient : public AbstractMessageHandler<pb::spotify::Message> {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
@ -48,13 +48,16 @@ public:
|
|||
|
||||
void Init(quint16 port);
|
||||
|
||||
protected:
|
||||
void MessageArrived(const pb::spotify::Message& message);
|
||||
void SocketClosed();
|
||||
|
||||
private slots:
|
||||
void HandleMessage(const spotify_pb::SpotifyMessage& message);
|
||||
void ProcessEvents();
|
||||
|
||||
private:
|
||||
void SendLoginCompleted(bool success, const QString& error,
|
||||
spotify_pb::LoginResponse_Error error_code);
|
||||
pb::spotify::LoginResponse_Error error_code);
|
||||
void SendPlaybackError(const QString& error);
|
||||
void SendSearchResponse(sp_search* result);
|
||||
|
||||
|
@ -103,35 +106,35 @@ private:
|
|||
static void SP_CALLCONV AlbumBrowseComplete(sp_albumbrowse* result, void* userdata);
|
||||
|
||||
// Request handlers.
|
||||
void Login(const spotify_pb::LoginRequest& req);
|
||||
void Search(const spotify_pb::SearchRequest& req);
|
||||
void LoadPlaylist(const spotify_pb::LoadPlaylistRequest& req);
|
||||
void SyncPlaylist(const spotify_pb::SyncPlaylistRequest& req);
|
||||
void StartPlayback(const spotify_pb::PlaybackRequest& req);
|
||||
void Login(const pb::spotify::LoginRequest& req);
|
||||
void Search(const pb::spotify::SearchRequest& req);
|
||||
void LoadPlaylist(const pb::spotify::LoadPlaylistRequest& req);
|
||||
void SyncPlaylist(const pb::spotify::SyncPlaylistRequest& req);
|
||||
void StartPlayback(const pb::spotify::PlaybackRequest& req);
|
||||
void Seek(qint64 offset_bytes);
|
||||
void LoadImage(const QString& id_b64);
|
||||
void BrowseAlbum(const QString& uri);
|
||||
void SetPlaybackSettings(const spotify_pb::PlaybackSettings& req);
|
||||
void SetPlaybackSettings(const pb::spotify::PlaybackSettings& req);
|
||||
|
||||
void SendPlaylistList();
|
||||
|
||||
void ConvertTrack(sp_track* track, spotify_pb::Track* pb);
|
||||
void ConvertAlbum(sp_album* album, spotify_pb::Track* pb);
|
||||
void ConvertAlbumBrowse(sp_albumbrowse* browse, spotify_pb::Track* pb);
|
||||
void ConvertTrack(sp_track* track, pb::spotify::Track* pb);
|
||||
void ConvertAlbum(sp_album* album, pb::spotify::Track* pb);
|
||||
void ConvertAlbumBrowse(sp_albumbrowse* browse, pb::spotify::Track* pb);
|
||||
|
||||
// Gets the appropriate sp_playlist* but does not load it.
|
||||
sp_playlist* GetPlaylist(spotify_pb::PlaylistType type, int user_index);
|
||||
sp_playlist* GetPlaylist(pb::spotify::PlaylistType type, int user_index);
|
||||
|
||||
private:
|
||||
struct PendingLoadPlaylist {
|
||||
spotify_pb::LoadPlaylistRequest request_;
|
||||
pb::spotify::LoadPlaylistRequest request_;
|
||||
sp_playlist* playlist_;
|
||||
QList<sp_track*> tracks_;
|
||||
bool offline_sync;
|
||||
};
|
||||
|
||||
struct PendingPlaybackRequest {
|
||||
spotify_pb::PlaybackRequest request_;
|
||||
pb::spotify::PlaybackRequest request_;
|
||||
sp_link* link_;
|
||||
sp_track* track_;
|
||||
|
||||
|
@ -150,12 +153,11 @@ private:
|
|||
void TryPlaybackAgain(const PendingPlaybackRequest& req);
|
||||
void TryImageAgain(sp_image* image);
|
||||
int GetDownloadProgress(sp_playlist* playlist);
|
||||
void SendDownloadProgress(spotify_pb::PlaylistType type, int index, int download_progress);
|
||||
void SendDownloadProgress(pb::spotify::PlaylistType type, int index, int download_progress);
|
||||
|
||||
QByteArray api_key_;
|
||||
|
||||
QTcpSocket* protocol_socket_;
|
||||
SpotifyMessageHandler* handler_;
|
||||
|
||||
sp_session_config spotify_config_;
|
||||
sp_session_callbacks spotify_callbacks_;
|
||||
|
@ -170,7 +172,7 @@ private:
|
|||
QList<PendingPlaybackRequest> pending_playback_requests_;
|
||||
QList<PendingImageRequest> pending_image_requests_;
|
||||
QMap<sp_image*, int> image_callbacks_registered_;
|
||||
QMap<sp_search*, spotify_pb::SearchRequest> pending_searches_;
|
||||
QMap<sp_search*, pb::spotify::SearchRequest> pending_searches_;
|
||||
QMap<sp_albumbrowse*, QString> pending_album_browses_;
|
||||
|
||||
QMap<sp_search*, QList<sp_albumbrowse*> > pending_search_album_browses_;
|
|
@ -0,0 +1,45 @@
|
|||
include_directories(${PROTOBUF_INCLUDE_DIRS})
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
|
||||
include_directories(${CMAKE_CURRENT_BINARY_DIR})
|
||||
include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-common)
|
||||
include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-tagreader)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src)
|
||||
|
||||
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
|
||||
|
||||
set(SOURCES
|
||||
fmpsparser.cpp
|
||||
main.cpp
|
||||
tagreaderworker.cpp
|
||||
)
|
||||
|
||||
set(HEADERS
|
||||
)
|
||||
|
||||
qt4_wrap_cpp(MOC ${HEADERS})
|
||||
|
||||
add_executable(clementine-tagreader
|
||||
${SOURCES}
|
||||
${MOC}
|
||||
)
|
||||
|
||||
target_link_libraries(clementine-tagreader
|
||||
${TAGLIB_LIBRARIES}
|
||||
${QT_QTCORE_LIBRARY}
|
||||
${QT_QTNETWORK_LIBRARY}
|
||||
libclementine-common
|
||||
libclementine-tagreader
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
target_link_libraries(clementine-tagreader
|
||||
/System/Library/Frameworks/Foundation.framework
|
||||
)
|
||||
endif(APPLE)
|
||||
|
||||
if(NOT APPLE)
|
||||
# macdeploy.py takes care of this on mac
|
||||
install(TARGETS clementine-tagreader
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
||||
endif(NOT APPLE)
|
|
@ -0,0 +1,57 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "tagreaderworker.h"
|
||||
#include "core/encoding.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QLocalSocket>
|
||||
#include <QStringList>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
QCoreApplication a(argc, argv);
|
||||
QStringList args(a.arguments());
|
||||
|
||||
if (args.count() != 2) {
|
||||
std::cerr << "This program is used internally by Clementine to parse tags in music files\n"
|
||||
"without exposing the whole application to crashes caused by malformed\n"
|
||||
"files. It is not meant to be run on its own.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
logging::Init();
|
||||
qLog(Info) << "TagReader worker connecting to" << args[1];
|
||||
|
||||
// Detect technically invalid usage of non-ASCII in ID3v1 tags.
|
||||
UniversalEncodingHandler handler;
|
||||
TagLib::ID3v1::Tag::setStringHandler(&handler);
|
||||
|
||||
// Connect to the parent process.
|
||||
QLocalSocket socket;
|
||||
socket.connectToServer(args[1]);
|
||||
if (!socket.waitForConnected(2000)) {
|
||||
std::cerr << "Failed to connect to the parent process.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
TagReaderWorker worker(&socket);
|
||||
|
||||
return a.exec();
|
||||
}
|
|
@ -0,0 +1,558 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "fmpsparser.h"
|
||||
#include "tagreaderworker.h"
|
||||
#include "core/encoding.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/timeconstants.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QFileInfo>
|
||||
#include <QTextCodec>
|
||||
#include <QUrl>
|
||||
|
||||
#include <aifffile.h>
|
||||
#include <asffile.h>
|
||||
#include <attachedpictureframe.h>
|
||||
#include <commentsframe.h>
|
||||
#include <fileref.h>
|
||||
#include <flacfile.h>
|
||||
#include <id3v2tag.h>
|
||||
#include <mp4file.h>
|
||||
#include <mp4tag.h>
|
||||
#include <mpcfile.h>
|
||||
#include <mpegfile.h>
|
||||
#include <oggfile.h>
|
||||
#include <oggflacfile.h>
|
||||
#include <speexfile.h>
|
||||
#include <tag.h>
|
||||
#include <textidentificationframe.h>
|
||||
#include <trueaudiofile.h>
|
||||
#include <tstring.h>
|
||||
#include <vorbisfile.h>
|
||||
#include <wavfile.h>
|
||||
|
||||
#include <boost/scoped_ptr.hpp>
|
||||
#include <sys/stat.h>
|
||||
|
||||
// Taglib added support for FLAC pictures in 1.7.0
|
||||
#if (TAGLIB_MAJOR_VERSION > 1) || (TAGLIB_MAJOR_VERSION == 1 && TAGLIB_MINOR_VERSION >= 7)
|
||||
# define TAGLIB_HAS_FLAC_PICTURELIST
|
||||
#endif
|
||||
|
||||
|
||||
using boost::scoped_ptr;
|
||||
|
||||
|
||||
class FileRefFactory {
|
||||
public:
|
||||
virtual ~FileRefFactory() {}
|
||||
virtual TagLib::FileRef* GetFileRef(const QString& filename) = 0;
|
||||
};
|
||||
|
||||
class TagLibFileRefFactory : public FileRefFactory {
|
||||
public:
|
||||
virtual TagLib::FileRef* GetFileRef(const QString& filename) {
|
||||
#ifdef Q_OS_WIN32
|
||||
return new TagLib::FileRef(filename.toStdWString().c_str());
|
||||
#else
|
||||
return new TagLib::FileRef(QFile::encodeName(filename).constData());
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
TagLib::String StdStringToTaglibString(const std::string& s) {
|
||||
return TagLib::String(s.c_str(), TagLib::String::UTF8);
|
||||
}
|
||||
|
||||
TagLib::String QStringToTaglibString(const QString& s) {
|
||||
return TagLib::String(s.toUtf8().constData(), TagLib::String::UTF8);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
TagReaderWorker::TagReaderWorker(QIODevice* socket, QObject* parent)
|
||||
: AbstractMessageHandler<pb::tagreader::Message>(socket, parent),
|
||||
factory_(new TagLibFileRefFactory),
|
||||
kEmbeddedCover("(embedded)")
|
||||
{
|
||||
}
|
||||
|
||||
void TagReaderWorker::MessageArrived(const pb::tagreader::Message& message) {
|
||||
pb::tagreader::Message reply;
|
||||
|
||||
if (message.has_read_file_request()) {
|
||||
ReadFile(QStringFromStdString(message.read_file_request().filename()),
|
||||
reply.mutable_read_file_response()->mutable_metadata());
|
||||
} else if (message.has_save_file_request()) {
|
||||
reply.mutable_save_file_response()->set_success(
|
||||
SaveFile(QStringFromStdString(message.save_file_request().filename()),
|
||||
message.save_file_request().metadata()));
|
||||
} else if (message.has_is_media_file_request()) {
|
||||
reply.mutable_is_media_file_response()->set_success(
|
||||
IsMediaFile(QStringFromStdString(message.is_media_file_request().filename())));
|
||||
} else if (message.has_load_embedded_art_request()) {
|
||||
QByteArray data = LoadEmbeddedArt(
|
||||
QStringFromStdString(message.load_embedded_art_request().filename()));
|
||||
reply.mutable_load_embedded_art_response()->set_data(
|
||||
data.constData(), data.size());
|
||||
}
|
||||
|
||||
SendReply(message, &reply);
|
||||
}
|
||||
|
||||
void TagReaderWorker::ReadFile(const QString& filename,
|
||||
pb::tagreader::SongMetadata* song) const {
|
||||
const QByteArray url(QUrl::fromLocalFile(filename).toEncoded());
|
||||
const QFileInfo info(filename);
|
||||
|
||||
qLog(Debug) << "Reading tags from" << filename;
|
||||
|
||||
song->set_basefilename(DataCommaSizeFromQString(info.fileName()));
|
||||
song->set_url(url.constData(), url.size());
|
||||
song->set_filesize(info.size());
|
||||
song->set_mtime(info.lastModified().toTime_t());
|
||||
song->set_ctime(info.created().toTime_t());
|
||||
|
||||
scoped_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
|
||||
if(fileref->isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This is single byte encoding, therefore can't be CJK.
|
||||
UniversalEncodingHandler detector(NS_FILTER_NON_CJK);
|
||||
|
||||
TagLib::Tag* tag = fileref->tag();
|
||||
QTextCodec* codec = NULL;
|
||||
if (tag) {
|
||||
TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(fileref->file());
|
||||
if (file && (file->ID3v2Tag() || file->ID3v1Tag())) {
|
||||
codec = detector.Guess(*fileref);
|
||||
}
|
||||
if (codec &&
|
||||
codec->name() != "UTF-8" &&
|
||||
codec->name() != "ISO-8859-1") {
|
||||
// Mark tags where we detect an unusual codec as suspicious.
|
||||
song->set_suspicious_tags(true);
|
||||
}
|
||||
|
||||
|
||||
Decode(tag->title(), NULL, song->mutable_title());
|
||||
Decode(tag->artist(), NULL, song->mutable_artist());
|
||||
Decode(tag->album(), NULL, song->mutable_album());
|
||||
Decode(tag->genre(), NULL, song->mutable_genre());
|
||||
song->set_year(tag->year());
|
||||
song->set_track(tag->track());
|
||||
song->set_valid(true);
|
||||
}
|
||||
|
||||
QString disc;
|
||||
QString compilation;
|
||||
if (TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
|
||||
if (file->ID3v2Tag()) {
|
||||
const TagLib::ID3v2::FrameListMap& map = file->ID3v2Tag()->frameListMap();
|
||||
|
||||
if (!map["TPOS"].isEmpty())
|
||||
disc = TStringToQString(map["TPOS"].front()->toString()).trimmed();
|
||||
|
||||
if (!map["TBPM"].isEmpty())
|
||||
song->set_bpm(TStringToQString(map["TBPM"].front()->toString()).trimmed().toFloat());
|
||||
|
||||
if (!map["TCOM"].isEmpty())
|
||||
Decode(map["TCOM"].front()->toString(), NULL, song->mutable_composer());
|
||||
|
||||
if (!map["TPE2"].isEmpty()) // non-standard: Apple, Microsoft
|
||||
Decode(map["TPE2"].front()->toString(), NULL, song->mutable_albumartist());
|
||||
|
||||
if (!map["TCMP"].isEmpty())
|
||||
compilation = TStringToQString(map["TCMP"].front()->toString()).trimmed();
|
||||
|
||||
if (!map["APIC"].isEmpty())
|
||||
song->set_art_automatic(kEmbeddedCover);
|
||||
|
||||
// Find a suitable comment tag. For now we ignore iTunNORM comments.
|
||||
for (int i=0 ; i<map["COMM"].size() ; ++i) {
|
||||
const TagLib::ID3v2::CommentsFrame* frame =
|
||||
dynamic_cast<const TagLib::ID3v2::CommentsFrame*>(map["COMM"][i]);
|
||||
|
||||
if (frame && TStringToQString(frame->description()) != "iTunNORM") {
|
||||
Decode(frame->text(), NULL, song->mutable_comment());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse FMPS frames
|
||||
for (int i=0 ; i<map["TXXX"].size() ; ++i) {
|
||||
const TagLib::ID3v2::UserTextIdentificationFrame* frame =
|
||||
dynamic_cast<const TagLib::ID3v2::UserTextIdentificationFrame*>(map["TXXX"][i]);
|
||||
|
||||
if (frame && frame->description().startsWith("FMPS_")) {
|
||||
ParseFMPSFrame(TStringToQString(frame->description()),
|
||||
TStringToQString(frame->fieldList()[1]),
|
||||
song);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (TagLib::Ogg::Vorbis::File* file = dynamic_cast<TagLib::Ogg::Vorbis::File*>(fileref->file())) {
|
||||
if (file->tag()) {
|
||||
ParseOggTag(file->tag()->fieldListMap(), NULL, &disc, &compilation, song);
|
||||
}
|
||||
Decode(tag->comment(), NULL, song->mutable_comment());
|
||||
} else if (TagLib::FLAC::File* file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
|
||||
if ( file->xiphComment() ) {
|
||||
ParseOggTag(file->xiphComment()->fieldListMap(), NULL, &disc, &compilation, song);
|
||||
#ifdef TAGLIB_HAS_FLAC_PICTURELIST
|
||||
if (!file->pictureList().isEmpty()) {
|
||||
song->set_art_automatic(kEmbeddedCover);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Decode(tag->comment(), NULL, song->mutable_comment());
|
||||
} else if (TagLib::MP4::File* file = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
|
||||
if (file->tag()) {
|
||||
TagLib::MP4::Tag* mp4_tag = file->tag();
|
||||
const TagLib::MP4::ItemListMap& items = mp4_tag->itemListMap();
|
||||
TagLib::MP4::ItemListMap::ConstIterator it = items.find("aART");
|
||||
if (it != items.end()) {
|
||||
TagLib::StringList album_artists = it->second.toStringList();
|
||||
if (!album_artists.isEmpty()) {
|
||||
Decode(album_artists.front(), NULL, song->mutable_albumartist());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (tag) {
|
||||
Decode(tag->comment(), NULL, song->mutable_comment());
|
||||
}
|
||||
|
||||
if (!disc.isEmpty()) {
|
||||
const int i = disc.indexOf('/');
|
||||
if (i != -1) {
|
||||
// disc.right( i ).toInt() is total number of discs, we don't use this at the moment
|
||||
song->set_disc(disc.left(i).toInt());
|
||||
} else {
|
||||
song->set_disc(disc.toInt());
|
||||
}
|
||||
}
|
||||
|
||||
if (compilation.isEmpty()) {
|
||||
// well, it wasn't set, but if the artist is VA assume it's a compilation
|
||||
if (QStringFromStdString(song->artist()).toLower() == "various artists") {
|
||||
song->set_compilation(true);
|
||||
}
|
||||
} else {
|
||||
song->set_compilation(compilation.toInt() == 1);
|
||||
}
|
||||
|
||||
if (fileref->audioProperties()) {
|
||||
song->set_bitrate(fileref->audioProperties()->bitrate());
|
||||
song->set_samplerate(fileref->audioProperties()->sampleRate());
|
||||
song->set_length_nanosec(fileref->audioProperties()->length() * kNsecPerSec);
|
||||
}
|
||||
|
||||
// Get the filetype if we can
|
||||
song->set_type(GuessFileType(fileref.get()));
|
||||
|
||||
// Set integer fields to -1 if they're not valid
|
||||
#define SetDefault(field) if (song->field() <= 0) { song->set_##field(-1); }
|
||||
SetDefault(track);
|
||||
SetDefault(disc);
|
||||
SetDefault(bpm);
|
||||
SetDefault(year);
|
||||
SetDefault(bitrate);
|
||||
SetDefault(samplerate);
|
||||
SetDefault(lastplayed);
|
||||
SetDefault(rating);
|
||||
#undef SetDefault
|
||||
}
|
||||
|
||||
void TagReaderWorker::Decode(const TagLib::String& tag, const QTextCodec* codec,
|
||||
std::string* output) {
|
||||
QString tmp;
|
||||
|
||||
if (codec && tag.isLatin1()) { // Never override UTF-8.
|
||||
const std::string fixed = QString::fromUtf8(tag.toCString(true)).toStdString();
|
||||
tmp = codec->toUnicode(fixed.c_str()).trimmed();
|
||||
} else {
|
||||
tmp = TStringToQString(tag).trimmed();
|
||||
}
|
||||
|
||||
output->assign(DataCommaSizeFromQString(tmp));
|
||||
}
|
||||
|
||||
void TagReaderWorker::Decode(const QString& tag, const QTextCodec* codec,
|
||||
std::string* output) {
|
||||
if (!codec) {
|
||||
output->assign(DataCommaSizeFromQString(tag));
|
||||
} else {
|
||||
const QString decoded(codec->toUnicode(tag.toUtf8()));
|
||||
output->assign(DataCommaSizeFromQString(decoded));
|
||||
}
|
||||
}
|
||||
|
||||
void TagReaderWorker::ParseFMPSFrame(const QString& name, const QString& value,
|
||||
pb::tagreader::SongMetadata* song) const {
|
||||
FMPSParser parser;
|
||||
if (!parser.Parse(value) || parser.is_empty())
|
||||
return;
|
||||
|
||||
QVariant var;
|
||||
if (name == "FMPS_Rating") {
|
||||
var = parser.result()[0][0];
|
||||
if (var.type() == QVariant::Double) {
|
||||
song->set_rating(var.toDouble());
|
||||
}
|
||||
} else if (name == "FMPS_Rating_User") {
|
||||
// Take a user rating only if there's no rating already set
|
||||
if (song->rating() == -1 && parser.result()[0].count() >= 2) {
|
||||
var = parser.result()[0][1];
|
||||
if (var.type() == QVariant::Double) {
|
||||
song->set_rating(var.toDouble());
|
||||
}
|
||||
}
|
||||
} else if (name == "FMPS_PlayCount") {
|
||||
var = parser.result()[0][0];
|
||||
if (var.type() == QVariant::Double) {
|
||||
song->set_playcount(var.toDouble());
|
||||
}
|
||||
} else if (name == "FMPS_PlayCount_User") {
|
||||
// Take a user rating only if there's no playcount already set
|
||||
if (song->rating() == -1 && parser.result()[0].count() >= 2) {
|
||||
var = parser.result()[0][1];
|
||||
if (var.type() == QVariant::Double) {
|
||||
song->set_playcount(var.toDouble());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TagReaderWorker::ParseOggTag(const TagLib::Ogg::FieldListMap& map,
|
||||
const QTextCodec* codec,
|
||||
QString* disc, QString* compilation,
|
||||
pb::tagreader::SongMetadata* song) const {
|
||||
if (!map["COMPOSER"].isEmpty())
|
||||
Decode(map["COMPOSER"].front(), codec, song->mutable_composer());
|
||||
|
||||
if (!map["ALBUMARTIST"].isEmpty()) {
|
||||
Decode(map["ALBUMARTIST"].front(), codec, song->mutable_albumartist());
|
||||
} else if (!map["ALBUM ARTIST"].isEmpty()) {
|
||||
Decode(map["ALBUM ARTIST"].front(), codec, song->mutable_albumartist());
|
||||
}
|
||||
|
||||
if (!map["BPM"].isEmpty() )
|
||||
song->set_bpm(TStringToQString( map["BPM"].front() ).trimmed().toFloat());
|
||||
|
||||
if (!map["DISCNUMBER"].isEmpty() )
|
||||
*disc = TStringToQString( map["DISCNUMBER"].front() ).trimmed();
|
||||
|
||||
if (!map["COMPILATION"].isEmpty() )
|
||||
*compilation = TStringToQString( map["COMPILATION"].front() ).trimmed();
|
||||
|
||||
if (!map["COVERART"].isEmpty())
|
||||
song->set_art_automatic(kEmbeddedCover);
|
||||
}
|
||||
|
||||
pb::tagreader::SongMetadata_Type TagReaderWorker::GuessFileType(
|
||||
TagLib::FileRef* fileref) const {
|
||||
#ifdef TAGLIB_WITH_ASF
|
||||
if (dynamic_cast<TagLib::ASF::File*>(fileref->file()))
|
||||
return pb::tagreader::SongMetadata_Type_ASF;
|
||||
#endif
|
||||
if (dynamic_cast<TagLib::FLAC::File*>(fileref->file()))
|
||||
return pb::tagreader::SongMetadata_Type_FLAC;
|
||||
#ifdef TAGLIB_WITH_MP4
|
||||
if (dynamic_cast<TagLib::MP4::File*>(fileref->file()))
|
||||
return pb::tagreader::SongMetadata_Type_MP4;
|
||||
#endif
|
||||
if (dynamic_cast<TagLib::MPC::File*>(fileref->file()))
|
||||
return pb::tagreader::SongMetadata_Type_MPC;
|
||||
if (dynamic_cast<TagLib::MPEG::File*>(fileref->file()))
|
||||
return pb::tagreader::SongMetadata_Type_MPEG;
|
||||
if (dynamic_cast<TagLib::Ogg::FLAC::File*>(fileref->file()))
|
||||
return pb::tagreader::SongMetadata_Type_OGGFLAC;
|
||||
if (dynamic_cast<TagLib::Ogg::Speex::File*>(fileref->file()))
|
||||
return pb::tagreader::SongMetadata_Type_OGGSPEEX;
|
||||
if (dynamic_cast<TagLib::Ogg::Vorbis::File*>(fileref->file()))
|
||||
return pb::tagreader::SongMetadata_Type_OGGVORBIS;
|
||||
if (dynamic_cast<TagLib::RIFF::AIFF::File*>(fileref->file()))
|
||||
return pb::tagreader::SongMetadata_Type_AIFF;
|
||||
if (dynamic_cast<TagLib::RIFF::WAV::File*>(fileref->file()))
|
||||
return pb::tagreader::SongMetadata_Type_WAV;
|
||||
if (dynamic_cast<TagLib::TrueAudio::File*>(fileref->file()))
|
||||
return pb::tagreader::SongMetadata_Type_TRUEAUDIO;
|
||||
|
||||
return pb::tagreader::SongMetadata_Type_UNKNOWN;
|
||||
}
|
||||
|
||||
bool TagReaderWorker::SaveFile(const QString& filename,
|
||||
const pb::tagreader::SongMetadata& song) const {
|
||||
if (filename.isNull())
|
||||
return false;
|
||||
|
||||
qLog(Debug) << "Saving tags to" << filename;
|
||||
|
||||
scoped_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
|
||||
|
||||
if (!fileref || fileref->isNull()) // The file probably doesn't exist
|
||||
return false;
|
||||
|
||||
fileref->tag()->setTitle(StdStringToTaglibString(song.title()));
|
||||
fileref->tag()->setArtist(StdStringToTaglibString(song.artist()));
|
||||
fileref->tag()->setAlbum(StdStringToTaglibString(song.album()));
|
||||
fileref->tag()->setGenre(StdStringToTaglibString(song.genre()));
|
||||
fileref->tag()->setComment(StdStringToTaglibString(song.comment()));
|
||||
fileref->tag()->setYear(song.year());
|
||||
fileref->tag()->setTrack(song.track());
|
||||
|
||||
if (TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
|
||||
TagLib::ID3v2::Tag* tag = file->ID3v2Tag(true);
|
||||
SetTextFrame("TPOS", song.disc() <= 0 -1 ? QString() : QString::number(song.disc()), tag);
|
||||
SetTextFrame("TBPM", song.bpm() <= 0 -1 ? QString() : QString::number(song.bpm()), tag);
|
||||
SetTextFrame("TCOM", song.composer(), tag);
|
||||
SetTextFrame("TPE2", song.albumartist(), tag);
|
||||
SetTextFrame("TCMP", std::string(song.compilation() ? "1" : "0"), tag);
|
||||
}
|
||||
else if (TagLib::Ogg::Vorbis::File* file = dynamic_cast<TagLib::Ogg::Vorbis::File*>(fileref->file())) {
|
||||
TagLib::Ogg::XiphComment* tag = file->tag();
|
||||
tag->addField("COMPOSER", StdStringToTaglibString(song.composer()), true);
|
||||
tag->addField("BPM", QStringToTaglibString(song.bpm() <= 0 -1 ? QString() : QString::number(song.bpm())), true);
|
||||
tag->addField("DISCNUMBER", QStringToTaglibString(song.disc() <= 0 -1 ? QString() : QString::number(song.disc())), true);
|
||||
tag->addField("COMPILATION", StdStringToTaglibString(song.compilation() ? "1" : "0"), true);
|
||||
}
|
||||
else if (TagLib::FLAC::File* file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
|
||||
TagLib::Ogg::XiphComment* tag = file->xiphComment();
|
||||
tag->addField("COMPOSER", StdStringToTaglibString(song.composer()), true);
|
||||
tag->addField("BPM", QStringToTaglibString(song.bpm() <= 0 -1 ? QString() : QString::number(song.bpm())), true);
|
||||
tag->addField("DISCNUMBER", QStringToTaglibString(song.disc() <= 0 -1 ? QString() : QString::number(song.disc())), true);
|
||||
tag->addField("COMPILATION", StdStringToTaglibString(song.compilation() ? "1" : "0"), true);
|
||||
}
|
||||
|
||||
bool ret = fileref->save();
|
||||
#ifdef Q_OS_LINUX
|
||||
if (ret) {
|
||||
// Linux: inotify doesn't seem to notice the change to the file unless we
|
||||
// change the timestamps as well. (this is what touch does)
|
||||
utimensat(0, QFile::encodeName(filename).constData(), NULL, 0);
|
||||
}
|
||||
#endif // Q_OS_LINUX
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void TagReaderWorker::SetTextFrame(const char* id, const QString& value,
|
||||
TagLib::ID3v2::Tag* tag) const {
|
||||
const QByteArray utf8(value.toUtf8());
|
||||
SetTextFrame(id, std::string(utf8.constData(), utf8.length()), tag);
|
||||
}
|
||||
|
||||
void TagReaderWorker::SetTextFrame(const char* id, const std::string& value,
|
||||
TagLib::ID3v2::Tag* tag) const {
|
||||
TagLib::ByteVector id_vector(id);
|
||||
|
||||
// Remove the frame if it already exists
|
||||
while (tag->frameListMap().contains(id_vector) &&
|
||||
tag->frameListMap()[id_vector].size() != 0) {
|
||||
tag->removeFrame(tag->frameListMap()[id_vector].front());
|
||||
}
|
||||
|
||||
// Create and add a new frame
|
||||
TagLib::ID3v2::TextIdentificationFrame* frame =
|
||||
new TagLib::ID3v2::TextIdentificationFrame(id_vector,
|
||||
TagLib::String::UTF8);
|
||||
frame->setText(StdStringToTaglibString(value));
|
||||
tag->addFrame(frame);
|
||||
}
|
||||
|
||||
bool TagReaderWorker::IsMediaFile(const QString& filename) const {
|
||||
qLog(Debug) << "Checking for valid file" << filename;
|
||||
|
||||
scoped_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
|
||||
return !fileref->isNull() && fileref->tag();
|
||||
}
|
||||
|
||||
QByteArray TagReaderWorker::LoadEmbeddedArt(const QString& filename) const {
|
||||
if (filename.isEmpty())
|
||||
return QByteArray();
|
||||
|
||||
qLog(Debug) << "Loading art from" << filename;
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
TagLib::FileRef ref(filename.toStdWString().c_str());
|
||||
#else
|
||||
TagLib::FileRef ref(QFile::encodeName(filename).constData());
|
||||
#endif
|
||||
|
||||
if (ref.isNull() || !ref.file())
|
||||
return QByteArray();
|
||||
|
||||
// MP3
|
||||
TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(ref.file());
|
||||
if (file && file->ID3v2Tag()) {
|
||||
TagLib::ID3v2::FrameList apic_frames = file->ID3v2Tag()->frameListMap()["APIC"];
|
||||
if (apic_frames.isEmpty())
|
||||
return QByteArray();
|
||||
|
||||
TagLib::ID3v2::AttachedPictureFrame* pic =
|
||||
static_cast<TagLib::ID3v2::AttachedPictureFrame*>(apic_frames.front());
|
||||
|
||||
return QByteArray((const char*) pic->picture().data(), pic->picture().size());
|
||||
}
|
||||
|
||||
// Ogg vorbis/speex
|
||||
TagLib::Ogg::XiphComment* xiph_comment =
|
||||
dynamic_cast<TagLib::Ogg::XiphComment*>(ref.file()->tag());
|
||||
|
||||
if (xiph_comment) {
|
||||
TagLib::Ogg::FieldListMap map = xiph_comment->fieldListMap();
|
||||
|
||||
// Ogg lacks a definitive standard for embedding cover art, but it seems
|
||||
// b64 encoding a field called COVERART is the general convention
|
||||
if (!map.contains("COVERART"))
|
||||
return QByteArray();
|
||||
|
||||
return QByteArray::fromBase64(map["COVERART"].toString().toCString());
|
||||
}
|
||||
|
||||
#ifdef TAGLIB_HAS_FLAC_PICTURELIST
|
||||
// Flac
|
||||
TagLib::FLAC::File* flac_file = dynamic_cast<TagLib::FLAC::File*>(ref.file());
|
||||
if (flac_file && flac_file->xiphComment()) {
|
||||
TagLib::List<TagLib::FLAC::Picture*> pics = flac_file->pictureList();
|
||||
if (!pics.isEmpty()) {
|
||||
// Use the first picture in the file - this could be made cleverer and
|
||||
// pick the front cover if it's present.
|
||||
|
||||
std::list<TagLib::FLAC::Picture*>::iterator it = pics.begin();
|
||||
TagLib::FLAC::Picture* picture = *it;
|
||||
|
||||
return QByteArray(picture->data().data(), picture->data().size());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
void TagReaderWorker::SocketClosed() {
|
||||
AbstractMessageHandler<pb::tagreader::Message>::SocketClosed();
|
||||
|
||||
qApp->exit();
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef TAGREADERWORKER_H
|
||||
#define TAGREADERWORKER_H
|
||||
|
||||
#include "tagreadermessages.pb.h"
|
||||
#include "core/messagehandler.h"
|
||||
|
||||
#include <taglib/xiphcomment.h>
|
||||
|
||||
|
||||
namespace TagLib {
|
||||
class FileRef;
|
||||
class String;
|
||||
|
||||
namespace ID3v2 {
|
||||
class Tag;
|
||||
}
|
||||
}
|
||||
|
||||
class FileRefFactory;
|
||||
|
||||
class TagReaderWorker : public AbstractMessageHandler<pb::tagreader::Message> {
|
||||
public:
|
||||
TagReaderWorker(QIODevice* socket, QObject* parent = NULL);
|
||||
|
||||
protected:
|
||||
void MessageArrived(const pb::tagreader::Message& message);
|
||||
void SocketClosed();
|
||||
|
||||
private:
|
||||
void ReadFile(const QString& filename, pb::tagreader::SongMetadata* song) const;
|
||||
bool SaveFile(const QString& filename, const pb::tagreader::SongMetadata& song) const;
|
||||
bool IsMediaFile(const QString& filename) const;
|
||||
QByteArray LoadEmbeddedArt(const QString& filename) const;
|
||||
|
||||
static void Decode(const TagLib::String& tag, const QTextCodec* codec,
|
||||
std::string* output);
|
||||
static void Decode(const QString& tag, const QTextCodec* codec,
|
||||
std::string* output);
|
||||
|
||||
void ParseFMPSFrame(const QString& name, const QString& value,
|
||||
pb::tagreader::SongMetadata* song) const;
|
||||
void ParseOggTag(const TagLib::Ogg::FieldListMap& map,
|
||||
const QTextCodec* codec,
|
||||
QString* disc, QString* compilation,
|
||||
pb::tagreader::SongMetadata* song) const;
|
||||
pb::tagreader::SongMetadata_Type GuessFileType(TagLib::FileRef* fileref) const;
|
||||
|
||||
void SetTextFrame(const char* id, const QString& value,
|
||||
TagLib::ID3v2::Tag* tag) const;
|
||||
void SetTextFrame(const char* id, const std::string& value,
|
||||
TagLib::ID3v2::Tag* tag) const;
|
||||
|
||||
private:
|
||||
FileRefFactory* factory_;
|
||||
|
||||
const std::string kEmbeddedCover;
|
||||
};
|
||||
|
||||
#endif // TAGREADERWORKER_H
|
|
@ -0,0 +1,41 @@
|
|||
include_directories(${PROTOBUF_INCLUDE_DIRS})
|
||||
include_directories(${CMAKE_CURRENT_BINARY_DIR})
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src)
|
||||
|
||||
set(SOURCES
|
||||
core/closure.cpp
|
||||
core/encoding.cpp
|
||||
core/logging.cpp
|
||||
core/messagehandler.cpp
|
||||
core/waitforsignal.cpp
|
||||
core/workerpool.cpp
|
||||
)
|
||||
|
||||
set(HEADERS
|
||||
core/closure.h
|
||||
core/messagehandler.h
|
||||
core/workerpool.h
|
||||
)
|
||||
|
||||
qt4_wrap_cpp(MOC ${HEADERS})
|
||||
|
||||
add_library(libclementine-common STATIC
|
||||
${SOURCES}
|
||||
${MOC}
|
||||
)
|
||||
|
||||
# Use protobuf-lite if it's available
|
||||
if(PROTOBUF_LITE_LIBRARY AND USE_PROTOBUF_LITE)
|
||||
set(protobuf ${PROTOBUF_LITE_LIBRARY})
|
||||
else(PROTOBUF_LITE_LIBRARY AND USE_PROTOBUF_LITE)
|
||||
set(protobuf ${PROTOBUF_LIBRARY})
|
||||
endif(PROTOBUF_LITE_LIBRARY AND USE_PROTOBUF_LITE)
|
||||
|
||||
target_link_libraries(libclementine-common
|
||||
${protobuf}
|
||||
${TAGLIB_LIBRARIES}
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
chardet
|
||||
)
|
||||
|
|
@ -19,24 +19,44 @@
|
|||
// compatible.
|
||||
|
||||
|
||||
#include "spotifymessages.pb.h"
|
||||
#include "spotifymessagehandler.h"
|
||||
#include "messagehandler.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
#include <QAbstractSocket>
|
||||
#include <QBuffer>
|
||||
#include <QLocalSocket>
|
||||
|
||||
SpotifyMessageHandler::SpotifyMessageHandler(QAbstractSocket* device, QObject* parent)
|
||||
_MessageHandlerBase::_MessageHandlerBase(QIODevice* device, QObject* parent)
|
||||
: QObject(parent),
|
||||
device_(device),
|
||||
device_(NULL),
|
||||
flush_abstract_socket_(NULL),
|
||||
flush_local_socket_(NULL),
|
||||
reading_protobuf_(false),
|
||||
expected_length_(0) {
|
||||
if (device) {
|
||||
SetDevice(device);
|
||||
}
|
||||
}
|
||||
|
||||
void _MessageHandlerBase::SetDevice(QIODevice* device) {
|
||||
device_ = device;
|
||||
|
||||
buffer_.open(QIODevice::ReadWrite);
|
||||
|
||||
connect(device, SIGNAL(readyRead()), SLOT(DeviceReadyRead()));
|
||||
|
||||
// Yeah I know.
|
||||
if (QAbstractSocket* socket = qobject_cast<QAbstractSocket*>(device)) {
|
||||
flush_abstract_socket_ = &QAbstractSocket::flush;
|
||||
connect(socket, SIGNAL(disconnected()), SLOT(SocketClosed()));
|
||||
} else if (QLocalSocket* socket = qobject_cast<QLocalSocket*>(device)) {
|
||||
flush_local_socket_ = &QLocalSocket::flush;
|
||||
connect(socket, SIGNAL(disconnected()), SLOT(SocketClosed()));
|
||||
} else {
|
||||
qFatal("Unsupported device type passed to _MessageHandlerBase");
|
||||
}
|
||||
}
|
||||
|
||||
void SpotifyMessageHandler::DeviceReadyRead() {
|
||||
void _MessageHandlerBase::DeviceReadyRead() {
|
||||
while (device_->bytesAvailable()) {
|
||||
if (!reading_protobuf_) {
|
||||
// Read the length of the next message
|
||||
|
@ -52,15 +72,12 @@ void SpotifyMessageHandler::DeviceReadyRead() {
|
|||
// Did we get everything?
|
||||
if (buffer_.size() == expected_length_) {
|
||||
// Parse the message
|
||||
spotify_pb::SpotifyMessage message;
|
||||
if (!message.ParseFromArray(buffer_.data().constData(), buffer_.size())) {
|
||||
if (!RawMessageArrived(buffer_.data())) {
|
||||
qLog(Error) << "Malformed protobuf message";
|
||||
device_->close();
|
||||
return;
|
||||
}
|
||||
|
||||
emit MessageArrived(message);
|
||||
|
||||
// Clear the buffer
|
||||
buffer_.close();
|
||||
buffer_.setData(QByteArray());
|
||||
|
@ -70,21 +87,37 @@ void SpotifyMessageHandler::DeviceReadyRead() {
|
|||
}
|
||||
}
|
||||
|
||||
void SpotifyMessageHandler::SendMessage(const spotify_pb::SpotifyMessage& message) {
|
||||
std::string data = message.SerializeAsString();
|
||||
WriteMessage(QByteArray(data.data(), data.size()));
|
||||
}
|
||||
|
||||
void SpotifyMessageHandler::SendMessageAsync(const spotify_pb::SpotifyMessage& message) {
|
||||
std::string data = message.SerializeAsString();
|
||||
metaObject()->invokeMethod(this, "WriteMessage", Qt::QueuedConnection,
|
||||
Q_ARG(QByteArray, QByteArray(data.data(), data.size())));
|
||||
}
|
||||
|
||||
void SpotifyMessageHandler::WriteMessage(const QByteArray& data) {
|
||||
void _MessageHandlerBase::WriteMessage(const QByteArray& data) {
|
||||
QDataStream s(device_);
|
||||
s << quint32(data.length());
|
||||
s.writeRawData(data.data(), data.length());
|
||||
|
||||
device_->flush();
|
||||
// Sorry.
|
||||
if (flush_abstract_socket_) {
|
||||
((static_cast<QAbstractSocket*>(device_))->*(flush_abstract_socket_))();
|
||||
} else if (flush_local_socket_) {
|
||||
((static_cast<QLocalSocket*>(device_))->*(flush_local_socket_))();
|
||||
}
|
||||
}
|
||||
|
||||
_MessageReplyBase::_MessageReplyBase(int id, QObject* parent)
|
||||
: QObject(parent),
|
||||
id_(id),
|
||||
finished_(false),
|
||||
success_(false)
|
||||
{
|
||||
}
|
||||
|
||||
bool _MessageReplyBase::WaitForFinished() {
|
||||
semaphore_.acquire();
|
||||
return success_;
|
||||
}
|
||||
|
||||
void _MessageReplyBase::Abort() {
|
||||
Q_ASSERT(!finished_);
|
||||
finished_ = true;
|
||||
success_ = false;
|
||||
|
||||
emit Finished(success_);
|
||||
semaphore_.release();
|
||||
}
|
|
@ -0,0 +1,283 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Note: this file is licensed under the Apache License instead of GPL because
|
||||
// it is used by the Spotify blob which links against libspotify and is not GPL
|
||||
// compatible.
|
||||
|
||||
|
||||
#ifndef MESSAGEHANDLER_H
|
||||
#define MESSAGEHANDLER_H
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QMap>
|
||||
#include <QMutex>
|
||||
#include <QMutexLocker>
|
||||
#include <QObject>
|
||||
#include <QSemaphore>
|
||||
#include <QThread>
|
||||
|
||||
class QAbstractSocket;
|
||||
class QIODevice;
|
||||
class QLocalSocket;
|
||||
|
||||
#define QStringFromStdString(x) \
|
||||
QString::fromUtf8(x.data(), x.size())
|
||||
#define DataCommaSizeFromQString(x) \
|
||||
x.toUtf8().constData(), x.toUtf8().length()
|
||||
|
||||
|
||||
// Base QObject for a reply future class that is returned immediately for
|
||||
// requests that will occur in the background. Similar to QNetworkReply.
|
||||
// Use MessageReply instead.
|
||||
class _MessageReplyBase : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
_MessageReplyBase(int id, QObject* parent = 0);
|
||||
|
||||
int id() const { return id_; }
|
||||
bool is_finished() const { return finished_; }
|
||||
bool is_successful() const { return success_; }
|
||||
|
||||
// Waits for the reply to finish by waiting on a semaphore. Never call this
|
||||
// from the MessageHandler's thread or it will block forever.
|
||||
// Returns true if the call was successful.
|
||||
bool WaitForFinished();
|
||||
|
||||
void Abort();
|
||||
|
||||
signals:
|
||||
void Finished(bool success);
|
||||
|
||||
protected:
|
||||
int id_;
|
||||
bool finished_;
|
||||
bool success_;
|
||||
|
||||
QSemaphore semaphore_;
|
||||
};
|
||||
|
||||
|
||||
// A reply future class that is returned immediately for requests that will
|
||||
// occur in the background. Similar to QNetworkReply.
|
||||
template <typename MessageType>
|
||||
class MessageReply : public _MessageReplyBase {
|
||||
public:
|
||||
MessageReply(int id, QObject* parent = 0);
|
||||
|
||||
const MessageType& message() const { return message_; }
|
||||
|
||||
void SetReply(const MessageType& message);
|
||||
|
||||
private:
|
||||
MessageType message_;
|
||||
};
|
||||
|
||||
|
||||
// Reads and writes uint32 length encoded protobufs to a socket.
|
||||
// This base QObject is separate from AbstractMessageHandler because moc can't
|
||||
// handle templated classes. Use AbstractMessageHandler instead.
|
||||
class _MessageHandlerBase : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// device can be NULL, in which case you must call SetDevice before writing
|
||||
// any messages.
|
||||
_MessageHandlerBase(QIODevice* device, QObject* parent);
|
||||
|
||||
void SetDevice(QIODevice* device);
|
||||
|
||||
protected slots:
|
||||
void WriteMessage(const QByteArray& data);
|
||||
void DeviceReadyRead();
|
||||
virtual void SocketClosed() {}
|
||||
|
||||
protected:
|
||||
virtual bool RawMessageArrived(const QByteArray& data) = 0;
|
||||
|
||||
protected:
|
||||
typedef bool (QAbstractSocket::*FlushAbstractSocket)();
|
||||
typedef bool (QLocalSocket::*FlushLocalSocket)();
|
||||
|
||||
QIODevice* device_;
|
||||
FlushAbstractSocket flush_abstract_socket_;
|
||||
FlushLocalSocket flush_local_socket_;
|
||||
|
||||
bool reading_protobuf_;
|
||||
quint32 expected_length_;
|
||||
QBuffer buffer_;
|
||||
};
|
||||
|
||||
|
||||
// Reads and writes uint32 length encoded MessageType messages to a socket.
|
||||
// You should subclass this and implement the MessageArrived(MessageType)
|
||||
// method.
|
||||
template <typename MessageType>
|
||||
class AbstractMessageHandler : public _MessageHandlerBase {
|
||||
public:
|
||||
AbstractMessageHandler(QIODevice* device, QObject* parent);
|
||||
|
||||
typedef MessageReply<MessageType> ReplyType;
|
||||
|
||||
// Serialises the message and writes it to the socket. This version MUST be
|
||||
// called from the thread in which the AbstractMessageHandler was created.
|
||||
void SendMessage(const MessageType& message);
|
||||
|
||||
// Serialises the message and writes it to the socket. This version may be
|
||||
// called from any thread.
|
||||
void SendMessageAsync(const MessageType& message);
|
||||
|
||||
// Creates a new reply future for the request with the next sequential ID,
|
||||
// and sets the request's ID to the ID of the reply. When a reply arrives
|
||||
// for this request the reply is triggered automatically and MessageArrived
|
||||
// is NOT called. Can be called from any thread.
|
||||
ReplyType* NewReply(MessageType* message);
|
||||
|
||||
// Same as NewReply, except the message is sent as well. Can be called from
|
||||
// any thread.
|
||||
ReplyType* SendMessageWithReply(MessageType* message);
|
||||
|
||||
// Sets the "id" field of reply to the same as the request, and sends the
|
||||
// reply on the socket. Used on the worker side.
|
||||
void SendReply(const MessageType& request, MessageType* reply);
|
||||
|
||||
protected:
|
||||
// Called when a message is received from the socket.
|
||||
virtual void MessageArrived(const MessageType& message) {}
|
||||
|
||||
// _MessageHandlerBase
|
||||
bool RawMessageArrived(const QByteArray& data);
|
||||
void SocketClosed();
|
||||
|
||||
private:
|
||||
QMutex mutex_;
|
||||
int next_id_;
|
||||
QMap<int, ReplyType*> pending_replies_;
|
||||
};
|
||||
|
||||
|
||||
template<typename MessageType>
|
||||
AbstractMessageHandler<MessageType>::AbstractMessageHandler(
|
||||
QIODevice* device, QObject* parent)
|
||||
: _MessageHandlerBase(device, parent),
|
||||
next_id_(1)
|
||||
{
|
||||
}
|
||||
|
||||
template<typename MessageType>
|
||||
void AbstractMessageHandler<MessageType>::SendMessage(const MessageType& message) {
|
||||
Q_ASSERT(QThread::currentThread() == thread());
|
||||
|
||||
std::string data = message.SerializeAsString();
|
||||
WriteMessage(QByteArray(data.data(), data.size()));
|
||||
}
|
||||
|
||||
template<typename MessageType>
|
||||
void AbstractMessageHandler<MessageType>::SendMessageAsync(const MessageType& message) {
|
||||
std::string data = message.SerializeAsString();
|
||||
metaObject()->invokeMethod(this, "WriteMessage", Qt::QueuedConnection,
|
||||
Q_ARG(QByteArray, QByteArray(data.data(), data.size())));
|
||||
}
|
||||
|
||||
template<typename MessageType>
|
||||
void AbstractMessageHandler<MessageType>::SendReply(const MessageType& request,
|
||||
MessageType* reply) {
|
||||
reply->set_id(request.id());
|
||||
SendMessage(*reply);
|
||||
}
|
||||
|
||||
template<typename MessageType>
|
||||
bool AbstractMessageHandler<MessageType>::RawMessageArrived(const QByteArray& data) {
|
||||
MessageType message;
|
||||
if (!message.ParseFromArray(data.constData(), data.size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ReplyType* reply = NULL;
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
reply = pending_replies_.take(message.id());
|
||||
}
|
||||
|
||||
if (reply) {
|
||||
// This is a reply to a message that we created earlier.
|
||||
reply->SetReply(message);
|
||||
} else {
|
||||
MessageArrived(message);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename MessageType>
|
||||
typename AbstractMessageHandler<MessageType>::ReplyType*
|
||||
AbstractMessageHandler<MessageType>::NewReply(
|
||||
MessageType* message) {
|
||||
ReplyType* reply = NULL;
|
||||
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
|
||||
const int id = next_id_ ++;
|
||||
reply = new ReplyType(id);
|
||||
pending_replies_[id] = reply;
|
||||
}
|
||||
|
||||
message->set_id(reply->id());
|
||||
return reply;
|
||||
}
|
||||
|
||||
template<typename MessageType>
|
||||
typename AbstractMessageHandler<MessageType>::ReplyType*
|
||||
AbstractMessageHandler<MessageType>::SendMessageWithReply(
|
||||
MessageType* message) {
|
||||
ReplyType* reply = NewReply(message);
|
||||
|
||||
SendMessageAsync(*message);
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
template<typename MessageType>
|
||||
void AbstractMessageHandler<MessageType>::SocketClosed() {
|
||||
QMutexLocker l(&mutex_);
|
||||
|
||||
foreach (ReplyType* reply, pending_replies_) {
|
||||
reply->Abort();
|
||||
}
|
||||
pending_replies_.clear();
|
||||
}
|
||||
|
||||
template<typename MessageType>
|
||||
MessageReply<MessageType>::MessageReply(int id, QObject* parent)
|
||||
: _MessageReplyBase(id, parent)
|
||||
{
|
||||
}
|
||||
|
||||
template<typename MessageType>
|
||||
void MessageReply<MessageType>::SetReply(const MessageType& message) {
|
||||
Q_ASSERT(!finished_);
|
||||
|
||||
message_.MergeFrom(message);
|
||||
finished_ = true;
|
||||
success_ = true;
|
||||
|
||||
emit Finished(success_);
|
||||
semaphore_.release();
|
||||
}
|
||||
|
||||
#endif // MESSAGEHANDLER_H
|
|
@ -0,0 +1,26 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "waitforsignal.h"
|
||||
|
||||
#include <QEventLoop>
|
||||
|
||||
void WaitForSignal(QObject* sender, const char* signal) {
|
||||
QEventLoop loop;
|
||||
QObject::connect(sender, signal, &loop, SLOT(quit()));
|
||||
loop.exec();
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef WAITFORSIGNAL_H
|
||||
#define WAITFORSIGNAL_H
|
||||
|
||||
class QObject;
|
||||
|
||||
void WaitForSignal(QObject* sender, const char* signal);
|
||||
|
||||
#endif // WAITFORSIGNAL_H
|
|
@ -0,0 +1,25 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "workerpool.h"
|
||||
|
||||
_WorkerPoolBase::_WorkerPoolBase(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,325 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef WORKERPOOL_H
|
||||
#define WORKERPOOL_H
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QFile>
|
||||
#include <QLocalServer>
|
||||
#include <QLocalSocket>
|
||||
#include <QObject>
|
||||
#include <QProcess>
|
||||
#include <QThread>
|
||||
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/waitforsignal.h"
|
||||
|
||||
|
||||
// Base class containing signals and slots - required because moc doesn't do
|
||||
// templated objects.
|
||||
class _WorkerPoolBase : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
_WorkerPoolBase(QObject* parent = 0);
|
||||
|
||||
signals:
|
||||
// Emitted when a worker failed to start. This usually happens when the
|
||||
// worker wasn't found, or couldn't be executed.
|
||||
void WorkerFailedToStart();
|
||||
|
||||
// A worker connected and a handler was created for it. The next call to
|
||||
// NextHandler() won't return NULL.
|
||||
void WorkerConnected();
|
||||
|
||||
protected slots:
|
||||
virtual void DoStart() {}
|
||||
virtual void NewConnection() {}
|
||||
virtual void ProcessError(QProcess::ProcessError) {}
|
||||
};
|
||||
|
||||
|
||||
// Manages a pool of one or more external processes. A local socket server is
|
||||
// started for each process, and the address is passed to the process as
|
||||
// argv[1]. The process is expected to connect back to the socket server, and
|
||||
// when it does a HandlerType is created for it.
|
||||
template <typename HandlerType>
|
||||
class WorkerPool : public _WorkerPoolBase {
|
||||
public:
|
||||
WorkerPool(QObject* parent = 0);
|
||||
~WorkerPool();
|
||||
|
||||
// Sets the name of the worker executable. This is looked for first in the
|
||||
// current directory, and then in $PATH. You must call this before calling
|
||||
// Start().
|
||||
void SetExecutableName(const QString& executable_name);
|
||||
|
||||
// Sets the number of worker process to use. Defaults to
|
||||
// 1 <= (processors / 2) <= 2.
|
||||
void SetWorkerCount(int count);
|
||||
|
||||
// Sets the prefix to use for the local server (on unix this is a named pipe
|
||||
// in /tmp). Defaults to QApplication::applicationName(). A random number
|
||||
// is appended to this name when creating each server.
|
||||
void SetLocalServerName(const QString& local_server_name);
|
||||
|
||||
// Starts all workers.
|
||||
void Start();
|
||||
|
||||
// Returns a handler in a round-robin fashion. Will block if no handlers are
|
||||
// available yet.
|
||||
HandlerType* NextHandler();
|
||||
|
||||
protected:
|
||||
void DoStart();
|
||||
void NewConnection();
|
||||
void ProcessError(QProcess::ProcessError error);
|
||||
|
||||
private:
|
||||
struct Worker {
|
||||
Worker() : local_server_(NULL), local_socket_(NULL), process_(NULL),
|
||||
handler_(NULL) {}
|
||||
|
||||
QLocalServer* local_server_;
|
||||
QLocalSocket* local_socket_;
|
||||
QProcess* process_;
|
||||
HandlerType* handler_;
|
||||
};
|
||||
|
||||
void StartOneWorker(Worker* worker);
|
||||
|
||||
template <typename T>
|
||||
Worker* FindWorker(T Worker::*member, T value) {
|
||||
for (typename QList<Worker>::iterator it = workers_.begin() ;
|
||||
it != workers_.end() ; ++it) {
|
||||
if ((*it).*member == value) {
|
||||
return &(*it);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void DeleteQObjectPointerLater(T** p) {
|
||||
if (*p) {
|
||||
(*p)->deleteLater();
|
||||
*p = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
QString local_server_name_;
|
||||
QString executable_name_;
|
||||
QString executable_path_;
|
||||
|
||||
int worker_count_;
|
||||
int next_worker_;
|
||||
QList<Worker> workers_;
|
||||
};
|
||||
|
||||
|
||||
template <typename HandlerType>
|
||||
WorkerPool<HandlerType>::WorkerPool(QObject* parent)
|
||||
: _WorkerPoolBase(parent),
|
||||
next_worker_(0)
|
||||
{
|
||||
worker_count_ = qBound(1, QThread::idealThreadCount() / 2, 2);
|
||||
local_server_name_ = qApp->applicationName().toLower();
|
||||
|
||||
if (local_server_name_.isEmpty())
|
||||
local_server_name_ = "workerpool";
|
||||
}
|
||||
|
||||
template <typename HandlerType>
|
||||
WorkerPool<HandlerType>::~WorkerPool() {
|
||||
foreach (const Worker& worker, workers_) {
|
||||
if (worker.local_socket_ && worker.process_) {
|
||||
// The worker is connected. Close his socket and wait for him to exit.
|
||||
qLog(Debug) << "Closing worker socket";
|
||||
worker.local_socket_->close();
|
||||
worker.process_->waitForFinished(500);
|
||||
}
|
||||
|
||||
if (worker.process_ && worker.process_->state() == QProcess::Running) {
|
||||
// The worker is still running - kill it.
|
||||
qLog(Debug) << "Killing worker process";
|
||||
worker.process_->terminate();
|
||||
if (!worker.process_->waitForFinished(500)) {
|
||||
worker.process_->kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename HandlerType>
|
||||
void WorkerPool<HandlerType>::SetWorkerCount(int count) {
|
||||
Q_ASSERT(workers_.isEmpty());
|
||||
worker_count_ = count;
|
||||
}
|
||||
|
||||
template <typename HandlerType>
|
||||
void WorkerPool<HandlerType>::SetLocalServerName(const QString& local_server_name) {
|
||||
Q_ASSERT(workers_.isEmpty());
|
||||
local_server_name_ = local_server_name;
|
||||
}
|
||||
|
||||
template <typename HandlerType>
|
||||
void WorkerPool<HandlerType>::SetExecutableName(const QString& executable_name) {
|
||||
Q_ASSERT(workers_.isEmpty());
|
||||
executable_name_ = executable_name;
|
||||
}
|
||||
|
||||
template <typename HandlerType>
|
||||
void WorkerPool<HandlerType>::Start() {
|
||||
metaObject()->invokeMethod(this, "DoStart");
|
||||
}
|
||||
|
||||
template <typename HandlerType>
|
||||
void WorkerPool<HandlerType>::DoStart() {
|
||||
Q_ASSERT(workers_.isEmpty());
|
||||
Q_ASSERT(!executable_name_.isEmpty());
|
||||
|
||||
// Find the executable if we can, default to searching $PATH
|
||||
executable_path_ = executable_name_;
|
||||
|
||||
QStringList search_path;
|
||||
search_path << qApp->applicationDirPath();
|
||||
#ifdef Q_OS_MAC
|
||||
search_path << qApp->applicationDirPath() + "/../PlugIns";
|
||||
#endif
|
||||
|
||||
foreach (const QString& path_prefix, search_path) {
|
||||
const QString executable_path = path_prefix + "/" + executable_name_;
|
||||
if (QFile::exists(executable_path)) {
|
||||
executable_path_ = executable_path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Start all the workers
|
||||
for (int i=0 ; i<worker_count_ ; ++i) {
|
||||
Worker worker;
|
||||
StartOneWorker(&worker);
|
||||
|
||||
workers_ << worker;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename HandlerType>
|
||||
void WorkerPool<HandlerType>::StartOneWorker(Worker* worker) {
|
||||
DeleteQObjectPointerLater(&worker->local_server_);
|
||||
DeleteQObjectPointerLater(&worker->local_socket_);
|
||||
DeleteQObjectPointerLater(&worker->process_);
|
||||
DeleteQObjectPointerLater(&worker->handler_);
|
||||
|
||||
worker->local_server_ = new QLocalServer(this);
|
||||
worker->process_ = new QProcess(this);
|
||||
|
||||
connect(worker->local_server_, SIGNAL(newConnection()), SLOT(NewConnection()));
|
||||
connect(worker->process_, SIGNAL(error(QProcess::ProcessError)),
|
||||
SLOT(ProcessError(QProcess::ProcessError)));
|
||||
|
||||
// Create a server, find an unused name and start listening
|
||||
forever {
|
||||
const int unique_number = qrand() ^ ((int)(quint64(this) & 0xFFFFFFFF));
|
||||
const QString name = QString("%1_%2").arg(local_server_name_).arg(unique_number);
|
||||
|
||||
if (worker->local_server_->listen(name)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
qLog(Debug) << "Starting worker" << executable_path_
|
||||
<< worker->local_server_->fullServerName();
|
||||
|
||||
// Start the process
|
||||
worker->process_->setProcessChannelMode(QProcess::ForwardedChannels);
|
||||
worker->process_->start(executable_path_,
|
||||
QStringList() << worker->local_server_->fullServerName());
|
||||
}
|
||||
|
||||
template <typename HandlerType>
|
||||
void WorkerPool<HandlerType>::NewConnection() {
|
||||
QLocalServer* server = qobject_cast<QLocalServer*>(sender());
|
||||
|
||||
// Find the worker with this server.
|
||||
Worker* worker = FindWorker(&Worker::local_server_, server);
|
||||
if (!worker)
|
||||
return;
|
||||
|
||||
qLog(Debug) << "Worker connected to" << server->fullServerName();
|
||||
|
||||
// Accept the connection.
|
||||
worker->local_socket_ = server->nextPendingConnection();
|
||||
|
||||
// We only ever accept one connection per worker, so destroy the server now.
|
||||
worker->local_socket_->setParent(this);
|
||||
worker->local_server_->deleteLater();
|
||||
worker->local_server_ = NULL;
|
||||
|
||||
// Create the handler.
|
||||
worker->handler_ = new HandlerType(worker->local_socket_, this);
|
||||
|
||||
emit WorkerConnected();
|
||||
}
|
||||
|
||||
template <typename HandlerType>
|
||||
void WorkerPool<HandlerType>::ProcessError(QProcess::ProcessError error) {
|
||||
QProcess* process = qobject_cast<QProcess*>(sender());
|
||||
|
||||
// Find the worker with this process.
|
||||
Worker* worker = FindWorker(&Worker::process_, process);
|
||||
if (!worker)
|
||||
return;
|
||||
|
||||
switch (error) {
|
||||
case QProcess::FailedToStart:
|
||||
// Failed to start errors are bad - it usually means the worker isn't
|
||||
// installed. Don't restart the process, but tell our owner, who will
|
||||
// probably want to do something fatal.
|
||||
qLog(Error) << "Worker failed to start";
|
||||
emit WorkerFailedToStart();
|
||||
break;
|
||||
|
||||
default:
|
||||
// On any other error we just restart the process.
|
||||
qLog(Debug) << "Worker failed with error" << error << "- restarting";
|
||||
StartOneWorker(worker);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename HandlerType>
|
||||
HandlerType* WorkerPool<HandlerType>::NextHandler() {
|
||||
forever {
|
||||
for (int i=0 ; i<workers_.count() ; ++i) {
|
||||
const int worker_index = (next_worker_ + i) % workers_.count();
|
||||
|
||||
if (workers_[worker_index].handler_) {
|
||||
next_worker_ = (worker_index + 1) % workers_.count();
|
||||
return workers_[worker_index].handler_;
|
||||
}
|
||||
}
|
||||
|
||||
// No workers were connected, wait for one.
|
||||
WaitForSignal(this, SIGNAL(WorkerConnected()));
|
||||
}
|
||||
}
|
||||
|
||||
#endif // WORKERPOOL_H
|
|
@ -0,0 +1,18 @@
|
|||
include_directories(${PROTOBUF_INCLUDE_DIRS})
|
||||
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/blobversion.h.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/blobversion.h)
|
||||
|
||||
set(MESSAGES
|
||||
spotifymessages.proto
|
||||
)
|
||||
|
||||
protobuf_generate_cpp(PROTO_SOURCES PROTO_HEADERS ${MESSAGES})
|
||||
|
||||
add_library(clementine-spotifyblob-messages STATIC
|
||||
${PROTO_SOURCES}
|
||||
)
|
||||
|
||||
target_link_libraries(clementine-spotifyblob-messages
|
||||
libclementine-common
|
||||
)
|
|
@ -19,7 +19,7 @@
|
|||
// compatible.
|
||||
|
||||
|
||||
package spotify_pb;
|
||||
package pb.spotify;
|
||||
|
||||
option optimize_for = LITE_RUNTIME;
|
||||
|
||||
|
@ -162,7 +162,10 @@ message PlaybackSettings {
|
|||
optional bool volume_normalisation = 2 [default = false];
|
||||
}
|
||||
|
||||
message SpotifyMessage {
|
||||
message Message {
|
||||
// Not currently used
|
||||
optional int32 id = 18;
|
||||
|
||||
optional LoginRequest login_request = 1;
|
||||
optional LoginResponse login_response = 2;
|
||||
optional Playlists playlists_updated = 3;
|
|
@ -0,0 +1,16 @@
|
|||
include_directories(${PROTOBUF_INCLUDE_DIRS})
|
||||
|
||||
set(MESSAGES
|
||||
tagreadermessages.proto
|
||||
)
|
||||
|
||||
protobuf_generate_cpp(PROTO_SOURCES PROTO_HEADERS ${MESSAGES})
|
||||
|
||||
add_library(libclementine-tagreader STATIC
|
||||
${PROTO_SOURCES}
|
||||
)
|
||||
|
||||
target_link_libraries(libclementine-tagreader
|
||||
libclementine-common
|
||||
)
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package pb.tagreader;
|
||||
|
||||
option optimize_for = LITE_RUNTIME;
|
||||
|
||||
|
||||
message SongMetadata {
|
||||
enum Type {
|
||||
UNKNOWN = 0;
|
||||
ASF = 1;
|
||||
FLAC = 2;
|
||||
MP4 = 3;
|
||||
MPC = 4;
|
||||
MPEG = 5;
|
||||
OGGFLAC = 6;
|
||||
OGGSPEEX = 7;
|
||||
OGGVORBIS = 8;
|
||||
AIFF = 9;
|
||||
WAV = 10;
|
||||
TRUEAUDIO = 11;
|
||||
CDDA = 12;
|
||||
STREAM = 99;
|
||||
}
|
||||
|
||||
optional bool valid = 1;
|
||||
optional string title = 2;
|
||||
optional string album = 3;
|
||||
optional string artist = 4;
|
||||
optional string albumartist = 5;
|
||||
optional string composer = 6;
|
||||
optional int32 track = 7;
|
||||
optional int32 disc = 8;
|
||||
optional float bpm = 9;
|
||||
optional int32 year = 10;
|
||||
optional string genre = 11;
|
||||
optional string comment = 12;
|
||||
optional bool compilation = 13;
|
||||
optional float rating = 14;
|
||||
optional int32 playcount = 15;
|
||||
optional int32 skipcount = 16;
|
||||
optional int32 lastplayed = 17;
|
||||
optional int32 score = 18;
|
||||
optional uint64 length_nanosec = 19;
|
||||
optional int32 bitrate = 20;
|
||||
optional int32 samplerate = 21;
|
||||
optional string url = 22;
|
||||
optional string basefilename = 23;
|
||||
optional int32 mtime = 24;
|
||||
optional int32 ctime = 25;
|
||||
optional int32 filesize = 26;
|
||||
optional bool suspicious_tags = 27;
|
||||
optional string art_automatic = 28;
|
||||
optional Type type = 29;
|
||||
}
|
||||
|
||||
message ReadFileRequest {
|
||||
optional string filename = 1;
|
||||
}
|
||||
|
||||
message ReadFileResponse {
|
||||
optional SongMetadata metadata = 1;
|
||||
}
|
||||
|
||||
message SaveFileRequest {
|
||||
optional string filename = 1;
|
||||
optional SongMetadata metadata = 2;
|
||||
}
|
||||
|
||||
message SaveFileResponse {
|
||||
optional bool success = 1;
|
||||
}
|
||||
|
||||
message IsMediaFileRequest {
|
||||
optional string filename = 1;
|
||||
}
|
||||
|
||||
message IsMediaFileResponse {
|
||||
optional bool success = 1;
|
||||
}
|
||||
|
||||
message LoadEmbeddedArtRequest {
|
||||
optional string filename = 1;
|
||||
}
|
||||
|
||||
message LoadEmbeddedArtResponse {
|
||||
optional bytes data = 1;
|
||||
}
|
||||
|
||||
message Message {
|
||||
optional int32 id = 1;
|
||||
|
||||
optional ReadFileRequest read_file_request = 2;
|
||||
optional ReadFileResponse read_file_response = 3;
|
||||
|
||||
optional SaveFileRequest save_file_request = 4;
|
||||
optional SaveFileResponse save_file_response = 5;
|
||||
|
||||
optional IsMediaFileRequest is_media_file_request = 6;
|
||||
optional IsMediaFileResponse is_media_file_response = 7;
|
||||
|
||||
optional LoadEmbeddedArtRequest load_embedded_art_request = 8;
|
||||
optional LoadEmbeddedArtResponse load_embedded_art_response = 9;
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
include_directories(${PROTOBUF_INCLUDE_DIRS})
|
||||
include_directories(${CMAKE_CURRENT_BINARY_DIR})
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src)
|
||||
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/blobversion.h.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/blobversion.h)
|
||||
|
||||
set(COMMON_SOURCES
|
||||
spotifymessagehandler.cpp
|
||||
)
|
||||
|
||||
set(COMMON_HEADERS
|
||||
spotifymessagehandler.h
|
||||
)
|
||||
|
||||
set(COMMON_MESSAGES
|
||||
spotifymessages.proto
|
||||
)
|
||||
|
||||
qt4_wrap_cpp(COMMON_MOC ${COMMON_HEADERS})
|
||||
protobuf_generate_cpp(PROTO_SOURCES PROTO_HEADERS ${COMMON_MESSAGES})
|
||||
|
||||
add_library(clementine-spotifyblob-messages STATIC
|
||||
${COMMON_SOURCES}
|
||||
${COMMON_MOC}
|
||||
${PROTO_SOURCES}
|
||||
)
|
||||
|
||||
# Use protobuf-lite if it's available
|
||||
if(PROTOBUF_LITE_LIBRARY AND USE_PROTOBUF_LITE)
|
||||
set(protobuf ${PROTOBUF_LITE_LIBRARY})
|
||||
else(PROTOBUF_LITE_LIBRARY AND USE_PROTOBUF_LITE)
|
||||
set(protobuf ${PROTOBUF_LIBRARY})
|
||||
endif(PROTOBUF_LITE_LIBRARY AND USE_PROTOBUF_LITE)
|
||||
|
||||
target_link_libraries(clementine-spotifyblob-messages
|
||||
${protobuf}
|
||||
${CMAKE_THREAD_LIBS_INIT}
|
||||
)
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Note: this file is licensed under the Apache License instead of GPL because
|
||||
// it is used by the Spotify blob which links against libspotify and is not GPL
|
||||
// compatible.
|
||||
|
||||
|
||||
#ifndef SPOTIFYMESSAGEHANDLER_H
|
||||
#define SPOTIFYMESSAGEHANDLER_H
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QObject>
|
||||
|
||||
class QAbstractSocket;
|
||||
|
||||
namespace spotify_pb {
|
||||
class SpotifyMessage;
|
||||
}
|
||||
|
||||
|
||||
#define QStringFromStdString(x) \
|
||||
QString::fromUtf8(x.data(), x.size())
|
||||
#define DataCommaSizeFromQString(x) \
|
||||
x.toUtf8().constData(), x.toUtf8().length()
|
||||
|
||||
|
||||
class SpotifyMessageHandler : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SpotifyMessageHandler(QAbstractSocket* device, QObject* parent);
|
||||
|
||||
void SendMessage(const spotify_pb::SpotifyMessage& message);
|
||||
void SendMessageAsync(const spotify_pb::SpotifyMessage& message);
|
||||
|
||||
signals:
|
||||
void MessageArrived(const spotify_pb::SpotifyMessage& message);
|
||||
|
||||
private slots:
|
||||
void WriteMessage(const QByteArray& data);
|
||||
void DeviceReadyRead();
|
||||
|
||||
private:
|
||||
QAbstractSocket* device_;
|
||||
|
||||
bool reading_protobuf_;
|
||||
quint32 expected_length_;
|
||||
QBuffer buffer_;
|
||||
};
|
||||
|
||||
#endif // SPOTIFYMESSAGEHANDLER_H
|
|
@ -44,6 +44,10 @@ if(HAVE_BREAKPAD)
|
|||
include_directories(../3rdparty/google-breakpad)
|
||||
endif(HAVE_BREAKPAD)
|
||||
|
||||
include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-common)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/ext/libclementine-tagreader)
|
||||
include_directories(${CMAKE_BINARY_DIR}/ext/libclementine-tagreader)
|
||||
|
||||
cmake_policy(SET CMP0011 NEW)
|
||||
include(../cmake/ParseArguments.cmake)
|
||||
include(../cmake/Translations.cmake)
|
||||
|
@ -60,20 +64,16 @@ set(SOURCES
|
|||
|
||||
core/backgroundstreams.cpp
|
||||
core/backgroundthread.cpp
|
||||
core/closure.cpp
|
||||
core/commandlineoptions.cpp
|
||||
core/crashreporting.cpp
|
||||
core/database.cpp
|
||||
core/deletefiles.cpp
|
||||
core/encoding.cpp
|
||||
core/filesystemmusicstorage.cpp
|
||||
core/filesystemwatcherinterface.cpp
|
||||
core/fht.cpp
|
||||
core/fmpsparser.cpp
|
||||
core/globalshortcutbackend.cpp
|
||||
core/globalshortcuts.cpp
|
||||
core/gnomeglobalshortcutbackend.cpp
|
||||
core/logging.cpp
|
||||
core/mergedproxymodel.cpp
|
||||
core/multisortfilterproxy.cpp
|
||||
core/musicstorage.cpp
|
||||
|
@ -89,6 +89,7 @@ set(SOURCES
|
|||
core/song.cpp
|
||||
core/songloader.cpp
|
||||
core/stylesheetloader.cpp
|
||||
core/tagreaderclient.cpp
|
||||
core/taskmanager.cpp
|
||||
core/urlhandler.cpp
|
||||
core/utilities.cpp
|
||||
|
@ -330,7 +331,6 @@ set(HEADERS
|
|||
|
||||
core/backgroundstreams.h
|
||||
core/backgroundthread.h
|
||||
core/closure.h
|
||||
core/crashreporting.h
|
||||
core/database.h
|
||||
core/deletefiles.h
|
||||
|
@ -345,6 +345,7 @@ set(HEADERS
|
|||
core/player.h
|
||||
core/qtfslistener.h
|
||||
core/songloader.h
|
||||
core/tagreaderclient.h
|
||||
core/taskmanager.h
|
||||
core/urlhandler.h
|
||||
|
||||
|
@ -705,6 +706,9 @@ optional_source(HAVE_SPOTIFY
|
|||
internet/spotifyserver.h
|
||||
internet/spotifyservice.h
|
||||
internet/spotifysettingspage.h
|
||||
INCLUDE_DIRECTORIES
|
||||
${CMAKE_SOURCE_DIR}/ext/libclementine-spotifyblob
|
||||
${CMAKE_BINARY_DIR}/ext/libclementine-spotifyblob
|
||||
)
|
||||
|
||||
optional_source(HAVE_QCA INCLUDE_DIRECTORIES ${QCA_INCLUDE_DIRS})
|
||||
|
@ -960,13 +964,14 @@ add_dependencies(clementine_lib pot)
|
|||
|
||||
|
||||
target_link_libraries(clementine_lib
|
||||
chardet
|
||||
libclementine-common
|
||||
libclementine-tagreader
|
||||
sha2
|
||||
${TAGLIB_LIBRARIES}
|
||||
${CHROMAPRINT_LIBRARIES}
|
||||
${ECHONEST_LIBRARIES}
|
||||
${GOBJECT_LIBRARIES}
|
||||
${GLIB_LIBRARIES}
|
||||
${TAGLIB_LIBRARIES}
|
||||
${QJSON_LIBRARIES}
|
||||
${QT_LIBRARIES}
|
||||
${GSTREAMER_BASE_LIBRARIES}
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
|
||||
#include "config.h"
|
||||
#include "commandlineoptions.h"
|
||||
#include "logging.h"
|
||||
#include "version.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <getopt.h>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#include "logging.h"
|
||||
#include "multisortfilterproxy.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
#include <QDate>
|
||||
#include <QDateTime>
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#include "organise.h"
|
||||
#include "taskmanager.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
|
@ -137,7 +138,7 @@ void Organise::ProcessSomeFiles() {
|
|||
|
||||
// Read metadata from the file
|
||||
Song song;
|
||||
song.InitFromFile(task.filename_, -1);
|
||||
TagReaderClient::Instance()->ReadFileBlocking(task.filename_, &song);
|
||||
if (!song.is_valid())
|
||||
continue;
|
||||
|
||||
|
|
|
@ -15,38 +15,15 @@
|
|||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "fmpsparser.h"
|
||||
#include "logging.h"
|
||||
#include "mpris_common.h"
|
||||
#include "song.h"
|
||||
#include "timeconstants.h"
|
||||
#include "core/encoding.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/messagehandler.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <aifffile.h>
|
||||
#include <asffile.h>
|
||||
#include <attachedpictureframe.h>
|
||||
#include <commentsframe.h>
|
||||
#include <fileref.h>
|
||||
#include <flacfile.h>
|
||||
#include <id3v1genres.h>
|
||||
#include <id3v2tag.h>
|
||||
#include <mp4file.h>
|
||||
#include <mp4tag.h>
|
||||
#include <mpcfile.h>
|
||||
#include <mpegfile.h>
|
||||
#include <oggfile.h>
|
||||
#include <oggflacfile.h>
|
||||
#include <speexfile.h>
|
||||
#include <tag.h>
|
||||
#include <textidentificationframe.h>
|
||||
#include <trueaudiofile.h>
|
||||
#include <tstring.h>
|
||||
#include <vorbisfile.h>
|
||||
#include <wavfile.h>
|
||||
|
||||
#ifdef HAVE_LIBLASTFM
|
||||
#include "internet/fixlastfm.h"
|
||||
#include <lastfm/Track>
|
||||
|
@ -62,6 +39,8 @@
|
|||
#include <QVariant>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
#include <id3v1genres.h>
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
# include <mswmdm.h>
|
||||
# include <QUuid>
|
||||
|
@ -79,7 +58,6 @@
|
|||
#include <boost/scoped_ptr.hpp>
|
||||
using boost::scoped_ptr;
|
||||
|
||||
#include "encoding.h"
|
||||
#include "utilities.h"
|
||||
#include "covers/albumcoverloader.h"
|
||||
#include "engines/enginebase.h"
|
||||
|
@ -87,12 +65,6 @@ using boost::scoped_ptr;
|
|||
#include "widgets/trackslider.h"
|
||||
|
||||
|
||||
// Taglib added support for FLAC pictures in 1.7.0
|
||||
#if (TAGLIB_MAJOR_VERSION > 1) || (TAGLIB_MAJOR_VERSION == 1 && TAGLIB_MINOR_VERSION >= 7)
|
||||
# define TAGLIB_HAS_FLAC_PICTURELIST
|
||||
#endif
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
QStringList Prepend(const QString& text, const QStringList& list) {
|
||||
|
@ -109,10 +81,6 @@ QStringList Updateify(const QStringList& list) {
|
|||
return ret;
|
||||
}
|
||||
|
||||
TagLib::String QStringToTaglibString(const QString& s) {
|
||||
return TagLib::String(s.toUtf8().constData(), TagLib::String::UTF8);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
|
@ -142,18 +110,10 @@ const QString Song::kFtsUpdateSpec = Updateify(Song::kFtsColumns).join(", ");
|
|||
const QString Song::kManuallyUnsetCover = "(unset)";
|
||||
const QString Song::kEmbeddedCover = "(embedded)";
|
||||
|
||||
TagLibFileRefFactory Song::kDefaultFactory;
|
||||
QMutex Song::sTaglibMutex;
|
||||
|
||||
|
||||
struct Song::Private : public QSharedData {
|
||||
Private();
|
||||
|
||||
// This is here and not in Song itself so we don't have to include
|
||||
// <xiphcomment.h> in the main header.
|
||||
void ParseOggTag(const TagLib::Ogg::FieldListMap& map, const QTextCodec* codec,
|
||||
QString* disc, QString* compilation);
|
||||
|
||||
bool valid_;
|
||||
int id_;
|
||||
|
||||
|
@ -254,37 +214,22 @@ Song::Private::Private()
|
|||
{
|
||||
}
|
||||
|
||||
TagLib::FileRef* TagLibFileRefFactory::GetFileRef(const QString& filename) {
|
||||
#ifdef Q_OS_WIN32
|
||||
return new TagLib::FileRef(filename.toStdWString().c_str());
|
||||
#else
|
||||
return new TagLib::FileRef(QFile::encodeName(filename).constData());
|
||||
#endif
|
||||
}
|
||||
|
||||
Song::Song()
|
||||
: d(new Private),
|
||||
factory_(&kDefaultFactory)
|
||||
: d(new Private)
|
||||
{
|
||||
}
|
||||
|
||||
Song::Song(const Song &other)
|
||||
: d(other.d),
|
||||
factory_(&kDefaultFactory)
|
||||
: d(other.d)
|
||||
{
|
||||
}
|
||||
|
||||
Song::Song(FileRefFactory* factory)
|
||||
: d(new Private),
|
||||
factory_(factory) {
|
||||
}
|
||||
|
||||
Song::~Song() {
|
||||
}
|
||||
|
||||
Song& Song::operator =(const Song& other) {
|
||||
d = other.d;
|
||||
factory_ = other.factory_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
@ -430,15 +375,6 @@ void Song::set_genre_id3(int id) {
|
|||
set_genre(TStringToQString(TagLib::ID3v1::genre(id)));
|
||||
}
|
||||
|
||||
QString Song::Decode(const TagLib::String& tag, const QTextCodec* codec) {
|
||||
if (codec && tag.isLatin1()) { // Never override UTF-8.
|
||||
const std::string fixed = QString::fromUtf8(tag.toCString(true)).toStdString();
|
||||
return codec->toUnicode(fixed.c_str()).trimmed();
|
||||
} else {
|
||||
return TStringToQString(tag).trimmed();
|
||||
}
|
||||
}
|
||||
|
||||
QString Song::Decode(const QString& tag, const QTextCodec* codec) {
|
||||
if (!codec) {
|
||||
return tag;
|
||||
|
@ -447,275 +383,71 @@ QString Song::Decode(const QString& tag, const QTextCodec* codec) {
|
|||
return codec->toUnicode(tag.toUtf8());
|
||||
}
|
||||
|
||||
bool Song::HasProperMediaFile() const {
|
||||
#ifndef QT_NO_DEBUG_OUTPUT
|
||||
if (qApp->thread() == QThread::currentThread())
|
||||
qLog(Warning) << "HasProperMediaFile() on GUI thread!";
|
||||
#endif
|
||||
|
||||
QMutexLocker l(&sTaglibMutex);
|
||||
scoped_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(d->url_.toLocalFile()));
|
||||
|
||||
return !fileref->isNull() && fileref->tag();
|
||||
}
|
||||
|
||||
void Song::InitFromFile(const QString& filename, int directory_id) {
|
||||
#ifndef QT_NO_DEBUG_OUTPUT
|
||||
if (qApp->thread() == QThread::currentThread())
|
||||
qLog(Warning) << "InitFromFile() on GUI thread!";
|
||||
#endif
|
||||
|
||||
void Song::InitFromProtobuf(const pb::tagreader::SongMetadata& pb) {
|
||||
d->init_from_file_ = true;
|
||||
|
||||
d->url_ = QUrl::fromLocalFile(filename);
|
||||
d->directory_id_ = directory_id;
|
||||
|
||||
QFileInfo info(filename);
|
||||
d->basefilename_ = info.fileName();
|
||||
|
||||
QMutexLocker l(&sTaglibMutex);
|
||||
scoped_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
|
||||
|
||||
if(fileref->isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
d->filesize_ = info.size();
|
||||
d->mtime_ = info.lastModified().toTime_t();
|
||||
d->ctime_ = info.created().toTime_t();
|
||||
|
||||
// This is single byte encoding, therefore can't be CJK.
|
||||
UniversalEncodingHandler detector(NS_FILTER_NON_CJK);
|
||||
|
||||
TagLib::Tag* tag = fileref->tag();
|
||||
QTextCodec* codec = NULL;
|
||||
if (tag) {
|
||||
TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(fileref->file());
|
||||
if (file && (file->ID3v2Tag() || file->ID3v1Tag())) {
|
||||
codec = detector.Guess(*fileref);
|
||||
}
|
||||
if (codec &&
|
||||
codec->name() != "UTF-8" &&
|
||||
codec->name() != "ISO-8859-1") {
|
||||
// Mark tags where we detect an unusual codec as suspicious.
|
||||
d->suspicious_tags_ = true;
|
||||
}
|
||||
|
||||
|
||||
d->title_ = Decode(tag->title());
|
||||
d->artist_ = Decode(tag->artist());
|
||||
d->album_ = Decode(tag->album());
|
||||
d->genre_ = Decode(tag->genre());
|
||||
d->year_ = tag->year();
|
||||
d->track_ = tag->track();
|
||||
|
||||
d->valid_ = true;
|
||||
}
|
||||
|
||||
QString disc;
|
||||
QString compilation;
|
||||
if (TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
|
||||
if (file->ID3v2Tag()) {
|
||||
const TagLib::ID3v2::FrameListMap& map = file->ID3v2Tag()->frameListMap();
|
||||
|
||||
if (!map["TPOS"].isEmpty())
|
||||
disc = TStringToQString(map["TPOS"].front()->toString()).trimmed();
|
||||
|
||||
if (!map["TBPM"].isEmpty())
|
||||
d->bpm_ = TStringToQString(map["TBPM"].front()->toString()).trimmed().toFloat();
|
||||
|
||||
if (!map["TCOM"].isEmpty())
|
||||
d->composer_ = Decode(map["TCOM"].front()->toString());
|
||||
|
||||
if (!map["TPE2"].isEmpty()) // non-standard: Apple, Microsoft
|
||||
d->albumartist_ = Decode(map["TPE2"].front()->toString());
|
||||
|
||||
if (!map["TCMP"].isEmpty())
|
||||
compilation = TStringToQString(map["TCMP"].front()->toString()).trimmed();
|
||||
|
||||
if (!map["APIC"].isEmpty())
|
||||
set_embedded_cover();
|
||||
|
||||
// Find a suitable comment tag. For now we ignore iTunNORM comments.
|
||||
for (int i=0 ; i<map["COMM"].size() ; ++i) {
|
||||
const TagLib::ID3v2::CommentsFrame* frame =
|
||||
dynamic_cast<const TagLib::ID3v2::CommentsFrame*>(map["COMM"][i]);
|
||||
|
||||
if (frame && TStringToQString(frame->description()) != "iTunNORM") {
|
||||
d->comment_ = Decode(frame->text());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse FMPS frames
|
||||
for (int i=0 ; i<map["TXXX"].size() ; ++i) {
|
||||
const TagLib::ID3v2::UserTextIdentificationFrame* frame =
|
||||
dynamic_cast<const TagLib::ID3v2::UserTextIdentificationFrame*>(map["TXXX"][i]);
|
||||
|
||||
if (frame && frame->description().startsWith("FMPS_")) {
|
||||
ParseFMPSFrame(TStringToQString(frame->description()),
|
||||
TStringToQString(frame->fieldList()[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (TagLib::Ogg::Vorbis::File* file = dynamic_cast<TagLib::Ogg::Vorbis::File*>(fileref->file())) {
|
||||
if (file->tag()) {
|
||||
d->ParseOggTag(file->tag()->fieldListMap(), NULL, &disc, &compilation);
|
||||
}
|
||||
d->comment_ = Decode(tag->comment());
|
||||
} else if (TagLib::FLAC::File* file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
|
||||
if ( file->xiphComment() ) {
|
||||
d->ParseOggTag(file->xiphComment()->fieldListMap(), NULL, &disc, &compilation);
|
||||
#ifdef TAGLIB_HAS_FLAC_PICTURELIST
|
||||
if (!file->pictureList().isEmpty()) {
|
||||
set_embedded_cover();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
d->comment_ = Decode(tag->comment());
|
||||
} else if (TagLib::MP4::File* file = dynamic_cast<TagLib::MP4::File*>(fileref->file())) {
|
||||
if (file->tag()) {
|
||||
TagLib::MP4::Tag* mp4_tag = file->tag();
|
||||
const TagLib::MP4::ItemListMap& items = mp4_tag->itemListMap();
|
||||
TagLib::MP4::ItemListMap::ConstIterator it = items.find("aART");
|
||||
if (it != items.end()) {
|
||||
TagLib::StringList album_artists = it->second.toStringList();
|
||||
if (!album_artists.isEmpty()) {
|
||||
d->albumartist_ = Decode(album_artists.front());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (tag) {
|
||||
d->comment_ = Decode(tag->comment());
|
||||
}
|
||||
|
||||
if ( !disc.isEmpty() ) {
|
||||
int i = disc.indexOf('/');
|
||||
if ( i != -1 )
|
||||
// disc.right( i ).toInt() is total number of discs, we don't use this at the moment
|
||||
d->disc_ = disc.left( i ).toInt();
|
||||
else
|
||||
d->disc_ = disc.toInt();
|
||||
}
|
||||
|
||||
if ( compilation.isEmpty() ) {
|
||||
// well, it wasn't set, but if the artist is VA assume it's a compilation
|
||||
if ( d->artist_.toLower() == "various artists" )
|
||||
d->compilation_ = true;
|
||||
} else {
|
||||
int i = compilation.toInt();
|
||||
d->compilation_ = (i == 1);
|
||||
}
|
||||
|
||||
if (fileref->audioProperties()) {
|
||||
d->bitrate_ = fileref->audioProperties()->bitrate();
|
||||
d->samplerate_ = fileref->audioProperties()->sampleRate();
|
||||
set_length_nanosec(fileref->audioProperties()->length() * kNsecPerSec);
|
||||
}
|
||||
|
||||
// Get the filetype if we can
|
||||
GuessFileType(fileref.get());
|
||||
|
||||
// Set integer fields to -1 if they're not valid
|
||||
#define intval(x) (x <= 0 ? -1 : x)
|
||||
d->track_ = intval(d->track_);
|
||||
d->disc_ = intval(d->disc_);
|
||||
d->bpm_ = intval(d->bpm_);
|
||||
d->year_ = intval(d->year_);
|
||||
d->bitrate_ = intval(d->bitrate_);
|
||||
d->samplerate_ = intval(d->samplerate_);
|
||||
d->lastplayed_ = intval(d->lastplayed_);
|
||||
d->rating_ = intval(d->rating_);
|
||||
#undef intval
|
||||
d->valid_ = pb.valid();
|
||||
d->title_ = QStringFromStdString(pb.title());
|
||||
d->album_ = QStringFromStdString(pb.album());
|
||||
d->artist_ = QStringFromStdString(pb.artist());
|
||||
d->albumartist_ = QStringFromStdString(pb.albumartist());
|
||||
d->composer_ = QStringFromStdString(pb.composer());
|
||||
d->track_ = pb.track();
|
||||
d->disc_ = pb.disc();
|
||||
d->bpm_ = pb.bpm();
|
||||
d->year_ = pb.year();
|
||||
d->genre_ = QStringFromStdString(pb.genre());
|
||||
d->comment_ = QStringFromStdString(pb.comment());
|
||||
d->compilation_ = pb.compilation();
|
||||
d->rating_ = pb.rating();
|
||||
d->playcount_ = pb.playcount();
|
||||
d->skipcount_ = pb.skipcount();
|
||||
d->lastplayed_ = pb.lastplayed();
|
||||
d->score_ = pb.score();
|
||||
set_length_nanosec(pb.length_nanosec());
|
||||
d->bitrate_ = pb.bitrate();
|
||||
d->samplerate_ = pb.samplerate();
|
||||
d->url_ = QUrl::fromEncoded(QByteArray(pb.url().data(), pb.url().size()));
|
||||
d->basefilename_ = QStringFromStdString(pb.basefilename());
|
||||
d->mtime_ = pb.mtime();
|
||||
d->ctime_ = pb.ctime();
|
||||
d->filesize_ = pb.filesize();
|
||||
d->suspicious_tags_ = pb.suspicious_tags();
|
||||
d->art_automatic_ = QStringFromStdString(pb.art_automatic());
|
||||
d->filetype_ = static_cast<FileType>(pb.type());
|
||||
}
|
||||
|
||||
void Song::ParseFMPSFrame(const QString& name, const QString& value) {
|
||||
FMPSParser parser;
|
||||
if (!parser.Parse(value) || parser.is_empty())
|
||||
return;
|
||||
void Song::ToProtobuf(pb::tagreader::SongMetadata* pb) const {
|
||||
const QByteArray url(d->url_.toEncoded());
|
||||
|
||||
QVariant var;
|
||||
if (name == "FMPS_Rating") {
|
||||
var = parser.result()[0][0];
|
||||
if (var.type() == QVariant::Double) {
|
||||
d->rating_ = var.toDouble();
|
||||
}
|
||||
} else if (name == "FMPS_Rating_User") {
|
||||
// Take a user rating only if there's no rating already set
|
||||
if (d->rating_ == -1 && parser.result()[0].count() >= 2) {
|
||||
var = parser.result()[0][1];
|
||||
if (var.type() == QVariant::Double) {
|
||||
d->rating_ = var.toDouble();
|
||||
}
|
||||
}
|
||||
} else if (name == "FMPS_PlayCount") {
|
||||
var = parser.result()[0][0];
|
||||
if (var.type() == QVariant::Double) {
|
||||
d->playcount_ = var.toDouble();
|
||||
}
|
||||
} else if (name == "FMPS_PlayCount_User") {
|
||||
// Take a user rating only if there's no playcount already set
|
||||
if (d->rating_ == -1 && parser.result()[0].count() >= 2) {
|
||||
var = parser.result()[0][1];
|
||||
if (var.type() == QVariant::Double) {
|
||||
d->playcount_ = var.toDouble();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Song::Private::ParseOggTag(const TagLib::Ogg::FieldListMap& map,
|
||||
const QTextCodec* codec,
|
||||
QString* disc, QString* compilation) {
|
||||
if (!map["COMPOSER"].isEmpty())
|
||||
composer_ = Decode(map["COMPOSER"].front(), codec);
|
||||
|
||||
if (!map["ALBUMARTIST"].isEmpty()) {
|
||||
albumartist_ = Decode(map["ALBUMARTIST"].front(), codec);
|
||||
} else if (!map["ALBUM ARTIST"].isEmpty()) {
|
||||
albumartist_ = Decode(map["ALBUM ARTIST"].front(), codec);
|
||||
}
|
||||
|
||||
if (!map["BPM"].isEmpty() )
|
||||
bpm_ = TStringToQString( map["BPM"].front() ).trimmed().toFloat();
|
||||
|
||||
if (!map["DISCNUMBER"].isEmpty() )
|
||||
*disc = TStringToQString( map["DISCNUMBER"].front() ).trimmed();
|
||||
|
||||
if (!map["COMPILATION"].isEmpty() )
|
||||
*compilation = TStringToQString( map["COMPILATION"].front() ).trimmed();
|
||||
|
||||
if (!map["COVERART"].isEmpty())
|
||||
art_automatic_ = kEmbeddedCover;
|
||||
}
|
||||
|
||||
void Song::GuessFileType(TagLib::FileRef* fileref) {
|
||||
#ifdef TAGLIB_WITH_ASF
|
||||
if (dynamic_cast<TagLib::ASF::File*>(fileref->file()))
|
||||
d->filetype_ = Type_Asf;
|
||||
#endif
|
||||
if (dynamic_cast<TagLib::FLAC::File*>(fileref->file()))
|
||||
d->filetype_ = Type_Flac;
|
||||
#ifdef TAGLIB_WITH_MP4
|
||||
if (dynamic_cast<TagLib::MP4::File*>(fileref->file()))
|
||||
d->filetype_ = Type_Mp4;
|
||||
#endif
|
||||
if (dynamic_cast<TagLib::MPC::File*>(fileref->file()))
|
||||
d->filetype_ = Type_Mpc;
|
||||
if (dynamic_cast<TagLib::MPEG::File*>(fileref->file()))
|
||||
d->filetype_ = Type_Mpeg;
|
||||
if (dynamic_cast<TagLib::Ogg::FLAC::File*>(fileref->file()))
|
||||
d->filetype_ = Type_OggFlac;
|
||||
if (dynamic_cast<TagLib::Ogg::Speex::File*>(fileref->file()))
|
||||
d->filetype_ = Type_OggSpeex;
|
||||
if (dynamic_cast<TagLib::Ogg::Vorbis::File*>(fileref->file()))
|
||||
d->filetype_ = Type_OggVorbis;
|
||||
if (dynamic_cast<TagLib::RIFF::AIFF::File*>(fileref->file()))
|
||||
d->filetype_ = Type_Aiff;
|
||||
if (dynamic_cast<TagLib::RIFF::WAV::File*>(fileref->file()))
|
||||
d->filetype_ = Type_Wav;
|
||||
if (dynamic_cast<TagLib::TrueAudio::File*>(fileref->file()))
|
||||
d->filetype_ = Type_TrueAudio;
|
||||
pb->set_valid(d->valid_);
|
||||
pb->set_title(DataCommaSizeFromQString(d->title_));
|
||||
pb->set_album(DataCommaSizeFromQString(d->album_));
|
||||
pb->set_artist(DataCommaSizeFromQString(d->artist_));
|
||||
pb->set_albumartist(DataCommaSizeFromQString(d->albumartist_));
|
||||
pb->set_composer(DataCommaSizeFromQString(d->composer_));
|
||||
pb->set_track(d->track_);
|
||||
pb->set_disc(d->disc_);
|
||||
pb->set_bpm(d->bpm_);
|
||||
pb->set_year(d->year_);
|
||||
pb->set_genre(DataCommaSizeFromQString(d->genre_));
|
||||
pb->set_comment(DataCommaSizeFromQString(d->comment_));
|
||||
pb->set_compilation(d->compilation_);
|
||||
pb->set_rating(d->rating_);
|
||||
pb->set_playcount(d->playcount_);
|
||||
pb->set_skipcount(d->skipcount_);
|
||||
pb->set_lastplayed(d->lastplayed_);
|
||||
pb->set_score(d->score_);
|
||||
pb->set_length_nanosec(length_nanosec());
|
||||
pb->set_bitrate(d->bitrate_);
|
||||
pb->set_samplerate(d->samplerate_);
|
||||
pb->set_url(url.constData(), url.size());
|
||||
pb->set_basefilename(DataCommaSizeFromQString(d->basefilename_));
|
||||
pb->set_mtime(d->mtime_);
|
||||
pb->set_ctime(d->ctime_);
|
||||
pb->set_filesize(d->filesize_);
|
||||
pb->set_suspicious_tags(d->suspicious_tags_);
|
||||
pb->set_art_automatic(DataCommaSizeFromQString(d->art_automatic_));
|
||||
pb->set_type(static_cast< ::pb::tagreader::SongMetadata_Type>(d->filetype_));
|
||||
}
|
||||
|
||||
void Song::InitFromQuery(const SqlRow& q, bool reliable_metadata, int col) {
|
||||
|
@ -1371,92 +1103,11 @@ bool Song::IsMetadataEqual(const Song& other) const {
|
|||
d->cue_path_ == other.d->cue_path_;
|
||||
}
|
||||
|
||||
void Song::SetTextFrame(const QString& id, const QString& value,
|
||||
TagLib::ID3v2::Tag* tag) {
|
||||
TagLib::ByteVector id_vector = id.toUtf8().constData();
|
||||
|
||||
// Remove the frame if it already exists
|
||||
while (tag->frameListMap().contains(id_vector) &&
|
||||
tag->frameListMap()[id_vector].size() != 0) {
|
||||
tag->removeFrame(tag->frameListMap()[id_vector].front());
|
||||
}
|
||||
|
||||
// Create and add a new frame
|
||||
TagLib::ID3v2::TextIdentificationFrame* frame =
|
||||
new TagLib::ID3v2::TextIdentificationFrame(id.toUtf8().constData(),
|
||||
TagLib::String::UTF8);
|
||||
frame->setText(QStringToTaglibString(value));
|
||||
tag->addFrame(frame);
|
||||
}
|
||||
|
||||
bool Song::IsEditable() const {
|
||||
return d->valid_ && !d->url_.isEmpty() && !is_stream() &&
|
||||
d->filetype_ != Type_Unknown && !has_cue();
|
||||
}
|
||||
|
||||
bool Song::Save() const {
|
||||
const QString filename = d->url_.toLocalFile();
|
||||
if (filename.isNull())
|
||||
return false;
|
||||
|
||||
QMutexLocker l(&sTaglibMutex);
|
||||
scoped_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
|
||||
|
||||
if (!fileref || fileref->isNull()) // The file probably doesn't exist
|
||||
return false;
|
||||
|
||||
fileref->tag()->setTitle(QStringToTaglibString(d->title_));
|
||||
fileref->tag()->setArtist(QStringToTaglibString(d->artist_));
|
||||
fileref->tag()->setAlbum(QStringToTaglibString(d->album_));
|
||||
fileref->tag()->setGenre(QStringToTaglibString(d->genre_));
|
||||
fileref->tag()->setComment(QStringToTaglibString(d->comment_));
|
||||
fileref->tag()->setYear(d->year_);
|
||||
fileref->tag()->setTrack(d->track_);
|
||||
|
||||
if (TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
|
||||
TagLib::ID3v2::Tag* tag = file->ID3v2Tag(true);
|
||||
SetTextFrame("TPOS", d->disc_ <= 0 -1 ? QString() : QString::number(d->disc_), tag);
|
||||
SetTextFrame("TBPM", d->bpm_ <= 0 -1 ? QString() : QString::number(d->bpm_), tag);
|
||||
SetTextFrame("TCOM", d->composer_, tag);
|
||||
SetTextFrame("TPE2", d->albumartist_, tag);
|
||||
SetTextFrame("TCMP", d->compilation_ ? "1" : "0", tag);
|
||||
}
|
||||
else if (TagLib::Ogg::Vorbis::File* file = dynamic_cast<TagLib::Ogg::Vorbis::File*>(fileref->file())) {
|
||||
TagLib::Ogg::XiphComment* tag = file->tag();
|
||||
tag->addField("COMPOSER", QStringToTaglibString(d->composer_), true);
|
||||
tag->addField("BPM", QStringToTaglibString(d->bpm_ <= 0 -1 ? QString() : QString::number(d->bpm_)), true);
|
||||
tag->addField("DISCNUMBER", QStringToTaglibString(d->disc_ <= 0 -1 ? QString() : QString::number(d->disc_)), true);
|
||||
tag->addField("COMPILATION", QStringToTaglibString(d->compilation_ ? "1" : "0"), true);
|
||||
}
|
||||
else if (TagLib::FLAC::File* file = dynamic_cast<TagLib::FLAC::File*>(fileref->file())) {
|
||||
TagLib::Ogg::XiphComment* tag = file->xiphComment();
|
||||
tag->addField("COMPOSER", QStringToTaglibString(d->composer_), true);
|
||||
tag->addField("BPM", QStringToTaglibString(d->bpm_ <= 0 -1 ? QString() : QString::number(d->bpm_)), true);
|
||||
tag->addField("DISCNUMBER", QStringToTaglibString(d->disc_ <= 0 -1 ? QString() : QString::number(d->disc_)), true);
|
||||
tag->addField("COMPILATION", QStringToTaglibString(d->compilation_ ? "1" : "0"), true);
|
||||
}
|
||||
|
||||
bool ret = fileref->save();
|
||||
#ifdef Q_OS_LINUX
|
||||
if (ret) {
|
||||
// Linux: inotify doesn't seem to notice the change to the file unless we
|
||||
// change the timestamps as well. (this is what touch does)
|
||||
utimensat(0, QFile::encodeName(filename).constData(), NULL, 0);
|
||||
}
|
||||
#endif // Q_OS_LINUX
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool Song::Save(const Song& song) {
|
||||
return song.Save();
|
||||
}
|
||||
|
||||
QFuture<bool> Song::BackgroundSave() const {
|
||||
QFuture<bool> future = QtConcurrent::run(&Song::Save, Song(*this));
|
||||
return future;
|
||||
}
|
||||
|
||||
bool Song::operator==(const Song& other) const {
|
||||
// TODO: this isn't working for radios
|
||||
return url() == other.url() &&
|
||||
|
@ -1468,79 +1119,6 @@ uint qHash(const Song& song) {
|
|||
return qHash(song.url().toString()) ^ qHash(song.beginning_nanosec());
|
||||
}
|
||||
|
||||
QImage Song::LoadEmbeddedArt(const QString& filename) {
|
||||
QImage ret;
|
||||
if (filename.isEmpty())
|
||||
return ret;
|
||||
|
||||
QMutexLocker l(&sTaglibMutex);
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
TagLib::FileRef ref(filename.toStdWString().c_str());
|
||||
#else
|
||||
TagLib::FileRef ref(QFile::encodeName(filename).constData());
|
||||
#endif
|
||||
|
||||
if (ref.isNull() || !ref.file())
|
||||
return ret;
|
||||
|
||||
// MP3
|
||||
TagLib::MPEG::File* file = dynamic_cast<TagLib::MPEG::File*>(ref.file());
|
||||
if (file && file->ID3v2Tag()) {
|
||||
TagLib::ID3v2::FrameList apic_frames = file->ID3v2Tag()->frameListMap()["APIC"];
|
||||
if (apic_frames.isEmpty())
|
||||
return ret;
|
||||
|
||||
TagLib::ID3v2::AttachedPictureFrame* pic =
|
||||
static_cast<TagLib::ID3v2::AttachedPictureFrame*>(apic_frames.front());
|
||||
|
||||
ret.loadFromData((const uchar*) pic->picture().data(), pic->picture().size());
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Ogg vorbis/speex
|
||||
TagLib::Ogg::XiphComment* xiph_comment =
|
||||
dynamic_cast<TagLib::Ogg::XiphComment*>(ref.file()->tag());
|
||||
|
||||
if (xiph_comment) {
|
||||
TagLib::Ogg::FieldListMap map = xiph_comment->fieldListMap();
|
||||
|
||||
// Ogg lacks a definitive standard for embedding cover art, but it seems
|
||||
// b64 encoding a field called COVERART is the general convention
|
||||
if (!map.contains("COVERART"))
|
||||
return ret;
|
||||
|
||||
QByteArray image_data_b64(map["COVERART"].toString().toCString());
|
||||
QByteArray image_data = QByteArray::fromBase64(image_data_b64);
|
||||
|
||||
if (!ret.loadFromData(image_data))
|
||||
ret.loadFromData(image_data_b64); //maybe it's not b64 after all
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
#ifdef TAGLIB_HAS_FLAC_PICTURELIST
|
||||
// Flac
|
||||
TagLib::FLAC::File* flac_file = dynamic_cast<TagLib::FLAC::File*>(ref.file());
|
||||
if (flac_file && flac_file->xiphComment()) {
|
||||
TagLib::List<TagLib::FLAC::Picture*> pics = flac_file->pictureList();
|
||||
if (!pics.isEmpty()) {
|
||||
// Use the first picture in the file - this could be made cleverer and
|
||||
// pick the front cover if it's present.
|
||||
|
||||
std::list<TagLib::FLAC::Picture*>::iterator it = pics.begin();
|
||||
TagLib::FLAC::Picture* picture = *it;
|
||||
|
||||
QByteArray image_data(picture->data().data(), picture->data().size());
|
||||
ret.loadFromData(image_data);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool Song::IsOnSameAlbum(const Song& other) const {
|
||||
if (is_compilation() != other.is_compilation())
|
||||
return false;
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
#include <QVariantMap>
|
||||
|
||||
#include "config.h"
|
||||
#include "tagreadermessages.pb.h"
|
||||
#include "engines/engine_fwd.h"
|
||||
|
||||
class QSqlQuery;
|
||||
|
@ -48,35 +49,13 @@ class QUrl;
|
|||
}
|
||||
#endif
|
||||
|
||||
namespace TagLib {
|
||||
class FileRef;
|
||||
class String;
|
||||
|
||||
namespace ID3v2 {
|
||||
class Tag;
|
||||
}
|
||||
}
|
||||
|
||||
class SqlRow;
|
||||
|
||||
|
||||
class FileRefFactory {
|
||||
public:
|
||||
virtual ~FileRefFactory() {}
|
||||
virtual TagLib::FileRef* GetFileRef(const QString& filename) = 0;
|
||||
};
|
||||
|
||||
class TagLibFileRefFactory : public FileRefFactory {
|
||||
public:
|
||||
virtual TagLib::FileRef* GetFileRef(const QString& filename);
|
||||
};
|
||||
|
||||
|
||||
class Song {
|
||||
public:
|
||||
Song();
|
||||
Song(const Song& other);
|
||||
Song(FileRefFactory* factory);
|
||||
~Song();
|
||||
|
||||
static const QStringList kColumns;
|
||||
|
@ -94,7 +73,8 @@ class Song {
|
|||
|
||||
static QString JoinSpec(const QString& table);
|
||||
|
||||
// Don't change these values - they're stored in the database
|
||||
// Don't change these values - they're stored in the database, and defined
|
||||
// in the tag reader protobuf.
|
||||
enum FileType {
|
||||
Type_Unknown = 0,
|
||||
Type_Asf = 1,
|
||||
|
@ -115,18 +95,10 @@ class Song {
|
|||
static QString TextForFiletype(FileType type);
|
||||
QString TextForFiletype() const { return TextForFiletype(filetype()); }
|
||||
|
||||
// Helper function to load embedded cover art from a music file. This is not
|
||||
// actually used by the Song class, but instead it is called by
|
||||
// AlbumCoverLoader and is here so it can lock on the taglib mutex.
|
||||
static QImage LoadEmbeddedArt(const QString& filename);
|
||||
// Checks if this Song can be properly initialized from it's media file.
|
||||
// This requires the 'filename' attribute to be set first.
|
||||
bool HasProperMediaFile() const;
|
||||
|
||||
// Constructors
|
||||
void Init(const QString& title, const QString& artist, const QString& album, qint64 length_nanosec);
|
||||
void Init(const QString& title, const QString& artist, const QString& album, qint64 beginning, qint64 end);
|
||||
void InitFromFile(const QString& filename, int directory_id);
|
||||
void InitFromProtobuf(const pb::tagreader::SongMetadata& pb);
|
||||
void InitFromQuery(const SqlRow& query, bool reliable_metadata, int col = 0);
|
||||
void InitFromFilePartial(const QString& filename); // Just store the filename: incomplete but fast
|
||||
#ifdef HAVE_LIBLASTFM
|
||||
|
@ -150,7 +122,6 @@ class Song {
|
|||
void ToWmdm(IWMDMMetaData* metadata) const;
|
||||
#endif
|
||||
|
||||
static QString Decode(const TagLib::String& tag, const QTextCodec* codec = NULL);
|
||||
static QString Decode(const QString& tag, const QTextCodec* codec = NULL);
|
||||
|
||||
// Save
|
||||
|
@ -160,6 +131,7 @@ class Song {
|
|||
void ToLastFM(lastfm::Track* track) const;
|
||||
#endif
|
||||
void ToXesam(QVariantMap* map) const;
|
||||
void ToProtobuf(pb::tagreader::SongMetadata* pb) const;
|
||||
|
||||
// Simple accessors
|
||||
bool is_valid() const;
|
||||
|
@ -236,8 +208,6 @@ class Song {
|
|||
|
||||
// Setters
|
||||
bool IsEditable() const;
|
||||
bool Save() const;
|
||||
QFuture<bool> BackgroundSave() const;
|
||||
|
||||
void set_id(int id);
|
||||
void set_valid(bool v);
|
||||
|
@ -296,24 +266,9 @@ class Song {
|
|||
|
||||
Song& operator=(const Song& other);
|
||||
|
||||
private:
|
||||
void GuessFileType(TagLib::FileRef* fileref);
|
||||
static bool Save(const Song& song);
|
||||
|
||||
// Helper methods for taglib
|
||||
static void SetTextFrame(const QString& id, const QString& value,
|
||||
TagLib::ID3v2::Tag* tag);
|
||||
void ParseFMPSFrame(const QString& name, const QString& value);
|
||||
|
||||
private:
|
||||
struct Private;
|
||||
QSharedDataPointer<Private> d;
|
||||
|
||||
FileRefFactory* factory_;
|
||||
|
||||
static TagLibFileRefFactory kDefaultFactory;
|
||||
|
||||
static QMutex sTaglibMutex;
|
||||
};
|
||||
Q_DECLARE_METATYPE(Song);
|
||||
|
||||
|
|
|
@ -19,13 +19,14 @@
|
|||
#include "songloader.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/song.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "internet/fixlastfm.h"
|
||||
#include "library/librarybackend.h"
|
||||
#include "library/sqlrow.h"
|
||||
#include "playlistparsers/parserbase.h"
|
||||
#include "playlistparsers/cueparser.h"
|
||||
#include "playlistparsers/playlistparser.h"
|
||||
#include "internet/fixlastfm.h"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QDirIterator>
|
||||
|
@ -287,7 +288,7 @@ SongLoader::Result SongLoader::LoadLocal(const QString& filename, bool block,
|
|||
// it's a normal media file
|
||||
} else {
|
||||
Song song;
|
||||
song.InitFromFile(filename, -1);
|
||||
TagReaderClient::Instance()->ReadFileBlocking(filename, &song);
|
||||
|
||||
song_list << song;
|
||||
|
||||
|
@ -317,7 +318,7 @@ void SongLoader::EffectiveSongsLoad() {
|
|||
} else {
|
||||
// it's a normal media file
|
||||
QString filename = song.url().toLocalFile();
|
||||
song.InitFromFile(filename, -1);
|
||||
TagReaderClient::Instance()->ReadFileBlocking(filename, &song);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "tagreaderclient.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
#include <QTcpServer>
|
||||
|
||||
|
||||
const char* TagReaderClient::kWorkerExecutableName = "clementine-tagreader";
|
||||
TagReaderClient* TagReaderClient::sInstance = NULL;
|
||||
|
||||
TagReaderClient::TagReaderClient(QObject* parent)
|
||||
: QObject(parent),
|
||||
worker_pool_(new WorkerPool<HandlerType>(this))
|
||||
{
|
||||
sInstance = this;
|
||||
|
||||
worker_pool_->SetExecutableName(kWorkerExecutableName);
|
||||
connect(worker_pool_, SIGNAL(WorkerFailedToStart()), SLOT(WorkerFailedToStart()));
|
||||
}
|
||||
|
||||
void TagReaderClient::Start() {
|
||||
worker_pool_->Start();
|
||||
}
|
||||
|
||||
void TagReaderClient::WorkerFailedToStart() {
|
||||
qLog(Error) << "The" << kWorkerExecutableName << "executable was not found"
|
||||
<< "in the current directory or on the PATH. Clementine will"
|
||||
<< "not be able to read music file tags without it.";
|
||||
}
|
||||
|
||||
TagReaderReply* TagReaderClient::ReadFile(const QString& filename) {
|
||||
pb::tagreader::Message message;
|
||||
pb::tagreader::ReadFileRequest* req = message.mutable_read_file_request();
|
||||
|
||||
req->set_filename(DataCommaSizeFromQString(filename));
|
||||
|
||||
return worker_pool_->NextHandler()->SendMessageWithReply(&message);
|
||||
}
|
||||
|
||||
TagReaderReply* TagReaderClient::SaveFile(const QString& filename, const Song& metadata) {
|
||||
pb::tagreader::Message message;
|
||||
pb::tagreader::SaveFileRequest* req = message.mutable_save_file_request();
|
||||
|
||||
req->set_filename(DataCommaSizeFromQString(filename));
|
||||
metadata.ToProtobuf(req->mutable_metadata());
|
||||
|
||||
return worker_pool_->NextHandler()->SendMessageWithReply(&message);
|
||||
}
|
||||
|
||||
TagReaderReply* TagReaderClient::IsMediaFile(const QString& filename) {
|
||||
pb::tagreader::Message message;
|
||||
pb::tagreader::IsMediaFileRequest* req = message.mutable_is_media_file_request();
|
||||
|
||||
req->set_filename(DataCommaSizeFromQString(filename));
|
||||
|
||||
return worker_pool_->NextHandler()->SendMessageWithReply(&message);
|
||||
}
|
||||
|
||||
TagReaderReply* TagReaderClient::LoadEmbeddedArt(const QString& filename) {
|
||||
pb::tagreader::Message message;
|
||||
pb::tagreader::LoadEmbeddedArtRequest* req = message.mutable_load_embedded_art_request();
|
||||
|
||||
req->set_filename(DataCommaSizeFromQString(filename));
|
||||
|
||||
return worker_pool_->NextHandler()->SendMessageWithReply(&message);
|
||||
}
|
||||
|
||||
void TagReaderClient::ReadFileBlocking(const QString& filename, Song* song) {
|
||||
Q_ASSERT(QThread::currentThread() != thread());
|
||||
|
||||
TagReaderReply* reply = ReadFile(filename);
|
||||
if (reply->WaitForFinished()) {
|
||||
song->InitFromProtobuf(reply->message().read_file_response().metadata());
|
||||
}
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
bool TagReaderClient::SaveFileBlocking(const QString& filename, const Song& metadata) {
|
||||
Q_ASSERT(QThread::currentThread() != thread());
|
||||
|
||||
bool ret = false;
|
||||
|
||||
TagReaderReply* reply = SaveFile(filename, metadata);
|
||||
if (reply->WaitForFinished()) {
|
||||
ret = reply->message().save_file_response().success();
|
||||
}
|
||||
reply->deleteLater();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool TagReaderClient::IsMediaFileBlocking(const QString& filename) {
|
||||
Q_ASSERT(QThread::currentThread() != thread());
|
||||
|
||||
bool ret = false;
|
||||
|
||||
TagReaderReply* reply = IsMediaFile(filename);
|
||||
if (reply->WaitForFinished()) {
|
||||
ret = reply->message().is_media_file_response().success();
|
||||
}
|
||||
reply->deleteLater();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
QImage TagReaderClient::LoadEmbeddedArtBlocking(const QString& filename) {
|
||||
Q_ASSERT(QThread::currentThread() != thread());
|
||||
|
||||
QImage ret;
|
||||
|
||||
TagReaderReply* reply = LoadEmbeddedArt(filename);
|
||||
if (reply->WaitForFinished()) {
|
||||
const std::string& data_str =
|
||||
reply->message().load_embedded_art_response().data();
|
||||
ret.loadFromData(QByteArray(data_str.data(), data_str.size()));
|
||||
}
|
||||
reply->deleteLater();
|
||||
|
||||
return ret;
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2011, David Sansome <me@davidsansome.com>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef TAGREADERCLIENT_H
|
||||
#define TAGREADERCLIENT_H
|
||||
|
||||
#include "song.h"
|
||||
#include "tagreadermessages.pb.h"
|
||||
#include "core/messagehandler.h"
|
||||
#include "core/workerpool.h"
|
||||
|
||||
#include <QStringList>
|
||||
|
||||
class QLocalServer;
|
||||
class QProcess;
|
||||
|
||||
class TagReaderClient : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
TagReaderClient(QObject* parent = 0);
|
||||
|
||||
typedef AbstractMessageHandler<pb::tagreader::Message> HandlerType;
|
||||
typedef HandlerType::ReplyType ReplyType;
|
||||
|
||||
static const char* kWorkerExecutableName;
|
||||
|
||||
void Start();
|
||||
|
||||
ReplyType* ReadFile(const QString& filename);
|
||||
ReplyType* SaveFile(const QString& filename, const Song& metadata);
|
||||
ReplyType* IsMediaFile(const QString& filename);
|
||||
ReplyType* LoadEmbeddedArt(const QString& filename);
|
||||
|
||||
// Convenience functions that call the above functions and wait for a
|
||||
// response. These block the calling thread with a semaphore, and must NOT
|
||||
// be called from the TagReaderClient's thread.
|
||||
void ReadFileBlocking(const QString& filename, Song* song);
|
||||
bool SaveFileBlocking(const QString& filename, const Song& metadata);
|
||||
bool IsMediaFileBlocking(const QString& filename);
|
||||
QImage LoadEmbeddedArtBlocking(const QString& filename);
|
||||
|
||||
// TODO: Make this not a singleton
|
||||
static TagReaderClient* Instance() { return sInstance; }
|
||||
|
||||
private slots:
|
||||
void WorkerFailedToStart();
|
||||
|
||||
private:
|
||||
static TagReaderClient* sInstance;
|
||||
|
||||
WorkerPool<HandlerType>* worker_pool_;
|
||||
QList<pb::tagreader::Message> message_queue_;
|
||||
};
|
||||
|
||||
typedef TagReaderClient::ReplyType TagReaderReply;
|
||||
|
||||
#endif // TAGREADERCLIENT_H
|
|
@ -19,6 +19,7 @@
|
|||
#include "config.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/network.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/utilities.h"
|
||||
#include "internet/internetmodel.h"
|
||||
|
||||
|
@ -137,7 +138,9 @@ AlbumCoverLoader::TryLoadResult AlbumCoverLoader::TryLoadImage(
|
|||
return TryLoadResult(false, true, default_);
|
||||
|
||||
if (filename == Song::kEmbeddedCover && !task.song_filename.isEmpty()) {
|
||||
QImage taglib_image = Song::LoadEmbeddedArt(task.song_filename);
|
||||
const QImage taglib_image =
|
||||
TagReaderClient::Instance()->LoadEmbeddedArtBlocking(task.song_filename);
|
||||
|
||||
if (!taglib_image.isNull())
|
||||
return TryLoadResult(false, true, ScaleAndPad(taglib_image));
|
||||
}
|
||||
|
@ -263,7 +266,8 @@ QPixmap AlbumCoverLoader::TryLoadPixmap(const QString& automatic,
|
|||
ret.load(manual);
|
||||
if (ret.isNull()) {
|
||||
if (automatic == Song::kEmbeddedCover && !filename.isNull())
|
||||
ret = QPixmap::fromImage(Song::LoadEmbeddedArt(filename));
|
||||
ret = QPixmap::fromImage(
|
||||
TagReaderClient::Instance()->LoadEmbeddedArtBlocking(filename));
|
||||
else if (!automatic.isEmpty())
|
||||
ret.load(automatic);
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
#include "internet/spotifyserver.h"
|
||||
#include "internet/spotifyservice.h"
|
||||
#include "playlist/songmimedata.h"
|
||||
#include "spotifyblob/common/spotifymessagehandler.h"
|
||||
|
||||
SpotifySearchProvider::SpotifySearchProvider(QObject* parent)
|
||||
: SearchProvider(parent),
|
||||
|
@ -44,12 +43,12 @@ SpotifyServer* SpotifySearchProvider::server() {
|
|||
return NULL;
|
||||
|
||||
server_ = service_->server();
|
||||
connect(server_, SIGNAL(SearchResults(spotify_pb::SearchResponse)),
|
||||
SLOT(SearchFinishedSlot(spotify_pb::SearchResponse)));
|
||||
connect(server_, SIGNAL(SearchResults(pb::spotify::SearchResponse)),
|
||||
SLOT(SearchFinishedSlot(pb::spotify::SearchResponse)));
|
||||
connect(server_, SIGNAL(ImageLoaded(QString,QImage)),
|
||||
SLOT(ArtLoadedSlot(QString,QImage)));
|
||||
connect(server_, SIGNAL(AlbumBrowseResults(spotify_pb::BrowseAlbumResponse)),
|
||||
SLOT(AlbumBrowseResponse(spotify_pb::BrowseAlbumResponse)));
|
||||
connect(server_, SIGNAL(AlbumBrowseResults(pb::spotify::BrowseAlbumResponse)),
|
||||
SLOT(AlbumBrowseResponse(pb::spotify::BrowseAlbumResponse)));
|
||||
connect(server_, SIGNAL(destroyed()), SLOT(ServerDestroyed()));
|
||||
|
||||
return server_;
|
||||
|
@ -75,7 +74,7 @@ void SpotifySearchProvider::SearchAsync(int id, const QString& query) {
|
|||
queries_[query_string] = state;
|
||||
}
|
||||
|
||||
void SpotifySearchProvider::SearchFinishedSlot(const spotify_pb::SearchResponse& response) {
|
||||
void SpotifySearchProvider::SearchFinishedSlot(const pb::spotify::SearchResponse& response) {
|
||||
QString query_string = QString::fromUtf8(response.request().query().c_str());
|
||||
QMap<QString, PendingState>::iterator it = queries_.find(query_string);
|
||||
if (it == queries_.end())
|
||||
|
@ -86,7 +85,7 @@ void SpotifySearchProvider::SearchFinishedSlot(const spotify_pb::SearchResponse&
|
|||
|
||||
ResultList ret;
|
||||
for (int i=0; i < response.result_size() ; ++i) {
|
||||
const spotify_pb::Track& track = response.result(i);
|
||||
const pb::spotify::Track& track = response.result(i);
|
||||
|
||||
Result result(this);
|
||||
result.type_ = globalsearch::Type_Track;
|
||||
|
@ -97,7 +96,7 @@ void SpotifySearchProvider::SearchFinishedSlot(const spotify_pb::SearchResponse&
|
|||
}
|
||||
|
||||
for (int i=0 ; i<response.album_size() ; ++i) {
|
||||
const spotify_pb::Album& album = response.album(i);
|
||||
const pb::spotify::Album& album = response.album(i);
|
||||
|
||||
Result result(this);
|
||||
result.type_ = globalsearch::Type_Album;
|
||||
|
@ -174,7 +173,7 @@ void SpotifySearchProvider::LoadTracksAsync(int id, const Result& result) {
|
|||
}
|
||||
}
|
||||
|
||||
void SpotifySearchProvider::AlbumBrowseResponse(const spotify_pb::BrowseAlbumResponse& response) {
|
||||
void SpotifySearchProvider::AlbumBrowseResponse(const pb::spotify::BrowseAlbumResponse& response) {
|
||||
QString uri = QStringFromStdString(response.uri());
|
||||
QMap<QString, int>::iterator it = pending_tracks_.find(uri);
|
||||
if (it == pending_tracks_.end())
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
#define SPOTIFYSEARCHPROVIDER_H
|
||||
|
||||
#include "searchprovider.h"
|
||||
#include "spotifyblob/common/spotifymessages.pb.h"
|
||||
#include "spotifymessages.pb.h"
|
||||
|
||||
class SpotifyServer;
|
||||
class SpotifyService;
|
||||
|
@ -40,10 +40,10 @@ public:
|
|||
|
||||
private slots:
|
||||
void ServerDestroyed();
|
||||
void SearchFinishedSlot(const spotify_pb::SearchResponse& response);
|
||||
void SearchFinishedSlot(const pb::spotify::SearchResponse& response);
|
||||
void ArtLoadedSlot(const QString& id, const QImage& image);
|
||||
|
||||
void AlbumBrowseResponse(const spotify_pb::BrowseAlbumResponse& response);
|
||||
void AlbumBrowseResponse(const pb::spotify::BrowseAlbumResponse& response);
|
||||
|
||||
private:
|
||||
SpotifyServer* server();
|
||||
|
|
|
@ -19,18 +19,15 @@
|
|||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
#include "spotifyblob/common/spotifymessages.pb.h"
|
||||
#include "spotifyblob/common/spotifymessagehandler.h"
|
||||
#include "spotifymessages.pb.h"
|
||||
|
||||
#include <QTcpServer>
|
||||
#include <QTcpSocket>
|
||||
#include <QTimer>
|
||||
|
||||
SpotifyServer::SpotifyServer(QObject* parent)
|
||||
: QObject(parent),
|
||||
: AbstractMessageHandler<pb::spotify::Message>(NULL, parent),
|
||||
server_(new QTcpServer(this)),
|
||||
protocol_socket_(NULL),
|
||||
handler_(NULL),
|
||||
logged_in_(false)
|
||||
{
|
||||
connect(server_, SIGNAL(newConnection()), SLOT(NewConnection()));
|
||||
|
@ -47,41 +44,39 @@ int SpotifyServer::server_port() const {
|
|||
}
|
||||
|
||||
void SpotifyServer::NewConnection() {
|
||||
delete protocol_socket_;
|
||||
delete handler_;
|
||||
QTcpSocket* socket = server_->nextPendingConnection();
|
||||
SetDevice(socket);
|
||||
|
||||
protocol_socket_ = server_->nextPendingConnection();
|
||||
handler_ = new SpotifyMessageHandler(protocol_socket_, this);
|
||||
connect(handler_, SIGNAL(MessageArrived(spotify_pb::SpotifyMessage)),
|
||||
SLOT(HandleMessage(spotify_pb::SpotifyMessage)));
|
||||
|
||||
qLog(Info) << "Connection from port" << protocol_socket_->peerPort();
|
||||
qLog(Info) << "Connection from port" << socket->peerPort();
|
||||
|
||||
// Send any login messages that were queued before the client connected
|
||||
foreach (const spotify_pb::SpotifyMessage& message, queued_login_messages_) {
|
||||
SendMessage(message);
|
||||
foreach (const pb::spotify::Message& message, queued_login_messages_) {
|
||||
SendOrQueueMessage(message);
|
||||
}
|
||||
queued_login_messages_.clear();
|
||||
|
||||
// Don't take any more connections from clients
|
||||
disconnect(server_, SIGNAL(newConnection()), this, 0);
|
||||
}
|
||||
|
||||
void SpotifyServer::SendMessage(const spotify_pb::SpotifyMessage& message) {
|
||||
void SpotifyServer::SendOrQueueMessage(const pb::spotify::Message& message) {
|
||||
const bool is_login_message = message.has_login_request();
|
||||
|
||||
QList<spotify_pb::SpotifyMessage>* queue =
|
||||
QList<pb::spotify::Message>* queue =
|
||||
is_login_message ? &queued_login_messages_ : &queued_messages_;
|
||||
|
||||
if (!protocol_socket_ || (!is_login_message && !logged_in_)) {
|
||||
if (!device_ || (!is_login_message && !logged_in_)) {
|
||||
queue->append(message);
|
||||
} else {
|
||||
handler_->SendMessage(message);
|
||||
SendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
void SpotifyServer::Login(const QString& username, const QString& password,
|
||||
spotify_pb::Bitrate bitrate, bool volume_normalisation) {
|
||||
spotify_pb::SpotifyMessage message;
|
||||
pb::spotify::Bitrate bitrate, bool volume_normalisation) {
|
||||
pb::spotify::Message message;
|
||||
|
||||
spotify_pb::LoginRequest* request = message.mutable_login_request();
|
||||
pb::spotify::LoginRequest* request = message.mutable_login_request();
|
||||
request->set_username(DataCommaSizeFromQString(username));
|
||||
if (!password.isEmpty()) {
|
||||
request->set_password(DataCommaSizeFromQString(password));
|
||||
|
@ -89,28 +84,28 @@ void SpotifyServer::Login(const QString& username, const QString& password,
|
|||
request->mutable_playback_settings()->set_bitrate(bitrate);
|
||||
request->mutable_playback_settings()->set_volume_normalisation(volume_normalisation);
|
||||
|
||||
SendMessage(message);
|
||||
SendOrQueueMessage(message);
|
||||
}
|
||||
|
||||
void SpotifyServer::SetPlaybackSettings(spotify_pb::Bitrate bitrate, bool volume_normalisation) {
|
||||
spotify_pb::SpotifyMessage message;
|
||||
void SpotifyServer::SetPlaybackSettings(pb::spotify::Bitrate bitrate, bool volume_normalisation) {
|
||||
pb::spotify::Message message;
|
||||
|
||||
spotify_pb::PlaybackSettings* request = message.mutable_set_playback_settings_request();
|
||||
pb::spotify::PlaybackSettings* request = message.mutable_set_playback_settings_request();
|
||||
request->set_bitrate(bitrate);
|
||||
request->set_volume_normalisation(volume_normalisation);
|
||||
|
||||
SendMessage(message);
|
||||
SendOrQueueMessage(message);
|
||||
}
|
||||
|
||||
void SpotifyServer::HandleMessage(const spotify_pb::SpotifyMessage& message) {
|
||||
void SpotifyServer::MessageArrived(const pb::spotify::Message& message) {
|
||||
if (message.has_login_response()) {
|
||||
const spotify_pb::LoginResponse& response = message.login_response();
|
||||
const pb::spotify::LoginResponse& response = message.login_response();
|
||||
logged_in_ = response.success();
|
||||
|
||||
if (response.success()) {
|
||||
// Send any messages that were queued before the client logged in
|
||||
foreach (const spotify_pb::SpotifyMessage& message, queued_messages_) {
|
||||
SendMessage(message);
|
||||
foreach (const pb::spotify::Message& message, queued_messages_) {
|
||||
SendOrQueueMessage(message);
|
||||
}
|
||||
queued_messages_.clear();
|
||||
}
|
||||
|
@ -120,18 +115,18 @@ void SpotifyServer::HandleMessage(const spotify_pb::SpotifyMessage& message) {
|
|||
} else if (message.has_playlists_updated()) {
|
||||
emit PlaylistsUpdated(message.playlists_updated());
|
||||
} else if (message.has_load_playlist_response()) {
|
||||
const spotify_pb::LoadPlaylistResponse& response = message.load_playlist_response();
|
||||
const pb::spotify::LoadPlaylistResponse& response = message.load_playlist_response();
|
||||
|
||||
switch (response.request().type()) {
|
||||
case spotify_pb::Inbox:
|
||||
case pb::spotify::Inbox:
|
||||
emit InboxLoaded(response);
|
||||
break;
|
||||
|
||||
case spotify_pb::Starred:
|
||||
case pb::spotify::Starred:
|
||||
emit StarredLoaded(response);
|
||||
break;
|
||||
|
||||
case spotify_pb::UserPlaylist:
|
||||
case pb::spotify::UserPlaylist:
|
||||
emit UserPlaylistLoaded(response);
|
||||
break;
|
||||
}
|
||||
|
@ -140,7 +135,7 @@ void SpotifyServer::HandleMessage(const spotify_pb::SpotifyMessage& message) {
|
|||
} else if (message.has_search_response()) {
|
||||
emit SearchResults(message.search_response());
|
||||
} else if (message.has_image_response()) {
|
||||
const spotify_pb::ImageResponse& response = message.image_response();
|
||||
const pb::spotify::ImageResponse& response = message.image_response();
|
||||
const QString id = QStringFromStdString(response.id());
|
||||
|
||||
if (response.has_data()) {
|
||||
|
@ -156,55 +151,55 @@ void SpotifyServer::HandleMessage(const spotify_pb::SpotifyMessage& message) {
|
|||
}
|
||||
}
|
||||
|
||||
void SpotifyServer::LoadPlaylist(spotify_pb::PlaylistType type, int index) {
|
||||
spotify_pb::SpotifyMessage message;
|
||||
spotify_pb::LoadPlaylistRequest* req = message.mutable_load_playlist_request();
|
||||
void SpotifyServer::LoadPlaylist(pb::spotify::PlaylistType type, int index) {
|
||||
pb::spotify::Message message;
|
||||
pb::spotify::LoadPlaylistRequest* req = message.mutable_load_playlist_request();
|
||||
|
||||
req->set_type(type);
|
||||
if (index != -1) {
|
||||
req->set_user_playlist_index(index);
|
||||
}
|
||||
|
||||
SendMessage(message);
|
||||
SendOrQueueMessage(message);
|
||||
}
|
||||
|
||||
void SpotifyServer::SyncPlaylist(
|
||||
spotify_pb::PlaylistType type, int index, bool offline) {
|
||||
spotify_pb::SpotifyMessage message;
|
||||
spotify_pb::SyncPlaylistRequest* req = message.mutable_sync_playlist_request();
|
||||
pb::spotify::PlaylistType type, int index, bool offline) {
|
||||
pb::spotify::Message message;
|
||||
pb::spotify::SyncPlaylistRequest* req = message.mutable_sync_playlist_request();
|
||||
req->mutable_request()->set_type(type);
|
||||
if (index != -1) {
|
||||
req->mutable_request()->set_user_playlist_index(index);
|
||||
}
|
||||
req->set_offline_sync(offline);
|
||||
|
||||
SendMessage(message);
|
||||
SendOrQueueMessage(message);
|
||||
}
|
||||
|
||||
void SpotifyServer::SyncInbox() {
|
||||
SyncPlaylist(spotify_pb::Inbox, -1, true);
|
||||
SyncPlaylist(pb::spotify::Inbox, -1, true);
|
||||
}
|
||||
|
||||
void SpotifyServer::SyncStarred() {
|
||||
SyncPlaylist(spotify_pb::Starred, -1, true);
|
||||
SyncPlaylist(pb::spotify::Starred, -1, true);
|
||||
}
|
||||
|
||||
void SpotifyServer::SyncUserPlaylist(int index) {
|
||||
Q_ASSERT(index >= 0);
|
||||
SyncPlaylist(spotify_pb::UserPlaylist, index, true);
|
||||
SyncPlaylist(pb::spotify::UserPlaylist, index, true);
|
||||
}
|
||||
|
||||
void SpotifyServer::LoadInbox() {
|
||||
LoadPlaylist(spotify_pb::Inbox);
|
||||
LoadPlaylist(pb::spotify::Inbox);
|
||||
}
|
||||
|
||||
void SpotifyServer::LoadStarred() {
|
||||
LoadPlaylist(spotify_pb::Starred);
|
||||
LoadPlaylist(pb::spotify::Starred);
|
||||
}
|
||||
|
||||
void SpotifyServer::LoadUserPlaylist(int index) {
|
||||
Q_ASSERT(index >= 0);
|
||||
LoadPlaylist(spotify_pb::UserPlaylist, index);
|
||||
LoadPlaylist(pb::spotify::UserPlaylist, index);
|
||||
}
|
||||
|
||||
void SpotifyServer::StartPlaybackLater(const QString& uri, quint16 port) {
|
||||
|
@ -218,44 +213,44 @@ void SpotifyServer::StartPlaybackLater(const QString& uri, quint16 port) {
|
|||
}
|
||||
|
||||
void SpotifyServer::StartPlayback(const QString& uri, quint16 port) {
|
||||
spotify_pb::SpotifyMessage message;
|
||||
spotify_pb::PlaybackRequest* req = message.mutable_playback_request();
|
||||
pb::spotify::Message message;
|
||||
pb::spotify::PlaybackRequest* req = message.mutable_playback_request();
|
||||
|
||||
req->set_track_uri(DataCommaSizeFromQString(uri));
|
||||
req->set_media_port(port);
|
||||
SendMessage(message);
|
||||
SendOrQueueMessage(message);
|
||||
}
|
||||
|
||||
void SpotifyServer::Seek(qint64 offset_bytes) {
|
||||
spotify_pb::SpotifyMessage message;
|
||||
spotify_pb::SeekRequest* req = message.mutable_seek_request();
|
||||
pb::spotify::Message message;
|
||||
pb::spotify::SeekRequest* req = message.mutable_seek_request();
|
||||
|
||||
req->set_offset_bytes(offset_bytes);
|
||||
SendMessage(message);
|
||||
SendOrQueueMessage(message);
|
||||
}
|
||||
|
||||
void SpotifyServer::Search(const QString& text, int limit, int limit_album) {
|
||||
spotify_pb::SpotifyMessage message;
|
||||
spotify_pb::SearchRequest* req = message.mutable_search_request();
|
||||
pb::spotify::Message message;
|
||||
pb::spotify::SearchRequest* req = message.mutable_search_request();
|
||||
|
||||
req->set_query(DataCommaSizeFromQString(text));
|
||||
req->set_limit(limit);
|
||||
req->set_limit_album(limit_album);
|
||||
SendMessage(message);
|
||||
SendOrQueueMessage(message);
|
||||
}
|
||||
|
||||
void SpotifyServer::LoadImage(const QString& id) {
|
||||
spotify_pb::SpotifyMessage message;
|
||||
spotify_pb::ImageRequest* req = message.mutable_image_request();
|
||||
pb::spotify::Message message;
|
||||
pb::spotify::ImageRequest* req = message.mutable_image_request();
|
||||
|
||||
req->set_id(DataCommaSizeFromQString(id));
|
||||
SendMessage(message);
|
||||
SendOrQueueMessage(message);
|
||||
}
|
||||
|
||||
void SpotifyServer::AlbumBrowse(const QString& uri) {
|
||||
spotify_pb::SpotifyMessage message;
|
||||
spotify_pb::BrowseAlbumRequest* req = message.mutable_browse_album_request();
|
||||
pb::spotify::Message message;
|
||||
pb::spotify::BrowseAlbumRequest* req = message.mutable_browse_album_request();
|
||||
|
||||
req->set_uri(DataCommaSizeFromQString(uri));
|
||||
SendMessage(message);
|
||||
SendOrQueueMessage(message);
|
||||
}
|
||||
|
|
|
@ -18,17 +18,17 @@
|
|||
#ifndef SPOTIFYSERVER_H
|
||||
#define SPOTIFYSERVER_H
|
||||
|
||||
#include "spotifyblob/common/spotifymessages.pb.h"
|
||||
#include "spotifymessages.pb.h"
|
||||
#include "core/messagehandler.h"
|
||||
|
||||
#include <QImage>
|
||||
#include <QObject>
|
||||
|
||||
class SpotifyMessageHandler;
|
||||
|
||||
class QTcpServer;
|
||||
class QTcpSocket;
|
||||
|
||||
class SpotifyServer : public QObject {
|
||||
class SpotifyServer : public AbstractMessageHandler<pb::spotify::Message> {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
@ -36,7 +36,7 @@ public:
|
|||
|
||||
void Init();
|
||||
void Login(const QString& username, const QString& password,
|
||||
spotify_pb::Bitrate bitrate, bool volume_normalisation);
|
||||
pb::spotify::Bitrate bitrate, bool volume_normalisation);
|
||||
|
||||
void LoadStarred();
|
||||
void SyncStarred();
|
||||
|
@ -48,7 +48,7 @@ public:
|
|||
void Search(const QString& text, int limit, int limit_album = 0);
|
||||
void LoadImage(const QString& id);
|
||||
void AlbumBrowse(const QString& uri);
|
||||
void SetPlaybackSettings(spotify_pb::Bitrate bitrate, bool volume_normalisation);
|
||||
void SetPlaybackSettings(pb::spotify::Bitrate bitrate, bool volume_normalisation);
|
||||
|
||||
int server_port() const;
|
||||
|
||||
|
@ -58,34 +58,34 @@ public slots:
|
|||
|
||||
signals:
|
||||
void LoginCompleted(bool success, const QString& error,
|
||||
spotify_pb::LoginResponse_Error error_code);
|
||||
void PlaylistsUpdated(const spotify_pb::Playlists& playlists);
|
||||
pb::spotify::LoginResponse_Error error_code);
|
||||
void PlaylistsUpdated(const pb::spotify::Playlists& playlists);
|
||||
|
||||
void StarredLoaded(const spotify_pb::LoadPlaylistResponse& response);
|
||||
void InboxLoaded(const spotify_pb::LoadPlaylistResponse& response);
|
||||
void UserPlaylistLoaded(const spotify_pb::LoadPlaylistResponse& response);
|
||||
void StarredLoaded(const pb::spotify::LoadPlaylistResponse& response);
|
||||
void InboxLoaded(const pb::spotify::LoadPlaylistResponse& response);
|
||||
void UserPlaylistLoaded(const pb::spotify::LoadPlaylistResponse& response);
|
||||
void PlaybackError(const QString& message);
|
||||
void SearchResults(const spotify_pb::SearchResponse& response);
|
||||
void SearchResults(const pb::spotify::SearchResponse& response);
|
||||
void ImageLoaded(const QString& id, const QImage& image);
|
||||
void SyncPlaylistProgress(const spotify_pb::SyncPlaylistProgress& progress);
|
||||
void AlbumBrowseResults(const spotify_pb::BrowseAlbumResponse& response);
|
||||
void SyncPlaylistProgress(const pb::spotify::SyncPlaylistProgress& progress);
|
||||
void AlbumBrowseResults(const pb::spotify::BrowseAlbumResponse& response);
|
||||
|
||||
protected:
|
||||
void MessageArrived(const pb::spotify::Message& message);
|
||||
|
||||
private slots:
|
||||
void NewConnection();
|
||||
void HandleMessage(const spotify_pb::SpotifyMessage& message);
|
||||
|
||||
private:
|
||||
void LoadPlaylist(spotify_pb::PlaylistType type, int index = -1);
|
||||
void SyncPlaylist(spotify_pb::PlaylistType type, int index, bool offline);
|
||||
void SendMessage(const spotify_pb::SpotifyMessage& message);
|
||||
void LoadPlaylist(pb::spotify::PlaylistType type, int index = -1);
|
||||
void SyncPlaylist(pb::spotify::PlaylistType type, int index, bool offline);
|
||||
void SendOrQueueMessage(const pb::spotify::Message& message);
|
||||
|
||||
QTcpServer* server_;
|
||||
QTcpSocket* protocol_socket_;
|
||||
SpotifyMessageHandler* handler_;
|
||||
bool logged_in_;
|
||||
|
||||
QList<spotify_pb::SpotifyMessage> queued_login_messages_;
|
||||
QList<spotify_pb::SpotifyMessage> queued_messages_;
|
||||
QList<pb::spotify::Message> queued_login_messages_;
|
||||
QList<pb::spotify::Message> queued_messages_;
|
||||
};
|
||||
|
||||
#endif // SPOTIFYSERVER_H
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#include "blobversion.h"
|
||||
#include "config.h"
|
||||
#include "internetmodel.h"
|
||||
#include "spotifyblobdownloader.h"
|
||||
|
@ -15,8 +16,6 @@
|
|||
#include "playlist/playlist.h"
|
||||
#include "playlist/playlistcontainer.h"
|
||||
#include "playlist/playlistmanager.h"
|
||||
#include "spotifyblob/common/blobversion.h"
|
||||
#include "spotifyblob/common/spotifymessagehandler.h"
|
||||
#include "widgets/didyoumean.h"
|
||||
#include "ui/iconloader.h"
|
||||
|
||||
|
@ -50,7 +49,7 @@ SpotifyService::SpotifyService(InternetModel* parent)
|
|||
context_menu_(NULL),
|
||||
search_delay_(new QTimer(this)),
|
||||
login_state_(LoginState_OtherError),
|
||||
bitrate_(spotify_pb::Bitrate320k),
|
||||
bitrate_(pb::spotify::Bitrate320k),
|
||||
volume_normalisation_(false)
|
||||
{
|
||||
// Build the search path for the binary blob.
|
||||
|
@ -136,7 +135,7 @@ void SpotifyService::Login(const QString& username, const QString& password) {
|
|||
}
|
||||
|
||||
void SpotifyService::LoginCompleted(bool success, const QString& error,
|
||||
spotify_pb::LoginResponse_Error error_code) {
|
||||
pb::spotify::LoginResponse_Error error_code) {
|
||||
if (login_task_id_) {
|
||||
model()->task_manager()->SetTaskFinished(login_task_id_);
|
||||
login_task_id_ = 0;
|
||||
|
@ -147,19 +146,19 @@ void SpotifyService::LoginCompleted(bool success, const QString& error,
|
|||
QString error_copy(error);
|
||||
|
||||
switch (error_code) {
|
||||
case spotify_pb::LoginResponse_Error_BadUsernameOrPassword:
|
||||
case pb::spotify::LoginResponse_Error_BadUsernameOrPassword:
|
||||
login_state_ = LoginState_BadCredentials;
|
||||
break;
|
||||
|
||||
case spotify_pb::LoginResponse_Error_UserBanned:
|
||||
case pb::spotify::LoginResponse_Error_UserBanned:
|
||||
login_state_ = LoginState_Banned;
|
||||
break;
|
||||
|
||||
case spotify_pb::LoginResponse_Error_UserNeedsPremium:
|
||||
case pb::spotify::LoginResponse_Error_UserNeedsPremium:
|
||||
login_state_ = LoginState_NoPremium;
|
||||
break;
|
||||
|
||||
case spotify_pb::LoginResponse_Error_ReloginFailed:
|
||||
case pb::spotify::LoginResponse_Error_ReloginFailed:
|
||||
if (login_state_ == LoginState_LoggedIn) {
|
||||
// This is the first time the relogin has failed - show a message this
|
||||
// time only.
|
||||
|
@ -205,8 +204,8 @@ void SpotifyService::ReloadSettings() {
|
|||
s.beginGroup(kSettingsGroup);
|
||||
|
||||
login_state_ = LoginState(s.value("login_state", LoginState_OtherError).toInt());
|
||||
bitrate_ = static_cast<spotify_pb::Bitrate>(
|
||||
s.value("bitrate", spotify_pb::Bitrate320k).toInt());
|
||||
bitrate_ = static_cast<pb::spotify::Bitrate>(
|
||||
s.value("bitrate", pb::spotify::Bitrate320k).toInt());
|
||||
volume_normalisation_ = s.value("volume_normalisation", false).toBool();
|
||||
|
||||
if (server_ && blob_process_) {
|
||||
|
@ -223,24 +222,24 @@ void SpotifyService::EnsureServerCreated(const QString& username,
|
|||
delete server_;
|
||||
server_ = new SpotifyServer(this);
|
||||
|
||||
connect(server_, SIGNAL(LoginCompleted(bool,QString,spotify_pb::LoginResponse_Error)),
|
||||
SLOT(LoginCompleted(bool,QString,spotify_pb::LoginResponse_Error)));
|
||||
connect(server_, SIGNAL(PlaylistsUpdated(spotify_pb::Playlists)),
|
||||
SLOT(PlaylistsUpdated(spotify_pb::Playlists)));
|
||||
connect(server_, SIGNAL(InboxLoaded(spotify_pb::LoadPlaylistResponse)),
|
||||
SLOT(InboxLoaded(spotify_pb::LoadPlaylistResponse)));
|
||||
connect(server_, SIGNAL(StarredLoaded(spotify_pb::LoadPlaylistResponse)),
|
||||
SLOT(StarredLoaded(spotify_pb::LoadPlaylistResponse)));
|
||||
connect(server_, SIGNAL(UserPlaylistLoaded(spotify_pb::LoadPlaylistResponse)),
|
||||
SLOT(UserPlaylistLoaded(spotify_pb::LoadPlaylistResponse)));
|
||||
connect(server_, SIGNAL(LoginCompleted(bool,QString,pb::spotify::LoginResponse_Error)),
|
||||
SLOT(LoginCompleted(bool,QString,pb::spotify::LoginResponse_Error)));
|
||||
connect(server_, SIGNAL(PlaylistsUpdated(pb::spotify::Playlists)),
|
||||
SLOT(PlaylistsUpdated(pb::spotify::Playlists)));
|
||||
connect(server_, SIGNAL(InboxLoaded(pb::spotify::LoadPlaylistResponse)),
|
||||
SLOT(InboxLoaded(pb::spotify::LoadPlaylistResponse)));
|
||||
connect(server_, SIGNAL(StarredLoaded(pb::spotify::LoadPlaylistResponse)),
|
||||
SLOT(StarredLoaded(pb::spotify::LoadPlaylistResponse)));
|
||||
connect(server_, SIGNAL(UserPlaylistLoaded(pb::spotify::LoadPlaylistResponse)),
|
||||
SLOT(UserPlaylistLoaded(pb::spotify::LoadPlaylistResponse)));
|
||||
connect(server_, SIGNAL(PlaybackError(QString)),
|
||||
SIGNAL(StreamError(QString)));
|
||||
connect(server_, SIGNAL(SearchResults(spotify_pb::SearchResponse)),
|
||||
SLOT(SearchResults(spotify_pb::SearchResponse)));
|
||||
connect(server_, SIGNAL(SearchResults(pb::spotify::SearchResponse)),
|
||||
SLOT(SearchResults(pb::spotify::SearchResponse)));
|
||||
connect(server_, SIGNAL(ImageLoaded(QString,QImage)),
|
||||
SIGNAL(ImageLoaded(QString,QImage)));
|
||||
connect(server_, SIGNAL(SyncPlaylistProgress(spotify_pb::SyncPlaylistProgress)),
|
||||
SLOT(SyncPlaylistProgress(spotify_pb::SyncPlaylistProgress)));
|
||||
connect(server_, SIGNAL(SyncPlaylistProgress(pb::spotify::SyncPlaylistProgress)),
|
||||
SLOT(SyncPlaylistProgress(pb::spotify::SyncPlaylistProgress)));
|
||||
|
||||
server_->Init();
|
||||
|
||||
|
@ -327,7 +326,7 @@ void SpotifyService::BlobDownloadFinished() {
|
|||
EnsureServerCreated();
|
||||
}
|
||||
|
||||
void SpotifyService::PlaylistsUpdated(const spotify_pb::Playlists& response) {
|
||||
void SpotifyService::PlaylistsUpdated(const pb::spotify::Playlists& response) {
|
||||
if (login_task_id_) {
|
||||
model()->task_manager()->SetTaskFinished(login_task_id_);
|
||||
login_task_id_ = 0;
|
||||
|
@ -367,7 +366,7 @@ void SpotifyService::PlaylistsUpdated(const spotify_pb::Playlists& response) {
|
|||
playlists_.clear();
|
||||
|
||||
for (int i=0 ; i<response.playlist_size() ; ++i) {
|
||||
const spotify_pb::Playlists::Playlist& msg = response.playlist(i);
|
||||
const pb::spotify::Playlists::Playlist& msg = response.playlist(i);
|
||||
|
||||
QStandardItem* item = new QStandardItem(QStringFromStdString(msg.name()));
|
||||
item->setData(InternetModel::Type_UserPlaylist, InternetModel::Role_Type);
|
||||
|
@ -383,13 +382,13 @@ void SpotifyService::PlaylistsUpdated(const spotify_pb::Playlists& response) {
|
|||
}
|
||||
}
|
||||
|
||||
bool SpotifyService::DoPlaylistsDiffer(const spotify_pb::Playlists& response) const {
|
||||
bool SpotifyService::DoPlaylistsDiffer(const pb::spotify::Playlists& response) const {
|
||||
if (playlists_.count() != response.playlist_size()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i=0 ; i<response.playlist_size() ; ++i) {
|
||||
const spotify_pb::Playlists::Playlist& msg = response.playlist(i);
|
||||
const pb::spotify::Playlists::Playlist& msg = response.playlist(i);
|
||||
const QStandardItem* item = PlaylistBySpotifyIndex(msg.index());
|
||||
|
||||
if (!item) {
|
||||
|
@ -404,11 +403,11 @@ bool SpotifyService::DoPlaylistsDiffer(const spotify_pb::Playlists& response) co
|
|||
return false;
|
||||
}
|
||||
|
||||
void SpotifyService::InboxLoaded(const spotify_pb::LoadPlaylistResponse& response) {
|
||||
void SpotifyService::InboxLoaded(const pb::spotify::LoadPlaylistResponse& response) {
|
||||
FillPlaylist(inbox_, response);
|
||||
}
|
||||
|
||||
void SpotifyService::StarredLoaded(const spotify_pb::LoadPlaylistResponse& response) {
|
||||
void SpotifyService::StarredLoaded(const pb::spotify::LoadPlaylistResponse& response) {
|
||||
FillPlaylist(starred_, response);
|
||||
}
|
||||
|
||||
|
@ -421,7 +420,7 @@ QStandardItem* SpotifyService::PlaylistBySpotifyIndex(int index) const {
|
|||
return NULL;
|
||||
}
|
||||
|
||||
void SpotifyService::UserPlaylistLoaded(const spotify_pb::LoadPlaylistResponse& response) {
|
||||
void SpotifyService::UserPlaylistLoaded(const pb::spotify::LoadPlaylistResponse& response) {
|
||||
// Find a playlist with this index
|
||||
QStandardItem* item = PlaylistBySpotifyIndex(response.request().user_playlist_index());
|
||||
if (item) {
|
||||
|
@ -429,7 +428,7 @@ void SpotifyService::UserPlaylistLoaded(const spotify_pb::LoadPlaylistResponse&
|
|||
}
|
||||
}
|
||||
|
||||
void SpotifyService::FillPlaylist(QStandardItem* item, const spotify_pb::LoadPlaylistResponse& response) {
|
||||
void SpotifyService::FillPlaylist(QStandardItem* item, const pb::spotify::LoadPlaylistResponse& response) {
|
||||
qLog(Debug) << "Filling playlist:" << item->text();
|
||||
if (item->hasChildren())
|
||||
item->removeRows(0, item->rowCount());
|
||||
|
@ -448,7 +447,7 @@ void SpotifyService::FillPlaylist(QStandardItem* item, const spotify_pb::LoadPla
|
|||
}
|
||||
}
|
||||
|
||||
void SpotifyService::SongFromProtobuf(const spotify_pb::Track& track, Song* song) {
|
||||
void SpotifyService::SongFromProtobuf(const pb::spotify::Track& track, Song* song) {
|
||||
song->set_rating(track.starred() ? 1.0 : 0.0);
|
||||
song->set_title(QStringFromStdString(track.title()));
|
||||
song->set_album(QStringFromStdString(track.album()));
|
||||
|
@ -544,7 +543,7 @@ void SpotifyService::DoSearch() {
|
|||
}
|
||||
}
|
||||
|
||||
void SpotifyService::SearchResults(const spotify_pb::SearchResponse& response) {
|
||||
void SpotifyService::SearchResults(const pb::spotify::SearchResponse& response) {
|
||||
if (QStringFromStdString(response.request().query()) != pending_search_) {
|
||||
qLog(Debug) << "Old search result for"
|
||||
<< QStringFromStdString(response.request().query())
|
||||
|
@ -622,17 +621,17 @@ void SpotifyService::LoadImage(const QString& id) {
|
|||
}
|
||||
|
||||
void SpotifyService::SyncPlaylistProgress(
|
||||
const spotify_pb::SyncPlaylistProgress& progress) {
|
||||
const pb::spotify::SyncPlaylistProgress& progress) {
|
||||
qLog(Debug) << "Sync progress:" << progress.sync_progress();
|
||||
int task_id = -1;
|
||||
switch (progress.request().type()) {
|
||||
case spotify_pb::Inbox:
|
||||
case pb::spotify::Inbox:
|
||||
task_id = inbox_sync_id_;
|
||||
break;
|
||||
case spotify_pb::Starred:
|
||||
case pb::spotify::Starred:
|
||||
task_id = starred_sync_id_;
|
||||
break;
|
||||
case spotify_pb::UserPlaylist: {
|
||||
case pb::spotify::UserPlaylist: {
|
||||
QMap<int, int>::const_iterator it = playlist_sync_ids_.constFind(
|
||||
progress.request().user_playlist_index());
|
||||
if (it != playlist_sync_ids_.constEnd()) {
|
||||
|
@ -650,7 +649,7 @@ void SpotifyService::SyncPlaylistProgress(
|
|||
model()->task_manager()->SetTaskProgress(task_id, progress.sync_progress(), 100);
|
||||
if (progress.sync_progress() == 100) {
|
||||
model()->task_manager()->SetTaskFinished(task_id);
|
||||
if (progress.request().type() == spotify_pb::UserPlaylist) {
|
||||
if (progress.request().type() == pb::spotify::UserPlaylist) {
|
||||
playlist_sync_ids_.remove(task_id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
#include "internetmodel.h"
|
||||
#include "internetservice.h"
|
||||
#include "spotifyblob/common/spotifymessages.pb.h"
|
||||
#include "spotifymessages.pb.h"
|
||||
|
||||
#include <QProcess>
|
||||
#include <QTimer>
|
||||
|
@ -71,7 +71,7 @@ public:
|
|||
LoginState login_state() const { return login_state_; }
|
||||
bool IsLoggedIn() const { return login_state_ == LoginState_LoggedIn; }
|
||||
|
||||
static void SongFromProtobuf(const spotify_pb::Track& track, Song* song);
|
||||
static void SongFromProtobuf(const pb::spotify::Track& track, Song* song);
|
||||
|
||||
signals:
|
||||
void BlobStateChanged();
|
||||
|
@ -86,24 +86,24 @@ protected:
|
|||
|
||||
private:
|
||||
void StartBlobProcess();
|
||||
void FillPlaylist(QStandardItem* item, const spotify_pb::LoadPlaylistResponse& response);
|
||||
void FillPlaylist(QStandardItem* item, const pb::spotify::LoadPlaylistResponse& response);
|
||||
void EnsureMenuCreated();
|
||||
|
||||
QStandardItem* PlaylistBySpotifyIndex(int index) const;
|
||||
bool DoPlaylistsDiffer(const spotify_pb::Playlists& response) const;
|
||||
bool DoPlaylistsDiffer(const pb::spotify::Playlists& response) const;
|
||||
|
||||
private slots:
|
||||
void EnsureServerCreated(const QString& username = QString(),
|
||||
const QString& password = QString());
|
||||
void BlobProcessError(QProcess::ProcessError error);
|
||||
void LoginCompleted(bool success, const QString& error,
|
||||
spotify_pb::LoginResponse_Error error_code);
|
||||
void PlaylistsUpdated(const spotify_pb::Playlists& response);
|
||||
void InboxLoaded(const spotify_pb::LoadPlaylistResponse& response);
|
||||
void StarredLoaded(const spotify_pb::LoadPlaylistResponse& response);
|
||||
void UserPlaylistLoaded(const spotify_pb::LoadPlaylistResponse& response);
|
||||
void SearchResults(const spotify_pb::SearchResponse& response);
|
||||
void SyncPlaylistProgress(const spotify_pb::SyncPlaylistProgress& progress);
|
||||
pb::spotify::LoginResponse_Error error_code);
|
||||
void PlaylistsUpdated(const pb::spotify::Playlists& response);
|
||||
void InboxLoaded(const pb::spotify::LoadPlaylistResponse& response);
|
||||
void StarredLoaded(const pb::spotify::LoadPlaylistResponse& response);
|
||||
void UserPlaylistLoaded(const pb::spotify::LoadPlaylistResponse& response);
|
||||
void SearchResults(const pb::spotify::SearchResponse& response);
|
||||
void SyncPlaylistProgress(const pb::spotify::SyncPlaylistProgress& progress);
|
||||
|
||||
void OpenSearchTab();
|
||||
void DoSearch();
|
||||
|
@ -141,7 +141,7 @@ private:
|
|||
QMap<int, int> playlist_sync_ids_;
|
||||
|
||||
LoginState login_state_;
|
||||
spotify_pb::Bitrate bitrate_;
|
||||
pb::spotify::Bitrate bitrate_;
|
||||
bool volume_normalisation_;
|
||||
};
|
||||
|
||||
|
|
|
@ -17,11 +17,11 @@
|
|||
|
||||
#include "spotifysettingspage.h"
|
||||
|
||||
#include "spotifymessages.pb.h"
|
||||
#include "spotifyservice.h"
|
||||
#include "internetmodel.h"
|
||||
#include "ui_spotifysettingspage.h"
|
||||
#include "core/network.h"
|
||||
#include "spotifyblob/common/spotifymessages.pb.h"
|
||||
#include "ui/iconloader.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
|
@ -56,9 +56,9 @@ SpotifySettingsPage::SpotifySettingsPage(SettingsDialog* dialog)
|
|||
ui_->login_state->AddCredentialField(ui_->password);
|
||||
ui_->login_state->AddCredentialGroup(ui_->account_group);
|
||||
|
||||
ui_->bitrate->addItem("96 " + tr("kbps"), spotify_pb::Bitrate96k);
|
||||
ui_->bitrate->addItem("160 " + tr("kbps"), spotify_pb::Bitrate160k);
|
||||
ui_->bitrate->addItem("320 " + tr("kbps"), spotify_pb::Bitrate320k);
|
||||
ui_->bitrate->addItem("96 " + tr("kbps"), pb::spotify::Bitrate96k);
|
||||
ui_->bitrate->addItem("160 " + tr("kbps"), pb::spotify::Bitrate160k);
|
||||
ui_->bitrate->addItem("320 " + tr("kbps"), pb::spotify::Bitrate320k);
|
||||
|
||||
BlobStateChanged();
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ void SpotifySettingsPage::Load() {
|
|||
validated_ = false;
|
||||
|
||||
ui_->bitrate->setCurrentIndex(ui_->bitrate->findData(
|
||||
s.value("bitrate", spotify_pb::Bitrate320k).toInt()));
|
||||
s.value("bitrate", pb::spotify::Bitrate320k).toInt()));
|
||||
ui_->volume_normalisation->setChecked(
|
||||
s.value("volume_normalisation", false).toBool());
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ class Library : public QObject {
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Library(BackgroundThread<Database>* db_thread, TaskManager* task_manager_,
|
||||
Library(BackgroundThread<Database>* db_thread, TaskManager* task_manager,
|
||||
QObject* parent);
|
||||
|
||||
static const char* kSongsTable;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
|
||||
#include "libraryplaylistitem.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
|
@ -36,7 +37,7 @@ QUrl LibraryPlaylistItem::Url() const {
|
|||
}
|
||||
|
||||
void LibraryPlaylistItem::Reload() {
|
||||
song_.InitFromFile(song_.url().toLocalFile(), song_.directory_id());
|
||||
TagReaderClient::Instance()->ReadFileBlocking(song_.url().toLocalFile(), &song_);
|
||||
}
|
||||
|
||||
bool LibraryPlaylistItem::InitFromQuery(const SqlRow& query) {
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#include "librarybackend.h"
|
||||
#include "core/filesystemwatcherinterface.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "playlistparsers/cueparser.h"
|
||||
|
||||
|
@ -451,7 +452,8 @@ void LibraryWatcher::UpdateNonCueAssociatedSong(const QString& file, const Song&
|
|||
}
|
||||
|
||||
Song song_on_disk;
|
||||
song_on_disk.InitFromFile(file, t->dir());
|
||||
song_on_disk.set_directory_id(t->dir());
|
||||
TagReaderClient::Instance()->ReadFileBlocking(file, &song_on_disk);
|
||||
|
||||
if(song_on_disk.is_valid()) {
|
||||
PreserveUserSetData(file, image, matching_song, &song_on_disk, t);
|
||||
|
@ -476,8 +478,10 @@ SongList LibraryWatcher::ScanNewFile(const QString& file, const QString& path,
|
|||
// media files. Playlist parser for CUEs considers every entry in sheet
|
||||
// valid and we don't want invalid media getting into library!
|
||||
foreach(const Song& cue_song, cue_parser_->Load(&cue, matching_cue, path)) {
|
||||
if(cue_song.url().toLocalFile() == file && cue_song.HasProperMediaFile()) {
|
||||
song_list << cue_song;
|
||||
if (cue_song.url().toLocalFile() == file) {
|
||||
if (TagReaderClient::Instance()->IsMediaFileBlocking(file)) {
|
||||
song_list << cue_song;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -488,7 +492,7 @@ SongList LibraryWatcher::ScanNewFile(const QString& file, const QString& path,
|
|||
// it's a normal media file
|
||||
} else {
|
||||
Song song;
|
||||
song.InitFromFile(file, -1);
|
||||
TagReaderClient::Instance()->ReadFileBlocking(file, &song);
|
||||
|
||||
if (song.is_valid()) {
|
||||
song_list << song;
|
||||
|
|
17
src/main.cpp
17
src/main.cpp
|
@ -35,6 +35,7 @@
|
|||
#include "core/player.h"
|
||||
#include "core/potranslator.h"
|
||||
#include "core/song.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "core/ubuntuunityhack.h"
|
||||
#include "core/utilities.h"
|
||||
|
@ -275,10 +276,6 @@ int main(int argc, char *argv[]) {
|
|||
}
|
||||
}
|
||||
|
||||
// Detect technically invalid usage of non-ASCII in ID3v1 tags.
|
||||
UniversalEncodingHandler handler;
|
||||
TagLib::ID3v1::Tag::setStringHandler(&handler);
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
// Force Clementine's menu to be shown in the Clementine window and not in
|
||||
// the Unity global menubar thing. See:
|
||||
|
@ -372,6 +369,14 @@ int main(int argc, char *argv[]) {
|
|||
CoverProviders cover_providers;
|
||||
cover_providers.AddProvider(new AmazonCoverProvider);
|
||||
|
||||
// Create the tag loader on another thread.
|
||||
TagReaderClient* tag_reader_client = new TagReaderClient;
|
||||
|
||||
QThread tag_reader_thread;
|
||||
tag_reader_thread.start();
|
||||
tag_reader_client->moveToThread(&tag_reader_thread);
|
||||
tag_reader_client->Start();
|
||||
|
||||
// Create some key objects
|
||||
scoped_ptr<BackgroundThread<Database> > database(
|
||||
new BackgroundThreadImplementation<Database, Database>(NULL));
|
||||
|
@ -431,6 +436,10 @@ int main(int argc, char *argv[]) {
|
|||
|
||||
int ret = a.exec();
|
||||
|
||||
tag_reader_client->deleteLater();
|
||||
tag_reader_thread.quit();
|
||||
tag_reader_thread.wait();
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
// The nvidia driver would cause Clementine (or any application that used
|
||||
// opengl) to use 100% cpu on shutdown. See:
|
||||
|
|
|
@ -25,8 +25,10 @@
|
|||
#include "songloaderinserter.h"
|
||||
#include "songmimedata.h"
|
||||
#include "songplaylistitem.h"
|
||||
#include "core/closure.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/modelfuturewatcher.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "internet/jamendoplaylistitem.h"
|
||||
#include "internet/jamendoservice.h"
|
||||
|
@ -321,24 +323,25 @@ bool Playlist::setData(const QModelIndex &index, const QVariant &value, int) {
|
|||
library_->AddOrUpdateSongs(SongList() << song);
|
||||
emit EditingFinished(index);
|
||||
} else {
|
||||
QFuture<bool> future = song.BackgroundSave();
|
||||
ModelFutureWatcher<bool>* watcher = new ModelFutureWatcher<bool>(index, this);
|
||||
watcher->setFuture(future);
|
||||
connect(watcher, SIGNAL(finished()), SLOT(SongSaveComplete()));
|
||||
TagReaderReply* reply = TagReaderClient::Instance()->SaveFile(
|
||||
song.url().toLocalFile(), song);
|
||||
|
||||
NewClosure(reply, SIGNAL(Finished(bool)),
|
||||
this, SLOT(SongSaveComplete(TagReaderReply*,QPersistentModelIndex)),
|
||||
reply, QPersistentModelIndex(index));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Playlist::SongSaveComplete() {
|
||||
ModelFutureWatcher<bool>* watcher = static_cast<ModelFutureWatcher<bool>*>(sender());
|
||||
watcher->deleteLater();
|
||||
const QPersistentModelIndex& index = watcher->index();
|
||||
if (index.isValid()) {
|
||||
void Playlist::SongSaveComplete(TagReaderReply* reply, const QPersistentModelIndex& index) {
|
||||
if (reply->is_successful() && index.isValid()) {
|
||||
QFuture<void> future = item_at(index.row())->BackgroundReload();
|
||||
ModelFutureWatcher<void>* watcher = new ModelFutureWatcher<void>(index, this);
|
||||
watcher->setFuture(future);
|
||||
connect(watcher, SIGNAL(finished()), SLOT(ItemReloadComplete()));
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
void Playlist::ItemReloadComplete() {
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
#include "playlistitem.h"
|
||||
#include "playlistsequence.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/song.h"
|
||||
#include "smartplaylists/generator_fwd.h"
|
||||
|
||||
|
@ -328,7 +329,7 @@ class Playlist : public QAbstractListModel {
|
|||
void TracksDequeued();
|
||||
void TracksEnqueued(const QModelIndex&, int begin, int end);
|
||||
void QueueLayoutChanged();
|
||||
void SongSaveComplete();
|
||||
void SongSaveComplete(TagReaderReply* reply, const QPersistentModelIndex& index);
|
||||
void ItemReloadComplete();
|
||||
void ItemsLoaded();
|
||||
void SongInsertVetoListenerDestroyed();
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
#include "playlistbackend.h"
|
||||
#include "songplaylistitem.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
|
||||
#include "library/sqlrow.h"
|
||||
|
||||
|
@ -40,8 +41,6 @@ bool SongPlaylistItem::InitFromQuery(const SqlRow& query) {
|
|||
|
||||
if (type() == "Stream") {
|
||||
song_.set_filetype(Song::Type_Stream);
|
||||
} else {
|
||||
song_.set_directory_id(-1);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -54,11 +53,8 @@ QUrl SongPlaylistItem::Url() const {
|
|||
void SongPlaylistItem::Reload() {
|
||||
if (song_.url().scheme() != "file")
|
||||
return;
|
||||
QString old_filename = song_.url().toLocalFile();
|
||||
int old_directory_id = song_.directory_id();
|
||||
|
||||
song_ = Song();
|
||||
song_.InitFromFile(old_filename, old_directory_id);
|
||||
TagReaderClient::Instance()->ReadFileBlocking(song_.url().toLocalFile(), &song_);
|
||||
}
|
||||
|
||||
Song SongPlaylistItem::Metadata() const {
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
|
||||
#include "parserbase.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "library/librarybackend.h"
|
||||
#include "library/libraryquery.h"
|
||||
#include "library/sqlrow.h"
|
||||
|
@ -74,7 +75,7 @@ void ParserBase::LoadSong(const QString& filename_or_url, qint64 beginning,
|
|||
if (library_song.is_valid()) {
|
||||
*song = library_song;
|
||||
} else {
|
||||
song->InitFromFile(filename, -1);
|
||||
TagReaderClient::Instance()->ReadFileBlocking(filename, song);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -20,6 +20,7 @@
|
|||
#include "trackselectiondialog.h"
|
||||
#include "ui_edittagdialog.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/utilities.h"
|
||||
#include "covers/albumcoverloader.h"
|
||||
#include "covers/coverproviders.h"
|
||||
|
@ -194,7 +195,7 @@ QList<EditTagDialog::Data> EditTagDialog::LoadData(const SongList& songs) const
|
|||
if (song.IsEditable()) {
|
||||
// Try reloading the tags from file
|
||||
Song copy(song);
|
||||
copy.InitFromFile(copy.url().toLocalFile(), copy.directory_id());
|
||||
TagReaderClient::Instance()->ReadFileBlocking(copy.url().toLocalFile(), ©);
|
||||
|
||||
if (copy.is_valid())
|
||||
ret << Data(copy);
|
||||
|
@ -606,7 +607,8 @@ void EditTagDialog::SaveData(const QList<Data>& data) {
|
|||
if (ref.current_.IsMetadataEqual(ref.original_))
|
||||
continue;
|
||||
|
||||
if (!ref.current_.Save()) {
|
||||
if (!TagReaderClient::Instance()->SaveFileBlocking(
|
||||
ref.current_.url().toLocalFile(), ref.current_)) {
|
||||
emit Error(tr("An error occurred writing metadata to '%1'").arg(ref.current_.url().toLocalFile()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1496,21 +1496,24 @@ void MainWindow::RenumberTracks() {
|
|||
|
||||
if (song.IsEditable()) {
|
||||
song.set_track(track);
|
||||
QFuture<bool> future = song.BackgroundSave();
|
||||
ModelFutureWatcher<bool>* watcher = new ModelFutureWatcher<bool>(source_index, this);
|
||||
watcher->setFuture(future);
|
||||
connect(watcher, SIGNAL(finished()), SLOT(SongSaveComplete()));
|
||||
|
||||
TagReaderReply* reply =
|
||||
TagReaderClient::Instance()->SaveFile(song.url().toLocalFile(), song);
|
||||
|
||||
NewClosure(reply, SIGNAL(Finished(bool)),
|
||||
this, SLOT(SongSaveComplete(TagReaderReply*,QPersistentModelIndex)),
|
||||
reply, QPersistentModelIndex(source_index));
|
||||
}
|
||||
track++;
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::SongSaveComplete() {
|
||||
ModelFutureWatcher<bool>* watcher = static_cast<ModelFutureWatcher<bool>*>(sender());
|
||||
watcher->deleteLater();
|
||||
if (watcher->index().isValid()) {
|
||||
playlists_->current()->ReloadItems(QList<int>() << watcher->index().row());
|
||||
void MainWindow::SongSaveComplete(TagReaderReply* reply,
|
||||
const QPersistentModelIndex& index) {
|
||||
if (reply->is_successful() && index.isValid()) {
|
||||
playlists_->current()->ReloadItems(QList<int>() << index.row());
|
||||
}
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
void MainWindow::SelectionSetValue() {
|
||||
|
@ -1528,10 +1531,12 @@ void MainWindow::SelectionSetValue() {
|
|||
Song song = playlists_->current()->item_at(row)->Metadata();
|
||||
|
||||
if (Playlist::set_column_value(song, column, column_value)) {
|
||||
QFuture<bool> future = song.BackgroundSave();
|
||||
ModelFutureWatcher<bool>* watcher = new ModelFutureWatcher<bool>(source_index, this);
|
||||
watcher->setFuture(future);
|
||||
connect(watcher, SIGNAL(finished()), SLOT(SongSaveComplete()));
|
||||
TagReaderReply* reply =
|
||||
TagReaderClient::Instance()->SaveFile(song.url().toLocalFile(), song);
|
||||
|
||||
NewClosure(reply, SIGNAL(Finished(bool)),
|
||||
this, SLOT(SongSaveComplete(TagReaderReply*,QPersistentModelIndex)),
|
||||
reply, QPersistentModelIndex(source_index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
|
||||
#include "config.h"
|
||||
#include "core/mac_startup.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "engines/engine_fwd.h"
|
||||
#include "library/librarymodel.h"
|
||||
#include "playlist/playlistitem.h"
|
||||
|
@ -220,7 +221,8 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||
|
||||
void NowPlayingWidgetPositionChanged(bool above_status_bar);
|
||||
|
||||
void SongSaveComplete();
|
||||
void SongSaveComplete(TagReaderReply* reply,
|
||||
const QPersistentModelIndex& index);
|
||||
|
||||
void ShowCoverManager();
|
||||
#ifdef HAVE_LIBLASTFM
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
#include "ui_organisedialog.h"
|
||||
#include "core/musicstorage.h"
|
||||
#include "core/organise.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
|
@ -172,7 +173,8 @@ void OrganiseDialog::LoadPreviewSongs(const QString& filename) {
|
|||
}
|
||||
|
||||
Song song;
|
||||
song.InitFromFile(filename, -1);
|
||||
TagReaderClient::Instance()->ReadFileBlocking(filename, &song);
|
||||
|
||||
if (song.is_valid())
|
||||
preview_songs_ << song;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
#include "iconloader.h"
|
||||
#include "trackselectiondialog.h"
|
||||
#include "ui_trackselectiondialog.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QFutureWatcher>
|
||||
|
@ -243,7 +244,7 @@ void TrackSelectionDialog::SaveData(const QList<Data>& data) {
|
|||
copy.set_album(new_metadata.album());
|
||||
copy.set_track(new_metadata.track());
|
||||
|
||||
copy.Save();
|
||||
TagReaderClient::Instance()->SaveFileBlocking(copy.url().toLocalFile(), copy);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue