Initial commit.
This commit is contained in:
191
src/device/afcdevice.cpp
Normal file
191
src/device/afcdevice.cpp
Normal file
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "afcdevice.h"
|
||||
#include "afcfile.h"
|
||||
#include "afctransfer.h"
|
||||
#include "devicemanager.h"
|
||||
#include "gpodloader.h"
|
||||
#include "imobiledeviceconnection.h"
|
||||
#include "core/application.h"
|
||||
#include "core/utilities.h"
|
||||
|
||||
#include <QThread>
|
||||
|
||||
AfcDevice::AfcDevice(const QUrl &url, DeviceLister* lister, const QString &unique_id, DeviceManager *manager, Application *app, int database_id, bool first_time)
|
||||
: GPodDevice(url, lister, unique_id, manager, app, database_id, first_time), transfer_(NULL)
|
||||
{
|
||||
}
|
||||
|
||||
AfcDevice::~AfcDevice() {
|
||||
Utilities::RemoveRecursive(local_path_);
|
||||
}
|
||||
|
||||
void AfcDevice::Init() {
|
||||
|
||||
// Make a new temporary directory for the iTunesDB. We copy it off the iPod
|
||||
// so that libgpod can have a local directory to use.
|
||||
local_path_ = Utilities::MakeTempDir();
|
||||
InitBackendDirectory(local_path_, first_time_, false);
|
||||
model_->Init();
|
||||
|
||||
transfer_ = new AfcTransfer(url_.host(), local_path_, app_->task_manager(), shared_from_this());
|
||||
transfer_->moveToThread(loader_thread_);
|
||||
|
||||
connect(transfer_, SIGNAL(TaskStarted(int)), SIGNAL(TaskStarted(int)));
|
||||
connect(transfer_, SIGNAL(CopyFinished(bool)), SLOT(CopyFinished(bool)));
|
||||
connect(loader_thread_, SIGNAL(started()), transfer_, SLOT(CopyFromDevice()));
|
||||
loader_thread_->start();
|
||||
|
||||
}
|
||||
|
||||
void AfcDevice::CopyFinished(bool success) {
|
||||
|
||||
transfer_->deleteLater();
|
||||
transfer_ = NULL;
|
||||
|
||||
if (!success) {
|
||||
app_->AddError(tr("An error occurred copying the iTunes database from the device"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Now load the songs from the local database
|
||||
loader_ = new GPodLoader(local_path_, app_->task_manager(), backend_,
|
||||
shared_from_this());
|
||||
loader_->set_music_path_prefix("afc://" + url_.host());
|
||||
loader_->set_song_type(Song::Type_Stream);
|
||||
loader_->moveToThread(loader_thread_);
|
||||
|
||||
connect(loader_, SIGNAL(Error(QString)), SIGNAL(Error(QString)));
|
||||
connect(loader_, SIGNAL(TaskStarted(int)), SIGNAL(TaskStarted(int)));
|
||||
connect(loader_, SIGNAL(LoadFinished(Itdb_iTunesDB*)), SLOT(LoadFinished(Itdb_iTunesDB*)));
|
||||
QMetaObject::invokeMethod(loader_, "LoadDatabase");
|
||||
|
||||
}
|
||||
|
||||
bool AfcDevice::StartCopy(QList<Song::FileType> *supported_types) {
|
||||
|
||||
GPodDevice::StartCopy(supported_types);
|
||||
connection_.reset(new iMobileDeviceConnection(url_.host()));
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
bool AfcDevice::CopyToStorage(const CopyJob &job) {
|
||||
|
||||
Q_ASSERT(db_);
|
||||
|
||||
Itdb_Track *track = AddTrackToITunesDb(job.metadata_);
|
||||
|
||||
// Get an unused filename on the device
|
||||
QString dest = connection_->GetUnusedFilename(db_, job.metadata_);
|
||||
if (dest.isEmpty()) {
|
||||
itdb_track_remove(track);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Copy the file
|
||||
{
|
||||
QFile source_file(job.source_);
|
||||
AfcFile dest_file(connection_.get(), dest);
|
||||
if (!Utilities::Copy(&source_file, &dest_file))
|
||||
return false;
|
||||
}
|
||||
|
||||
track->transferred = 1;
|
||||
|
||||
// Set the filetype_marker
|
||||
QString suffix = dest.section('.', -1, -1).toUpper();
|
||||
track->filetype_marker = 0;
|
||||
for (int i=0 ; i<4 ; ++i) {
|
||||
track->filetype_marker = track->filetype_marker << 8;
|
||||
if (i >= suffix.length())
|
||||
track->filetype_marker |= ' ';
|
||||
else
|
||||
track->filetype_marker |= suffix[i].toLatin1();
|
||||
}
|
||||
|
||||
// Set the filename
|
||||
track->ipod_path = strdup(dest.toUtf8().constData());
|
||||
itdb_filename_fs2ipod(track->ipod_path);
|
||||
|
||||
AddTrackToModel(track, "afc://" + url_.host());
|
||||
|
||||
// Remove the original if it was requested
|
||||
if (job.remove_original_) {
|
||||
QFile::remove(job.source_);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void AfcDevice::FinishCopy(bool success) {
|
||||
|
||||
// Temporarily unset the GUID so libgpod doesn't lock the device for syncing
|
||||
itdb_device_set_sysinfo(db_->device, "FirewireGuid", NULL);
|
||||
|
||||
GPodDevice::FinishCopy(success);
|
||||
|
||||
// Close the connection to the device
|
||||
connection_.reset();
|
||||
|
||||
}
|
||||
|
||||
void AfcDevice::FinaliseDatabase() {
|
||||
|
||||
// Set the GUID again to lock the device for syncing
|
||||
itdb_device_set_sysinfo(db_->device, "FirewireGuid", url_.host().toUtf8().constData());
|
||||
|
||||
// Copy the files back to the iPod
|
||||
// No need to start another thread since we're already in the organiser thread
|
||||
AfcTransfer transfer(url_.host(), local_path_, NULL, shared_from_this());
|
||||
|
||||
itdb_start_sync(db_);
|
||||
bool success = transfer.CopyToDevice(connection_.get());
|
||||
itdb_stop_sync(db_);
|
||||
|
||||
if (!success) {
|
||||
app_->AddError(tr("An error occurred copying the iTunes database onto the device"));
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool AfcDevice::DeleteFromStorage(const DeleteJob &job) {
|
||||
|
||||
const QString path = job.metadata_.url().toLocalFile();
|
||||
|
||||
if (!RemoveTrackFromITunesDb(path))
|
||||
return false;
|
||||
|
||||
// Remove the file
|
||||
if (afc_remove_path(connection_->afc(), path.toUtf8().constData()) != AFC_E_SUCCESS)
|
||||
return false;
|
||||
|
||||
// Remove it from our collection model
|
||||
songs_to_remove_ << job.metadata_;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
72
src/device/afcdevice.h
Normal file
72
src/device/afcdevice.h
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AFCDEVICE_H
|
||||
#define AFCDEVICE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "gpoddevice.h"
|
||||
|
||||
#include <QMutex>
|
||||
#include <QWaitCondition>
|
||||
|
||||
#include <boost/scoped_ptr.hpp>
|
||||
|
||||
#include <gpod/itdb.h>
|
||||
|
||||
class AfcTransfer;
|
||||
class GPodLoader;
|
||||
class iMobileDeviceConnection;
|
||||
|
||||
class AfcDevice : public GPodDevice {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Q_INVOKABLE AfcDevice(const QUrl &url, DeviceLister *lister, const QString &unique_id, DeviceManager *manager, Application *app, int database_id, bool first_time);
|
||||
~AfcDevice();
|
||||
|
||||
void Init();
|
||||
|
||||
static QStringList url_schemes() { return QStringList() << "afc"; }
|
||||
|
||||
bool StartCopy(QList<Song::FileType> *supported_types);
|
||||
bool CopyToStorage(const CopyJob &job);
|
||||
void FinishCopy(bool success);
|
||||
|
||||
bool DeleteFromStorage(const DeleteJob &job);
|
||||
|
||||
protected:
|
||||
void FinaliseDatabase();
|
||||
|
||||
private slots:
|
||||
void CopyFinished(bool success);
|
||||
|
||||
private:
|
||||
void RemoveRecursive(const QString &path);
|
||||
|
||||
private:
|
||||
AfcTransfer *transfer_;
|
||||
boost::scoped_ptr<iMobileDeviceConnection> connection_;
|
||||
|
||||
QString local_path_;
|
||||
};
|
||||
|
||||
#endif // AFCDEVICE_H
|
||||
91
src/device/afcfile.cpp
Normal file
91
src/device/afcfile.cpp
Normal file
@@ -0,0 +1,91 @@
|
||||
#include "config.h"
|
||||
|
||||
#include "afcfile.h"
|
||||
#include "imobiledeviceconnection.h"
|
||||
|
||||
#include <libimobiledevice/afc.h>
|
||||
|
||||
AfcFile::AfcFile(iMobileDeviceConnection *connection, const QString &path, QObject *parent)
|
||||
: QIODevice(parent),
|
||||
connection_(connection),
|
||||
handle_(0),
|
||||
path_(path)
|
||||
{
|
||||
}
|
||||
|
||||
AfcFile::~AfcFile() {
|
||||
close();
|
||||
}
|
||||
|
||||
bool AfcFile::open(QIODevice::OpenMode mode) {
|
||||
|
||||
afc_file_mode_t afc_mode;
|
||||
switch (mode) {
|
||||
case ReadOnly:
|
||||
afc_mode = AFC_FOPEN_RDONLY;
|
||||
break;
|
||||
case WriteOnly:
|
||||
afc_mode = AFC_FOPEN_WRONLY;
|
||||
break;
|
||||
case ReadWrite:
|
||||
afc_mode = AFC_FOPEN_RW;
|
||||
break;
|
||||
|
||||
default:
|
||||
afc_mode = AFC_FOPEN_RW;
|
||||
}
|
||||
afc_error_t err = afc_file_open(
|
||||
connection_->afc(), path_.toUtf8().constData(), afc_mode, &handle_);
|
||||
if (err != AFC_E_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return QIODevice::open(mode);
|
||||
|
||||
}
|
||||
|
||||
void AfcFile::close() {
|
||||
|
||||
if (handle_) {
|
||||
afc_file_close(connection_->afc(), handle_);
|
||||
}
|
||||
QIODevice::close();
|
||||
|
||||
}
|
||||
|
||||
bool AfcFile::seek(qint64 pos) {
|
||||
|
||||
afc_error_t err = afc_file_seek(connection_->afc(), handle_, pos, SEEK_SET);
|
||||
if (err != AFC_E_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
QIODevice::seek(pos);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
qint64 AfcFile::readData(char *data, qint64 max_size) {
|
||||
|
||||
uint32_t bytes_read = 0;
|
||||
afc_error_t err = afc_file_read(connection_->afc(), handle_, data, max_size, &bytes_read);
|
||||
if (err != AFC_E_SUCCESS) {
|
||||
return -1;
|
||||
}
|
||||
return bytes_read;
|
||||
|
||||
}
|
||||
|
||||
qint64 AfcFile::writeData(const char *data, qint64 max_size) {
|
||||
|
||||
uint32_t bytes_written = 0;
|
||||
afc_error_t err = afc_file_write(connection_->afc(), handle_, data, max_size, &bytes_written);
|
||||
if (err != AFC_E_SUCCESS) {
|
||||
return -1;
|
||||
}
|
||||
return bytes_written;
|
||||
|
||||
}
|
||||
|
||||
qint64 AfcFile::size() const {
|
||||
return connection_->GetFileInfo(path_, "st_size").toLongLong();
|
||||
}
|
||||
38
src/device/afcfile.h
Normal file
38
src/device/afcfile.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifndef AFCFILE_H
|
||||
#define AFCFILE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <QIODevice>
|
||||
|
||||
#include <libimobiledevice/afc.h>
|
||||
|
||||
class iMobileDeviceConnection;
|
||||
|
||||
class AfcFile : public QIODevice {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AfcFile(iMobileDeviceConnection* connection, const QString &path, QObject *parent = 0);
|
||||
~AfcFile();
|
||||
|
||||
// QIODevice
|
||||
void close();
|
||||
bool open(OpenMode mode);
|
||||
bool seek(qint64 pos);
|
||||
qint64 size() const;
|
||||
|
||||
private:
|
||||
// QIODevice
|
||||
qint64 readData(char *data, qint64 max_size);
|
||||
qint64 writeData(const char *data, qint64 max_size);
|
||||
|
||||
iMobileDeviceConnection *connection_;
|
||||
uint64_t handle_;
|
||||
|
||||
QString path_;
|
||||
};
|
||||
|
||||
#endif
|
||||
148
src/device/afctransfer.cpp
Normal file
148
src/device/afctransfer.cpp
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "afcfile.h"
|
||||
#include "afctransfer.h"
|
||||
#include "imobiledeviceconnection.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "core/utilities.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QtDebug>
|
||||
|
||||
#include <boost/scoped_ptr.hpp>
|
||||
|
||||
AfcTransfer::AfcTransfer(const QString &uuid, const QString &local_destination, TaskManager *task_manager, boost::shared_ptr<ConnectedDevice> device)
|
||||
: QObject(NULL), device_(device), task_manager_(task_manager), uuid_(uuid), local_destination_(local_destination)
|
||||
{
|
||||
|
||||
original_thread_ = thread();
|
||||
|
||||
important_directories_ << "/iTunes_Control/Artwork";
|
||||
important_directories_ << "/iTunes_Control/Device";
|
||||
important_directories_ << "/iTunes_Control/iTunes";
|
||||
}
|
||||
|
||||
AfcTransfer::~AfcTransfer() {
|
||||
}
|
||||
|
||||
void AfcTransfer::CopyFromDevice() {
|
||||
|
||||
int task_id = 0;
|
||||
if (task_manager_) {
|
||||
task_id = task_manager_->StartTask(tr("Copying iPod database"));
|
||||
emit TaskStarted(task_id);
|
||||
}
|
||||
|
||||
// Connect to the device
|
||||
iMobileDeviceConnection c(uuid_);
|
||||
|
||||
// Copy directories. If one fails we stop.
|
||||
bool success = true;
|
||||
foreach (const QString &dir, important_directories_) {
|
||||
if (!CopyDirFromDevice(&c, dir)) {
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (task_manager_) {
|
||||
moveToThread(original_thread_);
|
||||
task_manager_->SetTaskFinished(task_id);
|
||||
emit CopyFinished(success);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool AfcTransfer::CopyToDevice(iMobileDeviceConnection *connection) {
|
||||
|
||||
// Connect to the device
|
||||
if (!connection)
|
||||
connection = new iMobileDeviceConnection(uuid_);
|
||||
|
||||
foreach (const QString &dir, important_directories_)
|
||||
if (!CopyDirToDevice(connection, dir))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
bool AfcTransfer::CopyDirFromDevice(iMobileDeviceConnection *c, const QString &path) {
|
||||
|
||||
foreach (const QString &filename, c->ReadDirectory(path, QDir::Files | QDir::NoDotAndDotDot)) {
|
||||
if (!CopyFileFromDevice(c, path + "/" + filename))
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (const QString &dir, c->ReadDirectory(path, QDir::Dirs | QDir::NoDotAndDotDot)) {
|
||||
if (!CopyDirFromDevice(c, path + "/" + dir))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
bool AfcTransfer::CopyDirToDevice(iMobileDeviceConnection *c, const QString &path) {
|
||||
|
||||
QDir dir(local_destination_ + path);
|
||||
|
||||
if (!c->Exists(path)) {
|
||||
c->MkDir(path);
|
||||
}
|
||||
|
||||
foreach (const QString &filename, dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) {
|
||||
if (!CopyFileToDevice(c, path + "/" + filename))
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (const QString &dir, dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
|
||||
if (!CopyDirToDevice(c, path + "/" + dir))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
bool AfcTransfer::CopyFileFromDevice(iMobileDeviceConnection *c, const QString &path) {
|
||||
|
||||
QString local_filename = local_destination_ + path;
|
||||
QString local_dir = local_filename.section('/', 0, -2);
|
||||
|
||||
QDir d;
|
||||
d.mkpath(local_dir);
|
||||
|
||||
QFile dest(local_filename);
|
||||
AfcFile source(c, path);
|
||||
|
||||
return Utilities::Copy(&source, &dest);
|
||||
|
||||
}
|
||||
|
||||
bool AfcTransfer::CopyFileToDevice(iMobileDeviceConnection *c, const QString &path) {
|
||||
|
||||
QFile source(local_destination_ + path);
|
||||
AfcFile dest(c, path);
|
||||
|
||||
return Utilities::Copy(&source, &dest);
|
||||
|
||||
}
|
||||
70
src/device/afctransfer.h
Normal file
70
src/device/afctransfer.h
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef AFCTRANSFER_H
|
||||
#define AFCTRANSFER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
|
||||
#include <boost/shared_ptr.hpp>
|
||||
|
||||
class ConnectedDevice;
|
||||
class iMobileDeviceConnection;
|
||||
class TaskManager;
|
||||
|
||||
class QIODevice;
|
||||
|
||||
class AfcTransfer : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AfcTransfer(const QString &uuid, const QString &local_destination, TaskManager *task_manager, boost::shared_ptr<ConnectedDevice> device);
|
||||
~AfcTransfer();
|
||||
|
||||
bool CopyToDevice(iMobileDeviceConnection *connection);
|
||||
|
||||
public slots:
|
||||
void CopyFromDevice();
|
||||
|
||||
signals:
|
||||
void TaskStarted(int task_id);
|
||||
void CopyFinished(bool success);
|
||||
|
||||
private:
|
||||
bool CopyDirFromDevice(iMobileDeviceConnection *c, const QString &path);
|
||||
bool CopyDirToDevice(iMobileDeviceConnection *c, const QString &path);
|
||||
bool CopyFileFromDevice(iMobileDeviceConnection *c, const QString &path);
|
||||
bool CopyFileToDevice(iMobileDeviceConnection *c, const QString &path);
|
||||
|
||||
private:
|
||||
boost::shared_ptr<ConnectedDevice> device_;
|
||||
QThread *original_thread_;
|
||||
|
||||
TaskManager *task_manager_;
|
||||
QString uuid_;
|
||||
QString local_destination_;
|
||||
|
||||
QStringList important_directories_;
|
||||
};
|
||||
|
||||
#endif // AFCTRANSFER_H
|
||||
65
src/device/cddadevice.cpp
Normal file
65
src/device/cddadevice.cpp
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QMutexLocker>
|
||||
|
||||
#include "collection/collectionbackend.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
|
||||
#include "cddadevice.h"
|
||||
|
||||
CddaDevice::CddaDevice(const QUrl &url, DeviceLister *lister, const QString &unique_id, DeviceManager *manager, Application *app, int database_id, bool first_time)
|
||||
: ConnectedDevice(url, lister, unique_id, manager, app, database_id, first_time), cdda_song_loader_(url) {
|
||||
|
||||
connect(&cdda_song_loader_, SIGNAL(SongsLoaded(SongList)), this, SLOT(SongsLoaded(SongList)));
|
||||
connect(&cdda_song_loader_, SIGNAL(SongsDurationLoaded(SongList)), this, SLOT(SongsLoaded(SongList)));
|
||||
connect(&cdda_song_loader_, SIGNAL(SongsMetadataLoaded(SongList)), this, SLOT(SongsLoaded(SongList)));
|
||||
connect(this, SIGNAL(SongsDiscovered(SongList)), model_, SLOT(SongsDiscovered(SongList)));
|
||||
|
||||
}
|
||||
|
||||
CddaDevice::~CddaDevice() {}
|
||||
|
||||
void CddaDevice::Init() {
|
||||
|
||||
song_count_ = 0; // Reset song count, in case it was already set
|
||||
cdda_song_loader_.LoadSongs();
|
||||
|
||||
}
|
||||
|
||||
void CddaDevice::Refresh() {
|
||||
|
||||
if (!cdda_song_loader_.HasChanged()) {
|
||||
return;
|
||||
}
|
||||
Init();
|
||||
|
||||
}
|
||||
|
||||
void CddaDevice::SongsLoaded(const SongList &songs) {
|
||||
|
||||
model_->Reset();
|
||||
emit SongsDiscovered(songs);
|
||||
song_count_ = songs.size();
|
||||
|
||||
}
|
||||
|
||||
62
src/device/cddadevice.h
Normal file
62
src/device/cddadevice.h
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef CDDADEVICE_H
|
||||
#define CDDADEVICE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QMutex>
|
||||
|
||||
// These must come after Qt includes
|
||||
#include <cdio/cdio.h>
|
||||
#include <gst/audio/gstaudiocdsrc.h>
|
||||
|
||||
#include "cddasongloader.h"
|
||||
#include "connecteddevice.h"
|
||||
#include "core/song.h"
|
||||
#include "musicbrainz/musicbrainzclient.h"
|
||||
|
||||
class CddaDevice : public ConnectedDevice {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Q_INVOKABLE CddaDevice(const QUrl &url, DeviceLister *lister, const QString &unique_id, DeviceManager *manager, Application *app, int database_id, bool first_time);
|
||||
~CddaDevice();
|
||||
|
||||
void Init();
|
||||
void Refresh();
|
||||
bool CopyToStorage(const MusicStorage::CopyJob&) { return false; }
|
||||
bool DeleteFromStorage(const MusicStorage::DeleteJob&) { return false; }
|
||||
|
||||
static QStringList url_schemes() { return QStringList() << "cdda"; }
|
||||
|
||||
signals:
|
||||
void SongsDiscovered(const SongList &songs);
|
||||
|
||||
private slots:
|
||||
void SongsLoaded(const SongList &songs);
|
||||
|
||||
private:
|
||||
CddaSongLoader cdda_song_loader_;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
131
src/device/cddalister.cpp
Normal file
131
src/device/cddalister.cpp
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QMutex>
|
||||
#include <QThread>
|
||||
#include <QWaitCondition>
|
||||
|
||||
// This must come after Qt includes
|
||||
#include <cdio/cdio.h>
|
||||
|
||||
#include "cddalister.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/song.h"
|
||||
|
||||
QStringList CddaLister::DeviceUniqueIDs() { return devices_list_; }
|
||||
|
||||
QVariantList CddaLister::DeviceIcons(const QString &) {
|
||||
QVariantList icons;
|
||||
icons << QString("cd");
|
||||
return icons;
|
||||
}
|
||||
|
||||
QString CddaLister::DeviceManufacturer(const QString &id) {
|
||||
|
||||
CdIo_t* cdio = cdio_open(id.toLocal8Bit().constData(), DRIVER_DEVICE);
|
||||
cdio_hwinfo_t cd_info;
|
||||
if (cdio_get_hwinfo(cdio, &cd_info)) {
|
||||
cdio_destroy(cdio);
|
||||
return QString(cd_info.psz_vendor);
|
||||
}
|
||||
cdio_destroy(cdio);
|
||||
return QString();
|
||||
|
||||
}
|
||||
|
||||
QString CddaLister::DeviceModel(const QString &id) {
|
||||
|
||||
CdIo_t* cdio = cdio_open(id.toLocal8Bit().constData(), DRIVER_DEVICE);
|
||||
cdio_hwinfo_t cd_info;
|
||||
if (cdio_get_hwinfo(cdio, &cd_info)) {
|
||||
cdio_destroy(cdio);
|
||||
return QString(cd_info.psz_model);
|
||||
}
|
||||
cdio_destroy(cdio);
|
||||
return QString();
|
||||
|
||||
}
|
||||
|
||||
quint64 CddaLister::DeviceCapacity(const QString &) { return 0; }
|
||||
|
||||
quint64 CddaLister::DeviceFreeSpace(const QString &) { return 0; }
|
||||
|
||||
QVariantMap CddaLister::DeviceHardwareInfo(const QString &) {
|
||||
return QVariantMap();
|
||||
}
|
||||
|
||||
QString CddaLister::MakeFriendlyName(const QString &id) {
|
||||
|
||||
CdIo_t* cdio = cdio_open(id.toLocal8Bit().constData(), DRIVER_DEVICE);
|
||||
cdio_hwinfo_t cd_info;
|
||||
if (cdio_get_hwinfo(cdio, &cd_info)) {
|
||||
cdio_destroy(cdio);
|
||||
return QString(cd_info.psz_model);
|
||||
}
|
||||
cdio_destroy(cdio);
|
||||
return QString("CD (") + id + ")";
|
||||
|
||||
}
|
||||
|
||||
QList<QUrl> CddaLister::MakeDeviceUrls(const QString &id) {
|
||||
return QList<QUrl>() << QUrl("cdda://" + id);
|
||||
}
|
||||
|
||||
void CddaLister::UnmountDevice(const QString &id) {
|
||||
cdio_eject_media_drive(id.toLocal8Bit().constData());
|
||||
}
|
||||
|
||||
void CddaLister::UpdateDeviceFreeSpace(const QString&) {}
|
||||
|
||||
void CddaLister::Init() {
|
||||
|
||||
cdio_init();
|
||||
#ifdef Q_OS_DARWIN
|
||||
if (!cdio_have_driver(DRIVER_OSX)) {
|
||||
qLog(Error) << "libcdio was compiled without support for OS X!";
|
||||
}
|
||||
#endif
|
||||
char** devices = cdio_get_devices(DRIVER_DEVICE);
|
||||
if (!devices) {
|
||||
qLog(Debug) << "No CD devices found";
|
||||
return;
|
||||
}
|
||||
for (; *devices != nullptr; ++devices) {
|
||||
QString device(*devices);
|
||||
QFileInfo device_info(device);
|
||||
if (device_info.isSymLink()) {
|
||||
device = device_info.symLinkTarget();
|
||||
}
|
||||
#ifdef Q_OS_DARWIN
|
||||
// Every track is detected as a separate device on Darwin. The raw disk looks like /dev/rdisk1
|
||||
if (!device.contains(QRegExp("^/dev/rdisk[0-9]$"))) {
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
if (!devices_list_.contains(device)) {
|
||||
devices_list_ << device;
|
||||
emit DeviceAdded(device);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
53
src/device/cddalister.h
Normal file
53
src/device/cddalister.h
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef CDDALISTER_H
|
||||
#define CDDALISTER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QStringList>
|
||||
|
||||
#include "devicelister.h"
|
||||
|
||||
class CddaLister : public DeviceLister {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CddaLister() {}
|
||||
|
||||
QStringList DeviceUniqueIDs();
|
||||
QVariantList DeviceIcons(const QString &id);
|
||||
QString DeviceManufacturer(const QString &id);
|
||||
QString DeviceModel(const QString &id);
|
||||
quint64 DeviceCapacity(const QString &id);
|
||||
quint64 DeviceFreeSpace(const QString &id);
|
||||
QVariantMap DeviceHardwareInfo(const QString &id);
|
||||
bool AskForScan(const QString&) const { return false; }
|
||||
QString MakeFriendlyName(const QString&);
|
||||
QList<QUrl> MakeDeviceUrls(const QString&);
|
||||
void UnmountDevice(const QString&);
|
||||
void UpdateDeviceFreeSpace(const QString&);
|
||||
void Init();
|
||||
|
||||
private:
|
||||
QStringList devices_list_;
|
||||
};
|
||||
#endif // CDDALISTER_H
|
||||
216
src/device/cddasongloader.cpp
Normal file
216
src/device/cddasongloader.cpp
Normal file
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <gst/gst.h>
|
||||
#include <gst/tag/tag.h>
|
||||
|
||||
#include "cddasongloader.h"
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/timeconstants.h"
|
||||
|
||||
CddaSongLoader::CddaSongLoader(const QUrl &url, QObject *parent)
|
||||
: QObject(parent),
|
||||
url_(url),
|
||||
cdda_(nullptr),
|
||||
cdio_(nullptr) {}
|
||||
|
||||
CddaSongLoader::~CddaSongLoader() {
|
||||
if (cdio_) cdio_destroy(cdio_);
|
||||
}
|
||||
|
||||
QUrl CddaSongLoader::GetUrlFromTrack(int track_number) const {
|
||||
|
||||
if (url_.isEmpty()) {
|
||||
return QUrl(QString("cdda://%1").arg(track_number));
|
||||
}
|
||||
else {
|
||||
return QUrl(QString("cdda://%1/%2").arg(url_.path()).arg(track_number));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CddaSongLoader::LoadSongs() {
|
||||
|
||||
QMutexLocker locker(&mutex_load_);
|
||||
cdio_ = cdio_open(url_.path().toLocal8Bit().constData(), DRIVER_DEVICE);
|
||||
if (cdio_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
// Create gstreamer cdda element
|
||||
GError *error = nullptr;
|
||||
cdda_ = gst_element_make_from_uri(GST_URI_SRC, "cdda://", nullptr, &error);
|
||||
if (error) {
|
||||
qLog(Error) << error->code << error->message;
|
||||
}
|
||||
if (cdda_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!url_.isEmpty()) {
|
||||
g_object_set(cdda_, "device", g_strdup(url_.path().toLocal8Bit().constData()), nullptr);
|
||||
}
|
||||
if (g_object_class_find_property (G_OBJECT_GET_CLASS (cdda_), "paranoia-mode")) {
|
||||
g_object_set (cdda_, "paranoia-mode", 0, NULL);
|
||||
}
|
||||
|
||||
// Change the element's state to ready and paused, to be able to query it
|
||||
if (gst_element_set_state(cdda_, GST_STATE_READY) == GST_STATE_CHANGE_FAILURE || gst_element_set_state(cdda_, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) {
|
||||
gst_element_set_state(cdda_, GST_STATE_NULL);
|
||||
gst_object_unref(GST_OBJECT(cdda_));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get number of tracks
|
||||
GstFormat fmt = gst_format_get_by_nick("track");
|
||||
GstFormat out_fmt = fmt;
|
||||
gint64 num_tracks = 0;
|
||||
if (!gst_element_query_duration(cdda_, out_fmt, &num_tracks) || out_fmt != fmt) {
|
||||
qLog(Error) << "Error while querying cdda GstElement";
|
||||
gst_object_unref(GST_OBJECT(cdda_));
|
||||
return;
|
||||
}
|
||||
|
||||
SongList songs;
|
||||
for (int track_number = 1; track_number <= num_tracks; track_number++) {
|
||||
// Init song
|
||||
Song song;
|
||||
song.set_id(track_number);
|
||||
song.set_valid(true);
|
||||
song.set_filetype(Song::Type_Cdda);
|
||||
song.set_url(GetUrlFromTrack(track_number));
|
||||
song.set_title(QString("Track %1").arg(track_number));
|
||||
song.set_track(track_number);
|
||||
songs << song;
|
||||
}
|
||||
emit SongsLoaded(songs);
|
||||
|
||||
|
||||
gst_tag_register_musicbrainz_tags();
|
||||
|
||||
GstElement *pipeline = gst_pipeline_new("pipeline");
|
||||
GstElement *sink = gst_element_factory_make ("fakesink", NULL);
|
||||
gst_bin_add_many (GST_BIN (pipeline), cdda_, sink, NULL);
|
||||
gst_element_link (cdda_, sink);
|
||||
gst_element_set_state(pipeline, GST_STATE_READY);
|
||||
gst_element_set_state(pipeline, GST_STATE_PAUSED);
|
||||
|
||||
// Get TOC and TAG messages
|
||||
GstMessage *msg = nullptr;
|
||||
GstMessage *msg_toc = nullptr;
|
||||
GstMessage *msg_tag = nullptr;
|
||||
while ((msg = gst_bus_timed_pop_filtered(GST_ELEMENT_BUS(pipeline), GST_SECOND, (GstMessageType)(GST_MESSAGE_TOC | GST_MESSAGE_TAG)))) {
|
||||
if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TOC) {
|
||||
if (msg_toc) gst_message_unref(msg_toc); // Shouldn't happen, but just in case
|
||||
msg_toc = msg;
|
||||
} else if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_TAG) {
|
||||
if (msg_tag) gst_message_unref(msg_tag);
|
||||
msg_tag = msg;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle TOC message: get tracks duration
|
||||
if (msg_toc) {
|
||||
GstToc *toc;
|
||||
gst_message_parse_toc (msg_toc, &toc, nullptr);
|
||||
if (toc) {
|
||||
GList *entries = gst_toc_get_entries(toc);
|
||||
if (entries && songs.size() <= g_list_length (entries)) {
|
||||
int i = 0;
|
||||
for (GList *node = entries; node != nullptr; node = node->next) {
|
||||
GstTocEntry *entry = static_cast<GstTocEntry*>(node->data);
|
||||
quint64 duration = 0;
|
||||
gint64 start, stop;
|
||||
if (gst_toc_entry_get_start_stop_times (entry, &start, &stop)) duration = stop - start;
|
||||
songs[i++].set_length_nanosec(duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
gst_message_unref(msg_toc);
|
||||
}
|
||||
emit SongsDurationLoaded(songs);
|
||||
|
||||
// Handle TAG message: generate MusicBrainz DiscId
|
||||
if (msg_tag) {
|
||||
GstTagList *tags = nullptr;
|
||||
gst_message_parse_tag(msg_tag, &tags);
|
||||
char *string_mb = nullptr;
|
||||
if (gst_tag_list_get_string(tags, GST_TAG_CDDA_MUSICBRAINZ_DISCID, &string_mb)) {
|
||||
QString musicbrainz_discid(string_mb);
|
||||
qLog(Info) << "MusicBrainz discid: " << musicbrainz_discid;
|
||||
|
||||
MusicBrainzClient *musicbrainz_client = new MusicBrainzClient;
|
||||
connect(musicbrainz_client, SIGNAL(Finished(const QString&, const QString&, MusicBrainzClient::ResultList)), SLOT(AudioCDTagsLoaded(const QString&, const QString&, MusicBrainzClient::ResultList)));
|
||||
musicbrainz_client->StartDiscIdRequest(musicbrainz_discid);
|
||||
g_free(string_mb);
|
||||
gst_message_unref(msg_tag);
|
||||
gst_tag_list_free(tags);
|
||||
}
|
||||
}
|
||||
|
||||
gst_element_set_state(pipeline, GST_STATE_NULL);
|
||||
// This will also cause cdda_ to be unref'd.
|
||||
gst_object_unref(pipeline);
|
||||
|
||||
}
|
||||
|
||||
void CddaSongLoader::AudioCDTagsLoaded(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results) {
|
||||
|
||||
MusicBrainzClient *musicbrainz_client = qobject_cast<MusicBrainzClient*>(sender());
|
||||
musicbrainz_client->deleteLater();
|
||||
SongList songs;
|
||||
if (results.size() == 0) return;
|
||||
int track_number = 1;
|
||||
for (const MusicBrainzClient::Result &ret : results) {
|
||||
Song song;
|
||||
song.set_artist(artist);
|
||||
song.set_album(album);
|
||||
song.set_title(ret.title_);
|
||||
song.set_length_nanosec(ret.duration_msec_ * kNsecPerMsec);
|
||||
song.set_track(track_number);
|
||||
song.set_year(ret.year_);
|
||||
song.set_id(track_number);
|
||||
song.set_filetype(Song::Type_Cdda);
|
||||
song.set_valid(true);
|
||||
// We need to set url: that's how playlist will find the correct item to
|
||||
// update
|
||||
song.set_url(GetUrlFromTrack(track_number++));
|
||||
songs << song;
|
||||
}
|
||||
emit SongsMetadataLoaded(songs);
|
||||
|
||||
}
|
||||
|
||||
bool CddaSongLoader::HasChanged() {
|
||||
|
||||
if ((cdio_ && cdda_) && cdio_get_media_changed(cdio_) != 1) {
|
||||
return false;
|
||||
}
|
||||
// Check if mutex is already token (i.e. init is already taking place)
|
||||
if (!mutex_load_.tryLock()) {
|
||||
return false;
|
||||
}
|
||||
mutex_load_.unlock();
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
72
src/device/cddasongloader.h
Normal file
72
src/device/cddasongloader.h
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef CDDASONGLOADER_H
|
||||
#define CDDASONGLOADER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
|
||||
// These must come after Qt includes (issue 3247)
|
||||
#include <cdio/cdio.h>
|
||||
#include <gst/audio/gstaudiocdsrc.h>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "musicbrainz/musicbrainzclient.h"
|
||||
|
||||
// This class provides a (hopefully) nice, high level interface to get CD
|
||||
// information and load tracks
|
||||
class CddaSongLoader : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CddaSongLoader(
|
||||
// Url of the CD device. Will use the default device if empty
|
||||
const QUrl &url = QUrl(),
|
||||
QObject *parent = nullptr);
|
||||
~CddaSongLoader();
|
||||
|
||||
// Load songs.
|
||||
// Signals declared below will be emitted anytime new information will be available.
|
||||
void LoadSongs();
|
||||
bool HasChanged();
|
||||
|
||||
signals:
|
||||
void SongsLoaded(const SongList &songs);
|
||||
void SongsDurationLoaded(const SongList &songs);
|
||||
void SongsMetadataLoaded(const SongList &songs);
|
||||
|
||||
private slots:
|
||||
void AudioCDTagsLoaded(const QString &artist, const QString &album, const MusicBrainzClient::ResultList &results);
|
||||
|
||||
private:
|
||||
QUrl GetUrlFromTrack(int track_number) const;
|
||||
|
||||
QUrl url_;
|
||||
GstElement *cdda_;
|
||||
CdIo_t *cdio_;
|
||||
QMutex mutex_load_;
|
||||
};
|
||||
|
||||
#endif // CDDASONGLOADER_H
|
||||
|
||||
121
src/device/connecteddevice.cpp
Normal file
121
src/device/connecteddevice.cpp
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "connecteddevice.h"
|
||||
#include "devicelister.h"
|
||||
#include "devicemanager.h"
|
||||
#include "core/application.h"
|
||||
#include "core/database.h"
|
||||
#include "core/logging.h"
|
||||
#include "collection/collection.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
|
||||
#include <QtDebug>
|
||||
|
||||
ConnectedDevice::ConnectedDevice(const QUrl &url, DeviceLister *lister, const QString &unique_id, DeviceManager *manager, Application *app, int database_id, bool first_time)
|
||||
: QObject(manager),
|
||||
app_(app),
|
||||
url_(url),
|
||||
first_time_(first_time),
|
||||
lister_(lister),
|
||||
unique_id_(unique_id),
|
||||
database_id_(database_id),
|
||||
manager_(manager),
|
||||
backend_(nullptr),
|
||||
model_(nullptr),
|
||||
song_count_(0) {
|
||||
|
||||
qLog(Info) << "connected" << url << unique_id << first_time;
|
||||
|
||||
// Create the backend in the database thread.
|
||||
backend_ = new CollectionBackend();
|
||||
backend_->moveToThread(app_->database()->thread());
|
||||
|
||||
connect(backend_, SIGNAL(TotalSongCountUpdated(int)), SLOT(BackendTotalSongCountUpdated(int)));
|
||||
|
||||
backend_->Init(app_->database(),
|
||||
QString("device_%1_songs").arg(database_id),
|
||||
QString("device_%1_directories").arg(database_id),
|
||||
QString("device_%1_subdirectories").arg(database_id),
|
||||
QString("device_%1_fts").arg(database_id));
|
||||
|
||||
// Create the model
|
||||
model_ = new CollectionModel(backend_, app_, this);
|
||||
}
|
||||
|
||||
ConnectedDevice::~ConnectedDevice() { backend_->deleteLater(); }
|
||||
|
||||
void ConnectedDevice::InitBackendDirectory(const QString &mount_point, bool first_time, bool rewrite_path) {
|
||||
|
||||
if (first_time || backend_->GetAllDirectories().isEmpty()) {
|
||||
backend_->AddDirectory(mount_point);
|
||||
}
|
||||
else {
|
||||
if (rewrite_path) {
|
||||
// This is a bit of a hack. The device might not be mounted at the same
|
||||
// path each time, so if it's different we have to munge all the paths in
|
||||
// the database to fix it. This can be done entirely in sqlite so it's
|
||||
// relatively fast...
|
||||
|
||||
// Get the directory it was mounted at last time. Devices only have one
|
||||
// directory (the root).
|
||||
Directory dir = backend_->GetAllDirectories()[0];
|
||||
if (dir.path != mount_point) {
|
||||
// The directory is different, commence the munging.
|
||||
qLog(Info) << "Changing path from" << dir.path << "to" << mount_point;
|
||||
backend_->ChangeDirPath(dir.id, dir.path, mount_point);
|
||||
}
|
||||
}
|
||||
|
||||
// Load the directory properly now
|
||||
backend_->LoadDirectoriesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
void ConnectedDevice::Eject() {
|
||||
manager_->UnmountAsync(manager_->FindDeviceById(unique_id_));
|
||||
}
|
||||
|
||||
void ConnectedDevice::FinishCopy(bool) {
|
||||
lister_->UpdateDeviceFreeSpace(unique_id_);
|
||||
}
|
||||
|
||||
void ConnectedDevice::FinishDelete(bool) {
|
||||
lister_->UpdateDeviceFreeSpace(unique_id_);
|
||||
}
|
||||
|
||||
MusicStorage::TranscodeMode ConnectedDevice::GetTranscodeMode() const {
|
||||
int index = manager_->FindDeviceById(unique_id_);
|
||||
return MusicStorage::TranscodeMode(manager_->index(index).data(DeviceManager::Role_TranscodeMode).toInt());
|
||||
}
|
||||
|
||||
Song::FileType ConnectedDevice::GetTranscodeFormat() const {
|
||||
int index = manager_->FindDeviceById(unique_id_);
|
||||
return Song::FileType(manager_->index(index).data(DeviceManager::Role_TranscodeFormat).toInt());
|
||||
}
|
||||
|
||||
void ConnectedDevice::BackendTotalSongCountUpdated(int count) {
|
||||
song_count_ = count;
|
||||
emit SongCountUpdated(count);
|
||||
}
|
||||
|
||||
97
src/device/connecteddevice.h
Normal file
97
src/device/connecteddevice.h
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef CONNECTEDDEVICE_H
|
||||
#define CONNECTEDDEVICE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/musicstorage.h"
|
||||
#include "core/song.h"
|
||||
|
||||
class Application;
|
||||
class Database;
|
||||
class DeviceLister;
|
||||
class DeviceManager;
|
||||
class CollectionBackend;
|
||||
class CollectionModel;
|
||||
|
||||
class ConnectedDevice : public QObject,
|
||||
public virtual MusicStorage,
|
||||
public std::enable_shared_from_this<ConnectedDevice> {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ConnectedDevice(const QUrl &url, DeviceLister *lister, const QString &unique_id, DeviceManager *manager, Application *app, int database_id, bool first_time);
|
||||
~ConnectedDevice();
|
||||
|
||||
virtual void Init() = 0;
|
||||
// For some devices (e.g. CD devices) we don't have callbacks to be notified
|
||||
// when something change: we can call this method to refresh device's state
|
||||
virtual void Refresh() {}
|
||||
|
||||
virtual TranscodeMode GetTranscodeMode() const;
|
||||
virtual Song::FileType GetTranscodeFormat() const;
|
||||
|
||||
DeviceLister *lister() const { return lister_; }
|
||||
QString unique_id() const { return unique_id_; }
|
||||
CollectionModel *model() const { return model_; }
|
||||
QUrl url() const { return url_; }
|
||||
int song_count() const { return song_count_; }
|
||||
|
||||
virtual void FinishCopy(bool success);
|
||||
virtual void FinishDelete(bool success);
|
||||
|
||||
virtual void Eject();
|
||||
|
||||
signals:
|
||||
void TaskStarted(int id);
|
||||
void SongCountUpdated(int count);
|
||||
|
||||
protected:
|
||||
void InitBackendDirectory(const QString &mount_point, bool first_time, bool rewrite_path = true);
|
||||
|
||||
protected:
|
||||
Application *app_;
|
||||
|
||||
QUrl url_;
|
||||
bool first_time_;
|
||||
DeviceLister *lister_;
|
||||
QString unique_id_;
|
||||
int database_id_;
|
||||
DeviceManager *manager_;
|
||||
|
||||
CollectionBackend *backend_;
|
||||
CollectionModel *model_;
|
||||
|
||||
int song_count_;
|
||||
|
||||
private slots:
|
||||
void BackendTotalSongCountUpdated(int count);
|
||||
};
|
||||
|
||||
#endif // CONNECTEDDEVICE_H
|
||||
|
||||
146
src/device/devicedatabasebackend.cpp
Normal file
146
src/device/devicedatabasebackend.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "devicedatabasebackend.h"
|
||||
#include "core/database.h"
|
||||
#include "core/scopedtransaction.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QSqlQuery>
|
||||
#include <QVariant>
|
||||
|
||||
const int DeviceDatabaseBackend::kDeviceSchemaVersion = 0;
|
||||
|
||||
DeviceDatabaseBackend::DeviceDatabaseBackend(QObject *parent)
|
||||
: QObject(parent) {}
|
||||
|
||||
void DeviceDatabaseBackend::Init(Database* db) { db_ = db; }
|
||||
|
||||
DeviceDatabaseBackend::DeviceList DeviceDatabaseBackend::GetAllDevices() {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
DeviceList ret;
|
||||
|
||||
QSqlQuery q(db);
|
||||
q.prepare("SELECT ROWID, unique_id, friendly_name, size, icon, transcode_mode, transcode_format FROM devices");
|
||||
q.exec();
|
||||
if (db_->CheckErrors(q)) return ret;
|
||||
|
||||
while (q.next()) {
|
||||
Device dev;
|
||||
dev.id_ = q.value(0).toInt();
|
||||
dev.unique_id_ = q.value(1).toString();
|
||||
dev.friendly_name_ = q.value(2).toString();
|
||||
dev.size_ = q.value(3).toLongLong();
|
||||
dev.icon_name_ = q.value(4).toString();
|
||||
dev.transcode_mode_ = MusicStorage::TranscodeMode(q.value(5).toInt());
|
||||
dev.transcode_format_ = Song::FileType(q.value(6).toInt());
|
||||
ret << dev;
|
||||
}
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
int DeviceDatabaseBackend::AddDevice(const Device &device) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
ScopedTransaction t(&db);
|
||||
|
||||
// Insert the device into the devices table
|
||||
QSqlQuery q(db);
|
||||
q.prepare("INSERT INTO devices (unique_id, friendly_name, size, icon, transcode_mode, transcode_format) VALUES (:unique_id, :friendly_name, :size, :icon, :transcode_mode, :transcode_format)");
|
||||
q.bindValue(":unique_id", device.unique_id_);
|
||||
q.bindValue(":friendly_name", device.friendly_name_);
|
||||
q.bindValue(":size", device.size_);
|
||||
q.bindValue(":icon", device.icon_name_);
|
||||
q.bindValue(":transcode_mode", device.transcode_mode_);
|
||||
q.bindValue(":transcode_format", device.transcode_format_);
|
||||
q.exec();
|
||||
if (db_->CheckErrors(q)) return -1;
|
||||
int id = q.lastInsertId().toInt();
|
||||
|
||||
// Create the songs tables for the device
|
||||
QString filename(":schema/device-schema.sql");
|
||||
QFile schema_file(filename);
|
||||
if (!schema_file.open(QIODevice::ReadOnly))
|
||||
qFatal("Couldn't open schema file %s", filename.toUtf8().constData());
|
||||
QString schema = QString::fromUtf8(schema_file.readAll());
|
||||
schema.replace("%deviceid", QString::number(id));
|
||||
|
||||
db_->ExecSchemaCommands(db, schema, 0, true);
|
||||
|
||||
t.Commit();
|
||||
return id;
|
||||
|
||||
}
|
||||
|
||||
void DeviceDatabaseBackend::RemoveDevice(int id) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
ScopedTransaction t(&db);
|
||||
|
||||
// Remove the device from the devices table
|
||||
QSqlQuery q(db);
|
||||
q.prepare("DELETE FROM devices WHERE ROWID=:id");
|
||||
q.bindValue(":id", id);
|
||||
q.exec();
|
||||
if (db_->CheckErrors(q)) return;
|
||||
|
||||
// Remove the songs tables for the device
|
||||
db.exec(QString("DROP TABLE device_%1_songs").arg(id));
|
||||
db.exec(QString("DROP TABLE device_%1_fts").arg(id));
|
||||
db.exec(QString("DROP TABLE device_%1_directories").arg(id));
|
||||
db.exec(QString("DROP TABLE device_%1_subdirectories").arg(id));
|
||||
|
||||
t.Commit();
|
||||
|
||||
}
|
||||
|
||||
void DeviceDatabaseBackend::SetDeviceOptions(int id, const QString &friendly_name, const QString &icon_name, MusicStorage::TranscodeMode mode, Song::FileType format) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery q(db);
|
||||
q.prepare(
|
||||
"UPDATE devices"
|
||||
" SET friendly_name=:friendly_name,"
|
||||
" icon=:icon_name,"
|
||||
" transcode_mode=:transcode_mode,"
|
||||
" transcode_format=:transcode_format"
|
||||
" WHERE ROWID=:id");
|
||||
q.bindValue(":friendly_name", friendly_name);
|
||||
q.bindValue(":icon_name", icon_name);
|
||||
q.bindValue(":transcode_mode", mode);
|
||||
q.bindValue(":transcode_format", format);
|
||||
q.bindValue(":id", id);
|
||||
q.exec();
|
||||
db_->CheckErrors(q);
|
||||
|
||||
}
|
||||
|
||||
68
src/device/devicedatabasebackend.h
Normal file
68
src/device/devicedatabasebackend.h
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DEVICEDATABASEBACKEND_H
|
||||
#define DEVICEDATABASEBACKEND_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "core/musicstorage.h"
|
||||
#include "core/song.h"
|
||||
|
||||
class Database;
|
||||
|
||||
class DeviceDatabaseBackend : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Q_INVOKABLE DeviceDatabaseBackend(QObject *parent = nullptr);
|
||||
|
||||
struct Device {
|
||||
Device() : id_(-1) {}
|
||||
|
||||
int id_;
|
||||
QString unique_id_;
|
||||
QString friendly_name_;
|
||||
quint64 size_;
|
||||
QString icon_name_;
|
||||
|
||||
MusicStorage::TranscodeMode transcode_mode_;
|
||||
Song::FileType transcode_format_;
|
||||
};
|
||||
typedef QList<Device> DeviceList;
|
||||
|
||||
static const int kDeviceSchemaVersion;
|
||||
|
||||
void Init(Database *db);
|
||||
Database *db() const { return db_; }
|
||||
|
||||
DeviceList GetAllDevices();
|
||||
int AddDevice(const Device& device);
|
||||
void RemoveDevice(int id);
|
||||
|
||||
void SetDeviceOptions(int id, const QString &friendly_name, const QString &icon_name, MusicStorage::TranscodeMode mode, Song::FileType format);
|
||||
|
||||
private:
|
||||
Database *db_;
|
||||
};
|
||||
|
||||
#endif // DEVICEDATABASEBACKEND_H
|
||||
290
src/device/devicekitlister.cpp
Normal file
290
src/device/devicekitlister.cpp
Normal file
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <sys/statvfs.h>
|
||||
|
||||
#include <QtDebug>
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "devicekitlister.h"
|
||||
#include "filesystemdevice.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/utilities.h"
|
||||
#include "dbus/udisks.h"
|
||||
#include "dbus/udisksdevice.h"
|
||||
|
||||
#ifdef HAVE_LIBGPOD
|
||||
#include "gpoddevice.h"
|
||||
#endif
|
||||
|
||||
DeviceKitLister::DeviceKitLister() {}
|
||||
DeviceKitLister::~DeviceKitLister() {}
|
||||
//qLog(Debug) << __PRETTY_FUNCTION__;
|
||||
|
||||
QString DeviceKitLister::DeviceData::unique_id() const {
|
||||
return QString("DeviceKit/%1/%2/%3/%4").arg(drive_serial, drive_vendor, drive_model).arg(device_size);
|
||||
}
|
||||
|
||||
void DeviceKitLister::Init() {
|
||||
|
||||
interface_.reset(new OrgFreedesktopUDisksInterface(OrgFreedesktopUDisksInterface::staticInterfaceName(), "/org/freedesktop/UDisks", QDBusConnection::systemBus()));
|
||||
|
||||
// Get all the devices currently attached
|
||||
QDBusPendingReply<QList<QDBusObjectPath> > reply = interface_->EnumerateDevices();
|
||||
reply.waitForFinished();
|
||||
|
||||
if (!reply.isValid()) {
|
||||
qLog(Warning) << "Error enumerating DeviceKit-disks devices:" << reply.error().name() << reply.error().message();
|
||||
interface_.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for changes
|
||||
connect(interface_.get(), SIGNAL(DeviceAdded(QDBusObjectPath)), SLOT(DBusDeviceAdded(QDBusObjectPath)));
|
||||
connect(interface_.get(), SIGNAL(DeviceRemoved(QDBusObjectPath)), SLOT(DBusDeviceRemoved(QDBusObjectPath)));
|
||||
connect(interface_.get(), SIGNAL(DeviceChanged(QDBusObjectPath)), SLOT(DBusDeviceChanged(QDBusObjectPath)));
|
||||
|
||||
// Get information about each one
|
||||
QMap<QString, DeviceData> device_data;
|
||||
for (const QDBusObjectPath &path : reply.value()) {
|
||||
DeviceData data = ReadDeviceData(path);
|
||||
if (data.suitable) device_data[data.unique_id()] = data;
|
||||
}
|
||||
|
||||
// Update the internal cache
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
device_data_ = device_data;
|
||||
}
|
||||
|
||||
// Notify about the changes
|
||||
for (const QString &id : device_data.keys()) {
|
||||
emit DeviceAdded(id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QStringList DeviceKitLister::DeviceUniqueIDs() {
|
||||
QMutexLocker l(&mutex_);
|
||||
return device_data_.keys();
|
||||
}
|
||||
|
||||
QVariantList DeviceKitLister::DeviceIcons(const QString &id) {
|
||||
|
||||
QString path = LockAndGetDeviceInfo(id, &DeviceData::device_mount_paths)[0];
|
||||
return QVariantList()
|
||||
<< GuessIconForPath(path)
|
||||
<< GuessIconForModel(DeviceManufacturer(id), DeviceModel(id))
|
||||
<< LockAndGetDeviceInfo(id, &DeviceData::device_presentation_icon_name);
|
||||
}
|
||||
|
||||
QString DeviceKitLister::DeviceManufacturer(const QString &id) {
|
||||
return LockAndGetDeviceInfo(id, &DeviceData::drive_vendor);
|
||||
}
|
||||
|
||||
QString DeviceKitLister::DeviceModel(const QString &id) {
|
||||
return LockAndGetDeviceInfo(id, &DeviceData::drive_model);
|
||||
}
|
||||
|
||||
quint64 DeviceKitLister::DeviceCapacity(const QString &id) {
|
||||
return LockAndGetDeviceInfo(id, &DeviceData::device_size);
|
||||
}
|
||||
|
||||
quint64 DeviceKitLister::DeviceFreeSpace(const QString &id) {
|
||||
return LockAndGetDeviceInfo(id, &DeviceData::free_space);
|
||||
}
|
||||
|
||||
QVariantMap DeviceKitLister::DeviceHardwareInfo(const QString &id) {
|
||||
|
||||
QVariantMap ret;
|
||||
|
||||
QMutexLocker l(&mutex_);
|
||||
if (!device_data_.contains(id)) return ret;
|
||||
const DeviceData &data = device_data_[id];
|
||||
|
||||
ret[QT_TR_NOOP("DBus path")] = data.dbus_path;
|
||||
ret[QT_TR_NOOP("Serial number")] = data.drive_serial;
|
||||
ret[QT_TR_NOOP("Mount points")] = data.device_mount_paths.join(", ");
|
||||
ret[QT_TR_NOOP("Device")] = data.device_file;
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QString DeviceKitLister::MakeFriendlyName(const QString &id) {
|
||||
|
||||
QMutexLocker l(&mutex_);
|
||||
if (!device_data_.contains(id)) return QString();
|
||||
const DeviceData &data = device_data_[id];
|
||||
|
||||
if (!data.device_presentation_name.isEmpty())
|
||||
return data.device_presentation_name;
|
||||
if (!data.drive_model.isEmpty() && !data.drive_vendor.isEmpty())
|
||||
return data.drive_vendor + " " + data.drive_model;
|
||||
if (!data.drive_model.isEmpty()) return data.drive_model;
|
||||
return data.drive_serial;
|
||||
|
||||
}
|
||||
|
||||
DeviceKitLister::DeviceData DeviceKitLister::ReadDeviceData(const QDBusObjectPath &path) const {
|
||||
|
||||
DeviceData ret;
|
||||
|
||||
OrgFreedesktopUDisksDeviceInterface device(OrgFreedesktopUDisksInterface::staticInterfaceName(), path.path(), QDBusConnection::systemBus());
|
||||
if (!device.isValid()) {
|
||||
qLog(Warning) << "Error connecting to the device interface on" << path.path();
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Don't do anything with internal drives, hidden drives, or partition tables
|
||||
if (device.deviceIsSystemInternal() || device.devicePresentationHide() || device.deviceMountPaths().isEmpty() || device.deviceIsPartitionTable()) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret.suitable = true;
|
||||
ret.dbus_path = path.path();
|
||||
ret.drive_serial = device.driveSerial();
|
||||
ret.drive_model = device.driveModel();
|
||||
ret.drive_vendor = device.driveVendor();
|
||||
ret.device_file = device.deviceFile();
|
||||
ret.device_presentation_name = device.devicePresentationName();
|
||||
ret.device_presentation_icon_name = device.devicePresentationIconName();
|
||||
ret.device_size = device.deviceSize();
|
||||
ret.device_mount_paths = device.deviceMountPaths();
|
||||
|
||||
// Get free space info
|
||||
if (!ret.device_mount_paths.isEmpty())
|
||||
ret.free_space = Utilities::FileSystemFreeSpace(ret.device_mount_paths[0]);
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void DeviceKitLister::DBusDeviceAdded(const QDBusObjectPath &path) {
|
||||
|
||||
DeviceData data = ReadDeviceData(path);
|
||||
if (!data.suitable) return;
|
||||
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
device_data_[data.unique_id()] = data;
|
||||
}
|
||||
|
||||
emit DeviceAdded(data.unique_id());
|
||||
|
||||
}
|
||||
|
||||
void DeviceKitLister::DBusDeviceRemoved(const QDBusObjectPath &path) {
|
||||
|
||||
QString id;
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
id = FindUniqueIdByPath(path);
|
||||
if (id.isNull()) return;
|
||||
|
||||
device_data_.remove(id);
|
||||
}
|
||||
|
||||
emit DeviceRemoved(id);
|
||||
|
||||
}
|
||||
|
||||
void DeviceKitLister::DBusDeviceChanged(const QDBusObjectPath &path) {
|
||||
|
||||
bool already_known = false;
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
already_known = !FindUniqueIdByPath(path).isNull();
|
||||
}
|
||||
|
||||
DeviceData data = ReadDeviceData(path);
|
||||
|
||||
if (already_known && !data.suitable)
|
||||
DBusDeviceRemoved(path);
|
||||
else if (!already_known && data.suitable)
|
||||
DBusDeviceAdded(path);
|
||||
else if (already_known && data.suitable) {
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
device_data_[data.unique_id()] = data;
|
||||
}
|
||||
emit DeviceChanged(data.unique_id());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QString DeviceKitLister::FindUniqueIdByPath(const QDBusObjectPath &path) const {
|
||||
|
||||
for (const DeviceData &data : device_data_) {
|
||||
if (data.dbus_path == path.path()) return data.unique_id();
|
||||
}
|
||||
return QString();
|
||||
|
||||
}
|
||||
|
||||
QList<QUrl> DeviceKitLister::MakeDeviceUrls(const QString &id) {
|
||||
|
||||
QString mount_point = LockAndGetDeviceInfo(id, &DeviceData::device_mount_paths)[0];
|
||||
|
||||
return QList<QUrl>() << MakeUrlFromLocalPath(mount_point);
|
||||
}
|
||||
|
||||
void DeviceKitLister::UnmountDevice(const QString &id) {
|
||||
|
||||
QString path = LockAndGetDeviceInfo(id, &DeviceData::dbus_path);
|
||||
|
||||
OrgFreedesktopUDisksDeviceInterface device(OrgFreedesktopUDisksInterface::staticInterfaceName(), path, QDBusConnection::systemBus());
|
||||
if (!device.isValid()) {
|
||||
qLog(Warning) << "Error connecting to the device interface on" << path;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the device's parent drive
|
||||
QString drive_path = device.partitionSlave().path();
|
||||
OrgFreedesktopUDisksDeviceInterface drive(OrgFreedesktopUDisksInterface::staticInterfaceName(), drive_path, QDBusConnection::systemBus());
|
||||
if (!drive.isValid()) {
|
||||
qLog(Warning) << "Error connecting to the drive interface on" << drive_path;
|
||||
return;
|
||||
}
|
||||
|
||||
// Unmount the filesystem
|
||||
QDBusPendingReply<> reply = device.FilesystemUnmount(QStringList());
|
||||
reply.waitForFinished();
|
||||
|
||||
// Eject the drive
|
||||
drive.DriveEject(QStringList());
|
||||
// Don't bother waiting for the eject to finish
|
||||
|
||||
}
|
||||
|
||||
void DeviceKitLister::UpdateDeviceFreeSpace(const QString &id) {
|
||||
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
if (!device_data_.contains(id)) return;
|
||||
|
||||
DeviceData &data = device_data_[id];
|
||||
if (!data.device_mount_paths.isEmpty())
|
||||
data.free_space = Utilities::FileSystemFreeSpace(data.device_mount_paths[0]);
|
||||
}
|
||||
|
||||
emit DeviceChanged(id);
|
||||
}
|
||||
|
||||
110
src/device/devicekitlister.h
Normal file
110
src/device/devicekitlister.h
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DEVICEKITLISTER_H
|
||||
#define DEVICEKITLISTER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QMutex>
|
||||
#include <QStringList>
|
||||
|
||||
#include "devicelister.h"
|
||||
|
||||
class OrgFreedesktopUDisksInterface;
|
||||
|
||||
class QDBusObjectPath;
|
||||
|
||||
class DeviceKitLister : public DeviceLister {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DeviceKitLister();
|
||||
~DeviceKitLister();
|
||||
|
||||
QStringList DeviceUniqueIDs();
|
||||
QVariantList DeviceIcons(const QString &id);
|
||||
QString DeviceManufacturer(const QString &id);
|
||||
QString DeviceModel(const QString &id);
|
||||
quint64 DeviceCapacity(const QString &id);
|
||||
quint64 DeviceFreeSpace(const QString &id);
|
||||
QVariantMap DeviceHardwareInfo(const QString &id);
|
||||
|
||||
QString MakeFriendlyName(const QString &id);
|
||||
QList<QUrl> MakeDeviceUrls(const QString &id);
|
||||
|
||||
void UnmountDevice(const QString &id);
|
||||
|
||||
public slots:
|
||||
void UpdateDeviceFreeSpace(const QString &id);
|
||||
|
||||
protected:
|
||||
void Init();
|
||||
|
||||
private slots:
|
||||
void DBusDeviceAdded(const QDBusObjectPath &path);
|
||||
void DBusDeviceRemoved(const QDBusObjectPath &path);
|
||||
void DBusDeviceChanged(const QDBusObjectPath &path);
|
||||
|
||||
private:
|
||||
struct DeviceData {
|
||||
DeviceData() : suitable(false), device_size(0), free_space(0) {}
|
||||
|
||||
QString unique_id() const;
|
||||
|
||||
bool suitable;
|
||||
QString dbus_path;
|
||||
QString drive_serial;
|
||||
QString drive_model;
|
||||
QString drive_vendor;
|
||||
QString device_file;
|
||||
QString device_presentation_name;
|
||||
QString device_presentation_icon_name;
|
||||
QStringList device_mount_paths;
|
||||
quint64 device_size;
|
||||
quint64 free_space;
|
||||
};
|
||||
|
||||
DeviceData ReadDeviceData(const QDBusObjectPath &path) const;
|
||||
|
||||
// You MUST hold the mutex while calling this function
|
||||
QString FindUniqueIdByPath(const QDBusObjectPath &path) const;
|
||||
|
||||
template <typename T>
|
||||
T LockAndGetDeviceInfo(const QString &id, T DeviceData::*field);
|
||||
|
||||
private:
|
||||
std::unique_ptr<OrgFreedesktopUDisksInterface> interface_;
|
||||
|
||||
QMutex mutex_;
|
||||
QMap<QString, DeviceData> device_data_;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
T DeviceKitLister::LockAndGetDeviceInfo(const QString &id, T DeviceData::*field) {
|
||||
QMutexLocker l(&mutex_);
|
||||
if (!device_data_.contains(id)) return T();
|
||||
|
||||
return device_data_[id].*field;
|
||||
}
|
||||
|
||||
#endif // DEVICEKITLISTER_H
|
||||
234
src/device/devicelister.cpp
Normal file
234
src/device/devicelister.cpp
Normal file
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QStringList>
|
||||
#include <QThread>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "devicelister.h"
|
||||
|
||||
#ifdef HAVE_LIBGPOD
|
||||
#include <gpod/itdb.h>
|
||||
#endif
|
||||
|
||||
DeviceLister::DeviceLister() : thread_(nullptr) {}
|
||||
|
||||
DeviceLister::~DeviceLister() {
|
||||
|
||||
if (thread_) {
|
||||
thread_->quit();
|
||||
thread_->wait(1000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DeviceLister::Start() {
|
||||
|
||||
thread_ = new QThread;
|
||||
connect(thread_, SIGNAL(started()), SLOT(ThreadStarted()));
|
||||
|
||||
moveToThread(thread_);
|
||||
thread_->start();
|
||||
|
||||
}
|
||||
|
||||
void DeviceLister::ThreadStarted() { Init(); }
|
||||
|
||||
namespace {
|
||||
|
||||
#ifdef HAVE_LIBGPOD
|
||||
|
||||
QString GetIpodColour(Itdb_IpodModel model) {
|
||||
|
||||
switch (model) {
|
||||
case ITDB_IPOD_MODEL_MINI_GREEN:
|
||||
case ITDB_IPOD_MODEL_NANO_GREEN:
|
||||
case ITDB_IPOD_MODEL_SHUFFLE_GREEN:
|
||||
return "green";
|
||||
|
||||
case ITDB_IPOD_MODEL_MINI_BLUE:
|
||||
case ITDB_IPOD_MODEL_NANO_BLUE:
|
||||
case ITDB_IPOD_MODEL_SHUFFLE_BLUE:
|
||||
return "blue";
|
||||
|
||||
case ITDB_IPOD_MODEL_MINI_PINK:
|
||||
case ITDB_IPOD_MODEL_NANO_PINK:
|
||||
case ITDB_IPOD_MODEL_SHUFFLE_PINK:
|
||||
return "pink";
|
||||
|
||||
case ITDB_IPOD_MODEL_MINI_GOLD:
|
||||
return "gold";
|
||||
|
||||
case ITDB_IPOD_MODEL_NANO_WHITE:
|
||||
case ITDB_IPOD_MODEL_VIDEO_WHITE:
|
||||
return "white";
|
||||
|
||||
case ITDB_IPOD_MODEL_NANO_SILVER:
|
||||
case ITDB_IPOD_MODEL_CLASSIC_SILVER:
|
||||
return "silver";
|
||||
|
||||
case ITDB_IPOD_MODEL_NANO_RED:
|
||||
case ITDB_IPOD_MODEL_SHUFFLE_RED:
|
||||
return "red";
|
||||
|
||||
case ITDB_IPOD_MODEL_NANO_YELLOW:
|
||||
return "yellow";
|
||||
|
||||
case ITDB_IPOD_MODEL_NANO_PURPLE:
|
||||
case ITDB_IPOD_MODEL_SHUFFLE_PURPLE:
|
||||
return "purple";
|
||||
|
||||
case ITDB_IPOD_MODEL_NANO_ORANGE:
|
||||
case ITDB_IPOD_MODEL_SHUFFLE_ORANGE:
|
||||
return "orange";
|
||||
|
||||
case ITDB_IPOD_MODEL_NANO_BLACK:
|
||||
case ITDB_IPOD_MODEL_VIDEO_BLACK:
|
||||
case ITDB_IPOD_MODEL_CLASSIC_BLACK:
|
||||
return "black";
|
||||
|
||||
default:
|
||||
return QString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QString GetIpodModel(Itdb_IpodModel model) {
|
||||
|
||||
switch (model) {
|
||||
case ITDB_IPOD_MODEL_MINI:
|
||||
case ITDB_IPOD_MODEL_MINI_BLUE:
|
||||
case ITDB_IPOD_MODEL_MINI_PINK:
|
||||
case ITDB_IPOD_MODEL_MINI_GREEN:
|
||||
case ITDB_IPOD_MODEL_MINI_GOLD:
|
||||
return "mini";
|
||||
|
||||
case ITDB_IPOD_MODEL_NANO_WHITE:
|
||||
case ITDB_IPOD_MODEL_NANO_BLACK:
|
||||
case ITDB_IPOD_MODEL_NANO_SILVER:
|
||||
case ITDB_IPOD_MODEL_NANO_BLUE:
|
||||
case ITDB_IPOD_MODEL_NANO_GREEN:
|
||||
case ITDB_IPOD_MODEL_NANO_PINK:
|
||||
case ITDB_IPOD_MODEL_NANO_RED:
|
||||
case ITDB_IPOD_MODEL_NANO_YELLOW:
|
||||
case ITDB_IPOD_MODEL_NANO_PURPLE:
|
||||
case ITDB_IPOD_MODEL_NANO_ORANGE:
|
||||
return "nano";
|
||||
|
||||
case ITDB_IPOD_MODEL_SHUFFLE:
|
||||
case ITDB_IPOD_MODEL_SHUFFLE_SILVER:
|
||||
case ITDB_IPOD_MODEL_SHUFFLE_PINK:
|
||||
case ITDB_IPOD_MODEL_SHUFFLE_BLUE:
|
||||
case ITDB_IPOD_MODEL_SHUFFLE_GREEN:
|
||||
case ITDB_IPOD_MODEL_SHUFFLE_ORANGE:
|
||||
case ITDB_IPOD_MODEL_SHUFFLE_RED:
|
||||
return "shuffle";
|
||||
|
||||
case ITDB_IPOD_MODEL_COLOR:
|
||||
case ITDB_IPOD_MODEL_REGULAR:
|
||||
case ITDB_IPOD_MODEL_CLASSIC_SILVER:
|
||||
case ITDB_IPOD_MODEL_CLASSIC_BLACK:
|
||||
return "standard";
|
||||
|
||||
case ITDB_IPOD_MODEL_COLOR_U2:
|
||||
case ITDB_IPOD_MODEL_REGULAR_U2:
|
||||
return "U2";
|
||||
|
||||
default:
|
||||
return QString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
QUrl DeviceLister::MakeUrlFromLocalPath(const QString &path) const {
|
||||
|
||||
if (IsIpod(path)) {
|
||||
QUrl ret;
|
||||
ret.setScheme("ipod");
|
||||
ret.setPath(QDir::fromNativeSeparators(path));
|
||||
return ret;
|
||||
}
|
||||
|
||||
return QUrl::fromLocalFile(path);
|
||||
|
||||
}
|
||||
|
||||
bool DeviceLister::IsIpod(const QString &path) const {
|
||||
return QFile::exists(path + "/iTunes_Control") ||
|
||||
QFile::exists(path + "/iPod_Control") ||
|
||||
QFile::exists(path + "/iTunes/iTunes_Control");
|
||||
}
|
||||
|
||||
QStringList DeviceLister::GuessIconForPath(const QString &path) {
|
||||
|
||||
QStringList ret;
|
||||
|
||||
#ifdef HAVE_LIBGPOD
|
||||
if (IsIpod(path)) {
|
||||
Itdb_Device* device = itdb_device_new();
|
||||
itdb_device_set_mountpoint(device, path.toLocal8Bit().constData());
|
||||
const Itdb_IpodInfo* info = itdb_device_get_ipod_info(device);
|
||||
|
||||
QString colour = GetIpodColour(info->ipod_model);
|
||||
QString model = GetIpodModel(info->ipod_model);
|
||||
|
||||
itdb_device_free(device);
|
||||
|
||||
if (!colour.isEmpty()) {
|
||||
QString colour_icon = "multimedia-player-ipod-%1-%2";
|
||||
ret << colour_icon.arg(model, colour);
|
||||
}
|
||||
|
||||
if (!model.isEmpty()) {
|
||||
QString model_icon = "multimedia-player-ipod-%1";
|
||||
ret << model_icon.arg(model);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QStringList DeviceLister::GuessIconForModel(const QString &vendor, const QString &model) {
|
||||
|
||||
QStringList ret;
|
||||
if (vendor.startsWith("Google") && model.contains("Nexus")) {
|
||||
ret << "phone-google-nexus-one";
|
||||
}
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
int DeviceLister::MountDevice(const QString &id) {
|
||||
|
||||
const int ret = next_mount_request_id_++;
|
||||
emit DeviceMounted(id, ret, true);
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
98
src/device/devicelister.h
Normal file
98
src/device/devicelister.h
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DEVICELISTER_H
|
||||
#define DEVICELISTER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include <QUrl>
|
||||
|
||||
class ConnectedDevice;
|
||||
class DeviceManager;
|
||||
|
||||
class DeviceLister : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DeviceLister();
|
||||
virtual ~DeviceLister();
|
||||
|
||||
// Tries to start the thread and initialise the engine. This object will be
|
||||
// moved to the new thread.
|
||||
void Start();
|
||||
|
||||
// If two listers know about the same device, then the metadata will get
|
||||
// taken from the one with the highest priority.
|
||||
virtual int priority() const { return 100; }
|
||||
|
||||
// Query information about the devices that are available. Must be thread-safe.
|
||||
virtual QStringList DeviceUniqueIDs() = 0;
|
||||
virtual QVariantList DeviceIcons(const QString &id) = 0;
|
||||
virtual QString DeviceManufacturer(const QString &id) = 0;
|
||||
virtual QString DeviceModel(const QString &id) = 0;
|
||||
virtual quint64 DeviceCapacity(const QString &id) = 0;
|
||||
virtual quint64 DeviceFreeSpace(const QString &id) = 0;
|
||||
virtual QVariantMap DeviceHardwareInfo(const QString &id) = 0;
|
||||
virtual bool DeviceNeedsMount(const QString &id) { return false; }
|
||||
|
||||
// When connecting to a device for the first time, do we want an user's
|
||||
// confirmation for scanning it? (by default yes)
|
||||
virtual bool AskForScan(const QString&) const { return true; }
|
||||
|
||||
virtual QString MakeFriendlyName(const QString &id) = 0;
|
||||
virtual QList<QUrl> MakeDeviceUrls(const QString &id) = 0;
|
||||
|
||||
// Ensure the device is mounted. This should run asynchronously and emit
|
||||
// DeviceMounted when it's done.
|
||||
virtual int MountDevice(const QString &id);
|
||||
|
||||
// Do whatever needs to be done to safely remove the device.
|
||||
virtual void UnmountDevice(const QString &id) = 0;
|
||||
|
||||
public slots:
|
||||
virtual void UpdateDeviceFreeSpace(const QString &id) = 0;
|
||||
virtual void ShutDown() {}
|
||||
|
||||
signals:
|
||||
void DeviceAdded(const QString &id);
|
||||
void DeviceRemoved(const QString &id);
|
||||
void DeviceChanged(const QString &id);
|
||||
void DeviceMounted(const QString &id, int request_id, bool success);
|
||||
|
||||
protected:
|
||||
virtual void Init() = 0;
|
||||
QUrl MakeUrlFromLocalPath(const QString &path) const;
|
||||
bool IsIpod(const QString &path) const;
|
||||
|
||||
QStringList GuessIconForPath(const QString &path);
|
||||
QStringList GuessIconForModel(const QString &vendor, const QString &model);
|
||||
|
||||
protected:
|
||||
QThread* thread_;
|
||||
int next_mount_request_id_;
|
||||
|
||||
private slots:
|
||||
void ThreadStarted();
|
||||
};
|
||||
|
||||
#endif // DEVICELISTER_H
|
||||
|
||||
789
src/device/devicemanager.cpp
Normal file
789
src/device/devicemanager.cpp
Normal file
@@ -0,0 +1,789 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QIcon>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QUrl>
|
||||
|
||||
#include "devicemanager.h"
|
||||
|
||||
#include "devicedatabasebackend.h"
|
||||
#include "devicekitlister.h"
|
||||
#include "devicestatefiltermodel.h"
|
||||
#include "filesystemdevice.h"
|
||||
#include "core/application.h"
|
||||
#include "core/concurrentrun.h"
|
||||
#include "core/database.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/musicstorage.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "core/utilities.h"
|
||||
#include "core/iconloader.h"
|
||||
|
||||
#if defined(HAVE_AUDIOCD) && defined(HAVE_GSTREAMER)
|
||||
#include "cddalister.h"
|
||||
#include "cddadevice.h"
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_DARWIN) and defined(HAVE_LIBMTP)
|
||||
#include "macdevicelister.h"
|
||||
#endif
|
||||
#ifdef HAVE_LIBGPOD
|
||||
#include "gpoddevice.h"
|
||||
#endif
|
||||
#ifdef HAVE_GIO
|
||||
#include "giolister.h"
|
||||
#endif
|
||||
#ifdef HAVE_IMOBILEDEVICE
|
||||
# include "afcdevice.h"
|
||||
# include "ilister.h"
|
||||
#endif
|
||||
#ifdef HAVE_LIBMTP
|
||||
#include "mtpdevice.h"
|
||||
#endif
|
||||
#ifdef HAVE_UDISKS2
|
||||
#include "udisks2lister.h"
|
||||
#endif
|
||||
|
||||
using std::bind;
|
||||
|
||||
const int DeviceManager::kDeviceIconSize = 32;
|
||||
const int DeviceManager::kDeviceIconOverlaySize = 16;
|
||||
|
||||
DeviceManager::DeviceInfo::DeviceInfo()
|
||||
: database_id_(-1),
|
||||
transcode_mode_(MusicStorage::Transcode_Unsupported),
|
||||
transcode_format_(Song::Type_Unknown),
|
||||
task_percentage_(-1) {}
|
||||
|
||||
DeviceDatabaseBackend::Device DeviceManager::DeviceInfo::SaveToDb() const {
|
||||
|
||||
DeviceDatabaseBackend::Device ret;
|
||||
ret.friendly_name_ = friendly_name_;
|
||||
ret.size_ = size_;
|
||||
ret.id_ = database_id_;
|
||||
ret.icon_name_ = icon_name_;
|
||||
ret.transcode_mode_ = transcode_mode_;
|
||||
ret.transcode_format_ = transcode_format_;
|
||||
|
||||
QStringList unique_ids;
|
||||
for (const Backend &backend : backends_) {
|
||||
unique_ids << backend.unique_id_;
|
||||
}
|
||||
ret.unique_id_ = unique_ids.join(",");
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::DeviceInfo::InitFromDb(const DeviceDatabaseBackend::Device &dev) {
|
||||
|
||||
database_id_ = dev.id_;
|
||||
friendly_name_ = dev.friendly_name_;
|
||||
size_ = dev.size_;
|
||||
transcode_mode_ = dev.transcode_mode_;
|
||||
transcode_format_ = dev.transcode_format_;
|
||||
|
||||
QStringList icon_names = dev.icon_name_.split(',');
|
||||
QVariantList icons;
|
||||
for (const QString &icon_name : icon_names) {
|
||||
icons << icon_name;
|
||||
}
|
||||
|
||||
LoadIcon(icons, friendly_name_);
|
||||
|
||||
QStringList unique_ids = dev.unique_id_.split(',');
|
||||
for (const QString &id : unique_ids) {
|
||||
backends_ << Backend(nullptr, id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::DeviceInfo::LoadIcon(const QVariantList &icons, const QString &name_hint) {
|
||||
|
||||
icon_name_ = "device";
|
||||
|
||||
if (icons.isEmpty()) {
|
||||
icon_ = IconLoader::Load(icon_name_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to load the icon with that exact name first
|
||||
for (const QVariant &icon : icons) {
|
||||
if (!icon.value<QPixmap>().isNull()) {
|
||||
icon_ = QIcon(icon.value<QPixmap>());
|
||||
return;
|
||||
}
|
||||
else {
|
||||
if (!icon.toString().isEmpty()) icon_ = IconLoader::Load(icon.toString());
|
||||
if (!icon_.isNull()) {
|
||||
icon_name_ = icon.toString();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString hint = QString(icons.first().toString() + name_hint).toLower();
|
||||
|
||||
if (hint.contains("phone"))
|
||||
icon_name_ = "device-phone";
|
||||
else if (hint.contains("ipod") || hint.contains("apple"))
|
||||
icon_name_ = "device-ipod";
|
||||
else if ((hint.contains("usb")) && (hint.contains("reader")))
|
||||
icon_name_ = "device-usb-flash";
|
||||
else if (hint.contains("usb"))
|
||||
icon_name_ = "device-usb-drive";
|
||||
else
|
||||
icon_name_ = "device";
|
||||
|
||||
icon_ = IconLoader::Load(icon_name_);
|
||||
|
||||
}
|
||||
|
||||
const DeviceManager::DeviceInfo::Backend *DeviceManager::DeviceInfo::BestBackend() const {
|
||||
|
||||
int best_priority = -1;
|
||||
const Backend *ret = nullptr;
|
||||
|
||||
for (int i = 0; i < backends_.count(); ++i) {
|
||||
if (backends_[i].lister_ && backends_[i].lister_->priority() > best_priority) {
|
||||
best_priority = backends_[i].lister_->priority();
|
||||
ret = &(backends_[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ret && !backends_.isEmpty()) return &(backends_[0]);
|
||||
return ret;
|
||||
}
|
||||
|
||||
DeviceManager::DeviceManager(Application *app, QObject *parent)
|
||||
: QAbstractListModel(parent),
|
||||
app_(app),
|
||||
not_connected_overlay_(IconLoader::Load("edit-delete"))
|
||||
{
|
||||
thread_pool_.setMaxThreadCount(1);
|
||||
connect(app_->task_manager(), SIGNAL(TasksChanged()), SLOT(TasksChanged()));
|
||||
|
||||
// Create the backend in the database thread
|
||||
backend_ = new DeviceDatabaseBackend;
|
||||
backend_->moveToThread(app_->database()->thread());
|
||||
backend_->Init(app_->database());
|
||||
|
||||
// This reads from the database and contends on the database mutex, which can
|
||||
// be very slow on startup.
|
||||
ConcurrentRun::Run<void>(&thread_pool_, bind(&DeviceManager::LoadAllDevices, this));
|
||||
|
||||
// This proxy model only shows connected devices
|
||||
connected_devices_model_ = new DeviceStateFilterModel(this);
|
||||
connected_devices_model_->setSourceModel(this);
|
||||
|
||||
// CD devices are detected via the DiskArbitration framework instead on Darwin.
|
||||
#if defined(HAVE_AUDIOCD) && defined(HAVE_GSTREAMER) && !defined(Q_OS_DARWIN)
|
||||
AddLister(new CddaLister);
|
||||
#endif
|
||||
#ifdef HAVE_DEVICEKIT
|
||||
AddLister(new DeviceKitLister);
|
||||
#endif
|
||||
#ifdef HAVE_UDISKS2
|
||||
AddLister(new Udisks2Lister);
|
||||
#endif
|
||||
#ifdef HAVE_GIO
|
||||
AddLister(new GioLister);
|
||||
#endif
|
||||
#if defined(Q_OS_DARWIN) and defined(HAVE_LIBMTP)
|
||||
AddLister(new MacDeviceLister);
|
||||
#endif
|
||||
|
||||
AddDeviceClass<FilesystemDevice>();
|
||||
|
||||
#if defined(HAVE_AUDIOCD) && defined(HAVE_GSTREAMER)
|
||||
AddDeviceClass<CddaDevice>();
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_LIBGPOD
|
||||
AddDeviceClass<GPodDevice>();
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_LIBMTP
|
||||
AddDeviceClass<MtpDevice>();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
DeviceManager::~DeviceManager() {
|
||||
|
||||
for (DeviceLister *lister : listers_) {
|
||||
lister->ShutDown();
|
||||
delete lister;
|
||||
}
|
||||
|
||||
backend_->deleteLater();
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::LoadAllDevices() {
|
||||
|
||||
Q_ASSERT(QThread::currentThread() != qApp->thread());
|
||||
DeviceDatabaseBackend::DeviceList devices = backend_->GetAllDevices();
|
||||
for (const DeviceDatabaseBackend::Device &device : devices) {
|
||||
DeviceInfo info;
|
||||
info.InitFromDb(device);
|
||||
|
||||
beginInsertRows(QModelIndex(), devices_.count(), devices_.count());
|
||||
devices_ << info;
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int DeviceManager::rowCount(const QModelIndex&) const {
|
||||
return devices_.count();
|
||||
}
|
||||
|
||||
QVariant DeviceManager::data(const QModelIndex &index, int role) const {
|
||||
|
||||
if (!index.isValid() || index.column() != 0) return QVariant();
|
||||
|
||||
const DeviceInfo &info = devices_[index.row()];
|
||||
|
||||
switch (role) {
|
||||
case Qt::DisplayRole: {
|
||||
QString text;
|
||||
if (!info.friendly_name_.isEmpty())
|
||||
text = info.friendly_name_;
|
||||
else
|
||||
text = info.BestBackend()->unique_id_;
|
||||
|
||||
if (info.size_)
|
||||
text = text + QString(" (%1)").arg(Utilities::PrettySize(info.size_));
|
||||
if (info.device_.get()) info.device_->Refresh();
|
||||
return text;
|
||||
}
|
||||
|
||||
case Qt::DecorationRole: {
|
||||
QPixmap pixmap = info.icon_.pixmap(kDeviceIconSize);
|
||||
|
||||
if (info.backends_.isEmpty() || !info.BestBackend()->lister_) {
|
||||
// Disconnected but remembered
|
||||
QPainter p(&pixmap);
|
||||
p.drawPixmap(kDeviceIconSize - kDeviceIconOverlaySize, kDeviceIconSize - kDeviceIconOverlaySize, not_connected_overlay_.pixmap(kDeviceIconOverlaySize));
|
||||
}
|
||||
|
||||
return pixmap;
|
||||
}
|
||||
|
||||
case Role_FriendlyName:
|
||||
return info.friendly_name_;
|
||||
|
||||
case Role_UniqueId:
|
||||
return info.BestBackend()->unique_id_;
|
||||
|
||||
case Role_IconName:
|
||||
return info.icon_name_;
|
||||
|
||||
case Role_Capacity:
|
||||
case MusicStorage::Role_Capacity:
|
||||
return info.size_;
|
||||
|
||||
case Role_FreeSpace:
|
||||
case MusicStorage::Role_FreeSpace:
|
||||
return info.BestBackend()->lister_ ? info.BestBackend()->lister_->DeviceFreeSpace(info.BestBackend()->unique_id_) : QVariant();
|
||||
|
||||
case Role_State:
|
||||
if (info.device_) return State_Connected;
|
||||
if (info.BestBackend()->lister_) {
|
||||
if (info.BestBackend()->lister_->DeviceNeedsMount(info.BestBackend()->unique_id_)) return State_NotMounted;
|
||||
return State_NotConnected;
|
||||
}
|
||||
return State_Remembered;
|
||||
|
||||
case Role_UpdatingPercentage:
|
||||
if (info.task_percentage_ == -1) return QVariant();
|
||||
return info.task_percentage_;
|
||||
|
||||
case MusicStorage::Role_Storage:
|
||||
if (!info.device_ && info.database_id_ != -1)
|
||||
const_cast<DeviceManager*>(this)->Connect(index.row());
|
||||
if (!info.device_) return QVariant();
|
||||
return QVariant::fromValue<std::shared_ptr<MusicStorage>>(info.device_);
|
||||
|
||||
case MusicStorage::Role_StorageForceConnect:
|
||||
if (!info.device_) {
|
||||
if (info.database_id_ == -1 && !info.BestBackend()->lister_->DeviceNeedsMount(info.BestBackend()->unique_id_)) {
|
||||
|
||||
if (info.BestBackend()->lister_->AskForScan(info.BestBackend()->unique_id_)) {
|
||||
std::unique_ptr<QMessageBox> dialog(new QMessageBox(QMessageBox::Information, tr("Connect device"), tr("This is the first time you have connected this device. Strawberry will now scan the device to find music files - this may take some time."), QMessageBox::Cancel));
|
||||
QPushButton *connect = dialog->addButton(tr("Connect device"), QMessageBox::AcceptRole);
|
||||
dialog->exec();
|
||||
|
||||
if (dialog->clickedButton() != connect) return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
const_cast<DeviceManager*>(this)->Connect(index.row());
|
||||
}
|
||||
if (!info.device_) return QVariant();
|
||||
return QVariant::fromValue<std::shared_ptr<MusicStorage>>(info.device_);
|
||||
|
||||
case Role_MountPath: {
|
||||
if (!info.device_) return QVariant();
|
||||
|
||||
QString ret = info.device_->url().path();
|
||||
#ifdef Q_OS_WIN32
|
||||
if (ret.startsWith('/')) ret.remove(0, 1);
|
||||
#endif
|
||||
return QDir::toNativeSeparators(ret);
|
||||
}
|
||||
|
||||
case Role_TranscodeMode:
|
||||
return info.transcode_mode_;
|
||||
|
||||
case Role_TranscodeFormat:
|
||||
return info.transcode_format_;
|
||||
|
||||
case Role_SongCount:
|
||||
if (!info.device_) return QVariant();
|
||||
return info.device_->song_count();
|
||||
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
void DeviceManager::AddLister(DeviceLister *lister) {
|
||||
|
||||
listers_ << lister;
|
||||
connect(lister, SIGNAL(DeviceAdded(QString)), SLOT(PhysicalDeviceAdded(QString)));
|
||||
connect(lister, SIGNAL(DeviceRemoved(QString)), SLOT(PhysicalDeviceRemoved(QString)));
|
||||
connect(lister, SIGNAL(DeviceChanged(QString)), SLOT(PhysicalDeviceChanged(QString)));
|
||||
|
||||
lister->Start();
|
||||
|
||||
}
|
||||
|
||||
int DeviceManager::FindDeviceById(const QString &id) const {
|
||||
|
||||
for (int i = 0; i < devices_.count(); ++i) {
|
||||
for (const DeviceInfo::Backend &backend : devices_[i].backends_) {
|
||||
if (backend.unique_id_ == id) return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
|
||||
}
|
||||
|
||||
int DeviceManager::FindDeviceByUrl(const QList<QUrl> &urls) const {
|
||||
|
||||
if (urls.isEmpty()) return -1;
|
||||
|
||||
for (int i = 0; i < devices_.count(); ++i) {
|
||||
for (const DeviceInfo::Backend &backend : devices_[i].backends_) {
|
||||
if (!backend.lister_) continue;
|
||||
|
||||
QList<QUrl> device_urls =
|
||||
backend.lister_->MakeDeviceUrls(backend.unique_id_);
|
||||
for (const QUrl &url : device_urls) {
|
||||
if (urls.contains(url)) return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::PhysicalDeviceAdded(const QString &id) {
|
||||
|
||||
DeviceLister *lister = qobject_cast<DeviceLister*>(sender());
|
||||
|
||||
qLog(Info) << "Device added:" << id;
|
||||
|
||||
// Do we have this device already?
|
||||
int i = FindDeviceById(id);
|
||||
if (i != -1) {
|
||||
DeviceInfo &info = devices_[i];
|
||||
for (int backend_index = 0 ; backend_index < info.backends_.count() ; ++backend_index) {
|
||||
if (info.backends_[backend_index].unique_id_ == id) {
|
||||
info.backends_[backend_index].lister_ = lister;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
emit dataChanged(index(i, 0), index(i, 0));
|
||||
} else {
|
||||
// Check if we have another device with the same URL
|
||||
i = FindDeviceByUrl(lister->MakeDeviceUrls(id));
|
||||
if (i != -1) {
|
||||
// Add this device's lister to the existing device
|
||||
DeviceInfo &info = devices_[i];
|
||||
info.backends_ << DeviceInfo::Backend(lister, id);
|
||||
|
||||
// If the user hasn't saved the device in the DB yet then overwrite the
|
||||
// device's name and icon etc.
|
||||
if (info.database_id_ == -1 && info.BestBackend()->lister_ == lister) {
|
||||
info.friendly_name_ = lister->MakeFriendlyName(id);
|
||||
info.size_ = lister->DeviceCapacity(id);
|
||||
info.LoadIcon(lister->DeviceIcons(id), info.friendly_name_);
|
||||
}
|
||||
|
||||
emit dataChanged(index(i, 0), index(i, 0));
|
||||
} else {
|
||||
// It's a completely new device
|
||||
DeviceInfo info;
|
||||
info.backends_ << DeviceInfo::Backend(lister, id);
|
||||
info.friendly_name_ = lister->MakeFriendlyName(id);
|
||||
info.size_ = lister->DeviceCapacity(id);
|
||||
info.LoadIcon(lister->DeviceIcons(id), info.friendly_name_);
|
||||
|
||||
beginInsertRows(QModelIndex(), devices_.count(), devices_.count());
|
||||
devices_ << info;
|
||||
endInsertRows();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::PhysicalDeviceRemoved(const QString &id) {
|
||||
|
||||
DeviceLister *lister = qobject_cast<DeviceLister*>(sender());
|
||||
|
||||
qLog(Info) << "Device removed:" << id;
|
||||
|
||||
int i = FindDeviceById(id);
|
||||
if (i == -1) {
|
||||
// Shouldn't happen
|
||||
return;
|
||||
}
|
||||
|
||||
DeviceInfo &info = devices_[i];
|
||||
|
||||
if (info.database_id_ != -1) {
|
||||
// Keep the structure around, but just "disconnect" it
|
||||
for (int backend_index = 0 ; backend_index < info.backends_.count() ; ++backend_index) {
|
||||
if (info.backends_[backend_index].unique_id_ == id) {
|
||||
info.backends_[backend_index].lister_ = nullptr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (info.device_ && info.device_->lister() == lister) info.device_.reset();
|
||||
|
||||
if (!info.device_) emit DeviceDisconnected(i);
|
||||
|
||||
emit dataChanged(index(i, 0), index(i, 0));
|
||||
}
|
||||
else {
|
||||
// If this was the last lister for the device then remove it from the model
|
||||
for (int backend_index = 0 ; backend_index < info.backends_.count() ; ++backend_index) {
|
||||
if (info.backends_[backend_index].unique_id_ == id) {
|
||||
info.backends_.removeAt(backend_index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (info.backends_.isEmpty()) {
|
||||
beginRemoveRows(QModelIndex(), i, i);
|
||||
devices_.removeAt(i);
|
||||
|
||||
for (const QModelIndex &idx : persistentIndexList()) {
|
||||
if (idx.row() == i)
|
||||
changePersistentIndex(idx, QModelIndex());
|
||||
else if (idx.row() > i)
|
||||
changePersistentIndex(idx, index(idx.row() - 1, idx.column()));
|
||||
}
|
||||
|
||||
endRemoveRows();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::PhysicalDeviceChanged(const QString &id) {
|
||||
|
||||
DeviceLister *lister = qobject_cast<DeviceLister*>(sender());
|
||||
Q_UNUSED(lister);
|
||||
|
||||
int i = FindDeviceById(id);
|
||||
if (i == -1) {
|
||||
// Shouldn't happen
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO
|
||||
}
|
||||
|
||||
std::shared_ptr<ConnectedDevice> DeviceManager::Connect(int row) {
|
||||
|
||||
DeviceInfo &info = devices_[row];
|
||||
if (info.device_) // Already connected
|
||||
return info.device_;
|
||||
|
||||
std::shared_ptr<ConnectedDevice> ret;
|
||||
|
||||
if (!info.BestBackend()->lister_) // Not physically connected
|
||||
return ret;
|
||||
|
||||
if (info.BestBackend()->lister_->DeviceNeedsMount(info.BestBackend()->unique_id_)) {
|
||||
// Mount the device
|
||||
info.BestBackend()->lister_->MountDevice(info.BestBackend()->unique_id_);
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool first_time = (info.database_id_ == -1);
|
||||
if (first_time) {
|
||||
// We haven't stored this device in the database before
|
||||
info.database_id_ = backend_->AddDevice(info.SaveToDb());
|
||||
}
|
||||
|
||||
// Get the device URLs
|
||||
QList<QUrl> urls = info.BestBackend()->lister_->MakeDeviceUrls(info.BestBackend()->unique_id_);
|
||||
if (urls.isEmpty()) return ret;
|
||||
|
||||
// Take the first URL that we have a handler for
|
||||
QUrl device_url;
|
||||
for (const QUrl &url : urls) {
|
||||
qLog(Info) << "Connecting" << url;
|
||||
|
||||
// Find a device class for this URL's scheme
|
||||
if (device_classes_.contains(url.scheme())) {
|
||||
device_url = url;
|
||||
break;
|
||||
}
|
||||
|
||||
// If we get here it means that this URL scheme wasn't supported. If it
|
||||
// was "ipod" or "mtp" then the user compiled out support and the device
|
||||
// won't work properly.
|
||||
if (url.scheme() == "mtp" || url.scheme() == "gphoto2") {
|
||||
if (QMessageBox::critical(nullptr, tr("This device will not work properly"),
|
||||
tr("This is an MTP device, but you compiled Strawberry without libmtp support.") + " " +
|
||||
tr("If you continue, this device will work slowly and songs copied to it may not work."),
|
||||
QMessageBox::Abort, QMessageBox::Ignore) == QMessageBox::Abort)
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (url.scheme() == "ipod") {
|
||||
if (QMessageBox::critical(nullptr, tr("This device will not work properly"),
|
||||
tr("This is an iPod, but you compiled Strawberry without libgpod support.") + " " +
|
||||
tr("If you continue, this device will work slowly and songs copied to it may not work."),
|
||||
QMessageBox::Abort, QMessageBox::Ignore) == QMessageBox::Abort)
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
if (device_url.isEmpty()) {
|
||||
// Munge the URL list into a string list
|
||||
QStringList url_strings;
|
||||
for (const QUrl &url : urls) {
|
||||
url_strings << url.toString();
|
||||
}
|
||||
|
||||
app_->AddError(tr("This type of device is not supported: %1").arg(url_strings.join(", ")));
|
||||
return ret;
|
||||
}
|
||||
|
||||
QMetaObject meta_object = device_classes_.value(device_url.scheme());
|
||||
QObject *instance = meta_object.newInstance(
|
||||
Q_ARG(QUrl, device_url),
|
||||
Q_ARG(DeviceLister*, info.BestBackend()->lister_),
|
||||
Q_ARG(QString, info.BestBackend()->unique_id_),
|
||||
Q_ARG(DeviceManager*, this), Q_ARG(Application*, app_),
|
||||
Q_ARG(int, info.database_id_), Q_ARG(bool, first_time));
|
||||
ret.reset(static_cast<ConnectedDevice*>(instance));
|
||||
|
||||
if (!ret) {
|
||||
qLog(Warning) << "Could not create device for" << device_url.toString();
|
||||
}
|
||||
else {
|
||||
ret->Init();
|
||||
|
||||
info.device_ = ret;
|
||||
emit dataChanged(index(row), index(row));
|
||||
connect(info.device_.get(), SIGNAL(TaskStarted(int)), SLOT(DeviceTaskStarted(int)));
|
||||
connect(info.device_.get(), SIGNAL(SongCountUpdated(int)), SLOT(DeviceSongCountUpdated(int)));
|
||||
}
|
||||
|
||||
emit DeviceConnected(row);
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
std::shared_ptr<ConnectedDevice> DeviceManager::GetConnectedDevice(int row)
|
||||
const {
|
||||
return devices_[row].device_;
|
||||
}
|
||||
|
||||
int DeviceManager::GetDatabaseId(int row) const {
|
||||
return devices_[row].database_id_;
|
||||
}
|
||||
|
||||
DeviceLister *DeviceManager::GetLister(int row) const {
|
||||
return devices_[row].BestBackend()->lister_;
|
||||
}
|
||||
|
||||
void DeviceManager::Disconnect(int row) {
|
||||
|
||||
DeviceInfo &info = devices_[row];
|
||||
if (!info.device_) // Already disconnected
|
||||
return;
|
||||
|
||||
info.device_.reset();
|
||||
emit DeviceDisconnected(row);
|
||||
emit dataChanged(index(row), index(row));
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::Forget(int row) {
|
||||
|
||||
DeviceInfo &info = devices_[row];
|
||||
if (info.database_id_ == -1) return;
|
||||
|
||||
if (info.device_) Disconnect(row);
|
||||
|
||||
backend_->RemoveDevice(info.database_id_);
|
||||
info.database_id_ = -1;
|
||||
|
||||
if (!info.BestBackend()->lister_) {
|
||||
// It's not attached any more so remove it from the list
|
||||
beginRemoveRows(QModelIndex(), row, row);
|
||||
devices_.removeAt(row);
|
||||
|
||||
for (const QModelIndex &idx : persistentIndexList()) {
|
||||
if (idx.row() == row)
|
||||
changePersistentIndex(idx, QModelIndex());
|
||||
else if (idx.row() > row)
|
||||
changePersistentIndex(idx, index(idx.row() - 1, idx.column()));
|
||||
}
|
||||
|
||||
endRemoveRows();
|
||||
}
|
||||
else {
|
||||
// It's still attached, set the name and icon back to what they were
|
||||
// originally
|
||||
const QString id = info.BestBackend()->unique_id_;
|
||||
|
||||
info.friendly_name_ = info.BestBackend()->lister_->MakeFriendlyName(id);
|
||||
info.LoadIcon(info.BestBackend()->lister_->DeviceIcons(id), info.friendly_name_);
|
||||
|
||||
dataChanged(index(row, 0), index(row, 0));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::SetDeviceOptions(int row, const QString &friendly_name, const QString &icon_name, MusicStorage::TranscodeMode mode, Song::FileType format) {
|
||||
|
||||
DeviceInfo &info = devices_[row];
|
||||
info.friendly_name_ = friendly_name;
|
||||
info.LoadIcon(QVariantList() << icon_name, friendly_name);
|
||||
info.transcode_mode_ = mode;
|
||||
info.transcode_format_ = format;
|
||||
|
||||
emit dataChanged(index(row, 0), index(row, 0));
|
||||
|
||||
if (info.database_id_ != -1)
|
||||
backend_->SetDeviceOptions(info.database_id_, friendly_name, icon_name, mode, format);
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::DeviceTaskStarted(int id) {
|
||||
|
||||
ConnectedDevice *device = qobject_cast<ConnectedDevice*>(sender());
|
||||
if (!device) return;
|
||||
|
||||
for (int i = 0; i < devices_.count(); ++i) {
|
||||
DeviceInfo &info = devices_[i];
|
||||
if (info.device_.get() == device) {
|
||||
active_tasks_[id] = index(i);
|
||||
info.task_percentage_ = 0;
|
||||
emit dataChanged(index(i), index(i));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::TasksChanged() {
|
||||
|
||||
QList<TaskManager::Task> tasks = app_->task_manager()->GetTasks();
|
||||
QList<QPersistentModelIndex> finished_tasks = active_tasks_.values();
|
||||
|
||||
for (const TaskManager::Task &task : tasks) {
|
||||
if (!active_tasks_.contains(task.id)) continue;
|
||||
|
||||
QPersistentModelIndex index = active_tasks_[task.id];
|
||||
if (!index.isValid()) continue;
|
||||
|
||||
DeviceInfo &info = devices_[index.row()];
|
||||
if (task.progress_max)
|
||||
info.task_percentage_ = float(task.progress) / task.progress_max * 100;
|
||||
else
|
||||
info.task_percentage_ = 0;
|
||||
emit dataChanged(index, index);
|
||||
finished_tasks.removeAll(index);
|
||||
}
|
||||
|
||||
for (const QPersistentModelIndex &index : finished_tasks) {
|
||||
if (!index.isValid()) continue;
|
||||
|
||||
DeviceInfo &info = devices_[index.row()];
|
||||
info.task_percentage_ = -1;
|
||||
emit dataChanged(index, index);
|
||||
|
||||
active_tasks_.remove(active_tasks_.key(index));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::UnmountAsync(int row) {
|
||||
Q_ASSERT(QMetaObject::invokeMethod(this, "Unmount", Q_ARG(int, row)));
|
||||
}
|
||||
|
||||
void DeviceManager::Unmount(int row) {
|
||||
|
||||
DeviceInfo &info = devices_[row];
|
||||
if (info.database_id_ != -1 && !info.device_) return;
|
||||
|
||||
if (info.device_) Disconnect(row);
|
||||
|
||||
if (info.BestBackend()->lister_)
|
||||
info.BestBackend()->lister_->UnmountDevice(info.BestBackend()->unique_id_);
|
||||
|
||||
}
|
||||
|
||||
void DeviceManager::DeviceSongCountUpdated(int count) {
|
||||
|
||||
ConnectedDevice *device = qobject_cast<ConnectedDevice*>(sender());
|
||||
if (!device) return;
|
||||
|
||||
int row = FindDeviceById(device->unique_id());
|
||||
if (row == -1) return;
|
||||
|
||||
emit dataChanged(index(row), index(row));
|
||||
|
||||
}
|
||||
|
||||
198
src/device/devicemanager.h
Normal file
198
src/device/devicemanager.h
Normal file
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DEVICEMANAGER_H
|
||||
#define DEVICEMANAGER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QIcon>
|
||||
#include <QThreadPool>
|
||||
|
||||
#include "devicedatabasebackend.h"
|
||||
|
||||
#include "collection/collectionmodel.h"
|
||||
|
||||
class Application;
|
||||
class ConnectedDevice;
|
||||
class Database;
|
||||
class DeviceLister;
|
||||
class DeviceStateFilterModel;
|
||||
class TaskManager;
|
||||
|
||||
class DeviceManager : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DeviceManager(Application *app, QObject *parent = nullptr);
|
||||
~DeviceManager();
|
||||
|
||||
enum Role {
|
||||
Role_State = CollectionModel::LastRole,
|
||||
Role_UniqueId,
|
||||
Role_FriendlyName,
|
||||
Role_Capacity,
|
||||
Role_FreeSpace,
|
||||
Role_IconName,
|
||||
Role_UpdatingPercentage,
|
||||
Role_MountPath,
|
||||
Role_TranscodeMode,
|
||||
Role_TranscodeFormat,
|
||||
Role_SongCount,
|
||||
LastRole,
|
||||
};
|
||||
|
||||
enum State {
|
||||
State_Remembered,
|
||||
State_NotMounted,
|
||||
State_NotConnected,
|
||||
State_Connected,
|
||||
};
|
||||
|
||||
static const int kDeviceIconSize;
|
||||
static const int kDeviceIconOverlaySize;
|
||||
|
||||
DeviceStateFilterModel *connected_devices_model() const { return connected_devices_model_; }
|
||||
|
||||
// Get info about devices
|
||||
int GetDatabaseId(int row) const;
|
||||
DeviceLister *GetLister(int row) const;
|
||||
std::shared_ptr<ConnectedDevice> GetConnectedDevice(int row) const;
|
||||
|
||||
int FindDeviceById(const QString &id) const;
|
||||
int FindDeviceByUrl(const QList<QUrl> &url) const;
|
||||
|
||||
// Actions on devices
|
||||
std::shared_ptr<ConnectedDevice> Connect(int row);
|
||||
void Disconnect(int row);
|
||||
void Forget(int row);
|
||||
void UnmountAsync(int row);
|
||||
|
||||
void SetDeviceOptions(int row, const QString &friendly_name, const QString &icon_name, MusicStorage::TranscodeMode mode, Song::FileType format);
|
||||
|
||||
// QAbstractListModel
|
||||
int rowCount(const QModelIndex &parent) const;
|
||||
QVariant data(const QModelIndex &index, int role) const;
|
||||
|
||||
public slots:
|
||||
void Unmount(int row);
|
||||
|
||||
signals:
|
||||
void DeviceConnected(int row);
|
||||
void DeviceDisconnected(int row);
|
||||
|
||||
private slots:
|
||||
void PhysicalDeviceAdded(const QString &id);
|
||||
void PhysicalDeviceRemoved(const QString &id);
|
||||
void PhysicalDeviceChanged(const QString &id);
|
||||
void DeviceTaskStarted(int id);
|
||||
void TasksChanged();
|
||||
void DeviceSongCountUpdated(int count);
|
||||
void LoadAllDevices();
|
||||
|
||||
private:
|
||||
// Devices can be in three different states:
|
||||
// 1) Remembered in the database but not physically connected at the moment.
|
||||
// database_id valid, lister null, device null
|
||||
// 2) Physically connected but the user hasn't "connected" it to Strawberry
|
||||
// yet.
|
||||
// database_id == -1, lister valid, device null
|
||||
// 3) Physically connected and connected to Strawberry
|
||||
// database_id valid, lister valid, device valid
|
||||
// Devices in all states will have a unique_id.
|
||||
struct DeviceInfo {
|
||||
DeviceInfo();
|
||||
|
||||
// A device can be discovered in different ways (devicekit, gio, etc.)
|
||||
// Sometimes the same device is discovered more than once. In this case
|
||||
// the device will have multiple "backends".
|
||||
struct Backend {
|
||||
Backend(DeviceLister *lister = nullptr, const QString &id = QString())
|
||||
: lister_(lister), unique_id_(id) {}
|
||||
|
||||
DeviceLister *lister_; // nullptr if not physically connected
|
||||
QString unique_id_;
|
||||
};
|
||||
|
||||
// Serialising to the database
|
||||
void InitFromDb(const DeviceDatabaseBackend::Device &dev);
|
||||
DeviceDatabaseBackend::Device SaveToDb() const;
|
||||
|
||||
// Tries to load a good icon for the device. Sets icon_name_ and icon_.
|
||||
void LoadIcon(const QVariantList &icons, const QString &name_hint);
|
||||
|
||||
// Gets the best backend available (the one with the highest priority)
|
||||
const Backend *BestBackend() const;
|
||||
|
||||
int database_id_; // -1 if not remembered in the database
|
||||
std::shared_ptr<ConnectedDevice>
|
||||
device_; // nullptr if not connected
|
||||
QList<Backend> backends_;
|
||||
|
||||
QString friendly_name_;
|
||||
quint64 size_;
|
||||
|
||||
QString icon_name_;
|
||||
QIcon icon_;
|
||||
|
||||
MusicStorage::TranscodeMode transcode_mode_;
|
||||
Song::FileType transcode_format_;
|
||||
|
||||
int task_percentage_;
|
||||
};
|
||||
|
||||
void AddLister(DeviceLister *lister);
|
||||
template <typename T> void AddDeviceClass();
|
||||
|
||||
DeviceDatabaseBackend::Device InfoToDatabaseDevice(const DeviceInfo &info) const;
|
||||
|
||||
private:
|
||||
Application *app_;
|
||||
DeviceDatabaseBackend *backend_;
|
||||
|
||||
DeviceStateFilterModel *connected_devices_model_;
|
||||
|
||||
QIcon not_connected_overlay_;
|
||||
|
||||
QList<DeviceLister*> listers_;
|
||||
QList<DeviceInfo> devices_;
|
||||
|
||||
QMultiMap<QString, QMetaObject> device_classes_;
|
||||
|
||||
// Map of task ID to device index
|
||||
QMap<int, QPersistentModelIndex> active_tasks_;
|
||||
|
||||
QThreadPool thread_pool_;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
void DeviceManager::AddDeviceClass() {
|
||||
QStringList schemes = T::url_schemes();
|
||||
QMetaObject obj = T::staticMetaObject;
|
||||
|
||||
for (const QString &scheme : schemes) {
|
||||
device_classes_.insert(scheme, obj);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // DEVICEMANAGER_H
|
||||
301
src/device/deviceproperties.cpp
Normal file
301
src/device/deviceproperties.cpp
Normal file
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "deviceproperties.h"
|
||||
#include "ui_deviceproperties.h"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include <QFutureWatcher>
|
||||
#include <QScrollBar>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
#include "connecteddevice.h"
|
||||
#include "devicelister.h"
|
||||
#include "devicemanager.h"
|
||||
#include "core/utilities.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "transcoder/transcoder.h"
|
||||
|
||||
DeviceProperties::DeviceProperties(QWidget *parent)
|
||||
: QDialog(parent),
|
||||
ui_(new Ui_DeviceProperties),
|
||||
manager_(nullptr),
|
||||
updating_formats_(false) {
|
||||
ui_->setupUi(this);
|
||||
|
||||
connect(ui_->open_device, SIGNAL(clicked()), SLOT(OpenDevice()));
|
||||
|
||||
// Maximum height of the icon widget
|
||||
ui_->icon->setMaximumHeight(
|
||||
ui_->icon->iconSize().height() +
|
||||
ui_->icon->horizontalScrollBar()->sizeHint().height() +
|
||||
ui_->icon->spacing() * 2 + 5);
|
||||
}
|
||||
|
||||
DeviceProperties::~DeviceProperties() { delete ui_; }
|
||||
|
||||
void DeviceProperties::SetDeviceManager(DeviceManager *manager) {
|
||||
|
||||
manager_ = manager;
|
||||
connect(manager_, SIGNAL(dataChanged(QModelIndex,QModelIndex)), SLOT(ModelChanged()));
|
||||
connect(manager_, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(ModelChanged()));
|
||||
connect(manager_, SIGNAL(rowsRemoved(QModelIndex,int,int)), SLOT(ModelChanged()));
|
||||
|
||||
}
|
||||
|
||||
void DeviceProperties::ShowDevice(int row) {
|
||||
|
||||
if (ui_->icon->count() == 0) {
|
||||
// Only load the icons the first time the dialog is shown
|
||||
QStringList icon_names = QStringList()
|
||||
<< "device"
|
||||
<< "device-usb-drive"
|
||||
<< "device-usb-flash"
|
||||
<< "cd"
|
||||
<< "device-ipod"
|
||||
<< "device-ipod-nano"
|
||||
<< "device-phone";
|
||||
|
||||
|
||||
for (const QString &icon_name : icon_names) {
|
||||
QListWidgetItem *item = new QListWidgetItem(IconLoader::Load(icon_name), QString(), ui_->icon);
|
||||
item->setData(Qt::UserRole, icon_name);
|
||||
}
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
// Load the transcode formats the first time the dialog is shown
|
||||
for (const TranscoderPreset &preset : Transcoder::GetAllPresets()) {
|
||||
ui_->transcode_format->addItem(preset.name_, preset.type_);
|
||||
}
|
||||
ui_->transcode_format->model()->sort(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
index_ = manager_->index(row);
|
||||
|
||||
// Basic information
|
||||
ui_->name->setText(index_.data(DeviceManager::Role_FriendlyName).toString());
|
||||
|
||||
// Find the right icon
|
||||
QString icon_name = index_.data(DeviceManager::Role_IconName).toString();
|
||||
for (int i = 0; i < ui_->icon->count(); ++i) {
|
||||
if (ui_->icon->item(i)->data(Qt::UserRole).toString() == icon_name) {
|
||||
ui_->icon->setCurrentRow(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateHardwareInfo();
|
||||
UpdateFormats();
|
||||
|
||||
show();
|
||||
|
||||
}
|
||||
|
||||
void DeviceProperties::AddHardwareInfo(int row, const QString &key, const QString &value) {
|
||||
ui_->hardware_info->setItem(row, 0, new QTableWidgetItem(key));
|
||||
ui_->hardware_info->setItem(row, 1, new QTableWidgetItem(value));
|
||||
}
|
||||
|
||||
void DeviceProperties::ModelChanged() {
|
||||
|
||||
if (!isVisible()) return;
|
||||
|
||||
if (!index_.isValid())
|
||||
reject(); // Device went away
|
||||
else {
|
||||
UpdateHardwareInfo();
|
||||
UpdateFormats();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DeviceProperties::UpdateHardwareInfo() {
|
||||
|
||||
// Hardware information
|
||||
QString id = index_.data(DeviceManager::Role_UniqueId).toString();
|
||||
if (DeviceLister *lister = manager_->GetLister(index_.row())) {
|
||||
QVariantMap info = lister->DeviceHardwareInfo(id);
|
||||
|
||||
// Remove empty items
|
||||
for (const QString &key : info.keys()) {
|
||||
if (info[key].isNull() || info[key].toString().isEmpty())
|
||||
info.remove(key);
|
||||
}
|
||||
|
||||
ui_->hardware_info_stack->setCurrentWidget(ui_->hardware_info_page);
|
||||
ui_->hardware_info->clear();
|
||||
ui_->hardware_info->setRowCount(2 + info.count());
|
||||
|
||||
int row = 0;
|
||||
AddHardwareInfo(row++, tr("Model"), lister->DeviceModel(id));
|
||||
AddHardwareInfo(row++, tr("Manufacturer"), lister->DeviceManufacturer(id));
|
||||
for (const QString &key : info.keys()) {
|
||||
AddHardwareInfo(row++, tr(key.toLatin1()), info[key].toString());
|
||||
}
|
||||
|
||||
ui_->hardware_info->sortItems(0);
|
||||
}
|
||||
else {
|
||||
ui_->hardware_info_stack->setCurrentWidget(ui_->hardware_info_not_connected_page);
|
||||
}
|
||||
|
||||
// Size
|
||||
quint64 total = index_.data(DeviceManager::Role_Capacity).toLongLong();
|
||||
|
||||
QVariant free_var = index_.data(DeviceManager::Role_FreeSpace);
|
||||
if (free_var.isValid()) {
|
||||
quint64 free = free_var.toLongLong();
|
||||
|
||||
ui_->free_space_bar->set_total_bytes(total);
|
||||
ui_->free_space_bar->set_free_bytes(free);
|
||||
ui_->free_space_bar->show();
|
||||
}
|
||||
else {
|
||||
ui_->free_space_bar->hide();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DeviceProperties::UpdateFormats() {
|
||||
|
||||
QString id = index_.data(DeviceManager::Role_UniqueId).toString();
|
||||
DeviceLister *lister = manager_->GetLister(index_.row());
|
||||
std::shared_ptr<ConnectedDevice> device =
|
||||
manager_->GetConnectedDevice(index_.row());
|
||||
|
||||
// Transcode mode
|
||||
MusicStorage::TranscodeMode mode = MusicStorage::TranscodeMode(
|
||||
index_.data(DeviceManager::Role_TranscodeMode).toInt());
|
||||
switch (mode) {
|
||||
case MusicStorage::Transcode_Always:
|
||||
ui_->transcode_all->setChecked(true);
|
||||
break;
|
||||
|
||||
case MusicStorage::Transcode_Never:
|
||||
ui_->transcode_off->setChecked(true);
|
||||
break;
|
||||
|
||||
case MusicStorage::Transcode_Unsupported:
|
||||
default:
|
||||
ui_->transcode_unsupported->setChecked(true);
|
||||
break;
|
||||
}
|
||||
|
||||
// If there's no lister then the device is physically disconnected
|
||||
if (!lister) {
|
||||
ui_->formats_stack->setCurrentWidget(ui_->formats_page_not_connected);
|
||||
ui_->open_device->setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// If there's a lister but no device then the user just needs to open the
|
||||
// device. This will cause a rescan so we don't do it automatically.
|
||||
if (!device) {
|
||||
ui_->formats_stack->setCurrentWidget(ui_->formats_page_not_connected);
|
||||
ui_->open_device->setEnabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!updating_formats_) {
|
||||
// Get the device's supported formats list. This takes a long time and it
|
||||
// blocks, so do it in the background.
|
||||
supported_formats_.clear();
|
||||
|
||||
QFuture<bool> future = QtConcurrent::run(std::bind(&ConnectedDevice::GetSupportedFiletypes, device, &supported_formats_));
|
||||
NewClosure(future, this, SLOT(UpdateFormatsFinished(QFuture<bool>)), future);
|
||||
|
||||
ui_->formats_stack->setCurrentWidget(ui_->formats_page_loading);
|
||||
updating_formats_ = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void DeviceProperties::accept() {
|
||||
|
||||
QDialog::accept();
|
||||
|
||||
// Transcode mode
|
||||
MusicStorage::TranscodeMode mode = MusicStorage::Transcode_Unsupported;
|
||||
if (ui_->transcode_all->isChecked())
|
||||
mode = MusicStorage::Transcode_Always;
|
||||
else if (ui_->transcode_off->isChecked())
|
||||
mode = MusicStorage::Transcode_Never;
|
||||
else if (ui_->transcode_unsupported->isChecked())
|
||||
mode = MusicStorage::Transcode_Unsupported;
|
||||
|
||||
// Transcode format
|
||||
Song::FileType format = Song::FileType(ui_->transcode_format->itemData(ui_->transcode_format->currentIndex()).toInt());
|
||||
|
||||
// By default no icon is selected and thus no current item is selected
|
||||
QString icon_name;
|
||||
if (ui_->icon->currentItem() != nullptr) {
|
||||
icon_name = ui_->icon->currentItem()->data(Qt::UserRole).toString();
|
||||
}
|
||||
|
||||
manager_->SetDeviceOptions(index_.row(), ui_->name->text(), icon_name, mode, format);
|
||||
|
||||
}
|
||||
|
||||
void DeviceProperties::OpenDevice() { manager_->Connect(index_.row()); }
|
||||
|
||||
void DeviceProperties::UpdateFormatsFinished(QFuture<bool> future) {
|
||||
|
||||
updating_formats_ = false;
|
||||
|
||||
if (!future.result()) {
|
||||
supported_formats_.clear();
|
||||
}
|
||||
|
||||
// Hide widgets if there are no supported types
|
||||
ui_->supported_formats_container->setVisible(!supported_formats_.isEmpty());
|
||||
ui_->transcode_unsupported->setEnabled(!supported_formats_.isEmpty());
|
||||
|
||||
if (ui_->transcode_unsupported->isChecked() && supported_formats_.isEmpty()) {
|
||||
ui_->transcode_off->setChecked(true);
|
||||
}
|
||||
|
||||
// Populate supported types list
|
||||
ui_->supported_formats->clear();
|
||||
for (Song::FileType type : supported_formats_) {
|
||||
QListWidgetItem *item = new QListWidgetItem(Song::TextForFiletype(type));
|
||||
ui_->supported_formats->addItem(item);
|
||||
}
|
||||
ui_->supported_formats->sortItems();
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
// Set the format combobox item
|
||||
TranscoderPreset preset = Transcoder::PresetForFileType(Song::FileType(index_.data(DeviceManager::Role_TranscodeFormat).toInt()));
|
||||
if (preset.type_ == Song::Type_Unknown) {
|
||||
// The user hasn't chosen a format for this device yet, so work our way down
|
||||
// a list of some preferred formats, picking the first one that is supported
|
||||
preset = Transcoder::PresetForFileType(Transcoder::PickBestFormat(supported_formats_));
|
||||
}
|
||||
ui_->transcode_format->setCurrentIndex(ui_->transcode_format->findText(preset.name_));
|
||||
#endif
|
||||
|
||||
ui_->formats_stack->setCurrentWidget(ui_->formats_page);
|
||||
|
||||
}
|
||||
|
||||
68
src/device/deviceproperties.h
Normal file
68
src/device/deviceproperties.h
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DEVICEPROPERTIES_H
|
||||
#define DEVICEPROPERTIES_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QFuture>
|
||||
#include <QPersistentModelIndex>
|
||||
|
||||
#include "core/song.h"
|
||||
|
||||
class DeviceManager;
|
||||
class Ui_DeviceProperties;
|
||||
|
||||
class DeviceProperties : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DeviceProperties(QWidget *parent = nullptr);
|
||||
~DeviceProperties();
|
||||
|
||||
void SetDeviceManager(DeviceManager *manager);
|
||||
void ShowDevice(int row);
|
||||
|
||||
public slots:
|
||||
void accept();
|
||||
|
||||
private:
|
||||
void UpdateHardwareInfo();
|
||||
void AddHardwareInfo(int row, const QString &key, const QString &value);
|
||||
void UpdateFormats();
|
||||
|
||||
private slots:
|
||||
void ModelChanged();
|
||||
void OpenDevice();
|
||||
void UpdateFormatsFinished(QFuture<bool> future);
|
||||
|
||||
private:
|
||||
Ui_DeviceProperties *ui_;
|
||||
|
||||
DeviceManager *manager_;
|
||||
QPersistentModelIndex index_;
|
||||
|
||||
bool updating_formats_;
|
||||
QList<Song::FileType> supported_formats_;
|
||||
};
|
||||
|
||||
#endif // DEVICEPROPERTIES_H
|
||||
446
src/device/deviceproperties.ui
Normal file
446
src/device/deviceproperties.ui
Normal file
@@ -0,0 +1,446 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>DeviceProperties</class>
|
||||
<widget class="QDialog" name="DeviceProperties">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>514</width>
|
||||
<height>488</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Device Properties</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="../../data/data.qrc">
|
||||
<normaloff>:/icons/64x64/strawberry.png</normaloff>:/icons/64x64/strawberry.png</iconset>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="tab">
|
||||
<attribute name="title">
|
||||
<string>Information</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Name</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="name"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Icon</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QListWidget" name="icon">
|
||||
<property name="verticalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>48</width>
|
||||
<height>48</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="movement">
|
||||
<enum>QListView::Static</enum>
|
||||
</property>
|
||||
<property name="flow">
|
||||
<enum>QListView::LeftToRight</enum>
|
||||
</property>
|
||||
<property name="isWrapping" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="viewMode">
|
||||
<enum>QListView::IconMode</enum>
|
||||
</property>
|
||||
<property name="uniformItemSizes">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="FreeSpaceBar" name="free_space_bar" native="true"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Hardware information</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="hardware_info_stack">
|
||||
<property name="currentIndex">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="hardware_info_not_connected_page">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>Hardware information is only available while the device is connected.</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="hardware_info_page">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="hardware_info">
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::NoSelection</enum>
|
||||
</property>
|
||||
<property name="cornerButtonEnabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="columnCount">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column/>
|
||||
<column/>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_2">
|
||||
<attribute name="title">
|
||||
<string>File formats</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_9">
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="formats_stack">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="formats_page">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_7">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="supported_formats_container">
|
||||
<property name="title">
|
||||
<string>Supported formats</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>This device supports the following file formats:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListWidget" name="supported_formats">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::NoSelection</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Strawberry can automatically convert the music you copy to this device into a format that it can play.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="transcode_off">
|
||||
<property name="text">
|
||||
<string>Do not convert any music</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="transcode_unsupported">
|
||||
<property name="text">
|
||||
<string>Convert any music that the device can't play</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="transcode_all">
|
||||
<property name="text">
|
||||
<string>Convert all music</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Preferred format</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="transcode_format">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>183</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="formats_page_not_connected">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_8">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>This device must be connected and opened before Strawberry can see what file formats it supports.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QPushButton" name="open_device">
|
||||
<property name="text">
|
||||
<string>Open device</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>309</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="formats_page_loading">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_10">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="BusyIndicator" name="label_8"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Querying device...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>358</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>FreeSpaceBar</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>widgets/freespacebar.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BusyIndicator</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>widgets/busyindicator.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>name</tabstop>
|
||||
<tabstop>icon</tabstop>
|
||||
<tabstop>hardware_info</tabstop>
|
||||
<tabstop>tabWidget</tabstop>
|
||||
<tabstop>supported_formats</tabstop>
|
||||
<tabstop>transcode_format</tabstop>
|
||||
<tabstop>buttonBox</tabstop>
|
||||
</tabstops>
|
||||
<resources>
|
||||
<include location="../../data/data.qrc"/>
|
||||
</resources>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>DeviceProperties</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>DeviceProperties</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
47
src/device/devicestatefiltermodel.cpp
Normal file
47
src/device/devicestatefiltermodel.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "devicestatefiltermodel.h"
|
||||
|
||||
DeviceStateFilterModel::DeviceStateFilterModel(QObject *parent, DeviceManager::State state)
|
||||
: QSortFilterProxyModel(parent),
|
||||
state_(state)
|
||||
{
|
||||
connect(this, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(ProxyRowCountChanged()));
|
||||
connect(this, SIGNAL(rowsRemoved(QModelIndex,int,int)), SLOT(ProxyRowCountChanged()));
|
||||
connect(this, SIGNAL(modelReset()), SLOT(ProxyRowCountChanged()));
|
||||
}
|
||||
|
||||
bool DeviceStateFilterModel::filterAcceptsRow(int row, const QModelIndex&) const {
|
||||
return sourceModel()->index(row, 0).data(DeviceManager::Role_State).toInt() != state_;
|
||||
}
|
||||
|
||||
void DeviceStateFilterModel::ProxyRowCountChanged() {
|
||||
emit IsEmptyChanged(rowCount() == 0);
|
||||
}
|
||||
|
||||
void DeviceStateFilterModel::setSourceModel(QAbstractItemModel* sourceModel) {
|
||||
QSortFilterProxyModel::setSourceModel(sourceModel);
|
||||
setDynamicSortFilter(true);
|
||||
setSortCaseSensitivity(Qt::CaseInsensitive);
|
||||
sort(0);
|
||||
}
|
||||
52
src/device/devicestatefiltermodel.h
Normal file
52
src/device/devicestatefiltermodel.h
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DEVICESTATEFILTERMODEL_H
|
||||
#define DEVICESTATEFILTERMODEL_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
#include "devicemanager.h"
|
||||
|
||||
class DeviceStateFilterModel : public QSortFilterProxyModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DeviceStateFilterModel(QObject *parent, DeviceManager::State state = DeviceManager::State_Remembered);
|
||||
|
||||
void setSourceModel(QAbstractItemModel *sourceModel);
|
||||
|
||||
signals:
|
||||
void IsEmptyChanged(bool is_empty);
|
||||
|
||||
protected:
|
||||
bool filterAcceptsRow(int row, const QModelIndex &parent) const;
|
||||
|
||||
private slots:
|
||||
void ProxyRowCountChanged();
|
||||
|
||||
private:
|
||||
DeviceManager::State state_;
|
||||
};
|
||||
|
||||
#endif // DEVICESTATEFILTERMODEL_H
|
||||
|
||||
443
src/device/deviceview.cpp
Normal file
443
src/device/deviceview.cpp
Normal file
@@ -0,0 +1,443 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QContextMenuEvent>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
#include "deviceview.h"
|
||||
|
||||
#include "connecteddevice.h"
|
||||
#include "devicelister.h"
|
||||
#include "devicemanager.h"
|
||||
#include "deviceproperties.h"
|
||||
#include "core/application.h"
|
||||
#include "core/deletefiles.h"
|
||||
#include "core/mergedproxymodel.h"
|
||||
#include "core/mimedata.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "collection/collectiondirectorymodel.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "collection/collectionview.h"
|
||||
#include "dialogs/organisedialog.h"
|
||||
#include "dialogs/organiseerrordialog.h"
|
||||
|
||||
const int DeviceItemDelegate::kIconPadding = 6;
|
||||
|
||||
DeviceItemDelegate::DeviceItemDelegate(QObject *parent)
|
||||
: CollectionItemDelegate(parent) {}
|
||||
|
||||
void DeviceItemDelegate::paint(QPainter *p, const QStyleOptionViewItem &opt, const QModelIndex &index) const {
|
||||
|
||||
// Is it a device or a collection item?
|
||||
if (index.data(DeviceManager::Role_State).isNull()) {
|
||||
CollectionItemDelegate::paint(p, opt, index);
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw the background
|
||||
const QStyleOptionViewItemV3 *vopt = qstyleoption_cast<const QStyleOptionViewItemV3*>(&opt);
|
||||
const QWidget *widget = vopt->widget;
|
||||
QStyle *style = widget->style() ? widget->style() : QApplication::style();
|
||||
style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, p, widget);
|
||||
|
||||
p->save();
|
||||
|
||||
// Font for the status line
|
||||
QFont status_font(opt.font);
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
status_font.setPointSize(status_font.pointSize() - 1);
|
||||
#else
|
||||
status_font.setPointSize(status_font.pointSize() - 2);
|
||||
#endif
|
||||
|
||||
const int text_height = QFontMetrics(opt.font).height() + QFontMetrics(status_font).height();
|
||||
|
||||
QRect line1(opt.rect);
|
||||
QRect line2(opt.rect);
|
||||
line1.setTop(line1.top() + (opt.rect.height() - text_height) / 2);
|
||||
line2.setTop(line1.top() + QFontMetrics(opt.font).height());
|
||||
line1.setLeft(line1.left() + DeviceManager::kDeviceIconSize + kIconPadding);
|
||||
line2.setLeft(line2.left() + DeviceManager::kDeviceIconSize + kIconPadding);
|
||||
|
||||
// Change the color for selected items
|
||||
if (opt.state & QStyle::State_Selected) {
|
||||
p->setPen(opt.palette.color(QPalette::HighlightedText));
|
||||
}
|
||||
|
||||
// Draw the icon
|
||||
p->drawPixmap(opt.rect.topLeft(), index.data(Qt::DecorationRole).value<QPixmap>());
|
||||
|
||||
// Draw the first line (device name)
|
||||
p->drawText(line1, Qt::AlignLeft | Qt::AlignTop, index.data().toString());
|
||||
|
||||
// Draw the second line (status)
|
||||
DeviceManager::State state = static_cast<DeviceManager::State>(index.data(DeviceManager::Role_State).toInt());
|
||||
QVariant progress = index.data(DeviceManager::Role_UpdatingPercentage);
|
||||
QString status_text;
|
||||
|
||||
if (progress.isValid()) {
|
||||
status_text = tr("Updating %1%...").arg(progress.toInt());
|
||||
}
|
||||
else {
|
||||
switch (state) {
|
||||
case DeviceManager::State_Remembered:
|
||||
status_text = tr("Not connected");
|
||||
break;
|
||||
|
||||
case DeviceManager::State_NotMounted:
|
||||
status_text = tr("Not mounted - double click to mount");
|
||||
break;
|
||||
|
||||
case DeviceManager::State_NotConnected:
|
||||
status_text = tr("Double click to open");
|
||||
break;
|
||||
|
||||
case DeviceManager::State_Connected: {
|
||||
QVariant song_count = index.data(DeviceManager::Role_SongCount);
|
||||
if (song_count.isValid()) {
|
||||
int count = song_count.toInt();
|
||||
if (count == 1) // TODO: Fix this properly
|
||||
status_text = tr("%1 song").arg(count);
|
||||
else
|
||||
status_text = tr("%1 songs").arg(count);
|
||||
}
|
||||
else {
|
||||
status_text = index.data(DeviceManager::Role_MountPath).toString();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (opt.state & QStyle::State_Selected)
|
||||
p->setPen(opt.palette.color(QPalette::HighlightedText));
|
||||
else
|
||||
p->setPen(opt.palette.color(QPalette::Dark));
|
||||
p->setFont(status_font);
|
||||
p->drawText(line2, Qt::AlignLeft | Qt::AlignTop, status_text);
|
||||
|
||||
p->restore();
|
||||
}
|
||||
|
||||
DeviceView::DeviceView(QWidget *parent)
|
||||
: AutoExpandingTreeView(parent),
|
||||
app_(nullptr),
|
||||
merged_model_(nullptr),
|
||||
sort_model_(nullptr),
|
||||
properties_dialog_(new DeviceProperties),
|
||||
device_menu_(nullptr),
|
||||
collection_menu_(nullptr) {
|
||||
setItemDelegate(new DeviceItemDelegate(this));
|
||||
SetExpandOnReset(false);
|
||||
setAttribute(Qt::WA_MacShowFocusRect, false);
|
||||
setHeaderHidden(true);
|
||||
setAllColumnsShowFocus(true);
|
||||
setDragEnabled(true);
|
||||
setDragDropMode(QAbstractItemView::DragOnly);
|
||||
setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
}
|
||||
|
||||
DeviceView::~DeviceView() {}
|
||||
|
||||
void DeviceView::SetApplication(Application *app) {
|
||||
|
||||
Q_ASSERT(app_ == nullptr);
|
||||
app_ = app;
|
||||
|
||||
connect(app_->device_manager(), SIGNAL(DeviceConnected(int)), SLOT(DeviceConnected(int)));
|
||||
connect(app_->device_manager(), SIGNAL(DeviceDisconnected(int)), SLOT(DeviceDisconnected(int)));
|
||||
|
||||
sort_model_ = new QSortFilterProxyModel(this);
|
||||
sort_model_->setSourceModel(app_->device_manager());
|
||||
sort_model_->setDynamicSortFilter(true);
|
||||
sort_model_->setSortCaseSensitivity(Qt::CaseInsensitive);
|
||||
sort_model_->sort(0);
|
||||
|
||||
merged_model_ = new MergedProxyModel(this);
|
||||
merged_model_->setSourceModel(sort_model_);
|
||||
|
||||
connect(merged_model_, SIGNAL(SubModelReset(QModelIndex, QAbstractItemModel*)), SLOT(RecursivelyExpand(QModelIndex)));
|
||||
|
||||
setModel(merged_model_);
|
||||
properties_dialog_->SetDeviceManager(app_->device_manager());
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
organise_dialog_.reset(new OrganiseDialog(app_->task_manager()));
|
||||
organise_dialog_->SetDestinationModel(app_->collection_model()->directory_model());
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void DeviceView::contextMenuEvent(QContextMenuEvent *e) {
|
||||
|
||||
if (!device_menu_) {
|
||||
device_menu_ = new QMenu(this);
|
||||
collection_menu_ = new QMenu(this);
|
||||
|
||||
// Device menu
|
||||
eject_action_ = device_menu_->addAction(IconLoader::Load("media-eject"), tr("Safely remove device"), this, SLOT(Unmount()));
|
||||
forget_action_ = device_menu_->addAction(IconLoader::Load("list-remove"), tr("Forget device"), this, SLOT(Forget()));
|
||||
device_menu_->addSeparator();
|
||||
properties_action_ = device_menu_->addAction(IconLoader::Load("configure"), tr("Device properties..."), this, SLOT(Properties()));
|
||||
|
||||
// Collection menu
|
||||
add_to_playlist_action_ = collection_menu_->addAction(IconLoader::Load("media-play"), tr("Append to current playlist"), this, SLOT(AddToPlaylist()));
|
||||
load_action_ = collection_menu_->addAction(IconLoader::Load("media-play"), tr("Replace current playlist"), this, SLOT(Load()));
|
||||
open_in_new_playlist_ = collection_menu_->addAction(IconLoader::Load("document-new"), tr("Open in new playlist"), this, SLOT(OpenInNewPlaylist()));
|
||||
|
||||
collection_menu_->addSeparator();
|
||||
#ifdef HAVE_GSTREAMER
|
||||
organise_action_ = collection_menu_->addAction(IconLoader::Load("edit-copy"), tr("Copy to collection..."), this, SLOT(Organise()));
|
||||
#endif
|
||||
delete_action_ = collection_menu_->addAction(IconLoader::Load("edit-delete"), tr("Delete from device..."), this, SLOT(Delete()));
|
||||
}
|
||||
|
||||
menu_index_ = currentIndex();
|
||||
|
||||
const QModelIndex device_index = MapToDevice(menu_index_);
|
||||
const QModelIndex collection_index = MapToCollection(menu_index_);
|
||||
|
||||
if (device_index.isValid()) {
|
||||
const bool is_plugged_in = app_->device_manager()->GetLister(device_index.row());
|
||||
const bool is_remembered = app_->device_manager()->GetDatabaseId(device_index.row()) != -1;
|
||||
|
||||
forget_action_->setEnabled(is_remembered);
|
||||
eject_action_->setEnabled(is_plugged_in);
|
||||
|
||||
device_menu_->popup(e->globalPos());
|
||||
} else if (collection_index.isValid()) {
|
||||
const QModelIndex parent_device_index = FindParentDevice(menu_index_);
|
||||
|
||||
bool is_filesystem_device = false;
|
||||
if (parent_device_index.isValid()) {
|
||||
std::shared_ptr<ConnectedDevice> device =
|
||||
app_->device_manager()->GetConnectedDevice(parent_device_index.row());
|
||||
if (device && !device->LocalPath().isEmpty()) is_filesystem_device = true;
|
||||
}
|
||||
|
||||
organise_action_->setEnabled(is_filesystem_device);
|
||||
|
||||
collection_menu_->popup(e->globalPos());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QModelIndex DeviceView::MapToDevice(const QModelIndex &merged_model_index) const {
|
||||
|
||||
QModelIndex sort_model_index = merged_model_->mapToSource(merged_model_index);
|
||||
if (sort_model_index.model() != sort_model_)
|
||||
return QModelIndex();
|
||||
|
||||
return sort_model_->mapToSource(sort_model_index);
|
||||
|
||||
}
|
||||
|
||||
QModelIndex DeviceView::FindParentDevice(const QModelIndex &merged_model_index) const {
|
||||
|
||||
QModelIndex index = merged_model_->FindSourceParent(merged_model_index);
|
||||
if (index.model() != sort_model_) return QModelIndex();
|
||||
|
||||
return sort_model_->mapToSource(index);
|
||||
|
||||
}
|
||||
|
||||
QModelIndex DeviceView::MapToCollection(const QModelIndex &merged_model_index) const {
|
||||
|
||||
QModelIndex sort_model_index = merged_model_->mapToSource(merged_model_index);
|
||||
if (const QSortFilterProxyModel *sort_model = qobject_cast<const QSortFilterProxyModel*>(sort_model_index.model())) {
|
||||
return sort_model->mapToSource(sort_model_index);
|
||||
}
|
||||
|
||||
return QModelIndex();
|
||||
|
||||
}
|
||||
|
||||
void DeviceView::Connect() {
|
||||
QModelIndex device_idx = MapToDevice(menu_index_);
|
||||
app_->device_manager()->data(device_idx, MusicStorage::Role_StorageForceConnect);
|
||||
}
|
||||
|
||||
void DeviceView::DeviceConnected(int row) {
|
||||
|
||||
std::shared_ptr<ConnectedDevice> device =
|
||||
app_->device_manager()->GetConnectedDevice(row);
|
||||
if (!device) return;
|
||||
|
||||
QModelIndex sort_idx = sort_model_->mapFromSource(app_->device_manager()->index(row));
|
||||
|
||||
QSortFilterProxyModel *sort_model = new QSortFilterProxyModel(device->model());
|
||||
sort_model->setSourceModel(device->model());
|
||||
sort_model->setSortRole(CollectionModel::Role_SortText);
|
||||
sort_model->setDynamicSortFilter(true);
|
||||
sort_model->sort(0);
|
||||
merged_model_->AddSubModel(sort_idx, sort_model);
|
||||
|
||||
expand(menu_index_);
|
||||
}
|
||||
|
||||
void DeviceView::DeviceDisconnected(int row) {
|
||||
merged_model_->RemoveSubModel(sort_model_->mapFromSource(app_->device_manager()->index(row)));
|
||||
}
|
||||
|
||||
void DeviceView::Forget() {
|
||||
|
||||
QModelIndex device_idx = MapToDevice(menu_index_);
|
||||
QString unique_id = app_->device_manager()->data(device_idx, DeviceManager::Role_UniqueId).toString();
|
||||
if (app_->device_manager()->GetLister(device_idx.row()) && app_->device_manager()->GetLister(device_idx.row())->AskForScan(unique_id)) {
|
||||
std::unique_ptr<QMessageBox> dialog(new QMessageBox(
|
||||
QMessageBox::Question, tr("Forget device"),
|
||||
tr("Forgetting a device will remove it from this list and Strawberry will have to rescan all the songs again next time you connect it."),
|
||||
QMessageBox::Cancel, this));
|
||||
QPushButton *forget = dialog->addButton(tr("Forget device"), QMessageBox::DestructiveRole);
|
||||
dialog->exec();
|
||||
|
||||
if (dialog->clickedButton() != forget) return;
|
||||
}
|
||||
|
||||
app_->device_manager()->Forget(device_idx.row());
|
||||
|
||||
}
|
||||
|
||||
void DeviceView::Properties() {
|
||||
properties_dialog_->ShowDevice(MapToDevice(menu_index_).row());
|
||||
}
|
||||
|
||||
void DeviceView::mouseDoubleClickEvent(QMouseEvent *event) {
|
||||
|
||||
AutoExpandingTreeView::mouseDoubleClickEvent(event);
|
||||
|
||||
QModelIndex merged_index = indexAt(event->pos());
|
||||
QModelIndex device_index = MapToDevice(merged_index);
|
||||
if (device_index.isValid()) {
|
||||
if (!app_->device_manager()->GetConnectedDevice(device_index.row())) {
|
||||
menu_index_ = merged_index;
|
||||
Connect();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SongList DeviceView::GetSelectedSongs() const {
|
||||
|
||||
QModelIndexList selected_merged_indexes = selectionModel()->selectedRows();
|
||||
SongList songs;
|
||||
for (const QModelIndex &merged_index : selected_merged_indexes) {
|
||||
QModelIndex collection_index = MapToCollection(merged_index);
|
||||
if (!collection_index.isValid()) continue;
|
||||
|
||||
const CollectionModel *collection = qobject_cast<const CollectionModel*>(collection_index.model());
|
||||
if (!collection) continue;
|
||||
|
||||
songs << collection->GetChildSongs(collection_index);
|
||||
}
|
||||
return songs;
|
||||
|
||||
}
|
||||
|
||||
void DeviceView::Load() {
|
||||
|
||||
QMimeData *data = model()->mimeData(selectedIndexes());
|
||||
if (MimeData *mime_data = qobject_cast<MimeData*>(data)) {
|
||||
mime_data->clear_first_ = true;
|
||||
}
|
||||
emit AddToPlaylistSignal(data);
|
||||
}
|
||||
|
||||
void DeviceView::AddToPlaylist() {
|
||||
emit AddToPlaylistSignal(model()->mimeData(selectedIndexes()));
|
||||
}
|
||||
|
||||
void DeviceView::OpenInNewPlaylist() {
|
||||
QMimeData *data = model()->mimeData(selectedIndexes());
|
||||
if (MimeData *mime_data = qobject_cast<MimeData*>(data)) {
|
||||
mime_data->open_in_new_playlist_ = true;
|
||||
}
|
||||
emit AddToPlaylistSignal(data);
|
||||
}
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
void DeviceView::Delete() {
|
||||
|
||||
if (selectedIndexes().isEmpty()) return;
|
||||
|
||||
// Take the device of the first selected item
|
||||
QModelIndex device_index = FindParentDevice(selectedIndexes()[0]);
|
||||
if (!device_index.isValid()) return;
|
||||
|
||||
if (QMessageBox::question(this, tr("Delete files"),
|
||||
tr("These files will be deleted from the device, are you sure you want to continue?"),
|
||||
QMessageBox::Yes, QMessageBox::Cancel) != QMessageBox::Yes)
|
||||
return;
|
||||
|
||||
std::shared_ptr<MusicStorage> storage = device_index.data(MusicStorage::Role_Storage).value<std::shared_ptr<MusicStorage>>();
|
||||
|
||||
DeleteFiles *delete_files = new DeleteFiles(app_->task_manager(), storage);
|
||||
connect(delete_files, SIGNAL(Finished(SongList)), SLOT(DeleteFinished(SongList)));
|
||||
delete_files->Start(GetSelectedSongs());
|
||||
|
||||
}
|
||||
|
||||
void DeviceView::Organise() {
|
||||
|
||||
SongList songs = GetSelectedSongs();
|
||||
QStringList filenames;
|
||||
for (const Song &song : songs) {
|
||||
filenames << song.url().toLocalFile();
|
||||
}
|
||||
|
||||
organise_dialog_->SetCopy(true);
|
||||
organise_dialog_->SetFilenames(filenames);
|
||||
organise_dialog_->show();
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
void DeviceView::Unmount() {
|
||||
QModelIndex device_idx = MapToDevice(menu_index_);
|
||||
app_->device_manager()->Unmount(device_idx.row());
|
||||
}
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
void DeviceView::DeleteFinished(const SongList &songs_with_errors) {
|
||||
|
||||
if (songs_with_errors.isEmpty()) return;
|
||||
|
||||
OrganiseErrorDialog *dialog = new OrganiseErrorDialog(this);
|
||||
dialog->Show(OrganiseErrorDialog::Type_Delete, songs_with_errors);
|
||||
// It deletes itself when the user closes it
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
bool DeviceView::CanRecursivelyExpand(const QModelIndex &index) const {
|
||||
// Never expand devices
|
||||
return index.parent().isValid();
|
||||
}
|
||||
|
||||
121
src/device/deviceview.h
Normal file
121
src/device/deviceview.h
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DEVICEVIEW_H
|
||||
#define DEVICEVIEW_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "collection/collectionview.h"
|
||||
#include "widgets/autoexpandingtreeview.h"
|
||||
|
||||
class QAction;
|
||||
class QMenu;
|
||||
class QSortFilterProxyModel;
|
||||
|
||||
class Application;
|
||||
class DeviceManager;
|
||||
class DeviceProperties;
|
||||
class CollectionModel;
|
||||
class MergedProxyModel;
|
||||
|
||||
class DeviceItemDelegate : public CollectionItemDelegate {
|
||||
public:
|
||||
DeviceItemDelegate(QObject *parent);
|
||||
|
||||
static const int kIconPadding;
|
||||
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
|
||||
};
|
||||
|
||||
class DeviceView : public AutoExpandingTreeView {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DeviceView(QWidget *parent = nullptr);
|
||||
~DeviceView();
|
||||
|
||||
void SetApplication(Application *app);
|
||||
|
||||
protected:
|
||||
void contextMenuEvent(QContextMenuEvent*);
|
||||
void mouseDoubleClickEvent(QMouseEvent *event);
|
||||
|
||||
private slots:
|
||||
// Device menu actions
|
||||
void Connect();
|
||||
void Unmount();
|
||||
void Forget();
|
||||
void Properties();
|
||||
|
||||
// Collection menu actions
|
||||
void Load();
|
||||
void AddToPlaylist();
|
||||
void OpenInNewPlaylist();
|
||||
#ifdef HAVE_GSTREAMER
|
||||
void Organise();
|
||||
void Delete();
|
||||
#endif
|
||||
|
||||
void DeviceConnected(int row);
|
||||
void DeviceDisconnected(int row);
|
||||
|
||||
#ifdef HAVE_GSTREAMER
|
||||
void DeleteFinished(const SongList &songs_with_errors);
|
||||
#endif
|
||||
|
||||
// AutoExpandingTreeView
|
||||
bool CanRecursivelyExpand(const QModelIndex &index) const;
|
||||
|
||||
private:
|
||||
QModelIndex MapToDevice(const QModelIndex &merged_model_index) const;
|
||||
QModelIndex MapToCollection(const QModelIndex &merged_model_index) const;
|
||||
QModelIndex FindParentDevice(const QModelIndex &merged_model_index) const;
|
||||
SongList GetSelectedSongs() const;
|
||||
|
||||
private:
|
||||
Application *app_;
|
||||
MergedProxyModel *merged_model_;
|
||||
QSortFilterProxyModel *sort_model_;
|
||||
|
||||
std::unique_ptr<DeviceProperties> properties_dialog_;
|
||||
#ifdef HAVE_GSTREAMER
|
||||
std::unique_ptr<OrganiseDialog> organise_dialog_;
|
||||
#endif
|
||||
|
||||
QMenu *device_menu_;
|
||||
QAction *eject_action_;
|
||||
QAction *forget_action_;
|
||||
QAction *properties_action_;
|
||||
|
||||
QMenu *collection_menu_;
|
||||
QAction *load_action_;
|
||||
QAction *add_to_playlist_action_;
|
||||
QAction *open_in_new_playlist_;
|
||||
QAction *organise_action_;
|
||||
QAction *delete_action_;
|
||||
|
||||
QModelIndex menu_index_;
|
||||
};
|
||||
|
||||
#endif // DEVICEVIEW_H
|
||||
59
src/device/deviceviewcontainer.cpp
Normal file
59
src/device/deviceviewcontainer.cpp
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "deviceviewcontainer.h"
|
||||
#include "ui_deviceviewcontainer.h"
|
||||
|
||||
#include "core/iconloader.h"
|
||||
|
||||
DeviceViewContainer::DeviceViewContainer(QWidget *parent) : QWidget(parent), ui_(new Ui::DeviceViewContainer), loaded_icons_(false) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
|
||||
QPalette palette(ui_->windows_is_broken_frame->palette());
|
||||
palette.setColor(QPalette::Background, QColor(255, 255, 222));
|
||||
ui_->windows_is_broken_frame->setPalette(palette);
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
ui_->windows_is_broken_frame->show();
|
||||
#else
|
||||
ui_->windows_is_broken_frame->hide();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
DeviceViewContainer::~DeviceViewContainer() { delete ui_; }
|
||||
|
||||
void DeviceViewContainer::showEvent(QShowEvent *e) {
|
||||
|
||||
if (!loaded_icons_) {
|
||||
loaded_icons_ = true;
|
||||
|
||||
ui_->close_frame_button->setIcon(IconLoader::Load("edit-delete"));
|
||||
ui_->warning_icon->setPixmap(IconLoader::Load("dialog-warning").pixmap(22));
|
||||
}
|
||||
|
||||
QWidget::showEvent(e);
|
||||
|
||||
}
|
||||
|
||||
DeviceView *DeviceViewContainer::view() const { return ui_->view; }
|
||||
51
src/device/deviceviewcontainer.h
Normal file
51
src/device/deviceviewcontainer.h
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DEVICEVIEWCONTAINER_H
|
||||
#define DEVICEVIEWCONTAINER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
namespace Ui {
|
||||
class DeviceViewContainer;
|
||||
}
|
||||
|
||||
class DeviceView;
|
||||
|
||||
class DeviceViewContainer : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DeviceViewContainer(QWidget *parent = nullptr);
|
||||
~DeviceViewContainer();
|
||||
|
||||
DeviceView* view() const;
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent *);
|
||||
|
||||
private:
|
||||
Ui::DeviceViewContainer *ui_;
|
||||
bool loaded_icons_;
|
||||
};
|
||||
|
||||
#endif // DEVICEVIEWCONTAINER_H
|
||||
99
src/device/deviceviewcontainer.ui
Normal file
99
src/device/deviceviewcontainer.ui
Normal file
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>DeviceViewContainer</class>
|
||||
<widget class="QWidget" name="DeviceViewContainer">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>391</width>
|
||||
<height>396</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QFrame" name="windows_is_broken_frame">
|
||||
<property name="autoFillBackground">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="warning_icon"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>iPods and USB devices currently don't work on Windows. Sorry!</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="close_frame_button"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="DeviceView" name="view" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>DeviceView</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>device/deviceview.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>close_frame_button</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>windows_is_broken_frame</receiver>
|
||||
<slot>hide()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>362</x>
|
||||
<y>31</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>369</x>
|
||||
<y>40</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
67
src/device/filesystemdevice.cpp
Normal file
67
src/device/filesystemdevice.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "devicelister.h"
|
||||
#include "devicemanager.h"
|
||||
#include "filesystemdevice.h"
|
||||
#include "core/application.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "collection/collectionwatcher.h"
|
||||
|
||||
#include <QThread>
|
||||
#include <QtDebug>
|
||||
|
||||
FilesystemDevice::FilesystemDevice(const QUrl &url, DeviceLister *lister, const QString &unique_id, DeviceManager *manager, Application *app, int database_id, bool first_time)
|
||||
: FilesystemMusicStorage(url.toLocalFile()), ConnectedDevice(url, lister, unique_id, manager, app, database_id, first_time), watcher_(new CollectionWatcher), watcher_thread_(new QThread(this))
|
||||
{
|
||||
|
||||
watcher_->moveToThread(watcher_thread_);
|
||||
watcher_thread_->start(QThread::IdlePriority);
|
||||
|
||||
watcher_->set_device_name(manager->data(manager->index(manager->FindDeviceById(unique_id)), DeviceManager::Role_FriendlyName).toString());
|
||||
watcher_->set_backend(backend_);
|
||||
watcher_->set_task_manager(app_->task_manager());
|
||||
|
||||
connect(backend_, SIGNAL(DirectoryDiscovered(Directory,SubdirectoryList)), watcher_, SLOT(AddDirectory(Directory,SubdirectoryList)));
|
||||
connect(backend_, SIGNAL(DirectoryDeleted(Directory)), watcher_, SLOT(RemoveDirectory(Directory)));
|
||||
connect(watcher_, SIGNAL(NewOrUpdatedSongs(SongList)), backend_, SLOT(AddOrUpdateSongs(SongList)));
|
||||
connect(watcher_, SIGNAL(SongsMTimeUpdated(SongList)), backend_, SLOT(UpdateMTimesOnly(SongList)));
|
||||
connect(watcher_, SIGNAL(SongsDeleted(SongList)), backend_, SLOT(DeleteSongs(SongList)));
|
||||
connect(watcher_, SIGNAL(SubdirsDiscovered(SubdirectoryList)), backend_, SLOT(AddOrUpdateSubdirs(SubdirectoryList)));
|
||||
connect(watcher_, SIGNAL(SubdirsMTimeUpdated(SubdirectoryList)), backend_, SLOT(AddOrUpdateSubdirs(SubdirectoryList)));
|
||||
connect(watcher_, SIGNAL(CompilationsNeedUpdating()), backend_, SLOT(UpdateCompilations()));
|
||||
connect(watcher_, SIGNAL(ScanStarted(int)), SIGNAL(TaskStarted(int)));
|
||||
|
||||
}
|
||||
|
||||
void FilesystemDevice::Init() {
|
||||
InitBackendDirectory(url_.toLocalFile(), first_time_);
|
||||
model_->Init();
|
||||
}
|
||||
|
||||
FilesystemDevice::~FilesystemDevice() {
|
||||
watcher_->deleteLater();
|
||||
watcher_thread_->exit();
|
||||
watcher_thread_->wait();
|
||||
}
|
||||
|
||||
52
src/device/filesystemdevice.h
Normal file
52
src/device/filesystemdevice.h
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef FILESYSTEMDEVICE_H
|
||||
#define FILESYSTEMDEVICE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "connecteddevice.h"
|
||||
#include "core/filesystemmusicstorage.h"
|
||||
|
||||
class DeviceManager;
|
||||
class CollectionWatcher;
|
||||
|
||||
class FilesystemDevice : public ConnectedDevice, public virtual FilesystemMusicStorage {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Q_INVOKABLE FilesystemDevice(
|
||||
const QUrl &url, DeviceLister *lister,
|
||||
const QString &unique_id, DeviceManager *manager,
|
||||
Application *app,
|
||||
int database_id, bool first_time);
|
||||
~FilesystemDevice();
|
||||
|
||||
void Init();
|
||||
|
||||
static QStringList url_schemes() { return QStringList() << "file"; }
|
||||
|
||||
private:
|
||||
CollectionWatcher *watcher_;
|
||||
QThread *watcher_thread_;
|
||||
};
|
||||
|
||||
#endif // FILESYSTEMDEVICE_H
|
||||
580
src/device/giolister.cpp
Normal file
580
src/device/giolister.cpp
Normal file
@@ -0,0 +1,580 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include <QFile>
|
||||
#include <QStringList>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "giolister.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/signalchecker.h"
|
||||
|
||||
using std::placeholders::_1;
|
||||
using std::placeholders::_2;
|
||||
using std::placeholders::_3;
|
||||
|
||||
QString GioLister::DeviceInfo::unique_id() const {
|
||||
|
||||
if (mount)
|
||||
return QString("Gio/%1/%2/%3").arg(mount_uuid, filesystem_type).arg(filesystem_size);
|
||||
|
||||
return QString("Gio/unmounted/%1").arg((qulonglong)volume.get());
|
||||
|
||||
}
|
||||
|
||||
bool GioLister::DeviceInfo::is_suitable() const {
|
||||
|
||||
if (!volume) return false; // This excludes smb or ssh mounts
|
||||
|
||||
if (drive && !drive_removable) return false; // This excludes internal drives
|
||||
|
||||
if (filesystem_type.isEmpty()) return true;
|
||||
|
||||
return filesystem_type != "udf" &&
|
||||
filesystem_type != "smb" &&
|
||||
filesystem_type != "cifs" &&
|
||||
filesystem_type != "ssh" &&
|
||||
filesystem_type != "isofs";
|
||||
|
||||
}
|
||||
|
||||
template <typename T, typename F>
|
||||
void OperationFinished(F f, GObject* object, GAsyncResult* result) {
|
||||
|
||||
T* obj = reinterpret_cast<T*>(object);
|
||||
GError* error = nullptr;
|
||||
|
||||
f(obj, result, &error);
|
||||
|
||||
if (error) {
|
||||
qLog(Error) << "Mount/unmount error:" << error->message;
|
||||
g_error_free(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GioLister::VolumeMountFinished(GObject* object, GAsyncResult* result, gpointer) {
|
||||
OperationFinished<GVolume>(std::bind(g_volume_mount_finish, _1, _2, _3), object, result);
|
||||
}
|
||||
|
||||
void GioLister::Init() {
|
||||
|
||||
monitor_.reset_without_add(g_volume_monitor_get());
|
||||
|
||||
// Get existing volumes
|
||||
GList* const volumes = g_volume_monitor_get_volumes(monitor_);
|
||||
for (GList* p = volumes; p; p = p->next) {
|
||||
GVolume* volume = static_cast<GVolume*>(p->data);
|
||||
|
||||
VolumeAdded(volume);
|
||||
g_object_unref(volume);
|
||||
}
|
||||
g_list_free(volumes);
|
||||
|
||||
// Get existing mounts
|
||||
GList* const mounts = g_volume_monitor_get_mounts(monitor_);
|
||||
for (GList* p = mounts; p; p = p->next) {
|
||||
GMount* mount = static_cast<GMount*>(p->data);
|
||||
|
||||
MountAdded(mount);
|
||||
g_object_unref(mount);
|
||||
}
|
||||
g_list_free(mounts);
|
||||
|
||||
// Connect signals from the monitor
|
||||
signals_.append(CHECKED_GCONNECT(monitor_, "volume-added", &VolumeAddedCallback, this));
|
||||
signals_.append(CHECKED_GCONNECT(monitor_, "volume-removed", &VolumeRemovedCallback, this));
|
||||
signals_.append(CHECKED_GCONNECT(monitor_, "mount-added", &MountAddedCallback, this));
|
||||
signals_.append(CHECKED_GCONNECT(monitor_, "mount-changed", &MountChangedCallback, this));
|
||||
signals_.append(CHECKED_GCONNECT(monitor_, "mount-removed", &MountRemovedCallback, this));
|
||||
|
||||
}
|
||||
|
||||
GioLister::~GioLister() {
|
||||
for (gulong signal : signals_) {
|
||||
g_signal_handler_disconnect(monitor_, signal);
|
||||
}
|
||||
}
|
||||
|
||||
QStringList GioLister::DeviceUniqueIDs() {
|
||||
QMutexLocker l(&mutex_);
|
||||
return devices_.keys();
|
||||
}
|
||||
|
||||
QVariantList GioLister::DeviceIcons(const QString& id) {
|
||||
|
||||
QVariantList ret;
|
||||
QMutexLocker l(&mutex_);
|
||||
if (!devices_.contains(id)) return ret;
|
||||
|
||||
const DeviceInfo& info = devices_[id];
|
||||
|
||||
if (info.mount) {
|
||||
ret << DeviceLister::GuessIconForPath(info.mount_path);
|
||||
ret << info.mount_icon_names;
|
||||
}
|
||||
|
||||
ret << DeviceLister::GuessIconForModel(QString(), info.mount_name);
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QString GioLister::DeviceManufacturer(const QString& id) { return QString(); }
|
||||
|
||||
QString GioLister::DeviceModel(const QString& id) {
|
||||
|
||||
QMutexLocker l(&mutex_);
|
||||
if (!devices_.contains(id)) return QString();
|
||||
const DeviceInfo& info = devices_[id];
|
||||
|
||||
return info.drive_name.isEmpty() ? info.volume_name : info.drive_name;
|
||||
|
||||
}
|
||||
|
||||
quint64 GioLister::DeviceCapacity(const QString& id) {
|
||||
return LockAndGetDeviceInfo(id, &DeviceInfo::filesystem_size);
|
||||
}
|
||||
|
||||
quint64 GioLister::DeviceFreeSpace(const QString& id) {
|
||||
return LockAndGetDeviceInfo(id, &DeviceInfo::filesystem_free);
|
||||
}
|
||||
|
||||
QString GioLister::MakeFriendlyName(const QString& id) {
|
||||
return DeviceModel(id);
|
||||
}
|
||||
|
||||
QVariantMap GioLister::DeviceHardwareInfo(const QString& id) {
|
||||
|
||||
QVariantMap ret;
|
||||
|
||||
QMutexLocker l(&mutex_);
|
||||
if (!devices_.contains(id)) return ret;
|
||||
const DeviceInfo& info = devices_[id];
|
||||
|
||||
ret[QT_TR_NOOP("Mount point")] = info.mount_path;
|
||||
ret[QT_TR_NOOP("Device")] = info.volume_unix_device;
|
||||
ret[QT_TR_NOOP("URI")] = info.mount_uri;
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QList<QUrl> GioLister::MakeDeviceUrls(const QString& id) {
|
||||
|
||||
QString mount_point;
|
||||
QString uri;
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
mount_point = devices_[id].mount_path;
|
||||
uri = devices_[id].mount_uri;
|
||||
}
|
||||
|
||||
// gphoto2 gives invalid hostnames with []:, characters in
|
||||
uri.replace(QRegExp("//\\[usb:(\\d+),(\\d+)\\]"), "//usb-\\1-\\2");
|
||||
|
||||
QUrl url(uri);
|
||||
|
||||
QList<QUrl> ret;
|
||||
|
||||
// Special case for file:// GIO URIs - we have to check whether they point
|
||||
// to an ipod.
|
||||
if (url.isValid() && url.scheme() == "file") {
|
||||
ret << MakeUrlFromLocalPath(url.path());
|
||||
} else {
|
||||
ret << url;
|
||||
}
|
||||
|
||||
ret << MakeUrlFromLocalPath(mount_point);
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void GioLister::VolumeAddedCallback(GVolumeMonitor*, GVolume* v, gpointer d) {
|
||||
static_cast<GioLister*>(d)->VolumeAdded(v);
|
||||
}
|
||||
|
||||
void GioLister::VolumeRemovedCallback(GVolumeMonitor*, GVolume* v, gpointer d) {
|
||||
static_cast<GioLister*>(d)->VolumeRemoved(v);
|
||||
}
|
||||
|
||||
void GioLister::MountAddedCallback(GVolumeMonitor*, GMount* m, gpointer d) {
|
||||
static_cast<GioLister*>(d)->MountAdded(m);
|
||||
}
|
||||
|
||||
void GioLister::MountChangedCallback(GVolumeMonitor*, GMount* m, gpointer d) {
|
||||
static_cast<GioLister*>(d)->MountChanged(m);
|
||||
}
|
||||
|
||||
void GioLister::MountRemovedCallback(GVolumeMonitor*, GMount* m, gpointer d) {
|
||||
static_cast<GioLister*>(d)->MountRemoved(m);
|
||||
}
|
||||
|
||||
void GioLister::VolumeAdded(GVolume* volume) {
|
||||
|
||||
g_object_ref(volume);
|
||||
|
||||
DeviceInfo info;
|
||||
info.ReadVolumeInfo(volume);
|
||||
#ifdef HAVE_AUDIOCD
|
||||
if (info.volume_root_uri.startsWith("cdda"))
|
||||
// Audio CD devices are already handled by CDDA lister
|
||||
return;
|
||||
#endif
|
||||
info.ReadDriveInfo(g_volume_get_drive(volume));
|
||||
info.ReadMountInfo(g_volume_get_mount(volume));
|
||||
if (!info.is_suitable()) return;
|
||||
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
devices_[info.unique_id()] = info;
|
||||
}
|
||||
|
||||
emit DeviceAdded(info.unique_id());
|
||||
|
||||
}
|
||||
|
||||
void GioLister::VolumeRemoved(GVolume* volume) {
|
||||
|
||||
QString id;
|
||||
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
id = FindUniqueIdByVolume(volume);
|
||||
if (id.isNull()) return;
|
||||
|
||||
devices_.remove(id);
|
||||
}
|
||||
|
||||
emit DeviceRemoved(id);
|
||||
}
|
||||
|
||||
void GioLister::MountAdded(GMount* mount) {
|
||||
|
||||
g_object_ref(mount);
|
||||
|
||||
DeviceInfo info;
|
||||
info.ReadVolumeInfo(g_mount_get_volume(mount));
|
||||
#ifdef HAVE_AUDIOCD
|
||||
if (info.volume_root_uri.startsWith("cdda"))
|
||||
// Audio CD devices are already handled by CDDA lister
|
||||
return;
|
||||
#endif
|
||||
info.ReadMountInfo(mount);
|
||||
info.ReadDriveInfo(g_mount_get_drive(mount));
|
||||
if (!info.is_suitable()) return;
|
||||
|
||||
QString old_id;
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
|
||||
// The volume might already exist - either mounted or unmounted.
|
||||
for (const QString& id : devices_.keys()) {
|
||||
if (devices_[id].volume == info.volume) {
|
||||
old_id = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!old_id.isEmpty() && old_id != info.unique_id()) {
|
||||
// If the ID has changed (for example, after it's been mounted), we need
|
||||
// to remove the old device.
|
||||
devices_.remove(old_id);
|
||||
emit DeviceRemoved(old_id);
|
||||
|
||||
old_id = QString();
|
||||
}
|
||||
devices_[info.unique_id()] = info;
|
||||
}
|
||||
|
||||
if (!old_id.isEmpty())
|
||||
emit DeviceChanged(old_id);
|
||||
else {
|
||||
emit DeviceAdded(info.unique_id());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GioLister::MountChanged(GMount* mount) {
|
||||
|
||||
QString id;
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
id = FindUniqueIdByMount(mount);
|
||||
if (id.isNull()) return;
|
||||
|
||||
g_object_ref(mount);
|
||||
|
||||
DeviceInfo new_info;
|
||||
new_info.ReadMountInfo(mount);
|
||||
new_info.ReadVolumeInfo(g_mount_get_volume(mount));
|
||||
new_info.ReadDriveInfo(g_mount_get_drive(mount));
|
||||
|
||||
// Ignore the change if the new info is useless
|
||||
if (new_info.invalid_enclosing_mount ||
|
||||
(devices_[id].filesystem_size != 0 && new_info.filesystem_size == 0) ||
|
||||
(!devices_[id].filesystem_type.isEmpty() && new_info.filesystem_type.isEmpty()))
|
||||
return;
|
||||
|
||||
devices_[id] = new_info;
|
||||
}
|
||||
|
||||
emit DeviceChanged(id);
|
||||
|
||||
}
|
||||
|
||||
void GioLister::MountRemoved(GMount* mount) {
|
||||
|
||||
QString id;
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
id = FindUniqueIdByMount(mount);
|
||||
if (id.isNull()) return;
|
||||
|
||||
devices_.remove(id);
|
||||
}
|
||||
|
||||
emit DeviceRemoved(id);
|
||||
|
||||
}
|
||||
|
||||
QString GioLister::DeviceInfo::ConvertAndFree(char* str) {
|
||||
QString ret = QString::fromUtf8(str);
|
||||
g_free(str);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void GioLister::DeviceInfo::ReadMountInfo(GMount* mount) {
|
||||
|
||||
// Get basic information
|
||||
this->mount.reset_without_add(mount);
|
||||
if (!mount) return;
|
||||
|
||||
mount_name = ConvertAndFree(g_mount_get_name(mount));
|
||||
|
||||
// Get the icon name(s)
|
||||
mount_icon_names.clear();
|
||||
GIcon* icon = g_mount_get_icon(mount);
|
||||
if (G_IS_THEMED_ICON(icon)) {
|
||||
const char* const* icons = g_themed_icon_get_names(G_THEMED_ICON(icon));
|
||||
for (const char* const* p = icons; *p; ++p) {
|
||||
mount_icon_names << QString::fromUtf8(*p);
|
||||
}
|
||||
}
|
||||
g_object_unref(icon);
|
||||
|
||||
GFile* root = g_mount_get_root(mount);
|
||||
|
||||
// Get the mount path
|
||||
mount_path = ConvertAndFree(g_file_get_path(root));
|
||||
mount_uri = ConvertAndFree(g_file_get_uri(root));
|
||||
|
||||
// Do a sanity check to make sure the root is actually this mount - when a
|
||||
// device is unmounted GIO sends a changed signal before the removed signal,
|
||||
// and we end up reading information about the / filesystem by mistake.
|
||||
GError* error = nullptr;
|
||||
GMount* actual_mount = g_file_find_enclosing_mount(root, nullptr, &error);
|
||||
if (error || !actual_mount) {
|
||||
g_error_free(error);
|
||||
invalid_enclosing_mount = true;
|
||||
}
|
||||
else if (actual_mount) {
|
||||
g_object_unref(actual_mount);
|
||||
}
|
||||
|
||||
// Query the filesystem info for size, free space, and type
|
||||
error = nullptr;
|
||||
GFileInfo* info = g_file_query_filesystem_info(
|
||||
root, G_FILE_ATTRIBUTE_FILESYSTEM_SIZE
|
||||
"," G_FILE_ATTRIBUTE_FILESYSTEM_FREE "," G_FILE_ATTRIBUTE_FILESYSTEM_TYPE,
|
||||
nullptr, &error);
|
||||
if (error) {
|
||||
qLog(Warning) << error->message;
|
||||
g_error_free(error);
|
||||
}
|
||||
else {
|
||||
filesystem_size = g_file_info_get_attribute_uint64(
|
||||
info, G_FILE_ATTRIBUTE_FILESYSTEM_SIZE);
|
||||
filesystem_free = g_file_info_get_attribute_uint64(
|
||||
info, G_FILE_ATTRIBUTE_FILESYSTEM_FREE);
|
||||
filesystem_type = QString::fromUtf8(g_file_info_get_attribute_string(
|
||||
info, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE));
|
||||
g_object_unref(info);
|
||||
}
|
||||
|
||||
// Query the file's info for a filesystem ID
|
||||
// Only afc devices (that I know of) give reliably unique IDs
|
||||
if (filesystem_type == "afc") {
|
||||
error = nullptr;
|
||||
info = g_file_query_info(root, G_FILE_ATTRIBUTE_ID_FILESYSTEM, G_FILE_QUERY_INFO_NONE, nullptr, &error);
|
||||
if (error) {
|
||||
qLog(Warning) << error->message;
|
||||
g_error_free(error);
|
||||
}
|
||||
else {
|
||||
mount_uuid = QString::fromUtf8(g_file_info_get_attribute_string(info, G_FILE_ATTRIBUTE_ID_FILESYSTEM));
|
||||
g_object_unref(info);
|
||||
}
|
||||
}
|
||||
|
||||
g_object_unref(root);
|
||||
|
||||
}
|
||||
|
||||
void GioLister::DeviceInfo::ReadVolumeInfo(GVolume* volume) {
|
||||
|
||||
this->volume.reset_without_add(volume);
|
||||
if (!volume) return;
|
||||
|
||||
volume_name = ConvertAndFree(g_volume_get_name(volume));
|
||||
volume_uuid = ConvertAndFree(g_volume_get_uuid(volume));
|
||||
volume_unix_device = ConvertAndFree(g_volume_get_identifier(volume, G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE));
|
||||
|
||||
GFile* root = g_volume_get_activation_root(volume);
|
||||
if (root) {
|
||||
volume_root_uri = g_file_get_uri(root);
|
||||
g_object_unref(root);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GioLister::DeviceInfo::ReadDriveInfo(GDrive* drive) {
|
||||
this->drive.reset_without_add(drive);
|
||||
if (!drive) return;
|
||||
|
||||
drive_name = ConvertAndFree(g_drive_get_name(drive));
|
||||
drive_removable = g_drive_is_media_removable(drive);
|
||||
}
|
||||
|
||||
QString GioLister::FindUniqueIdByMount(GMount* mount) const {
|
||||
for (const DeviceInfo& info : devices_) {
|
||||
if (info.mount == mount) return info.unique_id();
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString GioLister::FindUniqueIdByVolume(GVolume* volume) const {
|
||||
for (const DeviceInfo& info : devices_) {
|
||||
if (info.volume == volume) return info.unique_id();
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
void GioLister::VolumeEjectFinished(GObject* object, GAsyncResult* result, gpointer) {
|
||||
OperationFinished<GVolume>(std::bind(g_volume_eject_with_operation_finish, _1, _2, _3), object, result);
|
||||
}
|
||||
|
||||
void GioLister::MountEjectFinished(GObject* object, GAsyncResult* result, gpointer) {
|
||||
OperationFinished<GMount>(std::bind(g_mount_eject_with_operation_finish, _1, _2, _3), object, result);
|
||||
}
|
||||
|
||||
void GioLister::MountUnmountFinished(GObject* object, GAsyncResult* result, gpointer) {
|
||||
OperationFinished<GMount>(std::bind(g_mount_unmount_with_operation_finish, _1, _2, _3), object, result);
|
||||
}
|
||||
|
||||
void GioLister::UnmountDevice(const QString& id) {
|
||||
|
||||
QMutexLocker l(&mutex_);
|
||||
if (!devices_.contains(id)) return;
|
||||
|
||||
const DeviceInfo& info = devices_[id];
|
||||
|
||||
if (!info.mount) return;
|
||||
|
||||
if (info.volume) {
|
||||
if (g_volume_can_eject(info.volume)) {
|
||||
g_volume_eject_with_operation(info.volume, G_MOUNT_UNMOUNT_NONE, nullptr, nullptr, (GAsyncReadyCallback)VolumeEjectFinished, nullptr);
|
||||
g_object_unref(info.volume);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (g_mount_can_eject(info.mount)) {
|
||||
g_mount_eject_with_operation(info.mount, G_MOUNT_UNMOUNT_NONE, nullptr, nullptr, (GAsyncReadyCallback)MountEjectFinished, nullptr);
|
||||
}
|
||||
else if (g_mount_can_unmount(info.mount)) {
|
||||
g_mount_unmount_with_operation(info.mount, G_MOUNT_UNMOUNT_NONE, nullptr, nullptr, (GAsyncReadyCallback)MountUnmountFinished, nullptr);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GioLister::UpdateDeviceFreeSpace(const QString& id) {
|
||||
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
if (!devices_.contains(id)) return;
|
||||
|
||||
DeviceInfo& device_info = devices_[id];
|
||||
|
||||
GFile* root = g_mount_get_root(device_info.mount);
|
||||
|
||||
GError* error = nullptr;
|
||||
GFileInfo* info = g_file_query_filesystem_info(root, G_FILE_ATTRIBUTE_FILESYSTEM_FREE, nullptr, &error);
|
||||
if (error) {
|
||||
qLog(Warning) << error->message;
|
||||
g_error_free(error);
|
||||
}
|
||||
else {
|
||||
device_info.filesystem_free = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_FILESYSTEM_FREE);
|
||||
g_object_unref(info);
|
||||
}
|
||||
|
||||
g_object_unref(root);
|
||||
}
|
||||
|
||||
emit DeviceChanged(id);
|
||||
|
||||
}
|
||||
|
||||
bool GioLister::DeviceNeedsMount(const QString& id) {
|
||||
QMutexLocker l(&mutex_);
|
||||
return devices_.contains(id) && !devices_[id].mount;
|
||||
}
|
||||
|
||||
int GioLister::MountDevice(const QString& id) {
|
||||
const int request_id = next_mount_request_id_++;
|
||||
metaObject()->invokeMethod(this, "DoMountDevice", Qt::QueuedConnection, Q_ARG(QString, id), Q_ARG(int, request_id));
|
||||
return request_id;
|
||||
}
|
||||
|
||||
void GioLister::DoMountDevice(const QString& id, int request_id) {
|
||||
|
||||
QMutexLocker l(&mutex_);
|
||||
if (!devices_.contains(id)) {
|
||||
emit DeviceMounted(id, request_id, false);
|
||||
return;
|
||||
}
|
||||
|
||||
const DeviceInfo& info = devices_[id];
|
||||
if (info.mount) {
|
||||
// Already mounted
|
||||
emit DeviceMounted(id, request_id, true);
|
||||
return;
|
||||
}
|
||||
|
||||
g_volume_mount(info.volume, G_MOUNT_MOUNT_NONE, nullptr, nullptr, VolumeMountFinished, nullptr);
|
||||
emit DeviceMounted(id, request_id, true);
|
||||
|
||||
}
|
||||
|
||||
152
src/device/giolister.h
Normal file
152
src/device/giolister.h
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef GIOLISTER_H
|
||||
#define GIOLISTER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "devicelister.h"
|
||||
#include "core/scopedgobject.h"
|
||||
|
||||
// Work around compile issue with glib >= 2.25
|
||||
#ifdef signals
|
||||
#undef signals
|
||||
#endif
|
||||
|
||||
#include <gio/gio.h>
|
||||
|
||||
#include <QMutex>
|
||||
#include <QStringList>
|
||||
|
||||
class GioLister : public DeviceLister {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
GioLister() {}
|
||||
~GioLister();
|
||||
|
||||
int priority() const { return 50; }
|
||||
|
||||
QStringList DeviceUniqueIDs();
|
||||
QVariantList DeviceIcons(const QString &id);
|
||||
QString DeviceManufacturer(const QString &id);
|
||||
QString DeviceModel(const QString &id);
|
||||
quint64 DeviceCapacity(const QString &id);
|
||||
quint64 DeviceFreeSpace(const QString &id);
|
||||
QVariantMap DeviceHardwareInfo(const QString &id);
|
||||
bool DeviceNeedsMount(const QString &id);
|
||||
|
||||
QString MakeFriendlyName(const QString &id);
|
||||
QList<QUrl> MakeDeviceUrls(const QString &id);
|
||||
|
||||
int MountDevice(const QString &id);
|
||||
void UnmountDevice(const QString &id);
|
||||
|
||||
public slots:
|
||||
void UpdateDeviceFreeSpace(const QString &id);
|
||||
|
||||
protected:
|
||||
void Init();
|
||||
|
||||
private:
|
||||
struct DeviceInfo {
|
||||
DeviceInfo() : drive_removable(false), filesystem_size(0),filesystem_free(0), invalid_enclosing_mount(false) {}
|
||||
|
||||
QString unique_id() const;
|
||||
bool is_suitable() const;
|
||||
|
||||
static QString ConvertAndFree(char *str);
|
||||
void ReadDriveInfo(GDrive *drive);
|
||||
void ReadVolumeInfo(GVolume *volume);
|
||||
void ReadMountInfo(GMount *mount);
|
||||
|
||||
// Only available if it's a physical drive
|
||||
ScopedGObject<GVolume> volume;
|
||||
QString volume_name;
|
||||
QString volume_unix_device;
|
||||
QString volume_root_uri;
|
||||
QString volume_uuid;
|
||||
|
||||
// Only available if it's a physical drive
|
||||
ScopedGObject<GDrive> drive;
|
||||
QString drive_name;
|
||||
bool drive_removable;
|
||||
|
||||
// Only available if it's mounted
|
||||
ScopedGObject<GMount> mount;
|
||||
QString mount_path;
|
||||
QString mount_uri;
|
||||
QString mount_name;
|
||||
QStringList mount_icon_names;
|
||||
QString mount_uuid;
|
||||
quint64 filesystem_size;
|
||||
quint64 filesystem_free;
|
||||
QString filesystem_type;
|
||||
|
||||
bool invalid_enclosing_mount;
|
||||
};
|
||||
|
||||
void VolumeAdded(GVolume *volume);
|
||||
void VolumeRemoved(GVolume *volume);
|
||||
|
||||
void MountAdded(GMount *mount);
|
||||
void MountChanged(GMount *mount);
|
||||
void MountRemoved(GMount *mount);
|
||||
|
||||
static void VolumeAddedCallback(GVolumeMonitor*, GVolume*, gpointer);
|
||||
static void VolumeRemovedCallback(GVolumeMonitor*, GVolume*, gpointer);
|
||||
|
||||
static void MountAddedCallback(GVolumeMonitor*, GMount*, gpointer);
|
||||
static void MountChangedCallback(GVolumeMonitor*, GMount*, gpointer);
|
||||
static void MountRemovedCallback(GVolumeMonitor*, GMount*, gpointer);
|
||||
|
||||
static void VolumeMountFinished(GObject *object, GAsyncResult *result, gpointer);
|
||||
static void VolumeEjectFinished(GObject *object, GAsyncResult *result, gpointer);
|
||||
static void MountEjectFinished(GObject *object, GAsyncResult *result, gpointer);
|
||||
static void MountUnmountFinished(GObject *object, GAsyncResult *result, gpointer);
|
||||
|
||||
// You MUST hold the mutex while calling this function
|
||||
QString FindUniqueIdByMount(GMount *mount) const;
|
||||
QString FindUniqueIdByVolume(GVolume *volume) const;
|
||||
|
||||
template <typename T>
|
||||
T LockAndGetDeviceInfo(const QString &id, T DeviceInfo::*field);
|
||||
|
||||
private slots:
|
||||
void DoMountDevice(const QString &id, int request_id);
|
||||
|
||||
private:
|
||||
ScopedGObject<GVolumeMonitor> monitor_;
|
||||
QList<gulong> signals_;
|
||||
|
||||
QMutex mutex_;
|
||||
QMap<QString, DeviceInfo> devices_;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
T GioLister::LockAndGetDeviceInfo(const QString &id, T DeviceInfo::*field) {
|
||||
QMutexLocker l(&mutex_);
|
||||
if (!devices_.contains(id)) return T();
|
||||
|
||||
return devices_[id].*field;
|
||||
}
|
||||
|
||||
#endif // GIOLISTER_H
|
||||
245
src/device/gpoddevice.cpp
Normal file
245
src/device/gpoddevice.cpp
Normal file
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <gpod/itdb.h>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QThread>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "devicemanager.h"
|
||||
#include "gpoddevice.h"
|
||||
#include "gpodloader.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/application.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
|
||||
GPodDevice::GPodDevice(const QUrl &url, DeviceLister *lister, const QString &unique_id, DeviceManager *manager, Application *app, int database_id, bool first_time)
|
||||
: ConnectedDevice(url, lister, unique_id, manager, app, database_id, first_time),
|
||||
loader_thread_(new QThread(this)),
|
||||
loader_(nullptr),
|
||||
db_(nullptr) {}
|
||||
|
||||
void GPodDevice::Init() {
|
||||
|
||||
InitBackendDirectory(url_.path(), first_time_);
|
||||
model_->Init();
|
||||
|
||||
loader_ = new GPodLoader(url_.path(), app_->task_manager(), backend_, shared_from_this());
|
||||
loader_->moveToThread(loader_thread_);
|
||||
|
||||
connect(loader_, SIGNAL(Error(QString)), SIGNAL(Error(QString)));
|
||||
connect(loader_, SIGNAL(TaskStarted(int)), SIGNAL(TaskStarted(int)));
|
||||
connect(loader_, SIGNAL(LoadFinished(Itdb_iTunesDB*)), SLOT(LoadFinished(Itdb_iTunesDB*)));
|
||||
connect(loader_thread_, SIGNAL(started()), loader_, SLOT(LoadDatabase()));
|
||||
loader_thread_->start();
|
||||
|
||||
}
|
||||
|
||||
GPodDevice::~GPodDevice() {}
|
||||
|
||||
void GPodDevice::LoadFinished(Itdb_iTunesDB *db) {
|
||||
|
||||
QMutexLocker l(&db_mutex_);
|
||||
db_ = db;
|
||||
db_wait_cond_.wakeAll();
|
||||
|
||||
loader_->deleteLater();
|
||||
loader_ = nullptr;
|
||||
|
||||
}
|
||||
|
||||
bool GPodDevice::StartCopy(QList<Song::FileType> *supported_filetypes) {
|
||||
|
||||
{
|
||||
// Wait for the database to be loaded
|
||||
QMutexLocker l(&db_mutex_);
|
||||
if (!db_) db_wait_cond_.wait(&db_mutex_);
|
||||
}
|
||||
|
||||
// Ensure only one "organise files" can be active at any one time
|
||||
db_busy_.lock();
|
||||
|
||||
if (supported_filetypes) GetSupportedFiletypes(supported_filetypes);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
Itdb_Track *GPodDevice::AddTrackToITunesDb(const Song &metadata) {
|
||||
|
||||
// Create the track
|
||||
Itdb_Track *track = itdb_track_new();
|
||||
metadata.ToItdb(track);
|
||||
|
||||
// Add it to the DB and the master playlist
|
||||
// The DB takes ownership of the track
|
||||
itdb_track_add(db_, track, -1);
|
||||
Itdb_Playlist *mpl = itdb_playlist_mpl(db_);
|
||||
itdb_playlist_add_track(mpl, track, -1);
|
||||
|
||||
return track;
|
||||
|
||||
}
|
||||
|
||||
void GPodDevice::AddTrackToModel(Itdb_Track *track, const QString &prefix) {
|
||||
|
||||
// Add it to our CollectionModel
|
||||
Song metadata_on_device;
|
||||
metadata_on_device.InitFromItdb(track, prefix);
|
||||
metadata_on_device.set_directory_id(1);
|
||||
songs_to_add_ << metadata_on_device;
|
||||
|
||||
}
|
||||
|
||||
bool GPodDevice::CopyToStorage(const CopyJob &job) {
|
||||
|
||||
Q_ASSERT(db_);
|
||||
|
||||
Itdb_Track *track = AddTrackToITunesDb(job.metadata_);
|
||||
|
||||
// Copy the file
|
||||
GError *error = nullptr;
|
||||
itdb_cp_track_to_ipod(track, QDir::toNativeSeparators(job.source_).toLocal8Bit().constData(), &error);
|
||||
if (error) {
|
||||
qLog(Error) << "copying failed:" << error->message;
|
||||
app_->AddError(QString::fromUtf8(error->message));
|
||||
g_error_free(error);
|
||||
|
||||
// Need to remove the track from the db again
|
||||
itdb_track_remove(track);
|
||||
return false;
|
||||
}
|
||||
|
||||
AddTrackToModel(track, url_.path());
|
||||
|
||||
// Remove the original if it was requested
|
||||
if (job.remove_original_) {
|
||||
QFile::remove(job.source_);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void GPodDevice::WriteDatabase(bool success) {
|
||||
|
||||
if (success) {
|
||||
// Write the itunes database
|
||||
GError *error = nullptr;
|
||||
itdb_write(db_, &error);
|
||||
if (error) {
|
||||
qLog(Error) << "writing database failed:" << error->message;
|
||||
app_->AddError(QString::fromUtf8(error->message));
|
||||
g_error_free(error);
|
||||
}
|
||||
else {
|
||||
FinaliseDatabase();
|
||||
|
||||
// Update the collection model
|
||||
if (!songs_to_add_.isEmpty()) backend_->AddOrUpdateSongs(songs_to_add_);
|
||||
if (!songs_to_remove_.isEmpty()) backend_->DeleteSongs(songs_to_remove_);
|
||||
}
|
||||
}
|
||||
|
||||
songs_to_add_.clear();
|
||||
songs_to_remove_.clear();
|
||||
db_busy_.unlock();
|
||||
|
||||
}
|
||||
|
||||
void GPodDevice::FinishCopy(bool success) {
|
||||
WriteDatabase(success);
|
||||
ConnectedDevice::FinishCopy(success);
|
||||
}
|
||||
|
||||
void GPodDevice::StartDelete() { StartCopy(nullptr); }
|
||||
|
||||
bool GPodDevice::RemoveTrackFromITunesDb(const QString &path, const QString &relative_to) {
|
||||
|
||||
QString ipod_filename = path;
|
||||
if (!relative_to.isEmpty() && path.startsWith(relative_to))
|
||||
ipod_filename.remove(0, relative_to.length() + (relative_to.endsWith('/') ? -1 : 0));
|
||||
|
||||
ipod_filename.replace('/', ':');
|
||||
|
||||
// Find the track in the itdb, identify it by its filename
|
||||
Itdb_Track *track = nullptr;
|
||||
for (GList *tracks = db_->tracks; tracks != nullptr; tracks = tracks->next) {
|
||||
Itdb_Track *t = static_cast<Itdb_Track*>(tracks->data);
|
||||
|
||||
if (t->ipod_path == ipod_filename) {
|
||||
track = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (track == nullptr) {
|
||||
qLog(Warning) << "Couldn't find song" << path << "in iTunesDB";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove the track from all playlists
|
||||
for (GList *playlists = db_->playlists ; playlists != nullptr ; playlists = playlists->next) {
|
||||
Itdb_Playlist *playlist = static_cast<Itdb_Playlist*>(playlists->data);
|
||||
|
||||
if (itdb_playlist_contains_track(playlist, track)) {
|
||||
itdb_playlist_remove_track(playlist, track);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the track from the database, this frees the struct too
|
||||
itdb_track_remove(track);
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
bool GPodDevice::DeleteFromStorage(const DeleteJob &job) {
|
||||
|
||||
Q_ASSERT(db_);
|
||||
|
||||
if (!RemoveTrackFromITunesDb(job.metadata_.url().toLocalFile(), url_.path()))
|
||||
return false;
|
||||
|
||||
// Remove the file
|
||||
if (!QFile::remove(job.metadata_.url().toLocalFile())) return false;
|
||||
|
||||
// Remove it from our collection model
|
||||
songs_to_remove_ << job.metadata_;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void GPodDevice::FinishDelete(bool success) {
|
||||
WriteDatabase(success);
|
||||
ConnectedDevice::FinishDelete(success);
|
||||
}
|
||||
|
||||
bool GPodDevice::GetSupportedFiletypes(QList<Song::FileType> *ret) {
|
||||
*ret << Song::Type_Mp4;
|
||||
*ret << Song::Type_Mpeg;
|
||||
return true;
|
||||
}
|
||||
|
||||
87
src/device/gpoddevice.h
Normal file
87
src/device/gpoddevice.h
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef GPODDEVICE_H
|
||||
#define GPODDEVICE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "connecteddevice.h"
|
||||
#include "core/musicstorage.h"
|
||||
|
||||
#include <QMutex>
|
||||
#include <QWaitCondition>
|
||||
|
||||
#include <gpod/itdb.h>
|
||||
|
||||
class GPodLoader;
|
||||
|
||||
class GPodDevice : public ConnectedDevice, public virtual MusicStorage {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Q_INVOKABLE GPodDevice(
|
||||
const QUrl &url, DeviceLister *lister,
|
||||
const QString &unique_id, DeviceManager *manager,
|
||||
Application *app,
|
||||
int database_id, bool first_time);
|
||||
~GPodDevice();
|
||||
|
||||
void Init();
|
||||
|
||||
static QStringList url_schemes() { return QStringList() << "ipod"; }
|
||||
|
||||
bool GetSupportedFiletypes(QList<Song::FileType> *ret);
|
||||
|
||||
bool StartCopy(QList<Song::FileType> *supported_types);
|
||||
bool CopyToStorage(const CopyJob &job);
|
||||
void FinishCopy(bool success);
|
||||
|
||||
void StartDelete();
|
||||
bool DeleteFromStorage(const DeleteJob &job);
|
||||
void FinishDelete(bool success);
|
||||
|
||||
protected slots:
|
||||
void LoadFinished(Itdb_iTunesDB *db);
|
||||
|
||||
protected:
|
||||
Itdb_Track *AddTrackToITunesDb(const Song &metadata);
|
||||
void AddTrackToModel(Itdb_Track *track, const QString &prefix);
|
||||
bool RemoveTrackFromITunesDb(const QString &path, const QString &relative_to = QString());
|
||||
virtual void FinaliseDatabase() {}
|
||||
|
||||
private:
|
||||
void WriteDatabase(bool success);
|
||||
|
||||
protected:
|
||||
QThread *loader_thread_;
|
||||
GPodLoader *loader_;
|
||||
|
||||
QWaitCondition db_wait_cond_;
|
||||
QMutex db_mutex_;
|
||||
Itdb_iTunesDB *db_;
|
||||
|
||||
QMutex db_busy_;
|
||||
SongList songs_to_add_;
|
||||
SongList songs_to_remove_;
|
||||
};
|
||||
|
||||
#endif // GPODDEVICE_H
|
||||
|
||||
97
src/device/gpodloader.cpp
Normal file
97
src/device/gpodloader.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <gpod/itdb.h>
|
||||
|
||||
#include <QDir>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "connecteddevice.h"
|
||||
#include "gpodloader.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/song.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
|
||||
GPodLoader::GPodLoader(const QString &mount_point, TaskManager *task_manager, CollectionBackend *backend, std::shared_ptr<ConnectedDevice> device)
|
||||
: QObject(nullptr),
|
||||
device_(device),
|
||||
mount_point_(mount_point),
|
||||
type_(Song::Type_Unknown),
|
||||
task_manager_(task_manager),
|
||||
backend_(backend) {
|
||||
original_thread_ = thread();
|
||||
}
|
||||
|
||||
GPodLoader::~GPodLoader() {}
|
||||
|
||||
void GPodLoader::LoadDatabase() {
|
||||
|
||||
int task_id = task_manager_->StartTask(tr("Loading iPod database"));
|
||||
emit TaskStarted(task_id);
|
||||
|
||||
// Load the iTunes database
|
||||
GError *error = nullptr;
|
||||
Itdb_iTunesDB *db = itdb_parse(QDir::toNativeSeparators(mount_point_).toLocal8Bit(), &error);
|
||||
|
||||
// Check for errors
|
||||
if (!db) {
|
||||
if (error) {
|
||||
qLog(Error) << "loading database failed:" << error->message;
|
||||
emit Error(QString::fromUtf8(error->message));
|
||||
g_error_free(error);
|
||||
} else {
|
||||
emit Error(tr("An error occurred loading the iTunes database"));
|
||||
}
|
||||
|
||||
task_manager_->SetTaskFinished(task_id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert all the tracks from libgpod structs into Song classes
|
||||
const QString prefix = path_prefix_.isEmpty() ? QDir::fromNativeSeparators(mount_point_) : path_prefix_;
|
||||
|
||||
SongList songs;
|
||||
for (GList *tracks = db->tracks; tracks != nullptr; tracks = tracks->next) {
|
||||
Itdb_Track *track = static_cast<Itdb_Track*>(tracks->data);
|
||||
|
||||
Song song;
|
||||
song.InitFromItdb(track, prefix);
|
||||
song.set_directory_id(1);
|
||||
|
||||
if (type_ != Song::Type_Unknown) song.set_filetype(type_);
|
||||
songs << song;
|
||||
}
|
||||
|
||||
// Need to remove all the existing songs in the database first
|
||||
backend_->DeleteSongs(backend_->FindSongsInDirectory(1));
|
||||
|
||||
// Add the songs we've just loaded
|
||||
backend_->AddOrUpdateSongs(songs);
|
||||
|
||||
moveToThread(original_thread_);
|
||||
|
||||
task_manager_->SetTaskFinished(task_id);
|
||||
emit LoadFinished(db);
|
||||
|
||||
}
|
||||
|
||||
68
src/device/gpodloader.h
Normal file
68
src/device/gpodloader.h
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef GPODLOADER_H
|
||||
#define GPODLOADER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <gpod/itdb.h>
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "core/song.h"
|
||||
|
||||
class ConnectedDevice;
|
||||
class CollectionBackend;
|
||||
class TaskManager;
|
||||
|
||||
class GPodLoader : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
GPodLoader(const QString &mount_point, TaskManager *task_manager, CollectionBackend *backend, std::shared_ptr<ConnectedDevice> device);
|
||||
~GPodLoader();
|
||||
|
||||
void set_music_path_prefix(const QString &prefix) { path_prefix_ = prefix; }
|
||||
void set_song_type(Song::FileType type) { type_ = type; }
|
||||
|
||||
public slots:
|
||||
void LoadDatabase();
|
||||
|
||||
signals:
|
||||
void Error(const QString &message);
|
||||
void TaskStarted(int task_id);
|
||||
void LoadFinished(Itdb_iTunesDB *db);
|
||||
|
||||
private:
|
||||
std::shared_ptr<ConnectedDevice> device_;
|
||||
QThread *original_thread_;
|
||||
|
||||
QString mount_point_;
|
||||
QString path_prefix_;
|
||||
Song::FileType type_;
|
||||
TaskManager *task_manager_;
|
||||
CollectionBackend *backend_;
|
||||
};
|
||||
|
||||
#endif // GPODLOADER_H
|
||||
|
||||
229
src/device/ilister.cpp
Normal file
229
src/device/ilister.cpp
Normal file
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QStringList>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "config.h"
|
||||
#include "ilister.h"
|
||||
#include "imobiledeviceconnection.h"
|
||||
|
||||
iLister::iLister() {
|
||||
}
|
||||
|
||||
iLister::~iLister() {
|
||||
}
|
||||
|
||||
void iLister::Init() {
|
||||
idevice_event_subscribe(&EventCallback, reinterpret_cast<void*>(this));
|
||||
}
|
||||
|
||||
void iLister::EventCallback(const idevice_event_t *event, void *context) {
|
||||
iLister *me = reinterpret_cast<iLister*>(context);
|
||||
|
||||
#ifdef IMOBILEDEVICE_USES_UDIDS
|
||||
const char *uuid = event->udid;
|
||||
#else
|
||||
const char *uuid = event->uuid;
|
||||
#endif
|
||||
|
||||
switch (event->event) {
|
||||
case IDEVICE_DEVICE_ADD:
|
||||
me->DeviceAddedCallback(uuid);
|
||||
break;
|
||||
|
||||
case IDEVICE_DEVICE_REMOVE:
|
||||
me->DeviceRemovedCallback(uuid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void iLister::DeviceAddedCallback(const char *uuid) {
|
||||
|
||||
DeviceInfo info = ReadDeviceInfo(uuid);
|
||||
QString id = UniqueId(uuid);
|
||||
|
||||
QString name = MakeFriendlyName(id);
|
||||
if (info.product_type == "iPhone 3,1" || info.product_type.startsWith("iPad")) {
|
||||
// iPhone 4 and iPad unsupported by libgpod as of 0.7.94.
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
devices_[id] = info;
|
||||
}
|
||||
|
||||
emit DeviceAdded(id);
|
||||
|
||||
}
|
||||
|
||||
void iLister::DeviceRemovedCallback(const char *uuid) {
|
||||
|
||||
QString id = UniqueId(uuid);
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
if (!devices_.contains(id))
|
||||
return;
|
||||
|
||||
devices_.remove(id);
|
||||
}
|
||||
|
||||
emit DeviceRemoved(id);
|
||||
|
||||
}
|
||||
|
||||
QString iLister::UniqueId(const char *uuid) {
|
||||
return "ithing/" + QString::fromUtf8(uuid);
|
||||
}
|
||||
|
||||
QStringList iLister::DeviceUniqueIDs() {
|
||||
return devices_.keys();
|
||||
}
|
||||
|
||||
QVariantList iLister::DeviceIcons(const QString &id) {
|
||||
return QVariantList() << "ipodtouchicon";
|
||||
}
|
||||
|
||||
QString iLister::DeviceManufacturer(const QString &id) {
|
||||
return "Apple";
|
||||
}
|
||||
|
||||
QString iLister::DeviceModel(const QString &id) {
|
||||
return LockAndGetDeviceInfo(id, &DeviceInfo::product_type);
|
||||
}
|
||||
|
||||
quint64 iLister::DeviceCapacity(const QString &id) {
|
||||
return LockAndGetDeviceInfo(id, &DeviceInfo::total_bytes);
|
||||
}
|
||||
|
||||
quint64 iLister::DeviceFreeSpace(const QString &id) {
|
||||
return LockAndGetDeviceInfo(id, &DeviceInfo::free_bytes);
|
||||
}
|
||||
|
||||
QVariantMap iLister::DeviceHardwareInfo(const QString &id) {
|
||||
|
||||
QVariantMap ret;
|
||||
ret[tr("Color")] = LockAndGetDeviceInfo(id, &DeviceInfo::colour);
|
||||
ret["IMEI"] = LockAndGetDeviceInfo(id, &DeviceInfo::imei);
|
||||
ret[tr("Password Protected")] = LockAndGetDeviceInfo(id, &DeviceInfo::password_protected);
|
||||
ret[tr("Timezone")] = LockAndGetDeviceInfo(id, &DeviceInfo::timezone);
|
||||
ret[tr("WiFi MAC Address")] = LockAndGetDeviceInfo(id, &DeviceInfo::wifi_mac);
|
||||
ret[tr("Bluetooth MAC Address")] = LockAndGetDeviceInfo(id, &DeviceInfo::bt_mac);
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QString iLister::MakeFriendlyName(const QString &id) {
|
||||
|
||||
QString name = LockAndGetDeviceInfo(id, &DeviceInfo::name);
|
||||
if (!name.isEmpty()) {
|
||||
return name;
|
||||
}
|
||||
|
||||
QString model_id = LockAndGetDeviceInfo(id, &DeviceInfo::product_type);
|
||||
|
||||
if (model_id.startsWith("iPhone")) {
|
||||
QString version = model_id.right(3);
|
||||
QChar major = version[0];
|
||||
QChar minor = version[2];
|
||||
if (major == '1' && minor == '1') {
|
||||
return "iPhone";
|
||||
}
|
||||
if (major == '1' && minor == '2') {
|
||||
return "iPhone 3G";
|
||||
}
|
||||
if (major == '2' && minor == '1') {
|
||||
return "iPhone 3GS";
|
||||
}
|
||||
if (major == '3' && minor == '1') {
|
||||
return "iPhone 4";
|
||||
}
|
||||
}
|
||||
else if (model_id.startsWith("iPod")) {
|
||||
return "iPod Touch";
|
||||
}
|
||||
else if (model_id.startsWith("iPad")) {
|
||||
return "iPad";
|
||||
}
|
||||
return model_id;
|
||||
|
||||
}
|
||||
|
||||
QList<QUrl> iLister::MakeDeviceUrls(const QString &id) {
|
||||
|
||||
QList<QUrl> ret;
|
||||
|
||||
QString uuid = LockAndGetDeviceInfo(id, &DeviceInfo::uuid);
|
||||
if (uuid.isEmpty())
|
||||
return ret;
|
||||
|
||||
ret << QUrl("afc://" + uuid + "/");
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void iLister::UnmountDevice(const QString &id) { }
|
||||
|
||||
iLister::DeviceInfo iLister::ReadDeviceInfo(const char *uuid) {
|
||||
|
||||
DeviceInfo ret;
|
||||
|
||||
iMobileDeviceConnection conn(uuid);
|
||||
ret.uuid = uuid;
|
||||
ret.product_type = conn.GetProperty("ProductType").toString();
|
||||
ret.free_bytes = conn.GetProperty("AmountDataAvailable", "com.apple.disk_usage").toULongLong();
|
||||
ret.total_bytes = conn.GetProperty("TotalDataCapacity", "com.apple.disk_usage").toULongLong();
|
||||
ret.name = conn.GetProperty("DeviceName").toString();
|
||||
|
||||
ret.colour = conn.GetProperty("DeviceColor").toString();
|
||||
ret.imei = conn.GetProperty("InternationalMobileEquipmentIdentity").toString();
|
||||
ret.hardware = conn.GetProperty("HardwareModel").toString();
|
||||
ret.password_protected = conn.GetProperty("PasswordProtected").toBool();
|
||||
ret.os_version = conn.GetProperty("ProductVersion").toString();
|
||||
ret.timezone = conn.GetProperty("TimeZone").toString();
|
||||
ret.wifi_mac = conn.GetProperty("WiFiAddress").toString();
|
||||
ret.bt_mac = conn.GetProperty("BluetoothAddress").toString();
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void iLister::UpdateDeviceFreeSpace(const QString &id) {
|
||||
|
||||
{
|
||||
QMutexLocker l(&mutex_);
|
||||
if (!devices_.contains(id))
|
||||
return;
|
||||
|
||||
DeviceInfo &info = devices_[id];
|
||||
iMobileDeviceConnection conn(info.uuid);
|
||||
|
||||
info.free_bytes = conn.GetProperty("AmountDataAvailable", "com.apple.disk_usage").toULongLong();
|
||||
}
|
||||
|
||||
emit DeviceChanged(id);
|
||||
|
||||
}
|
||||
102
src/device/ilister.h
Normal file
102
src/device/ilister.h
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef ILISTER_H
|
||||
#define ILISTER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <libimobiledevice/libimobiledevice.h>
|
||||
|
||||
#include <QMutex>
|
||||
|
||||
#include "devicelister.h"
|
||||
|
||||
class iLister : public DeviceLister {
|
||||
Q_OBJECT
|
||||
public:
|
||||
iLister();
|
||||
~iLister();
|
||||
|
||||
int priority() const { return 120; }
|
||||
|
||||
virtual QStringList DeviceUniqueIDs();
|
||||
virtual QVariantList DeviceIcons(const QString &id);
|
||||
virtual QString DeviceManufacturer(const QString &id);
|
||||
virtual QString DeviceModel(const QString &id);
|
||||
virtual quint64 DeviceCapacity(const QString &id);
|
||||
virtual quint64 DeviceFreeSpace(const QString &id);
|
||||
virtual QVariantMap DeviceHardwareInfo(const QString &id);
|
||||
virtual QString MakeFriendlyName(const QString &id);
|
||||
virtual QList<QUrl> MakeDeviceUrls(const QString &id);
|
||||
virtual void UnmountDevice(const QString &id);
|
||||
|
||||
public slots:
|
||||
virtual void UpdateDeviceFreeSpace(const QString &id);
|
||||
|
||||
private:
|
||||
struct DeviceInfo {
|
||||
DeviceInfo() : free_bytes(0), total_bytes(0) {}
|
||||
|
||||
QString uuid;
|
||||
QString product_type;
|
||||
quint64 free_bytes;
|
||||
quint64 total_bytes;
|
||||
QString name; // Name given to the iDevice by the user.
|
||||
|
||||
// Extra information.
|
||||
QString colour;
|
||||
QString imei;
|
||||
QString hardware;
|
||||
bool password_protected;
|
||||
QString os_version;
|
||||
QString timezone;
|
||||
QString wifi_mac;
|
||||
QString bt_mac;
|
||||
};
|
||||
|
||||
virtual void Init();
|
||||
|
||||
static void EventCallback(const idevice_event_t *event, void *context);
|
||||
|
||||
void DeviceAddedCallback(const char *uuid);
|
||||
void DeviceRemovedCallback(const char *uuid);
|
||||
|
||||
DeviceInfo ReadDeviceInfo(const char *uuid);
|
||||
static QString UniqueId(const char *uuid);
|
||||
|
||||
template <typename T>
|
||||
T LockAndGetDeviceInfo(const QString &id, T DeviceInfo::*field);
|
||||
|
||||
private:
|
||||
QMutex mutex_;
|
||||
QMap<QString, DeviceInfo> devices_;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
T iLister::LockAndGetDeviceInfo(const QString &id, T DeviceInfo::*field) {
|
||||
QMutexLocker l(&mutex_);
|
||||
if (!devices_.contains(id))
|
||||
return T();
|
||||
|
||||
return devices_[id].*field;
|
||||
}
|
||||
|
||||
#endif
|
||||
248
src/device/imobiledeviceconnection.cpp
Normal file
248
src/device/imobiledeviceconnection.cpp
Normal file
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <plist/plist.h>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QUrl>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "imobiledeviceconnection.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
iMobileDeviceConnection::iMobileDeviceConnection(const QString &uuid) : device_(NULL), afc_(NULL), afc_port_(0) {
|
||||
|
||||
idevice_error_t err = idevice_new(&device_, uuid.toUtf8().constData());
|
||||
if (err != IDEVICE_E_SUCCESS) {
|
||||
qLog(Warning) << "idevice error:" << err;
|
||||
return;
|
||||
}
|
||||
|
||||
lockdownd_client_t lockdown;
|
||||
|
||||
QByteArray label_ascii = QCoreApplication::applicationName().toLatin1();
|
||||
const char *label = label_ascii.constData();
|
||||
lockdownd_error_t lockdown_err = lockdownd_client_new_with_handshake(device_, &lockdown, label);
|
||||
if (lockdown_err != LOCKDOWN_E_SUCCESS) {
|
||||
qLog(Warning) << "lockdown error:" << lockdown_err;
|
||||
return;
|
||||
}
|
||||
|
||||
lockdown_err = lockdownd_start_service(lockdown, "com.apple.afc", &afc_port_);
|
||||
if (lockdown_err != LOCKDOWN_E_SUCCESS) {
|
||||
qLog(Warning) << "lockdown error:" << lockdown_err;
|
||||
lockdownd_client_free(lockdown);
|
||||
return;
|
||||
}
|
||||
|
||||
afc_error_t afc_err = afc_client_new(device_, afc_port_, &afc_);
|
||||
if (afc_err != 0) {
|
||||
qLog(Warning) << "afc error:" << afc_err;
|
||||
lockdownd_client_free(lockdown);
|
||||
return;
|
||||
}
|
||||
|
||||
lockdownd_client_free(lockdown);
|
||||
|
||||
}
|
||||
|
||||
iMobileDeviceConnection::~iMobileDeviceConnection() {
|
||||
|
||||
if (afc_) {
|
||||
afc_client_free(afc_);
|
||||
}
|
||||
if (device_) {
|
||||
idevice_free(device_);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template <typename T, typename F>
|
||||
T GetPListValue(plist_t node, F f) {
|
||||
T ret;
|
||||
f(node, &ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
QVariant iMobileDeviceConnection::GetProperty(const QString &property, const QString &domain) {
|
||||
|
||||
lockdownd_client_t lockdown;
|
||||
QByteArray label_ascii = QCoreApplication::applicationName().toLatin1();
|
||||
const char *label = label_ascii.constData();
|
||||
|
||||
lockdownd_error_t lockdown_err = lockdownd_client_new_with_handshake(device_, &lockdown, label);
|
||||
if (lockdown_err != LOCKDOWN_E_SUCCESS) {
|
||||
qLog(Warning) << "lockdown error:" << lockdown_err;
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
plist_t node = NULL;
|
||||
QByteArray domain_ascii = domain.toLatin1();
|
||||
const char *d = domain_ascii.isEmpty() ? NULL : domain_ascii.constData();
|
||||
//const char *d = domain.isEmpty() ? NULL : "com.apple.disk_usage";
|
||||
lockdownd_get_value(lockdown, d, property.toLatin1().constData(), &node);
|
||||
lockdownd_client_free(lockdown);
|
||||
|
||||
if (!node) {
|
||||
qLog(Warning) << "get_value failed" << property << domain;
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
switch (plist_get_node_type(node)) {
|
||||
case PLIST_BOOLEAN:
|
||||
return bool(GetPListValue<quint8>(node, plist_get_bool_val));
|
||||
|
||||
case PLIST_UINT:
|
||||
return QVariant::fromValue(GetPListValue<uint64_t>(node, plist_get_uint_val));
|
||||
|
||||
case PLIST_STRING: {
|
||||
char *data = GetPListValue<char*>(node, plist_get_string_val);
|
||||
QString ret = QString::fromUtf8(data);
|
||||
free(data);
|
||||
return ret;
|
||||
}
|
||||
|
||||
default:
|
||||
qLog(Warning) << "Unhandled PList type";
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QStringList iMobileDeviceConnection::ReadDirectory(const QString &path, QDir::Filters filters) {
|
||||
|
||||
char **list = NULL;
|
||||
afc_error_t err = afc_read_directory(afc_, path.toUtf8().constData(), &list);
|
||||
if (err != AFC_E_SUCCESS || !list) {
|
||||
return QStringList();
|
||||
}
|
||||
|
||||
QStringList ret;
|
||||
for (char **p = list ; *p != NULL ; ++p) {
|
||||
QString filename = QString::fromUtf8(*p);
|
||||
free(*p);
|
||||
|
||||
if (filters == QDir::NoFilter)
|
||||
ret << filename;
|
||||
else {
|
||||
if (filters & QDir::NoDotAndDotDot && (filename == "." || filename == ".."))
|
||||
continue;
|
||||
if (!(filters & QDir::Hidden) && filename.startsWith("."))
|
||||
continue;
|
||||
|
||||
QString filetype = GetFileInfo(path + "/" + filename, "st_ifmt");
|
||||
if ((filetype == "S_IFREG" && (filters & QDir::Files)) || (filetype == "S_IFDIR" && (filters & QDir::Dirs)) || (filetype == "S_IFLNK" && (!(filters & QDir::NoSymLinks))))
|
||||
ret << filename;
|
||||
}
|
||||
}
|
||||
free(list);
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
bool iMobileDeviceConnection::MkDir(const QString &path) {
|
||||
afc_error_t err = afc_make_directory(afc_, path.toUtf8().constData());
|
||||
return err == AFC_E_SUCCESS;
|
||||
}
|
||||
|
||||
QString iMobileDeviceConnection::GetFileInfo(const QString &path, const QString &key) {
|
||||
|
||||
QString ret;
|
||||
char **infolist = NULL;
|
||||
afc_error_t err = afc_get_file_info(afc_, path.toUtf8().constData(), &infolist);
|
||||
if (err != AFC_E_SUCCESS || !infolist) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
QString last_key;
|
||||
for (char **p = infolist ; *p != NULL ; ++p) {
|
||||
if (last_key.isNull()) {
|
||||
last_key = QString::fromUtf8(*p);
|
||||
}
|
||||
else {
|
||||
if (last_key == key)
|
||||
ret = QString::fromUtf8(*p);
|
||||
last_key = QString();
|
||||
}
|
||||
free(*p);
|
||||
}
|
||||
free(infolist);
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
bool iMobileDeviceConnection::Exists(const QString &path) {
|
||||
return !GetFileInfo(path, "st_ifmt").isNull();
|
||||
}
|
||||
|
||||
QString iMobileDeviceConnection::GetUnusedFilename(Itdb_iTunesDB *itdb, const Song &metadata) {
|
||||
|
||||
// This function does the same as itdb_cp_get_dest_filename, except it
|
||||
// accesses the device's filesystem through imobiledevice.
|
||||
|
||||
// Get the total number of F.. directories
|
||||
int total_musicdirs = 0;
|
||||
for ( ; ; ++total_musicdirs) {
|
||||
QString dir;
|
||||
dir.sprintf("/iTunes_Control/Music/F%02d", total_musicdirs);
|
||||
|
||||
if (!Exists(dir))
|
||||
break;
|
||||
}
|
||||
|
||||
if (total_musicdirs <= 0) {
|
||||
qLog(Warning) << "No 'F..'' directories found on iPod";
|
||||
return QString();
|
||||
}
|
||||
|
||||
// Pick one at random
|
||||
const int dir_num = qrand() % total_musicdirs;
|
||||
QString dir;
|
||||
dir.sprintf("/iTunes_Control/Music/F%02d", dir_num);
|
||||
|
||||
if (!Exists(dir)) {
|
||||
qLog(Warning) << "Music directory doesn't exist:" << dir;
|
||||
return QString();
|
||||
}
|
||||
|
||||
// Use the same file extension as the original file, default to mp3.
|
||||
QString extension = metadata.url().path().section('.', -1, -1).toLower();
|
||||
if (extension.isEmpty())
|
||||
extension = "mp3";
|
||||
|
||||
// Loop until we find an unused filename.
|
||||
// Use the same naming convention as libgpod, which is
|
||||
// "libgpod" + 6-digit random number
|
||||
static const int kRandMax = 999999;
|
||||
QString filename;
|
||||
forever {
|
||||
filename.sprintf("libgpod%06d", qrand() % kRandMax);
|
||||
filename += "." + extension;
|
||||
|
||||
if (!Exists(dir + "/" + filename))
|
||||
break;
|
||||
}
|
||||
|
||||
return dir + "/" + filename;
|
||||
|
||||
}
|
||||
63
src/device/imobiledeviceconnection.h
Normal file
63
src/device/imobiledeviceconnection.h
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef IMOBILEDEVICECONNECTION_H
|
||||
#define IMOBILEDEVICECONNECTION_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <libimobiledevice/afc.h>
|
||||
#include <libimobiledevice/libimobiledevice.h>
|
||||
#include <libimobiledevice/lockdown.h>
|
||||
|
||||
#include <gpod/itdb.h>
|
||||
|
||||
#include <QDir>
|
||||
#include <QStringList>
|
||||
#include <QVariant>
|
||||
|
||||
#include "core/song.h"
|
||||
|
||||
class iMobileDeviceConnection {
|
||||
public:
|
||||
iMobileDeviceConnection(const QString &uuid);
|
||||
~iMobileDeviceConnection();
|
||||
|
||||
afc_client_t afc() { return afc_; }
|
||||
|
||||
QVariant GetProperty(const QString &property, const QString &domain = QString());
|
||||
QStringList ReadDirectory(const QString &path, QDir::Filters filters = QDir::NoFilter);
|
||||
bool MkDir(const QString &path);
|
||||
|
||||
QString GetFileInfo(const QString &path, const QString &key);
|
||||
bool Exists(const QString &path);
|
||||
|
||||
QString GetUnusedFilename(Itdb_iTunesDB *itdb, const Song &metadata);
|
||||
|
||||
private:
|
||||
Q_DISABLE_COPY(iMobileDeviceConnection);
|
||||
|
||||
idevice_t device_;
|
||||
afc_client_t afc_;
|
||||
|
||||
uint16_t afc_port_;
|
||||
};
|
||||
|
||||
#endif // IMOBILEDEVICECONNECTION_H
|
||||
89
src/device/macdevicelister.h
Normal file
89
src/device/macdevicelister.h
Normal file
@@ -0,0 +1,89 @@
|
||||
#ifndef MACDEVICELISTER_H
|
||||
#define MACDEVICELISTER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QMutex>
|
||||
#include <QSet>
|
||||
#include <QThread>
|
||||
|
||||
#include <DiskArbitration/DADisk.h>
|
||||
#include <DiskArbitration/DADissenter.h>
|
||||
#include <IOKit/IOKitLib.h>
|
||||
|
||||
#include "devicelister.h"
|
||||
|
||||
class MacDeviceLister : public DeviceLister {
|
||||
Q_OBJECT
|
||||
public:
|
||||
MacDeviceLister();
|
||||
~MacDeviceLister();
|
||||
|
||||
virtual QStringList DeviceUniqueIDs();
|
||||
virtual QVariantList DeviceIcons(const QString &id);
|
||||
virtual QString DeviceManufacturer(const QString &id);
|
||||
virtual QString DeviceModel(const QString &id);
|
||||
virtual quint64 DeviceCapacity(const QString &id);
|
||||
virtual quint64 DeviceFreeSpace(const QString &id);
|
||||
virtual QVariantMap DeviceHardwareInfo(const QString &id);
|
||||
virtual bool AskForScan(const QString &serial) const;
|
||||
virtual QString MakeFriendlyName(const QString &id);
|
||||
virtual QList<QUrl> MakeDeviceUrls(const QString &id);
|
||||
|
||||
virtual void UnmountDevice(const QString &id);
|
||||
virtual void UpdateDeviceFreeSpace(const QString &id);
|
||||
|
||||
struct MTPDevice {
|
||||
MTPDevice() : capacity(0), free_space(0) {}
|
||||
QString vendor;
|
||||
QString product;
|
||||
quint16 vendor_id;
|
||||
quint16 product_id;
|
||||
|
||||
int quirks;
|
||||
int bus;
|
||||
int address;
|
||||
|
||||
quint64 capacity;
|
||||
quint64 free_space;
|
||||
};
|
||||
|
||||
public slots:
|
||||
virtual void ShutDown();
|
||||
|
||||
private:
|
||||
virtual void Init();
|
||||
|
||||
static void DiskAddedCallback(DADiskRef disk, void* context);
|
||||
static void DiskRemovedCallback(DADiskRef disk, void* context);
|
||||
static void USBDeviceAddedCallback(void* refcon, io_iterator_t it);
|
||||
static void USBDeviceRemovedCallback(void* refcon, io_iterator_t it);
|
||||
|
||||
static void DiskUnmountCallback(DADiskRef disk, DADissenterRef dissenter, void* context);
|
||||
|
||||
void FoundMTPDevice(const MTPDevice& device, const QString& serial);
|
||||
void RemovedMTPDevice(const QString& serial);
|
||||
|
||||
quint64 GetFreeSpace(const QUrl& url);
|
||||
quint64 GetCapacity(const QUrl& url);
|
||||
|
||||
bool IsCDDevice(const QString& serial) const;
|
||||
|
||||
DASessionRef loop_session_;
|
||||
CFRunLoopRef run_loop_;
|
||||
|
||||
QMap<QString, QString> current_devices_;
|
||||
QMap<QString, MTPDevice> mtp_devices_;
|
||||
QSet<QString> cd_devices_;
|
||||
|
||||
QMutex libmtp_mutex_;
|
||||
|
||||
static QSet<MTPDevice> sMTPDeviceList;
|
||||
};
|
||||
|
||||
uint qHash(const MacDeviceLister::MTPDevice& device);
|
||||
inline bool operator==(const MacDeviceLister::MTPDevice& a, const MacDeviceLister::MTPDevice& b) {
|
||||
return (a.vendor_id == b.vendor_id) && (a.product_id == b.product_id);
|
||||
}
|
||||
|
||||
#endif
|
||||
823
src/device/macdevicelister.mm
Normal file
823
src/device/macdevicelister.mm
Normal file
@@ -0,0 +1,823 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <boost/scope_exit.hpp>
|
||||
|
||||
#include <libmtp.h>
|
||||
|
||||
#include <CoreFoundation/CFRunLoop.h>
|
||||
#include <DiskArbitration/DiskArbitration.h>
|
||||
#include <IOKit/kext/KextManager.h>
|
||||
#include <IOKit/IOCFPlugin.h>
|
||||
#include <IOKit/usb/IOUSBLib.h>
|
||||
#include <IOKit/storage/IOMedia.h>
|
||||
#include <IOKit/storage/IOCDMedia.h>
|
||||
|
||||
#include <QtDebug>
|
||||
#include <QMutex>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include "config.h"
|
||||
#include "macdevicelister.h"
|
||||
#include "mtpconnection.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/scoped_cftyperef.h"
|
||||
#include "core/scoped_nsautorelease_pool.h"
|
||||
#include "core/scoped_nsobject.h"
|
||||
|
||||
#import <AppKit/NSWorkspace.h>
|
||||
#import <Foundation/NSDictionary.h>
|
||||
#import <Foundation/NSNotification.h>
|
||||
#import <Foundation/NSPathUtilities.h>
|
||||
#import <Foundation/NSString.h>
|
||||
#import <Foundation/NSURL.h>
|
||||
|
||||
#ifndef kUSBSerialNumberString
|
||||
#define kUSBSerialNumberString "USB Serial Number"
|
||||
#endif
|
||||
|
||||
#ifndef kUSBVendorString
|
||||
#define kUSBVendorString "USB Vendor Name"
|
||||
#endif
|
||||
|
||||
#ifndef kUSBProductString
|
||||
#define kUSBProductString "USB Product Name"
|
||||
#endif
|
||||
|
||||
// io_object_t, io_service_t, io_iterator_t etc. are all typedef'd to unsigned int,
|
||||
// hence the lack of templating here.
|
||||
class ScopedIOObject {
|
||||
public:
|
||||
explicit ScopedIOObject(io_object_t object = 0) : object_(object) {}
|
||||
|
||||
~ScopedIOObject() {
|
||||
if (object_) IOObjectRelease(object_);
|
||||
}
|
||||
|
||||
io_object_t get() const { return object_; }
|
||||
|
||||
private:
|
||||
io_object_t object_;
|
||||
|
||||
Q_DISABLE_COPY(ScopedIOObject);
|
||||
};
|
||||
|
||||
// Helpful MTP & USB links:
|
||||
// Apple USB device interface guide:
|
||||
// http://developer.apple.com/mac/collection/documentation/DeviceDrivers/Conceptual/USBBook/USBDeviceInterfaces/USBDevInterfaces.html
|
||||
// Example Apple code for requesting a USB device descriptor:
|
||||
// http://www.opensource.apple.com/source/IOUSBFamily/IOUSBFamily-208.4.5/USBProber/BusProbeClass.m
|
||||
// Libmtp's detection code:
|
||||
// http://libmtp.cvs.sourceforge.net/viewvc/libmtp/libmtp/src/libusb-glue.c?view=markup
|
||||
// Libusb's Mac code:
|
||||
// http://www.libusb.org/browser/libusb/libusb/os/darwin_usb.c
|
||||
// Microsoft OS Descriptors:
|
||||
// http://www.microsoft.com/whdc/connect/usb/os_desc.mspx
|
||||
// Symbian docs for implementing the device side:
|
||||
// http://developer.symbian.org/main/documentation/reference/s3/pdk/GUID-3FF0F248-EDF0-5348-BC43-869CE1B5B415.html
|
||||
// Libgphoto2 MTP detection code:
|
||||
// http://www.sfr-fresh.com/unix/privat/libgphoto2-2.4.10.1.tar.gz:a/libgphoto2-2.4.10.1/libgphoto2_port/usb/check-mtp-device.c
|
||||
|
||||
QSet<MacDeviceLister::MTPDevice> MacDeviceLister::sMTPDeviceList;
|
||||
|
||||
uint qHash(const MacDeviceLister::MTPDevice& d) {
|
||||
return qHash(d.vendor_id) ^ qHash(d.product_id);
|
||||
}
|
||||
|
||||
MacDeviceLister::MacDeviceLister() {}
|
||||
|
||||
MacDeviceLister::~MacDeviceLister() { CFRelease(loop_session_); }
|
||||
|
||||
void MacDeviceLister::Init() {
|
||||
ScopedNSAutoreleasePool pool;
|
||||
|
||||
// Populate MTP Device list.
|
||||
if (sMTPDeviceList.empty()) {
|
||||
LIBMTP_device_entry_t* devices = nullptr;
|
||||
int num = 0;
|
||||
if (LIBMTP_Get_Supported_Devices_List(&devices, &num) != 0) {
|
||||
qLog(Warning) << "Failed to get MTP device list";
|
||||
}
|
||||
else {
|
||||
for (int i = 0; i < num; ++i) {
|
||||
LIBMTP_device_entry_t device = devices[i];
|
||||
MTPDevice d;
|
||||
d.vendor = QString::fromAscii(device.vendor);
|
||||
d.vendor_id = device.vendor_id;
|
||||
d.product = QString::fromAscii(device.product);
|
||||
d.product_id = device.product_id;
|
||||
d.quirks = device.device_flags;
|
||||
sMTPDeviceList << d;
|
||||
}
|
||||
}
|
||||
|
||||
MTPDevice d;
|
||||
d.vendor = "SanDisk";
|
||||
d.vendor_id = 0x781;
|
||||
d.product = "Sansa Clip+";
|
||||
d.product_id = 0x74d0;
|
||||
|
||||
d.quirks = 0x2 | 0x4 | 0x40 | 0x4000;
|
||||
sMTPDeviceList << d;
|
||||
}
|
||||
|
||||
run_loop_ = CFRunLoopGetCurrent();
|
||||
|
||||
// Register for disk mounts/unmounts.
|
||||
loop_session_ = DASessionCreate(kCFAllocatorDefault);
|
||||
DARegisterDiskAppearedCallback(loop_session_, kDADiskDescriptionMatchVolumeMountable, &DiskAddedCallback, reinterpret_cast<void*>(this));
|
||||
DARegisterDiskDisappearedCallback(loop_session_, nullptr, &DiskRemovedCallback, reinterpret_cast<void*>(this));
|
||||
DASessionScheduleWithRunLoop(loop_session_, run_loop_, kCFRunLoopDefaultMode);
|
||||
|
||||
// Register for USB device connection/disconnection.
|
||||
IONotificationPortRef notification_port = IONotificationPortCreate(kIOMasterPortDefault);
|
||||
CFMutableDictionaryRef matching_dict = IOServiceMatching(kIOUSBDeviceClassName);
|
||||
// IOServiceAddMatchingNotification decreases reference count.
|
||||
CFRetain(matching_dict);
|
||||
io_iterator_t it;
|
||||
kern_return_t err = IOServiceAddMatchingNotification(
|
||||
notification_port,
|
||||
kIOFirstMatchNotification,
|
||||
matching_dict,
|
||||
&USBDeviceAddedCallback,
|
||||
reinterpret_cast<void*>(this),
|
||||
&it);
|
||||
if (err == KERN_SUCCESS) {
|
||||
USBDeviceAddedCallback(this, it);
|
||||
} else {
|
||||
qLog(Warning) << "Could not add notification on USB device connection";
|
||||
}
|
||||
|
||||
err = IOServiceAddMatchingNotification(
|
||||
notification_port,
|
||||
kIOTerminatedNotification,
|
||||
matching_dict,
|
||||
&USBDeviceRemovedCallback,
|
||||
reinterpret_cast<void*>(this),
|
||||
&it);
|
||||
if (err == KERN_SUCCESS) {
|
||||
USBDeviceRemovedCallback(this, it);
|
||||
} else {
|
||||
qLog(Warning) << "Could not add notification USB device removal";
|
||||
}
|
||||
|
||||
CFRunLoopSourceRef io_source = IONotificationPortGetRunLoopSource(notification_port);
|
||||
CFRunLoopAddSource(run_loop_, io_source, kCFRunLoopDefaultMode);
|
||||
|
||||
CFRunLoopRun();
|
||||
}
|
||||
|
||||
void MacDeviceLister::ShutDown() { CFRunLoopStop(run_loop_); }
|
||||
|
||||
// IOKit helpers.
|
||||
namespace {
|
||||
|
||||
// Caller is responsible for calling CFRelease().
|
||||
CFTypeRef GetUSBRegistryEntry(io_object_t device, CFStringRef key) {
|
||||
io_iterator_t it;
|
||||
if (IORegistryEntryGetParentIterator(device, kIOServicePlane, &it) == KERN_SUCCESS) {
|
||||
io_object_t next;
|
||||
while ((next = IOIteratorNext(it))) {
|
||||
CFTypeRef registry_entry = (CFStringRef)IORegistryEntryCreateCFProperty(
|
||||
next, key, kCFAllocatorDefault, 0);
|
||||
if (registry_entry) {
|
||||
IOObjectRelease(next);
|
||||
IOObjectRelease(it);
|
||||
return registry_entry;
|
||||
}
|
||||
|
||||
CFTypeRef ret = GetUSBRegistryEntry(next, key);
|
||||
if (ret) {
|
||||
IOObjectRelease(next);
|
||||
IOObjectRelease(it);
|
||||
return ret;
|
||||
}
|
||||
|
||||
IOObjectRelease(next);
|
||||
}
|
||||
}
|
||||
|
||||
IOObjectRelease(it);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QString GetUSBRegistryEntryString(io_object_t device, CFStringRef key) {
|
||||
ScopedCFTypeRef<CFStringRef> registry_string((CFStringRef)GetUSBRegistryEntry(device, key));
|
||||
if (registry_string) {
|
||||
return QString::fromUtf8([(NSString*)registry_string.get() UTF8String]);
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
NSObject* GetPropertyForDevice(io_object_t device, CFStringRef key) {
|
||||
CFMutableDictionaryRef properties;
|
||||
kern_return_t ret = IORegistryEntryCreateCFProperties(device, &properties, kCFAllocatorDefault, 0);
|
||||
|
||||
if (ret != KERN_SUCCESS) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
scoped_nsobject<NSDictionary> dict((NSDictionary*)properties); // Takes ownership.
|
||||
NSObject* prop = [dict objectForKey:(NSString*)key];
|
||||
if (prop) {
|
||||
// The dictionary goes out of scope so we should retain this object.
|
||||
[prop retain];
|
||||
return prop;
|
||||
}
|
||||
|
||||
io_object_t parent;
|
||||
ret = IORegistryEntryGetParentEntry(device, kIOServicePlane, &parent);
|
||||
if (ret == KERN_SUCCESS) {
|
||||
return GetPropertyForDevice(parent, key);
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
int GetUSBDeviceClass(io_object_t device) {
|
||||
ScopedCFTypeRef<CFTypeRef> interface_class(IORegistryEntrySearchCFProperty(
|
||||
device,
|
||||
kIOServicePlane,
|
||||
CFSTR(kUSBInterfaceClass),
|
||||
kCFAllocatorDefault,
|
||||
kIORegistryIterateRecursively));
|
||||
NSNumber* number = (NSNumber*)interface_class.get();
|
||||
if (number) {
|
||||
int ret = [number unsignedShortValue];
|
||||
return ret;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
QString GetIconForDevice(io_object_t device) {
|
||||
scoped_nsobject<NSDictionary> media_icon((NSDictionary*)GetPropertyForDevice(device, CFSTR("IOMediaIcon")));
|
||||
if (media_icon) {
|
||||
NSString* bundle = (NSString*)[media_icon objectForKey:@"CFBundleIdentifier"];
|
||||
NSString* file = (NSString*)[media_icon objectForKey:@"IOBundleResourceFile"];
|
||||
|
||||
scoped_nsobject<NSURL> bundle_url((NSURL*)KextManagerCreateURLForBundleIdentifier(kCFAllocatorDefault, (CFStringRef)bundle));
|
||||
|
||||
QString path = QString::fromUtf8([[bundle_url path] UTF8String]);
|
||||
path += "/Contents/Resources/";
|
||||
path += QString::fromUtf8([file UTF8String]);
|
||||
return path;
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString GetSerialForDevice(io_object_t device) {
|
||||
QString serial = GetUSBRegistryEntryString(device, CFSTR(kUSBSerialNumberString));
|
||||
if (!serial.isEmpty()) {
|
||||
return "USB/" + serial;
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString GetSerialForMTPDevice(io_object_t device) {
|
||||
scoped_nsobject<NSString> serial((NSString*) GetPropertyForDevice(device, CFSTR(kUSBSerialNumberString)));
|
||||
return QString(QString("MTP/") + QString::fromUtf8([serial UTF8String]));
|
||||
}
|
||||
|
||||
QString FindDeviceProperty(const QString& bsd_name, CFStringRef property) {
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(
|
||||
kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
|
||||
ScopedIOObject device(DADiskCopyIOMedia(disk.get()));
|
||||
QString ret = GetUSBRegistryEntryString(device.get(), property);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
quint64 MacDeviceLister::GetFreeSpace(const QUrl& url) {
|
||||
QMutexLocker l(&libmtp_mutex_);
|
||||
MtpConnection connection(url);
|
||||
if (!connection.is_valid()) {
|
||||
qLog(Warning) << "Error connecting to MTP device, couldn't get device free space";
|
||||
return -1;
|
||||
}
|
||||
LIBMTP_devicestorage_t* storage = connection.device()->storage;
|
||||
quint64 free_bytes = 0;
|
||||
while (storage) {
|
||||
free_bytes += storage->FreeSpaceInBytes;
|
||||
storage = storage->next;
|
||||
}
|
||||
return free_bytes;
|
||||
}
|
||||
|
||||
quint64 MacDeviceLister::GetCapacity(const QUrl& url) {
|
||||
QMutexLocker l(&libmtp_mutex_);
|
||||
MtpConnection connection(url);
|
||||
if (!connection.is_valid()) {
|
||||
qLog(Warning) << "Error connecting to MTP device, couldn't get device capacity";
|
||||
return -1;
|
||||
}
|
||||
LIBMTP_devicestorage_t* storage = connection.device()->storage;
|
||||
quint64 capacity_bytes = 0;
|
||||
while (storage) {
|
||||
capacity_bytes += storage->MaxCapacity;
|
||||
storage = storage->next;
|
||||
}
|
||||
return capacity_bytes;
|
||||
}
|
||||
|
||||
void MacDeviceLister::DiskAddedCallback(DADiskRef disk, void* context) {
|
||||
MacDeviceLister* me = reinterpret_cast<MacDeviceLister*>(context);
|
||||
|
||||
scoped_nsobject<NSDictionary> properties((NSDictionary*)DADiskCopyDescription(disk));
|
||||
|
||||
NSString* kind = [properties objectForKey:(NSString*)kDADiskDescriptionMediaKindKey];
|
||||
#ifdef HAVE_AUDIOCD
|
||||
if (kind && strcmp([kind UTF8String], kIOCDMediaClass) == 0) {
|
||||
// CD inserted.
|
||||
QString bsd_name = QString::fromAscii(DADiskGetBSDName(disk));
|
||||
me->cd_devices_ << bsd_name;
|
||||
emit me->DeviceAdded(bsd_name);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
NSURL* volume_path =
|
||||
[[properties objectForKey:(NSString*)kDADiskDescriptionVolumePathKey] copy];
|
||||
|
||||
if (volume_path) {
|
||||
ScopedIOObject device(DADiskCopyIOMedia(disk));
|
||||
ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(device.get()));
|
||||
if (class_name && CFStringCompare(class_name.get(), CFSTR(kIOMediaClass), 0) == kCFCompareEqualTo) {
|
||||
QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
|
||||
QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
|
||||
|
||||
CFMutableDictionaryRef cf_properties;
|
||||
kern_return_t ret = IORegistryEntryCreateCFProperties(
|
||||
device.get(), &cf_properties, kCFAllocatorDefault, 0);
|
||||
|
||||
if (ret == KERN_SUCCESS) {
|
||||
scoped_nsobject<NSDictionary> dict((NSDictionary*)cf_properties); // Takes ownership.
|
||||
if ([[dict objectForKey:@"Removable"] intValue] == 1) {
|
||||
QString serial = GetSerialForDevice(device.get());
|
||||
if (!serial.isEmpty()) {
|
||||
me->current_devices_[serial] = QString(DADiskGetBSDName(disk));
|
||||
emit me->DeviceAdded(serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MacDeviceLister::DiskRemovedCallback(DADiskRef disk, void* context) {
|
||||
MacDeviceLister* me = reinterpret_cast<MacDeviceLister*>(context);
|
||||
// We cannot access the USB tree when the disk is removed but we still get
|
||||
// the BSD disk name.
|
||||
|
||||
QString bsd_name = QString::fromAscii(DADiskGetBSDName(disk));
|
||||
if (me->cd_devices_.remove(bsd_name)) {
|
||||
emit me->DeviceRemoved(bsd_name);
|
||||
return;
|
||||
}
|
||||
|
||||
for (QMap<QString, QString>::iterator it = me->current_devices_.begin();
|
||||
it != me->current_devices_.end(); ++it) {
|
||||
if (it.value() == bsd_name) {
|
||||
emit me->DeviceRemoved(it.key());
|
||||
me->current_devices_.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool DeviceRequest(IOUSBDeviceInterface** dev,
|
||||
quint8 direction,
|
||||
quint8 type,
|
||||
quint8 recipient,
|
||||
quint8 request_code,
|
||||
quint16 value,
|
||||
quint16 index,
|
||||
quint16 length,
|
||||
QByteArray* data) {
|
||||
IOUSBDevRequest req;
|
||||
req.bmRequestType = USBmakebmRequestType(direction, type, recipient);
|
||||
req.bRequest = request_code;
|
||||
req.wValue = value;
|
||||
req.wIndex = index;
|
||||
req.wLength = length;
|
||||
data->resize(256);
|
||||
req.pData = data->data();
|
||||
kern_return_t err = (*dev)->DeviceRequest(dev, &req);
|
||||
if (err != kIOReturnSuccess) {
|
||||
return false;
|
||||
}
|
||||
data->resize(req.wLenDone);
|
||||
return true;
|
||||
}
|
||||
|
||||
int GetBusNumber(io_object_t o) {
|
||||
io_iterator_t it;
|
||||
kern_return_t err = IORegistryEntryGetParentIterator(o, kIOServicePlane, &it);
|
||||
if (err != KERN_SUCCESS) {
|
||||
return -1;
|
||||
}
|
||||
while ((o = IOIteratorNext(it))) {
|
||||
NSObject* bus = GetPropertyForDevice(o, CFSTR("USBBusNumber"));
|
||||
if (bus) {
|
||||
NSNumber* bus_num = (NSNumber*)bus;
|
||||
return [bus_num intValue];
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void MacDeviceLister::USBDeviceAddedCallback(void* refcon, io_iterator_t it) {
|
||||
MacDeviceLister* me = reinterpret_cast<MacDeviceLister*>(refcon);
|
||||
|
||||
io_object_t object;
|
||||
while ((object = IOIteratorNext(it))) {
|
||||
ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(object));
|
||||
BOOST_SCOPE_EXIT((object)) {
|
||||
IOObjectRelease(object);
|
||||
} BOOST_SCOPE_EXIT_END
|
||||
|
||||
if (CFStringCompare(class_name.get(), CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) {
|
||||
NSString* vendor = (NSString*)GetPropertyForDevice(object, CFSTR(kUSBVendorString));
|
||||
NSString* product = (NSString*)GetPropertyForDevice(object, CFSTR(kUSBProductString));
|
||||
NSNumber* vendor_id = (NSNumber*)GetPropertyForDevice(object, CFSTR(kUSBVendorID));
|
||||
NSNumber* product_id = (NSNumber*)GetPropertyForDevice(object, CFSTR(kUSBProductID));
|
||||
int interface_class = GetUSBDeviceClass(object);
|
||||
qLog(Debug) << "Interface class:" << interface_class;
|
||||
|
||||
QString serial = GetSerialForMTPDevice(object);
|
||||
|
||||
MTPDevice device;
|
||||
device.vendor = QString::fromUtf8([vendor UTF8String]);
|
||||
device.product = QString::fromUtf8([product UTF8String]);
|
||||
device.vendor_id = [vendor_id unsignedShortValue];
|
||||
device.product_id = [product_id unsignedShortValue];
|
||||
device.quirks = 0;
|
||||
|
||||
device.bus = -1;
|
||||
device.address = -1;
|
||||
|
||||
if (device.vendor_id == kAppleVendorID || // I think we can safely skip Apple products.
|
||||
// Blacklist ilok2 as this probe may be breaking it.
|
||||
(device.vendor_id == 0x088e && device.product_id == 0x5036) ||
|
||||
// Blacklist eLicenser
|
||||
(device.vendor_id == 0x0819 && device.product_id == 0x0101) ||
|
||||
// Skip HID devices, printers and hubs.
|
||||
interface_class == kUSBHIDInterfaceClass ||
|
||||
interface_class == kUSBPrintingInterfaceClass ||
|
||||
interface_class == kUSBHubClass) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NSNumber* addr = (NSNumber*)GetPropertyForDevice(object, CFSTR("USB Address"));
|
||||
int bus = GetBusNumber(object);
|
||||
if (!addr || bus == -1) {
|
||||
// Failed to get bus or address number.
|
||||
continue;
|
||||
}
|
||||
device.bus = bus;
|
||||
device.address = [addr intValue];
|
||||
|
||||
// First check the libmtp device list.
|
||||
QSet<MTPDevice>::const_iterator it = sMTPDeviceList.find(device);
|
||||
if (it != sMTPDeviceList.end()) {
|
||||
// Fill in quirks flags from libmtp.
|
||||
device.quirks = it->quirks;
|
||||
me->FoundMTPDevice(device, GetSerialForMTPDevice(object));
|
||||
continue;
|
||||
}
|
||||
|
||||
IOCFPlugInInterface** plugin_interface = nullptr;
|
||||
SInt32 score;
|
||||
kern_return_t err = IOCreatePlugInInterfaceForService(
|
||||
object,
|
||||
kIOUSBDeviceUserClientTypeID,
|
||||
kIOCFPlugInInterfaceID,
|
||||
&plugin_interface,
|
||||
&score);
|
||||
if (err != KERN_SUCCESS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IOUSBDeviceInterface** dev = nullptr;
|
||||
HRESULT result = (*plugin_interface)->QueryInterface(plugin_interface, CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID), (LPVOID*)&dev);
|
||||
|
||||
(*plugin_interface)->Release(plugin_interface);
|
||||
|
||||
if (result || !dev) {
|
||||
continue;
|
||||
}
|
||||
|
||||
err = (*dev)->USBDeviceOpen(dev);
|
||||
if (err != kIOReturnSuccess) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Automatically close & release usb device at scope exit.
|
||||
BOOST_SCOPE_EXIT((dev)) {
|
||||
(*dev)->USBDeviceClose(dev);
|
||||
(*dev)->Release(dev);
|
||||
} BOOST_SCOPE_EXIT_END
|
||||
|
||||
// Request the string descriptor at 0xee.
|
||||
// This is a magic string that indicates whether this device supports MTP.
|
||||
QByteArray data;
|
||||
bool ret = DeviceRequest(dev, kUSBIn, kUSBStandard, kUSBDevice, kUSBRqGetDescriptor, (kUSBStringDesc << 8) | 0xee, 0x0409, 2, &data);
|
||||
if (!ret) continue;
|
||||
|
||||
UInt8 string_len = data[0];
|
||||
|
||||
ret = DeviceRequest(dev, kUSBIn, kUSBStandard, kUSBDevice, kUSBRqGetDescriptor, (kUSBStringDesc << 8) | 0xee, 0x0409, string_len, &data);
|
||||
if (!ret) continue;
|
||||
|
||||
// The device actually returned something. That's a good sign.
|
||||
// Because this was designed by MS, the characters are in UTF-16 (LE?).
|
||||
QString str = QString::fromUtf16(reinterpret_cast<ushort*>(data.data() + 2), (data.size() / 2) - 2);
|
||||
|
||||
if (str.startsWith("MSFT100")) {
|
||||
// We got the OS descriptor!
|
||||
char vendor_code = data[16];
|
||||
ret = DeviceRequest(dev, kUSBIn, kUSBVendor, kUSBDevice, vendor_code, 0, 4, 256, &data);
|
||||
if (!ret || data.at(0) != 0x28)
|
||||
continue;
|
||||
|
||||
if (QString::fromAscii(data.data() + 0x12, 3) != "MTP") {
|
||||
// Not quite.
|
||||
continue;
|
||||
}
|
||||
|
||||
ret = DeviceRequest(dev, kUSBIn, kUSBVendor, kUSBDevice, vendor_code, 0, 5, 256, &data);
|
||||
if (!ret || data.at(0) != 0x28) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (QString::fromAscii(data.data() + 0x12, 3) != "MTP") {
|
||||
// Not quite.
|
||||
continue;
|
||||
}
|
||||
// Hurray! We made it!
|
||||
me->FoundMTPDevice(device, serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MacDeviceLister::USBDeviceRemovedCallback(void* refcon, io_iterator_t it) {
|
||||
MacDeviceLister* me = reinterpret_cast<MacDeviceLister*>(refcon);
|
||||
io_object_t object;
|
||||
while ((object = IOIteratorNext(it))) {
|
||||
ScopedCFTypeRef<CFStringRef> class_name(IOObjectCopyClass(object));
|
||||
BOOST_SCOPE_EXIT((object)) { IOObjectRelease(object); }
|
||||
BOOST_SCOPE_EXIT_END
|
||||
|
||||
if (CFStringCompare(class_name.get(), CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) {
|
||||
NSString* vendor = (NSString*)GetPropertyForDevice(object, CFSTR(kUSBVendorString));
|
||||
NSString* product = (NSString*)GetPropertyForDevice(object, CFSTR(kUSBProductString));
|
||||
NSNumber* vendor_id = (NSNumber*)GetPropertyForDevice(object, CFSTR(kUSBVendorID));
|
||||
NSNumber* product_id = (NSNumber*)GetPropertyForDevice(object, CFSTR(kUSBProductID));
|
||||
QString serial = GetSerialForMTPDevice(object);
|
||||
|
||||
MTPDevice device;
|
||||
device.vendor = QString::fromUtf8([vendor UTF8String]);
|
||||
device.product = QString::fromUtf8([product UTF8String]);
|
||||
device.vendor_id = [vendor_id unsignedShortValue];
|
||||
device.product_id = [product_id unsignedShortValue];
|
||||
|
||||
me->RemovedMTPDevice(serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MacDeviceLister::RemovedMTPDevice(const QString& serial) {
|
||||
int count = mtp_devices_.remove(serial);
|
||||
if (count) {
|
||||
qLog(Debug) << "MTP device removed:" << serial;
|
||||
emit DeviceRemoved(serial);
|
||||
}
|
||||
}
|
||||
|
||||
void MacDeviceLister::FoundMTPDevice(const MTPDevice& device, const QString& serial) {
|
||||
qLog(Debug) << "New MTP device detected!" << device.bus << device.address;
|
||||
mtp_devices_[serial] = device;
|
||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||
MTPDevice* d = &mtp_devices_[serial];
|
||||
d->capacity = GetCapacity(urls[0]);
|
||||
d->free_space = GetFreeSpace(urls[0]);
|
||||
emit DeviceAdded(serial);
|
||||
}
|
||||
|
||||
bool IsMTPSerial(const QString& serial) { return serial.startsWith("MTP"); }
|
||||
|
||||
bool MacDeviceLister::IsCDDevice(const QString& serial) const {
|
||||
return cd_devices_.contains(serial);
|
||||
}
|
||||
|
||||
QString MacDeviceLister::MakeFriendlyName(const QString& serial) {
|
||||
if (IsMTPSerial(serial)) {
|
||||
const MTPDevice& device = mtp_devices_[serial];
|
||||
if (device.vendor.isEmpty()) {
|
||||
return device.product;
|
||||
} else {
|
||||
return device.vendor + " " + device.product;
|
||||
}
|
||||
}
|
||||
|
||||
QString bsd_name = IsCDDevice(serial) ? *cd_devices_.find(serial) : current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
|
||||
if (IsCDDevice(serial)) {
|
||||
scoped_nsobject<NSDictionary> properties((NSDictionary*)DADiskCopyDescription(disk.get()));
|
||||
NSString* device_name = (NSString*)[properties.get() objectForKey:(NSString*)kDADiskDescriptionMediaNameKey];
|
||||
|
||||
return QString::fromUtf8([device_name UTF8String]);
|
||||
}
|
||||
|
||||
ScopedIOObject device(DADiskCopyIOMedia(disk));
|
||||
|
||||
QString vendor = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBVendorString));
|
||||
QString product = GetUSBRegistryEntryString(device.get(), CFSTR(kUSBProductString));
|
||||
|
||||
if (vendor.isEmpty()) {
|
||||
return product;
|
||||
}
|
||||
return vendor + " " + product;
|
||||
}
|
||||
|
||||
QList<QUrl> MacDeviceLister::MakeDeviceUrls(const QString& serial) {
|
||||
if (IsMTPSerial(serial)) {
|
||||
const MTPDevice& device = mtp_devices_[serial];
|
||||
QString str;
|
||||
str.sprintf("gphoto2://usb-%d-%d/", device.bus, device.address);
|
||||
QUrl url(str);
|
||||
url.addQueryItem("vendor", device.vendor);
|
||||
url.addQueryItem("vendor_id", QString::number(device.vendor_id));
|
||||
url.addQueryItem("product", device.product);
|
||||
url.addQueryItem("product_id", QString::number(device.product_id));
|
||||
url.addQueryItem("quirks", QString::number(device.quirks));
|
||||
return QList<QUrl>() << url;
|
||||
}
|
||||
|
||||
if (IsCDDevice(serial)) {
|
||||
return QList<QUrl>() << QUrl(QString("cdda:///dev/r" + serial));
|
||||
}
|
||||
|
||||
QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(
|
||||
kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
|
||||
scoped_nsobject<NSDictionary> properties((NSDictionary*)DADiskCopyDescription(disk.get()));
|
||||
scoped_nsobject<NSURL> volume_path([[properties objectForKey:(NSString*)kDADiskDescriptionVolumePathKey] copy]);
|
||||
|
||||
QString path = QString::fromUtf8([[volume_path path] UTF8String]);
|
||||
QUrl ret = MakeUrlFromLocalPath(path);
|
||||
|
||||
return QList<QUrl>() << ret;
|
||||
}
|
||||
|
||||
QStringList MacDeviceLister::DeviceUniqueIDs() {
|
||||
return current_devices_.keys() + mtp_devices_.keys();
|
||||
}
|
||||
|
||||
QVariantList MacDeviceLister::DeviceIcons(const QString& serial) {
|
||||
if (IsMTPSerial(serial)) {
|
||||
return QVariantList();
|
||||
}
|
||||
|
||||
if (IsCDDevice(serial)) {
|
||||
return QVariantList() << "cd";
|
||||
}
|
||||
|
||||
QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(
|
||||
kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
|
||||
ScopedIOObject device(DADiskCopyIOMedia(disk.get()));
|
||||
QString icon = GetIconForDevice(device.get());
|
||||
|
||||
scoped_nsobject<NSDictionary> properties((NSDictionary*)DADiskCopyDescription(disk));
|
||||
scoped_nsobject<NSURL> volume_path([[properties objectForKey:(NSString*)kDADiskDescriptionVolumePathKey] copy]);
|
||||
|
||||
QString path = QString::fromUtf8([[volume_path path] UTF8String]);
|
||||
|
||||
QVariantList ret;
|
||||
ret << GuessIconForPath(path);
|
||||
ret << GuessIconForModel(DeviceManufacturer(serial), DeviceModel(serial));
|
||||
if (!icon.isEmpty()) {
|
||||
ret << icon;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
QString MacDeviceLister::DeviceManufacturer(const QString& serial) {
|
||||
if (IsMTPSerial(serial)) {
|
||||
return mtp_devices_[serial].vendor;
|
||||
}
|
||||
return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBVendorString));
|
||||
}
|
||||
|
||||
QString MacDeviceLister::DeviceModel(const QString& serial) {
|
||||
if (IsMTPSerial(serial)) {
|
||||
return mtp_devices_[serial].product;
|
||||
}
|
||||
return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBProductString));
|
||||
}
|
||||
|
||||
quint64 MacDeviceLister::DeviceCapacity(const QString& serial) {
|
||||
if (IsMTPSerial(serial)) {
|
||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||
return mtp_devices_[serial].capacity;
|
||||
}
|
||||
QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
|
||||
io_object_t device = DADiskCopyIOMedia(disk);
|
||||
|
||||
NSNumber* capacity = (NSNumber*)GetPropertyForDevice(device, CFSTR("Size"));
|
||||
|
||||
quint64 ret = [capacity unsignedLongLongValue];
|
||||
|
||||
IOObjectRelease(device);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
quint64 MacDeviceLister::DeviceFreeSpace(const QString& serial) {
|
||||
if (IsMTPSerial(serial)) {
|
||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||
return mtp_devices_[serial].free_space;
|
||||
}
|
||||
QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, session.get(), bsd_name.toLatin1().constData()));
|
||||
|
||||
scoped_nsobject<NSDictionary> properties((NSDictionary*)DADiskCopyDescription(disk));
|
||||
scoped_nsobject<NSURL> volume_path([[properties objectForKey:(NSString*)kDADiskDescriptionVolumePathKey] copy]);
|
||||
|
||||
NSNumber* value = nil;
|
||||
NSError* error = nil;
|
||||
if ([volume_path getResourceValue:&value forKey: NSURLVolumeAvailableCapacityKey error: &error] && value) {
|
||||
return [value unsignedLongLongValue];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
QVariantMap MacDeviceLister::DeviceHardwareInfo(const QString& serial){return QVariantMap();}
|
||||
|
||||
bool MacDeviceLister::AskForScan(const QString& serial) const {
|
||||
return !IsCDDevice(serial);
|
||||
}
|
||||
|
||||
void MacDeviceLister::UnmountDevice(const QString& serial) {
|
||||
if (IsMTPSerial(serial)) return;
|
||||
|
||||
QString bsd_name = current_devices_[serial];
|
||||
ScopedCFTypeRef<DADiskRef> disk(DADiskCreateFromBSDName(kCFAllocatorDefault, loop_session_, bsd_name.toLatin1().constData()));
|
||||
|
||||
DADiskUnmount(disk, kDADiskUnmountOptionDefault, &DiskUnmountCallback, this);
|
||||
}
|
||||
|
||||
void MacDeviceLister::DiskUnmountCallback(DADiskRef disk, DADissenterRef dissenter, void* context) {
|
||||
if (dissenter) {
|
||||
qLog(Warning) << "Another app blocked the unmount";
|
||||
}
|
||||
else {
|
||||
DiskRemovedCallback(disk, context);
|
||||
}
|
||||
}
|
||||
|
||||
void MacDeviceLister::UpdateDeviceFreeSpace(const QString& serial) {
|
||||
if (IsMTPSerial(serial)) {
|
||||
if (mtp_devices_.contains(serial)) {
|
||||
QList<QUrl> urls = MakeDeviceUrls(serial);
|
||||
MTPDevice* d = &mtp_devices_[serial];
|
||||
d->free_space = GetFreeSpace(urls[0]);
|
||||
}
|
||||
}
|
||||
emit DeviceChanged(serial);
|
||||
}
|
||||
|
||||
93
src/device/mtpconnection.cpp
Normal file
93
src/device/mtpconnection.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QRegExp>
|
||||
#include <QtDebug>
|
||||
#include <QUrlQuery>
|
||||
|
||||
#include "mtpconnection.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
MtpConnection::MtpConnection(const QUrl &url) : device_(nullptr) {
|
||||
|
||||
QString hostname = url.host();
|
||||
// Parse the URL
|
||||
QRegExp host_re("^usb-(\\d+)-(\\d+)$");
|
||||
|
||||
if (host_re.indexIn(hostname) == -1) {
|
||||
qLog(Warning) << "Invalid MTP device:" << hostname;
|
||||
return;
|
||||
}
|
||||
|
||||
const unsigned int bus_location = host_re.cap(1).toInt();
|
||||
const unsigned int device_num = host_re.cap(2).toInt();
|
||||
|
||||
QUrlQuery url_query(url);
|
||||
if (url_query.hasQueryItem("vendor")) {
|
||||
LIBMTP_raw_device_t *raw_device = (LIBMTP_raw_device_t*)malloc(sizeof(LIBMTP_raw_device_t));
|
||||
raw_device->device_entry.vendor = url_query.queryItemValue("vendor").toLatin1().data();
|
||||
raw_device->device_entry.product = url_query.queryItemValue("product").toLatin1().data();
|
||||
raw_device->device_entry.vendor_id = url_query.queryItemValue("vendor_id").toUShort();
|
||||
raw_device->device_entry.product_id = url_query.queryItemValue("product_id").toUShort();
|
||||
raw_device->device_entry.device_flags = url_query.queryItemValue("quirks").toUInt();
|
||||
|
||||
raw_device->bus_location = bus_location;
|
||||
raw_device->devnum = device_num;
|
||||
|
||||
device_ = LIBMTP_Open_Raw_Device(raw_device);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get a list of devices from libmtp and figure out which one is ours
|
||||
int count = 0;
|
||||
LIBMTP_raw_device_t *raw_devices = nullptr;
|
||||
LIBMTP_error_number_t err = LIBMTP_Detect_Raw_Devices(&raw_devices, &count);
|
||||
if (err != LIBMTP_ERROR_NONE) {
|
||||
qLog(Warning) << "MTP error:" << err;
|
||||
return;
|
||||
}
|
||||
|
||||
LIBMTP_raw_device_t *raw_device = nullptr;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (raw_devices[i].bus_location == bus_location && raw_devices[i].devnum == device_num) {
|
||||
raw_device = &raw_devices[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!raw_device) {
|
||||
qLog(Warning) << "MTP device not found";
|
||||
free(raw_devices);
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect to the device
|
||||
device_ = LIBMTP_Open_Raw_Device(raw_device);
|
||||
|
||||
free(raw_devices);
|
||||
|
||||
}
|
||||
|
||||
MtpConnection::~MtpConnection() {
|
||||
if (device_) LIBMTP_Release_Device(device_);
|
||||
}
|
||||
|
||||
44
src/device/mtpconnection.h
Normal file
44
src/device/mtpconnection.h
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef MTPCONNECTION_H
|
||||
#define MTPCONNECTION_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <libmtp.h>
|
||||
|
||||
#include <QUrl>
|
||||
|
||||
class MtpConnection {
|
||||
public:
|
||||
MtpConnection(const QUrl &url);
|
||||
~MtpConnection();
|
||||
|
||||
bool is_valid() const { return device_; }
|
||||
LIBMTP_mtpdevice_t *device() const { return device_; }
|
||||
|
||||
private:
|
||||
Q_DISABLE_COPY(MtpConnection);
|
||||
|
||||
LIBMTP_mtpdevice_t *device_;
|
||||
};
|
||||
|
||||
#endif // MTPCONNECTION_H
|
||||
223
src/device/mtpdevice.cpp
Normal file
223
src/device/mtpdevice.cpp
Normal file
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <libmtp.h>
|
||||
|
||||
#include <QFile>
|
||||
#include <QThread>
|
||||
|
||||
#include "devicemanager.h"
|
||||
#include "mtpconnection.h"
|
||||
#include "mtpdevice.h"
|
||||
#include "mtploader.h"
|
||||
#include "core/application.h"
|
||||
#include "core/logging.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
|
||||
bool MtpDevice::sInitialisedLibMTP = false;
|
||||
|
||||
MtpDevice::MtpDevice(const QUrl &url, DeviceLister *lister, const QString &unique_id, DeviceManager *manager, Application *app, int database_id, bool first_time)
|
||||
: ConnectedDevice(url, lister, unique_id, manager, app, database_id, first_time), loader_thread_(new QThread(this)), loader_(nullptr) {
|
||||
|
||||
if (!sInitialisedLibMTP) {
|
||||
LIBMTP_Init();
|
||||
sInitialisedLibMTP = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MtpDevice::~MtpDevice() {}
|
||||
|
||||
void MtpDevice::Init() {
|
||||
|
||||
InitBackendDirectory("/", first_time_, false);
|
||||
model_->Init();
|
||||
|
||||
loader_ = new MtpLoader(url_, app_->task_manager(), backend_, shared_from_this());
|
||||
loader_->moveToThread(loader_thread_);
|
||||
|
||||
connect(loader_, SIGNAL(Error(QString)), SIGNAL(Error(QString)));
|
||||
connect(loader_, SIGNAL(TaskStarted(int)), SIGNAL(TaskStarted(int)));
|
||||
connect(loader_, SIGNAL(LoadFinished()), SLOT(LoadFinished()));
|
||||
connect(loader_thread_, SIGNAL(started()), loader_, SLOT(LoadDatabase()));
|
||||
|
||||
db_busy_.lock();
|
||||
loader_thread_->start();
|
||||
|
||||
}
|
||||
|
||||
void MtpDevice::LoadFinished() {
|
||||
|
||||
loader_->deleteLater();
|
||||
loader_ = nullptr;
|
||||
db_busy_.unlock();
|
||||
|
||||
}
|
||||
|
||||
bool MtpDevice::StartCopy(QList<Song::FileType> *supported_types) {
|
||||
|
||||
// Ensure only one "organise files" can be active at any one time
|
||||
db_busy_.lock();
|
||||
|
||||
// Connect to the device
|
||||
connection_.reset(new MtpConnection(url_));
|
||||
|
||||
// Did the caller want a list of supported types?
|
||||
if (supported_types) {
|
||||
if (!GetSupportedFiletypes(supported_types, connection_->device())) {
|
||||
FinishCopy(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
static int ProgressCallback(uint64_t const sent, uint64_t const total, void const *const data) {
|
||||
const MusicStorage::CopyJob *job = reinterpret_cast<const MusicStorage::CopyJob*>(data);
|
||||
job->progress_(float(sent) / total);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool MtpDevice::CopyToStorage(const CopyJob &job) {
|
||||
|
||||
if (!connection_->is_valid()) return false;
|
||||
|
||||
// Convert metadata
|
||||
LIBMTP_track_t track;
|
||||
job.metadata_.ToMTP(&track);
|
||||
|
||||
// Send the file
|
||||
int ret = LIBMTP_Send_Track_From_File(connection_->device(), job.source_.toUtf8().constData(), &track, ProgressCallback, &job);
|
||||
if (ret != 0) return false;
|
||||
|
||||
// Add it to our CollectionModel
|
||||
Song metadata_on_device;
|
||||
metadata_on_device.InitFromMTP(&track, url_.host());
|
||||
metadata_on_device.set_directory_id(1);
|
||||
songs_to_add_ << metadata_on_device;
|
||||
|
||||
// Remove the original if requested
|
||||
if (job.remove_original_) {
|
||||
if (!QFile::remove(job.source_)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void MtpDevice::FinishCopy(bool success) {
|
||||
|
||||
if (success) {
|
||||
if (!songs_to_add_.isEmpty()) backend_->AddOrUpdateSongs(songs_to_add_);
|
||||
if (!songs_to_remove_.isEmpty()) backend_->DeleteSongs(songs_to_remove_);
|
||||
}
|
||||
|
||||
songs_to_add_.clear();
|
||||
songs_to_remove_.clear();
|
||||
|
||||
connection_.reset();
|
||||
|
||||
db_busy_.unlock();
|
||||
|
||||
ConnectedDevice::FinishCopy(success);
|
||||
|
||||
}
|
||||
|
||||
void MtpDevice::StartDelete() { StartCopy(nullptr); }
|
||||
|
||||
bool MtpDevice::DeleteFromStorage(const DeleteJob &job) {
|
||||
|
||||
// Extract the ID from the song's URL
|
||||
QString filename = job.metadata_.url().path();
|
||||
filename.remove('/');
|
||||
|
||||
bool ok = false;
|
||||
uint32_t id = filename.toUInt(&ok);
|
||||
if (!ok) return false;
|
||||
|
||||
// Remove the file
|
||||
int ret = LIBMTP_Delete_Object(connection_->device(), id);
|
||||
if (ret != 0) return false;
|
||||
|
||||
// Remove it from our collection model
|
||||
songs_to_remove_ << job.metadata_;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void MtpDevice::FinishDelete(bool success) { FinishCopy(success); }
|
||||
|
||||
bool MtpDevice::GetSupportedFiletypes(QList<Song::FileType> *ret) {
|
||||
|
||||
QMutexLocker l(&db_busy_);
|
||||
MtpConnection connection(url_);
|
||||
if (!connection.is_valid()) {
|
||||
qLog(Warning) << "Error connecting to MTP device, couldn't get list of supported filetypes";
|
||||
return false;
|
||||
}
|
||||
|
||||
return GetSupportedFiletypes(ret, connection.device());
|
||||
|
||||
}
|
||||
|
||||
bool MtpDevice::GetSupportedFiletypes(QList<Song::FileType> *ret, LIBMTP_mtpdevice_t *device) {
|
||||
|
||||
uint16_t *list = nullptr;
|
||||
uint16_t length = 0;
|
||||
|
||||
if (LIBMTP_Get_Supported_Filetypes(device, &list, &length) || !list || !length)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < length; ++i) {
|
||||
switch (LIBMTP_filetype_t(list[i])) {
|
||||
case LIBMTP_FILETYPE_WAV: *ret << Song::Type_Wav; break;
|
||||
case LIBMTP_FILETYPE_MP2:
|
||||
case LIBMTP_FILETYPE_MP3: *ret << Song::Type_Mpeg; break;
|
||||
case LIBMTP_FILETYPE_WMA: *ret << Song::Type_Asf; break;
|
||||
case LIBMTP_FILETYPE_MP4:
|
||||
case LIBMTP_FILETYPE_M4A:
|
||||
case LIBMTP_FILETYPE_AAC: *ret << Song::Type_Mp4; break;
|
||||
case LIBMTP_FILETYPE_FLAC:
|
||||
*ret << Song::Type_Flac;
|
||||
*ret << Song::Type_OggFlac;
|
||||
break;
|
||||
case LIBMTP_FILETYPE_OGG:
|
||||
*ret << Song::Type_OggVorbis;
|
||||
*ret << Song::Type_OggSpeex;
|
||||
*ret << Song::Type_OggFlac;
|
||||
break;
|
||||
default:
|
||||
qLog(Error) << "Unknown MTP file format" << LIBMTP_Get_Filetype_Description(LIBMTP_filetype_t(list[i]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free(list);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
82
src/device/mtpdevice.h
Normal file
82
src/device/mtpdevice.h
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef MTPDEVICE_H
|
||||
#define MTPDEVICE_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QMutex>
|
||||
#include <QWaitCondition>
|
||||
|
||||
#include "connecteddevice.h"
|
||||
|
||||
struct LIBMTP_mtpdevice_struct;
|
||||
|
||||
class MtpConnection;
|
||||
class MtpLoader;
|
||||
|
||||
class MtpDevice : public ConnectedDevice {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Q_INVOKABLE MtpDevice(const QUrl &url, DeviceLister *lister, const QString &unique_id, DeviceManager *manager, Application *app, int database_id, bool first_time);
|
||||
~MtpDevice();
|
||||
|
||||
static QStringList url_schemes() { return QStringList() << "mtp" << "gphoto2"; }
|
||||
|
||||
void Init();
|
||||
|
||||
bool GetSupportedFiletypes(QList<Song::FileType>* ret);
|
||||
int GetFreeSpace();
|
||||
int GetCapacity();
|
||||
|
||||
bool StartCopy(QList<Song::FileType>* supported_types);
|
||||
bool CopyToStorage(const CopyJob& job);
|
||||
void FinishCopy(bool success);
|
||||
|
||||
void StartDelete();
|
||||
bool DeleteFromStorage(const DeleteJob& job);
|
||||
void FinishDelete(bool success);
|
||||
|
||||
private slots:
|
||||
void LoadFinished();
|
||||
|
||||
private:
|
||||
bool GetSupportedFiletypes(QList<Song::FileType> *ret, LIBMTP_mtpdevice_struct *device);
|
||||
int GetFreeSpace(LIBMTP_mtpdevice_struct* device);
|
||||
int GetCapacity(LIBMTP_mtpdevice_struct* device);
|
||||
|
||||
private:
|
||||
static bool sInitialisedLibMTP;
|
||||
|
||||
QThread *loader_thread_;
|
||||
MtpLoader *loader_;
|
||||
|
||||
QMutex db_busy_;
|
||||
SongList songs_to_add_;
|
||||
SongList songs_to_remove_;
|
||||
|
||||
std::unique_ptr<MtpConnection> connection_;
|
||||
};
|
||||
|
||||
#endif // MTPDEVICE_H
|
||||
90
src/device/mtploader.cpp
Normal file
90
src/device/mtploader.cpp
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "mtploader.h"
|
||||
|
||||
#include <libmtp.h>
|
||||
|
||||
#include "connecteddevice.h"
|
||||
#include "mtpconnection.h"
|
||||
#include "core/song.h"
|
||||
#include "core/taskmanager.h"
|
||||
#include "collection/collectionbackend.h"
|
||||
|
||||
MtpLoader::MtpLoader(const QUrl &url, TaskManager *task_manager, CollectionBackend *backend, std::shared_ptr<ConnectedDevice> device)
|
||||
: QObject(nullptr),
|
||||
device_(device),
|
||||
url_(url),
|
||||
task_manager_(task_manager),
|
||||
backend_(backend) {
|
||||
original_thread_ = thread();
|
||||
}
|
||||
|
||||
MtpLoader::~MtpLoader() {}
|
||||
|
||||
void MtpLoader::LoadDatabase() {
|
||||
|
||||
int task_id = task_manager_->StartTask(tr("Loading MTP device"));
|
||||
emit TaskStarted(task_id);
|
||||
|
||||
TryLoad();
|
||||
|
||||
moveToThread(original_thread_);
|
||||
|
||||
task_manager_->SetTaskFinished(task_id);
|
||||
emit LoadFinished();
|
||||
|
||||
}
|
||||
|
||||
bool MtpLoader::TryLoad() {
|
||||
|
||||
MtpConnection dev(url_);
|
||||
if (!dev.is_valid()) {
|
||||
emit Error(tr("Error connecting MTP device"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load the list of songs on the device
|
||||
SongList songs;
|
||||
LIBMTP_track_t *tracks = LIBMTP_Get_Tracklisting_With_Callback(dev.device(), nullptr, nullptr);
|
||||
while (tracks) {
|
||||
LIBMTP_track_t *track = tracks;
|
||||
|
||||
Song song;
|
||||
song.InitFromMTP(track, url_.host());
|
||||
song.set_directory_id(1);
|
||||
songs << song;
|
||||
|
||||
tracks = tracks->next;
|
||||
LIBMTP_destroy_track_t(track);
|
||||
}
|
||||
|
||||
// Need to remove all the existing songs in the database first
|
||||
backend_->DeleteSongs(backend_->FindSongsInDirectory(1));
|
||||
|
||||
// Add the songs we've just loaded
|
||||
backend_->AddOrUpdateSongs(songs);
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
63
src/device/mtploader.h
Normal file
63
src/device/mtploader.h
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef MTPLOADER_H
|
||||
#define MTPLOADER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
|
||||
class ConnectedDevice;
|
||||
class CollectionBackend;
|
||||
class TaskManager;
|
||||
|
||||
class MtpLoader : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MtpLoader(const QUrl &url, TaskManager *task_manager, CollectionBackend *backend, std::shared_ptr<ConnectedDevice> device);
|
||||
~MtpLoader();
|
||||
|
||||
public slots:
|
||||
void LoadDatabase();
|
||||
|
||||
signals:
|
||||
void Error(const QString &message);
|
||||
void TaskStarted(int task_id);
|
||||
void LoadFinished();
|
||||
|
||||
private:
|
||||
bool TryLoad();
|
||||
|
||||
private:
|
||||
std::shared_ptr<ConnectedDevice> device_;
|
||||
QThread* original_thread_;
|
||||
|
||||
QUrl url_;
|
||||
TaskManager *task_manager_;
|
||||
CollectionBackend *backend_;
|
||||
};
|
||||
|
||||
#endif // MTPLOADER_H
|
||||
|
||||
363
src/device/udisks2lister.cpp
Normal file
363
src/device/udisks2lister.cpp
Normal file
@@ -0,0 +1,363 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2016, Valeriy Malov <jazzvoid@gmail.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 "config.h"
|
||||
|
||||
#include "udisks2lister.h"
|
||||
|
||||
#include <QDBusConnection>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/utilities.h"
|
||||
#include "dbus/objectmanager.h"
|
||||
#include "dbus/udisks2block.h"
|
||||
#include "dbus/udisks2drive.h"
|
||||
#include "dbus/udisks2filesystem.h"
|
||||
#include "dbus/udisks2job.h"
|
||||
|
||||
constexpr char Udisks2Lister::udisks2_service_[];
|
||||
|
||||
Udisks2Lister::Udisks2Lister() {}
|
||||
|
||||
Udisks2Lister::~Udisks2Lister() {}
|
||||
|
||||
QStringList Udisks2Lister::DeviceUniqueIDs() {
|
||||
QReadLocker locker(&device_data_lock_);
|
||||
return device_data_.keys();
|
||||
}
|
||||
|
||||
QVariantList Udisks2Lister::DeviceIcons(const QString &id) {
|
||||
return QVariantList();
|
||||
}
|
||||
|
||||
QString Udisks2Lister::DeviceManufacturer(const QString &id) {
|
||||
|
||||
QReadLocker locker(&device_data_lock_);
|
||||
if (!device_data_.contains(id)) return "";
|
||||
return device_data_[id].vendor;
|
||||
|
||||
}
|
||||
|
||||
QString Udisks2Lister::DeviceModel(const QString &id) {
|
||||
|
||||
QReadLocker locker(&device_data_lock_);
|
||||
if (!device_data_.contains(id)) return "";
|
||||
return device_data_[id].model;
|
||||
|
||||
}
|
||||
|
||||
quint64 Udisks2Lister::DeviceCapacity(const QString &id) {
|
||||
|
||||
QReadLocker locker(&device_data_lock_);
|
||||
if (!device_data_.contains(id)) return 0;
|
||||
return device_data_[id].capacity;
|
||||
|
||||
}
|
||||
|
||||
quint64 Udisks2Lister::DeviceFreeSpace(const QString &id) {
|
||||
|
||||
QReadLocker locker(&device_data_lock_);
|
||||
if (!device_data_.contains(id)) return 0;
|
||||
return device_data_[id].free_space;
|
||||
|
||||
}
|
||||
|
||||
QVariantMap Udisks2Lister::DeviceHardwareInfo(const QString &id) {
|
||||
|
||||
QReadLocker locker(&device_data_lock_);
|
||||
if (!device_data_.contains(id)) return QVariantMap();
|
||||
|
||||
QVariantMap result;
|
||||
|
||||
const auto &data = device_data_[id];
|
||||
result[QT_TR_NOOP("DBus path")] = data.dbus_path;
|
||||
result[QT_TR_NOOP("Serial number")] = data.serial;
|
||||
result[QT_TR_NOOP("Mount points")] = data.mount_paths.join(", ");
|
||||
result[QT_TR_NOOP("Partition label")] = data.label;
|
||||
result[QT_TR_NOOP("UUID")] = data.uuid;
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
QString Udisks2Lister::MakeFriendlyName(const QString &id) {
|
||||
QReadLocker locker(&device_data_lock_);
|
||||
if (!device_data_.contains(id)) return "";
|
||||
return device_data_[id].friendly_name;
|
||||
}
|
||||
|
||||
QList<QUrl> Udisks2Lister::MakeDeviceUrls(const QString &id) {
|
||||
QReadLocker locker(&device_data_lock_);
|
||||
if (!device_data_.contains(id)) return QList<QUrl>();
|
||||
return QList<QUrl>() << QUrl::fromLocalFile(
|
||||
device_data_[id].mount_paths.at(0));
|
||||
}
|
||||
|
||||
void Udisks2Lister::UnmountDevice(const QString &id) {
|
||||
|
||||
QReadLocker locker(&device_data_lock_);
|
||||
if (!device_data_.contains(id)) return;
|
||||
|
||||
OrgFreedesktopUDisks2FilesystemInterface filesystem(
|
||||
udisks2_service_, device_data_[id].dbus_path,
|
||||
QDBusConnection::systemBus());
|
||||
|
||||
if (filesystem.isValid()) {
|
||||
auto unmount_result = filesystem.Unmount(QVariantMap());
|
||||
unmount_result.waitForFinished();
|
||||
|
||||
if (unmount_result.isError()) {
|
||||
qLog(Warning) << "Failed to unmount " << id << ": " << unmount_result.error();
|
||||
return;
|
||||
}
|
||||
|
||||
OrgFreedesktopUDisks2DriveInterface drive(udisks2_service_, device_data_[id].dbus_drive_path, QDBusConnection::systemBus());
|
||||
|
||||
if (drive.isValid()) {
|
||||
auto eject_result = drive.Eject(QVariantMap());
|
||||
eject_result.waitForFinished();
|
||||
|
||||
if (eject_result.isError())
|
||||
qLog(Warning) << "Failed to eject " << id << ": " << eject_result.error();
|
||||
}
|
||||
|
||||
device_data_.remove(id);
|
||||
DeviceRemoved(id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Udisks2Lister::UpdateDeviceFreeSpace(const QString &id) {
|
||||
QWriteLocker locker(&device_data_lock_);
|
||||
device_data_[id].free_space =
|
||||
Utilities::FileSystemFreeSpace(device_data_[id].mount_paths.at(0));
|
||||
|
||||
emit DeviceChanged(id);
|
||||
}
|
||||
|
||||
void Udisks2Lister::Init() {
|
||||
|
||||
udisks2_interface_.reset(new OrgFreedesktopDBusObjectManagerInterface(udisks2_service_, "/org/freedesktop/UDisks2", QDBusConnection::systemBus()));
|
||||
|
||||
QDBusPendingReply<ManagedObjectList> reply = udisks2_interface_->GetManagedObjects();
|
||||
reply.waitForFinished();
|
||||
|
||||
if (!reply.isValid()) {
|
||||
qLog(Warning) << "Error enumerating udisks2 devices:" << reply.error().name() << reply.error().message();
|
||||
udisks2_interface_.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const QDBusObjectPath &path : reply.value().keys()) {
|
||||
auto partition_data = ReadPartitionData(path);
|
||||
|
||||
if (!partition_data.dbus_path.isEmpty()) {
|
||||
QWriteLocker locker(&device_data_lock_);
|
||||
device_data_[partition_data.unique_id()] = partition_data;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &id : device_data_.keys()) {
|
||||
emit DeviceAdded(id);
|
||||
}
|
||||
|
||||
connect(udisks2_interface_.get(), SIGNAL(InterfacesAdded(QDBusObjectPath, InterfacesAndProperties)), SLOT(DBusInterfaceAdded(QDBusObjectPath, InterfacesAndProperties)));
|
||||
connect(udisks2_interface_.get(), SIGNAL(InterfacesRemoved(QDBusObjectPath, QStringList)), SLOT(DBusInterfaceRemoved(QDBusObjectPath, QStringList)));
|
||||
}
|
||||
|
||||
void Udisks2Lister::DBusInterfaceAdded(const QDBusObjectPath &path, const InterfacesAndProperties &interfaces) {
|
||||
|
||||
for (auto interface = interfaces.constBegin(); interface != interfaces.constEnd(); ++interface) {
|
||||
|
||||
if (interface.key() != "org.freedesktop.UDisks2.Job") continue;
|
||||
|
||||
std::shared_ptr<OrgFreedesktopUDisks2JobInterface> job = std::make_shared<OrgFreedesktopUDisks2JobInterface>(udisks2_service_, path.path(), QDBusConnection::systemBus());
|
||||
|
||||
if (!job->isValid()) continue;
|
||||
|
||||
bool is_mount_job = false;
|
||||
if (job->operation() == "filesystem-mount") {
|
||||
is_mount_job = true;
|
||||
}
|
||||
else if (job->operation() == "filesystem-unmount") {
|
||||
is_mount_job = false;
|
||||
}
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto mounted_partitions = job->objects();
|
||||
|
||||
if (mounted_partitions.isEmpty()) {
|
||||
qLog(Warning) << "Empty Udisks2 mount/umount job " << path.path();
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
QMutexLocker locker(&jobs_lock_);
|
||||
qLog(Debug) << "Adding pending job | DBus Path = " << job->path() << " | IsMountJob = " << is_mount_job << " | First partition = " << mounted_partitions.at(0).path();
|
||||
mounting_jobs_[path].dbus_interface = job;
|
||||
mounting_jobs_[path].is_mount = is_mount_job;
|
||||
mounting_jobs_[path].mounted_partitions = mounted_partitions;
|
||||
connect(job.get(), SIGNAL(Completed(bool, const QString&)), SLOT(JobCompleted(bool, const QString&)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Udisks2Lister::DBusInterfaceRemoved(const QDBusObjectPath &path, const QStringList &ifaces) {
|
||||
if (!isPendingJob(path)) RemoveDevice(path);
|
||||
}
|
||||
|
||||
bool Udisks2Lister::isPendingJob(const QDBusObjectPath &job_path) {
|
||||
QMutexLocker locker(&jobs_lock_);
|
||||
|
||||
if (!mounting_jobs_.contains(job_path)) return false;
|
||||
|
||||
mounting_jobs_.remove(job_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Udisks2Lister::RemoveDevice(const QDBusObjectPath &device_path) {
|
||||
|
||||
QWriteLocker locker(&device_data_lock_);
|
||||
QString id;
|
||||
for (const auto &data : device_data_) {
|
||||
if (data.dbus_path == device_path.path()) {
|
||||
id = data.unique_id();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (id.isEmpty()) return;
|
||||
|
||||
qLog(Debug) << "UDisks2 device removed: " << device_path.path();
|
||||
device_data_.remove(id);
|
||||
DeviceRemoved(id);
|
||||
|
||||
}
|
||||
|
||||
QList<QDBusObjectPath> Udisks2Lister::GetMountedPartitionsFromDBusArgument(const QDBusArgument &input) {
|
||||
|
||||
QList<QDBusObjectPath> result;
|
||||
|
||||
input.beginArray();
|
||||
while (!input.atEnd()) {
|
||||
QDBusObjectPath extractedPath;
|
||||
input >> extractedPath;
|
||||
result.push_back(extractedPath);
|
||||
}
|
||||
input.endArray();
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
void Udisks2Lister::JobCompleted(bool success, const QString &message) {
|
||||
|
||||
auto job = qobject_cast<OrgFreedesktopUDisks2JobInterface*>(sender());
|
||||
QDBusObjectPath jobPath(job->path());
|
||||
|
||||
if (!job->isValid() || !success || !mounting_jobs_.contains(jobPath)) return;
|
||||
|
||||
qLog(Debug) << "Pending Job Completed | Path = " << job->path() << " | Mount? = " << mounting_jobs_[jobPath].is_mount << " | Success = " << success;
|
||||
|
||||
for (const auto &mounted_object : mounting_jobs_[jobPath].mounted_partitions) {
|
||||
auto partition_data = ReadPartitionData(mounted_object);
|
||||
if (partition_data.dbus_path.isEmpty()) continue;
|
||||
|
||||
mounting_jobs_[jobPath].is_mount ? HandleFinishedMountJob(partition_data) : HandleFinishedUnmountJob(partition_data, mounted_object);
|
||||
}
|
||||
}
|
||||
|
||||
void Udisks2Lister::HandleFinishedMountJob(const Udisks2Lister::PartitionData &partition_data) {
|
||||
|
||||
qLog(Debug) << "UDisks2 mount job finished: Drive = " << partition_data.dbus_drive_path << " | Partition = " << partition_data.dbus_path;
|
||||
QWriteLocker locker(&device_data_lock_);
|
||||
device_data_[partition_data.unique_id()] = partition_data;
|
||||
DeviceAdded(partition_data.unique_id());
|
||||
|
||||
}
|
||||
|
||||
void Udisks2Lister::HandleFinishedUnmountJob(const Udisks2Lister::PartitionData &partition_data, const QDBusObjectPath &mounted_object) {
|
||||
|
||||
QWriteLocker locker(&device_data_lock_);
|
||||
QString id;
|
||||
for (auto &data : device_data_) {
|
||||
if (data.mount_paths.contains(mounted_object.path())) {
|
||||
qLog(Debug) << "UDisks2 umount job finished, found corresponding device: Drive = " << data.dbus_drive_path << " | Partition = " << data.dbus_path;
|
||||
data.mount_paths.removeOne(mounted_object.path());
|
||||
if (data.mount_paths.empty()) id = data.unique_id();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!id.isEmpty()) {
|
||||
qLog(Debug) << "Partition " << partition_data.dbus_path
|
||||
<< " has no more mount points, removing it from device list";
|
||||
device_data_.remove(id);
|
||||
DeviceRemoved(id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Udisks2Lister::PartitionData Udisks2Lister::ReadPartitionData(const QDBusObjectPath &path) {
|
||||
|
||||
PartitionData result;
|
||||
OrgFreedesktopUDisks2FilesystemInterface filesystem(udisks2_service_, path.path(), QDBusConnection::systemBus());
|
||||
OrgFreedesktopUDisks2BlockInterface block(udisks2_service_, path.path(), QDBusConnection::systemBus());
|
||||
|
||||
if (filesystem.isValid() && block.isValid() && !filesystem.mountPoints().empty()) {
|
||||
OrgFreedesktopUDisks2DriveInterface drive(udisks2_service_, block.drive().path(), QDBusConnection::systemBus());
|
||||
|
||||
if (drive.isValid() && drive.mediaRemovable()) {
|
||||
result.dbus_path = path.path();
|
||||
result.dbus_drive_path = block.drive().path();
|
||||
|
||||
result.serial = drive.serial();
|
||||
result.vendor = drive.vendor();
|
||||
result.model = drive.model();
|
||||
|
||||
result.label = block.idLabel();
|
||||
result.uuid = block.idUUID();
|
||||
result.capacity = drive.size();
|
||||
|
||||
if (!result.label.isEmpty())
|
||||
result.friendly_name = result.label;
|
||||
else
|
||||
result.friendly_name = result.model + " " + result.uuid;
|
||||
|
||||
for (const auto &path : filesystem.mountPoints())
|
||||
result.mount_paths.push_back(path);
|
||||
|
||||
result.free_space = Utilities::FileSystemFreeSpace(result.mount_paths.at(0));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
QString Udisks2Lister::PartitionData::unique_id() const {
|
||||
return QString("Udisks2/%1/%2/%3/%4/%5")
|
||||
.arg(serial, vendor, model)
|
||||
.arg(capacity)
|
||||
.arg(uuid);
|
||||
}
|
||||
|
||||
Udisks2Lister::Udisks2Job::Udisks2Job() : is_mount(true) {}
|
||||
|
||||
Udisks2Lister::PartitionData::PartitionData() : capacity(0), free_space(0) {}
|
||||
118
src/device/udisks2lister.h
Normal file
118
src/device/udisks2lister.h
Normal file
@@ -0,0 +1,118 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2016, Valeriy Malov <jazzvoid@gmail.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 UDISKS2LISTER_H
|
||||
#define UDISKS2LISTER_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QDBusArgument>
|
||||
#include <QMutex>
|
||||
#include <QReadWriteLock>
|
||||
#include <QStringList>
|
||||
|
||||
#include "dbus/metatypes.h"
|
||||
#include "devicelister.h"
|
||||
|
||||
class OrgFreedesktopDBusObjectManagerInterface;
|
||||
class OrgFreedesktopUDisks2JobInterface;
|
||||
|
||||
class Udisks2Lister : public DeviceLister {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Udisks2Lister();
|
||||
~Udisks2Lister();
|
||||
|
||||
QStringList DeviceUniqueIDs() override;
|
||||
QVariantList DeviceIcons(const QString &id) override;
|
||||
QString DeviceManufacturer(const QString &id) override;
|
||||
QString DeviceModel(const QString &id) override;
|
||||
quint64 DeviceCapacity(const QString &id) override;
|
||||
quint64 DeviceFreeSpace(const QString &id) override;
|
||||
QVariantMap DeviceHardwareInfo(const QString &id) override;
|
||||
|
||||
QString MakeFriendlyName(const QString &id) override;
|
||||
QList<QUrl> MakeDeviceUrls(const QString &id) override;
|
||||
|
||||
void UnmountDevice(const QString &id) override;
|
||||
|
||||
public slots:
|
||||
void UpdateDeviceFreeSpace(const QString &id) override;
|
||||
|
||||
protected:
|
||||
void Init() override;
|
||||
|
||||
private slots:
|
||||
void DBusInterfaceAdded(const QDBusObjectPath &path, const InterfacesAndProperties &ifaces);
|
||||
void DBusInterfaceRemoved(const QDBusObjectPath &path, const QStringList &ifaces);
|
||||
void JobCompleted(bool success, const QString &message);
|
||||
|
||||
private:
|
||||
bool isPendingJob(const QDBusObjectPath &job_path);
|
||||
void RemoveDevice(const QDBusObjectPath &device_path);
|
||||
QList<QDBusObjectPath> GetMountedPartitionsFromDBusArgument( const QDBusArgument &input);
|
||||
|
||||
struct Udisks2Job {
|
||||
Udisks2Job();
|
||||
bool is_mount;
|
||||
QList<QDBusObjectPath> mounted_partitions;
|
||||
std::shared_ptr<OrgFreedesktopUDisks2JobInterface> dbus_interface;
|
||||
};
|
||||
|
||||
QMutex jobs_lock_;
|
||||
QMap<QDBusObjectPath, Udisks2Job> mounting_jobs_;
|
||||
|
||||
private:
|
||||
struct PartitionData {
|
||||
PartitionData();
|
||||
|
||||
QString unique_id() const;
|
||||
|
||||
QString dbus_path;
|
||||
QString friendly_name;
|
||||
|
||||
// Device
|
||||
QString serial;
|
||||
QString vendor;
|
||||
QString model;
|
||||
quint64 capacity;
|
||||
QString dbus_drive_path;
|
||||
|
||||
// Paritition
|
||||
QString label;
|
||||
QString uuid;
|
||||
quint64 free_space;
|
||||
QStringList mount_paths;
|
||||
};
|
||||
|
||||
PartitionData ReadPartitionData(const QDBusObjectPath &path);
|
||||
void HandleFinishedMountJob(const Udisks2Lister::PartitionData &partition_data);
|
||||
void HandleFinishedUnmountJob(const Udisks2Lister::PartitionData &partition_data, const QDBusObjectPath &mounted_object);
|
||||
|
||||
QReadWriteLock device_data_lock_;
|
||||
QMap<QString, PartitionData> device_data_;
|
||||
|
||||
private:
|
||||
std::unique_ptr<OrgFreedesktopDBusObjectManagerInterface> udisks2_interface_;
|
||||
|
||||
static constexpr char udisks2_service_[] = "org.freedesktop.UDisks2";
|
||||
};
|
||||
|
||||
#endif // UDISKS2LISTER_H
|
||||
Reference in New Issue
Block a user