Initial commit.

This commit is contained in:
Jonas Kvinge
2018-02-27 18:06:05 +01:00
parent 85d9664df7
commit b2b1ba7abe
1393 changed files with 177311 additions and 1 deletions

191
src/device/afcdevice.cpp Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

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

View 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

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

View 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

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

View 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

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

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

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

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

View 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

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

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

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

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

View 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

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

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

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

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

View 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

View 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

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

View 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_);
}

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

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