Merge branch 'external-tagreader'

This commit is contained in:
David Sansome 2012-01-08 18:38:48 +00:00
commit 626ce20ec0
69 changed files with 2293 additions and 5703 deletions

View File

@ -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

View File

@ -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"

View File

@ -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)

View File

@ -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();
}

View File

@ -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_;

View File

@ -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)

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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

View File

@ -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
)

View File

@ -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();
}

View File

@ -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

View File

@ -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();
}

View File

@ -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

View File

@ -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)
{
}

View File

@ -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

View File

@ -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
)

View File

@ -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;

View File

@ -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
)

View File

@ -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;
}

View File

@ -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}
)

View File

@ -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

View File

@ -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}

View File

@ -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>

View File

@ -1,5 +1,5 @@
#include "logging.h"
#include "multisortfilterproxy.h"
#include "core/logging.h"
#include <QDate>
#include <QDateTime>

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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);
}
}
}

View File

@ -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;
}

View File

@ -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

View File

@ -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);
}

View File

@ -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())

View File

@ -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();

View File

@ -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);
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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_;
};

View File

@ -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());

View File

@ -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;

View File

@ -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) {

View File

@ -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;

View File

@ -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:

View File

@ -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() {

View File

@ -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();

View File

@ -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 {

View File

@ -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

View File

@ -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(), &copy);
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()));
}
}

View File

@ -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));
}
}
}

View File

@ -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

View File

@ -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;
}

View File

@ -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);
}
}