Fix mtp device support

This commit is contained in:
Jonas Kvinge 2019-07-19 19:56:37 +02:00
parent e4cefeaa8f
commit ea6cce7068
14 changed files with 201 additions and 70 deletions

View File

@ -1055,6 +1055,7 @@ void Song::ToItdb(Itdb_Track *track) const {
void Song::InitFromMTP(const LIBMTP_track_t *track, const QString &host) {
d->valid_ = true;
d->source_ = Source_Device;
set_title(QString::fromUtf8(track->title));
set_artist(QString::fromUtf8(track->artist));
@ -1063,7 +1064,7 @@ void Song::InitFromMTP(const LIBMTP_track_t *track, const QString &host) {
d->composer_ = QString::fromUtf8(track->composer);
d->track_ = track->tracknumber;
d->url_ = QUrl(QString("mtp://%1/%2").arg(host, track->item_id));
d->url_ = QUrl(QString("mtp://%1/%2").arg(host, QString::number(track->item_id)));
d->basefilename_ = QString::number(track->item_id);
d->filesize_ = track->filesize;
d->mtime_ = track->modificationdate;
@ -1072,7 +1073,7 @@ void Song::InitFromMTP(const LIBMTP_track_t *track, const QString &host) {
set_length_nanosec(track->duration * kNsecPerMsec);
d->samplerate_ = track->samplerate;
d->bitdepth_ = 0; //track->bitdepth;
d->bitdepth_ = 0;
d->bitrate_ = track->bitrate;
d->playcount_ = track->usecount;
@ -1087,11 +1088,12 @@ void Song::InitFromMTP(const LIBMTP_track_t *track, const QString &host) {
case LIBMTP_FILETYPE_FLAC: d->filetype_ = FileType_OggFlac; break;
case LIBMTP_FILETYPE_MP2: d->filetype_ = FileType_MPEG; break;
case LIBMTP_FILETYPE_M4A: d->filetype_ = FileType_MP4; break;
default: d->filetype_ = FileType_Unknown; break;
default:
d->filetype_ = FileType_Unknown;
d->valid_ = false;
break;
}
d->source_ = Source_Device;
}
void Song::ToMTP(LIBMTP_track_t *track) const {
@ -1101,14 +1103,18 @@ void Song::ToMTP(LIBMTP_track_t *track) const {
track->storage_id = 0;
track->title = strdup(d->title_.toUtf8().constData());
track->artist = strdup(d->artist_.toUtf8().constData());
track->artist = strdup(effective_albumartist().toUtf8().constData());
track->album = strdup(d->album_.toUtf8().constData());
track->genre = strdup(d->genre_.toUtf8().constData());
track->date = nullptr;
track->tracknumber = d->track_;
track->composer = strdup(d->composer_.toUtf8().constData());
if (d->composer_.isEmpty())
track->composer = nullptr;
else
track->composer = strdup(d->composer_.toUtf8().constData());
track->filename = strdup(d->basefilename_.toUtf8().constData());
track->filesize = d->filesize_;
track->modificationdate = d->mtime_;
@ -1123,15 +1129,15 @@ void Song::ToMTP(LIBMTP_track_t *track) const {
track->usecount = d->playcount_;
switch (d->filetype_) {
case FileType_ASF: track->filetype = LIBMTP_FILETYPE_ASF; break;
case FileType_MP4: track->filetype = LIBMTP_FILETYPE_MP4; break;
case FileType_MPEG: track->filetype = LIBMTP_FILETYPE_MP3; break;
case FileType_ASF: track->filetype = LIBMTP_FILETYPE_ASF; break;
case FileType_MP4: track->filetype = LIBMTP_FILETYPE_MP4; break;
case FileType_MPEG: track->filetype = LIBMTP_FILETYPE_MP3; break;
case FileType_FLAC:
case FileType_OggFlac: track->filetype = LIBMTP_FILETYPE_FLAC; break;
case FileType_OggFlac: track->filetype = LIBMTP_FILETYPE_FLAC; break;
case FileType_OggSpeex:
case FileType_OggVorbis: track->filetype = LIBMTP_FILETYPE_OGG; break;
case FileType_WAV: track->filetype = LIBMTP_FILETYPE_WAV; break;
default: track->filetype = LIBMTP_FILETYPE_UNDEF_AUDIO; break;
case FileType_OggVorbis: track->filetype = LIBMTP_FILETYPE_OGG; break;
case FileType_WAV: track->filetype = LIBMTP_FILETYPE_WAV; break;
default: track->filetype = LIBMTP_FILETYPE_UNDEF_AUDIO; break;
}
}

View File

@ -40,9 +40,7 @@ class CollectionModel;
class DeviceLister;
class DeviceManager;
class ConnectedDevice : public QObject,
public virtual MusicStorage,
public std::enable_shared_from_this<ConnectedDevice> {
class ConnectedDevice : public QObject, public virtual MusicStorage, public std::enable_shared_from_this<ConnectedDevice> {
Q_OBJECT
public:
@ -50,6 +48,7 @@ class ConnectedDevice : public QObject,
~ConnectedDevice();
virtual bool Init() = 0;
virtual void NewConnection() {}
virtual void ConnectAsync();
// 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

View File

@ -47,10 +47,12 @@ using std::placeholders::_3;
QString GioLister::DeviceInfo::unique_id() const {
if (!volume_root_uri.isEmpty()) return volume_root_uri;
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());
else
return QString("Gio/unmounted/%1").arg((qulonglong)volume.get());
}
@ -191,43 +193,65 @@ QVariantMap GioLister::DeviceHardwareInfo(const QString &id) {
QList<QUrl> GioLister::MakeDeviceUrls(const QString &id) {
QString volume_root_uri;
QString mount_point;
QString uri;
QString mount_uri;
QString unix_device;
{
QMutexLocker l(&mutex_);
volume_root_uri = devices_[id].volume_root_uri;
mount_point = devices_[id].mount_path;
uri = devices_[id].mount_uri;
mount_uri = devices_[id].mount_uri;
unix_device = devices_[id].volume_unix_device;
}
// gphoto2 gives invalid hostnames with []:, characters in
uri.replace(QRegExp("//\\[usb:(\\d+),(\\d+)\\]"), "//usb-\\1-\\2");
QStringList uris;
if (!volume_root_uri.isEmpty())
uris << volume_root_uri;
QUrl url(uri);
if (!mount_uri.isEmpty())
uris << mount_uri;
QList<QUrl> ret;
if (url.isValid()) {
QRegExp device_re("usb/(\\d+)/(\\d+)");
if (device_re.indexIn(unix_device) >= 0) {
QUrlQuery url_query(url);
url_query.addQueryItem("busnum", device_re.cap(1));
url_query.addQueryItem("devnum", device_re.cap(2));
url.setQuery(url_query);
}
for (QString uri : uris) {
// Special case for file:// GIO URIs - we have to check whether they point to an ipod.
if (url.scheme() == "file") {
ret << MakeUrlFromLocalPath(url.path());
// gphoto2 gives invalid hostnames with []:, characters in
uri.replace(QRegExp("//\\[usb:(\\d+),(\\d+)\\]"), "//usb-\\1-\\2");
QUrl url;
if (uri.contains(QRegExp("..+:.*"))) {
url = QUrl::fromEncoded(uri.toUtf8());
}
else {
url = MakeUrlFromLocalPath(uri);
}
if (url.isValid()) {
// Special case for file:// GIO URIs - we have to check whether they point to an ipod.
if (url.isLocalFile() && IsIpod(url.path())) {
url.setScheme("ipod");
}
QRegExp device_re("usb/(\\d+)/(\\d+)");
if (device_re.indexIn(unix_device) >= 0) {
QUrlQuery url_query(url);
url_query.addQueryItem("busnum", device_re.cap(1));
url_query.addQueryItem("devnum", device_re.cap(2));
url.setQuery(url_query);
}
ret << url;
}
}
ret << MakeUrlFromLocalPath(mount_point);
if (!mount_point.isEmpty()) {
ret << MakeUrlFromLocalPath(mount_point);
}
return ret;
@ -473,6 +497,7 @@ void GioLister::DeviceInfo::ReadVolumeInfo(GVolume *volume) {
}
void GioLister::DeviceInfo::ReadDriveInfo(GDrive *drive) {
this->drive.reset_without_add(drive);
if (!drive) return;
@ -485,6 +510,7 @@ QString GioLister::FindUniqueIdByMount(GMount *mount) const {
if (info.mount == mount) return info.unique_id();
}
return QString();
}
QString GioLister::FindUniqueIdByVolume(GVolume *volume) const {
@ -509,7 +535,7 @@ void GioLister::MountUnmountFinished(GObject *object, GAsyncResult *result, gpoi
void GioLister::UnmountDevice(const QString &id) {
QMutexLocker l(&mutex_);
if (!devices_.contains(id)) return;
if (!devices_.contains(id) || !devices_[id].mount || devices_[id].volume_root_uri.startsWith("mtp://")) return;
const DeviceInfo &info = devices_[id];
@ -537,7 +563,7 @@ void GioLister::UpdateDeviceFreeSpace(const QString &id) {
{
QMutexLocker l(&mutex_);
if (!devices_.contains(id)) return;
if (!devices_.contains(id) || !devices_[id].mount || devices_[id].volume_root_uri.startsWith("mtp://")) return;
DeviceInfo &device_info = devices_[id];
@ -563,10 +589,11 @@ void GioLister::UpdateDeviceFreeSpace(const QString &id) {
bool GioLister::DeviceNeedsMount(const QString &id) {
QMutexLocker l(&mutex_);
return devices_.contains(id) && !devices_[id].mount;
return devices_.contains(id) && !devices_[id].mount && !devices_[id].volume_root_uri.startsWith("mtp://");
}
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;

View File

@ -47,7 +47,7 @@ class DeviceManager;
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_thread_(new QThread()),
loader_(nullptr),
db_(nullptr) {}
@ -68,7 +68,13 @@ bool GPodDevice::Init() {
}
GPodDevice::~GPodDevice() {}
GPodDevice::~GPodDevice() {
if (loader_) {
loader_thread_->exit();
loader_->deleteLater();
loader_thread_->deleteLater();
}
}
void GPodDevice::ConnectAsync() {

View File

@ -88,7 +88,7 @@ Itdb_iTunesDB *GPodLoader::TryLoad() {
for (GList *tracks = db->tracks; tracks != nullptr; tracks = tracks->next) {
Itdb_Track *track = static_cast<Itdb_Track*>(tracks->data);
Song song;
Song song(Song::Source_Device);
song.InitFromItdb(track, prefix);
song.set_directory_id(1);

View File

@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2019, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -105,3 +106,42 @@ MtpConnection::~MtpConnection() {
if (device_) LIBMTP_Release_Device(device_);
}
bool MtpConnection::GetSupportedFiletypes(QList<Song::FileType> *ret) {
if (!device_) return false;
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::FileType_WAV; break;
case LIBMTP_FILETYPE_MP2:
case LIBMTP_FILETYPE_MP3: *ret << Song::FileType_MPEG; break;
case LIBMTP_FILETYPE_WMA: *ret << Song::FileType_ASF; break;
case LIBMTP_FILETYPE_MP4:
case LIBMTP_FILETYPE_M4A:
case LIBMTP_FILETYPE_AAC: *ret << Song::FileType_MP4; break;
case LIBMTP_FILETYPE_FLAC:
*ret << Song::FileType_FLAC;
*ret << Song::FileType_OggFlac;
break;
case LIBMTP_FILETYPE_OGG:
*ret << Song::FileType_OggVorbis;
*ret << Song::FileType_OggSpeex;
*ret << Song::FileType_OggFlac;
break;
default:
qLog(Error) << "Unknown MTP file format" << LIBMTP_Get_Filetype_Description(LIBMTP_filetype_t(list[i]));
break;
}
}
free(list);
return true;
}

View File

@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2019, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -24,18 +25,23 @@
#include "config.h"
#include <stdbool.h>
#include <memory>
#include <libmtp.h>
#include <QtGlobal>
#include <QList>
#include <QUrl>
class MtpConnection {
#include "core/song.h"
class MtpConnection : public QObject, public std::enable_shared_from_this<MtpConnection> {
public:
MtpConnection(const QUrl &url);
~MtpConnection();
bool is_valid() const { return device_; }
LIBMTP_mtpdevice_t *device() const { return device_; }
bool GetSupportedFiletypes(QList<Song::FileType> *ret);
private:
Q_DISABLE_COPY(MtpConnection);
@ -43,4 +49,4 @@ private:
LIBMTP_mtpdevice_t *device_;
};
#endif // MTPCONNECTION_H
#endif // MTPCONNECTION_H

View File

@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2019, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -50,7 +51,7 @@ class DeviceManager;
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) {
: ConnectedDevice(url, lister, unique_id, manager, app, database_id, first_time), loader_thread_(new QThread()), loader_(nullptr) {
if (!sInitialisedLibMTP) {
LIBMTP_Init();
@ -59,7 +60,15 @@ MtpDevice::MtpDevice(const QUrl &url, DeviceLister *lister, const QString &uniqu
}
MtpDevice::~MtpDevice() {}
MtpDevice::~MtpDevice() {
if (loader_) {
loader_thread_->exit();
loader_->deleteLater();
loader_ = nullptr;
db_busy_.unlock();
loader_thread_->deleteLater();
}
}
bool MtpDevice::Init() {
@ -79,6 +88,12 @@ bool MtpDevice::Init() {
}
void MtpDevice::NewConnection() {
connection_.reset(new MtpConnection(url_));
}
void MtpDevice::ConnectAsync() {
db_busy_.lock();
@ -96,7 +111,9 @@ void MtpDevice::LoadFinished(bool success) {
}
void MtpDevice::LoaderError(const QString& message) { app_->AddError(message); }
void MtpDevice::LoaderError(const QString& message) {
app_->AddError(message);
}
bool MtpDevice::StartCopy(QList<Song::FileType> *supported_types) {
@ -104,7 +121,8 @@ bool MtpDevice::StartCopy(QList<Song::FileType> *supported_types) {
db_busy_.lock();
// Connect to the device
connection_.reset(new MtpConnection(url_));
if (!connection_.get() || !connection_->is_valid()) NewConnection();
if (!connection_.get() || !connection_->is_valid()) return false;
// Did the caller want a list of supported types?
if (supported_types) {
@ -129,7 +147,7 @@ static int ProgressCallback(uint64_t const sent, uint64_t const total, void cons
bool MtpDevice::CopyToStorage(const CopyJob &job) {
if (!connection_->is_valid()) return false;
if (!connection_.get() || !connection_->is_valid()) return false;
// Convert metadata
LIBMTP_track_t track;
@ -140,9 +158,11 @@ bool MtpDevice::CopyToStorage(const CopyJob &job) {
if (ret != 0) return false;
// Add it to our CollectionModel
Song metadata_on_device;
Song metadata_on_device(Song::Source_Device);
metadata_on_device.InitFromMTP(&track, url_.host());
metadata_on_device.set_directory_id(1);
metadata_on_device.set_artist(metadata_on_device.effective_albumartist());
metadata_on_device.set_albumartist("");
songs_to_add_ << metadata_on_device;
// Remove the original if requested
@ -164,8 +184,6 @@ void MtpDevice::FinishCopy(bool success) {
songs_to_add_.clear();
songs_to_remove_.clear();
connection_.reset();
db_busy_.unlock();
ConnectedDevice::FinishCopy(success);
@ -176,6 +194,8 @@ void MtpDevice::StartDelete() { StartCopy(nullptr); }
bool MtpDevice::DeleteFromStorage(const DeleteJob &job) {
if (!connection_.get() || !connection_->is_valid()) return false;
// Extract the ID from the song's URL
QString filename = job.metadata_.url().path();
filename.remove('/');
@ -201,6 +221,7 @@ 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;

View File

@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2019, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -40,6 +41,7 @@
class Application;
class DeviceLister;
class DeviceManager;
class DeviceConnection;
class MtpConnection;
class MtpLoader;
struct LIBMTP_mtpdevice_struct;
@ -54,6 +56,7 @@ class MtpDevice : public ConnectedDevice {
static QStringList url_schemes() { return QStringList() << "mtp" << "gphoto2"; }
bool Init();
void NewConnection();
void ConnectAsync();
bool GetSupportedFiletypes(QList<Song::FileType>* ret);
@ -68,6 +71,8 @@ class MtpDevice : public ConnectedDevice {
bool DeleteFromStorage(const DeleteJob& job);
void FinishDelete(bool success);
MtpConnection *connection() { return connection_.get(); }
private slots:
void LoadFinished(bool success);
void LoaderError(const QString& message);
@ -87,7 +92,8 @@ class MtpDevice : public ConnectedDevice {
SongList songs_to_add_;
SongList songs_to_remove_;
std::unique_ptr<MtpConnection> connection_;
std::shared_ptr<MtpConnection> connection_;
};
#endif // MTPDEVICE_H

View File

@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2019, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -27,17 +28,19 @@
#include "core/taskmanager.h"
#include "core/song.h"
#include "core/logging.h"
#include "collection/collectionbackend.h"
#include "connecteddevice.h"
#include "mtpdevice.h"
#include "mtpconnection.h"
#include "mtploader.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),
connection_(nullptr) {
device_(device) {
original_thread_ = thread();
}
@ -61,23 +64,28 @@ void MtpLoader::LoadDatabase() {
bool MtpLoader::TryLoad() {
MtpConnection dev(url_);
if (!dev.is_valid()) {
MtpDevice *device = dynamic_cast<MtpDevice*>(device_.get()); // FIXME
if (!device->connection() || !device->connection()->is_valid())
device->NewConnection();
if (!device->connection() || !device->connection()->is_valid()) {
emit Error(tr("Error connecting MTP device %1").arg(url_.toString()));
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);
LIBMTP_track_t* tracks = LIBMTP_Get_Tracklisting_With_Callback(device->connection()->device(), nullptr, nullptr);
while (tracks) {
LIBMTP_track_t *track = tracks;
Song song;
Song song(Song::Source_Device);
song.InitFromMTP(track, url_.host());
song.set_directory_id(1);
songs << song;
if (song.is_valid() && !song.artist().isEmpty() && !song.title().isEmpty()) {
song.set_directory_id(1);
songs << song;
}
tracks = tracks->next;
LIBMTP_destroy_track_t(track);
}

View File

@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2019, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -34,6 +35,7 @@
class TaskManager;
class CollectionBackend;
class ConnectedDevice;
class MtpDevice;
class MtpConnection;
class MtpLoader : public QObject {
@ -57,13 +59,13 @@ class MtpLoader : public QObject {
bool TryLoad();
private:
std::shared_ptr<ConnectedDevice> device_;
QThread *original_thread_;
QUrl url_;
TaskManager *task_manager_;
CollectionBackend *backend_;
MtpConnection *connection_;
std::shared_ptr<ConnectedDevice> device_;
std::shared_ptr<MtpConnection> connection_;
QThread *original_thread_;
};
#endif // MTPLOADER_H

View File

@ -125,10 +125,11 @@ QList<QUrl> Udisks2Lister::MakeDeviceUrls(const QString &id) {
QList<QUrl> ret;
if (!device_data_.contains(id)) return ret;
// Special case for Apple
if(id.contains("iPod")) {
ret << MakeUrlFromLocalPath(device_data_[id].mount_paths.at(0));
} else {
ret << QUrl::fromLocalFile(device_data_[id].mount_paths.at(0));
if (id.contains("iPod")) {
ret << MakeUrlFromLocalPath(device_data_[id].mount_paths.at(0));
}
else {
ret << QUrl::fromLocalFile(device_data_[id].mount_paths.at(0));
}
return ret;
}

View File

@ -79,6 +79,13 @@ Organise::Organise(TaskManager *task_manager, std::shared_ptr<MusicStorage> dest
}
Organise::~Organise() {
if (thread_) {
thread_->quit();
thread_->deleteLater();
}
}
void Organise::Start() {
if (thread_) return;
@ -95,6 +102,7 @@ void Organise::Start() {
moveToThread(thread_);
thread_->start();
}
void Organise::ProcessSomeFiles() {

View File

@ -62,6 +62,7 @@ class Organise : public QObject {
typedef QList<NewSongInfo> NewSongInfoList;
Organise(TaskManager *task_manager, std::shared_ptr<MusicStorage> destination, const OrganiseFormat &format, bool copy, bool overwrite, bool mark_as_listened, bool albumcover, const NewSongInfoList &songs, bool eject_after);
~Organise();
static const int kBatchSize;
#ifdef HAVE_GSTREAMER