1747 lines
56 KiB
C++
1747 lines
56 KiB
C++
/*
|
|
* Cantata
|
|
*
|
|
* Copyright (c) 2011-2014 Craig Drummond <craig.p.drummond@gmail.com>
|
|
*
|
|
* ----
|
|
*
|
|
* This program 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 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program 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 this program; see the file COPYING. If not, write to
|
|
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
* Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#include "mtpdevice.h"
|
|
#include "models/musiclibrarymodel.h"
|
|
#include "models/musiclibraryitemsong.h"
|
|
#include "models/musiclibraryitemalbum.h"
|
|
#include "models/musiclibraryitemartist.h"
|
|
#include "models/musiclibraryitemroot.h"
|
|
#include "models/dirviewmodel.h"
|
|
#include "devicepropertiesdialog.h"
|
|
#include "devicepropertieswidget.h"
|
|
#include "gui/covers.h"
|
|
#include "mpd/song.h"
|
|
#include "encoders.h"
|
|
#include "transcodingjob.h"
|
|
#include "support/utils.h"
|
|
#include "mpd/mpdparseutils.h"
|
|
#include "mpd/mpdconnection.h"
|
|
#include "support/localize.h"
|
|
#include "filejob.h"
|
|
#include "support/configuration.h"
|
|
#include "support/thread.h"
|
|
#include <QTimer>
|
|
#include <QDir>
|
|
#include <QTemporaryFile>
|
|
#ifdef ENABLE_KDE_SUPPORT
|
|
#include <KDE/KMimeType>
|
|
#include <solid/genericinterface.h>
|
|
#else
|
|
#include "solid-lite/genericinterface.h"
|
|
#endif
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
//#define TIME_MTP_OPERATIONS
|
|
#ifdef TIME_MTP_OPERATIONS
|
|
#include <QDebug>
|
|
#include <QElapsedTimer>
|
|
#endif
|
|
|
|
// Enable the following #define to have Cantata attempt to ascertain the AlbumArtist tag by
|
|
// looking at the file path
|
|
#define MTP_FAKE_ALBUMARTIST_SUPPORT
|
|
|
|
// Enable the following #define to have Cantata attempt to ascertain the tracks's track number
|
|
// from its filename. This will only be done if the device returns '0' as the track number.
|
|
#define MTP_TRACKNUMBER_FROM_FILENAME
|
|
|
|
static const QLatin1String constMtpDefaultCover("AlbumArt.jpg");
|
|
static const uint32_t constRootFolder=0xffffffffU;
|
|
static const QString constMusicFolder=QLatin1String("Music");
|
|
|
|
static int progressMonitor(uint64_t const processed, uint64_t const total, void const * const data)
|
|
{
|
|
const MtpConnection *con=static_cast<const MtpConnection *>(data);
|
|
const_cast<MtpConnection *>(con)->emitProgress((int)(((processed*1.0)/(total*1.0)*100.0)+0.5));
|
|
return con->abortWasRequested() ? -1 : 0;
|
|
}
|
|
|
|
static int trackListMonitor(uint64_t const processed, uint64_t const total, void const * const data)
|
|
{
|
|
const MtpConnection *con=static_cast<const MtpConnection *>(data);
|
|
const_cast<MtpConnection *>(con)->trackListProgress(((processed*100.0)/(total*1.0))+0.5);
|
|
return con->abortWasRequested() ? -1 : 0;
|
|
}
|
|
|
|
static uint16_t fileReceiver(void *params, void *priv, uint32_t sendlen, unsigned char *data, uint32_t *putlen)
|
|
{
|
|
Q_UNUSED(params)
|
|
QByteArray *byteData=static_cast<QByteArray *>(priv);
|
|
(*byteData)+=QByteArray((char *)data, (int)sendlen);
|
|
*putlen = sendlen;
|
|
return LIBMTP_HANDLER_RETURN_OK;
|
|
}
|
|
|
|
MtpConnection::MtpConnection(unsigned int bus, unsigned int dev, bool aaSupport)
|
|
: device(0)
|
|
#ifdef MTP_CLEAN_ALBUMS
|
|
, albums(0)
|
|
#endif
|
|
, library(0)
|
|
, lastListPercent(-1)
|
|
, abortRequested(false)
|
|
, busNum(bus)
|
|
, devNum(dev)
|
|
, supprtAlbumArtistTag(aaSupport)
|
|
{
|
|
size=0;
|
|
used=0;
|
|
LIBMTP_Init();
|
|
thread=new Thread(metaObject()->className());
|
|
moveToThread(thread);
|
|
thread->start();
|
|
}
|
|
|
|
MtpConnection::~MtpConnection()
|
|
{
|
|
stop();
|
|
}
|
|
|
|
MusicLibraryItemRoot * MtpConnection::takeLibrary()
|
|
{
|
|
MusicLibraryItemRoot *lib=library;
|
|
library=0;
|
|
return lib;
|
|
}
|
|
|
|
void MtpConnection::emitProgress(int percent)
|
|
{
|
|
emit progress(percent);
|
|
}
|
|
|
|
void MtpConnection::trackListProgress(int percent)
|
|
{
|
|
if (percent!=lastListPercent) {
|
|
lastListPercent=percent;
|
|
emit updatePercentage(percent);
|
|
}
|
|
}
|
|
|
|
void MtpConnection::connectToDevice()
|
|
{
|
|
#ifdef TIME_MTP_OPERATIONS
|
|
QElapsedTimer timer;
|
|
QElapsedTimer totalTimer;
|
|
timer.start();
|
|
totalTimer.start();
|
|
#endif
|
|
|
|
device=0;
|
|
storage.clear();
|
|
defaultMusicFolder=0;
|
|
LIBMTP_raw_device_t *rawDevices=0;
|
|
int numDev=-1;
|
|
emit statusMessage(i18n("Connecting to device..."));
|
|
if (LIBMTP_ERROR_NONE!=LIBMTP_Detect_Raw_Devices(&rawDevices, &numDev) || numDev<=0) {
|
|
emit statusMessage(i18n("No devices found"));
|
|
return;
|
|
}
|
|
#ifdef TIME_MTP_OPERATIONS
|
|
qWarning() << "Connect to device:" << timer.elapsed();
|
|
timer.restart();
|
|
#endif
|
|
|
|
LIBMTP_mtpdevice_t *mptDev=0;
|
|
for (int i = 0; i < numDev; i++) {
|
|
if (0!=busNum && 0!=devNum) {
|
|
if (rawDevices[i].bus_location==busNum && rawDevices[i].devnum==devNum) {
|
|
#ifdef ENABLE_UNCACHED_MTP
|
|
mptDev = LIBMTP_Open_Raw_Device_Uncached(&rawDevices[i]);
|
|
#else
|
|
mptDev = LIBMTP_Open_Raw_Device(&rawDevices[i]);
|
|
#endif
|
|
break;
|
|
}
|
|
} else {
|
|
#ifdef ENABLE_UNCACHED_MTP
|
|
if ((mptDev = LIBMTP_Open_Raw_Device_Uncached(&rawDevices[i]))) {
|
|
break;
|
|
}
|
|
#else
|
|
if ((mptDev = LIBMTP_Open_Raw_Device(&rawDevices[i]))) {
|
|
break;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#ifdef TIME_MTP_OPERATIONS
|
|
qWarning() << "Open raw device:" << timer.elapsed();
|
|
timer.restart();
|
|
#endif
|
|
|
|
size=0;
|
|
used=0;
|
|
device=mptDev;
|
|
updateStorage();
|
|
#ifdef TIME_MTP_OPERATIONS
|
|
qWarning() << "Update storage:" << timer.elapsed();
|
|
#endif
|
|
|
|
free(rawDevices);
|
|
if (!device) {
|
|
emit statusMessage(i18n("No devices found"));
|
|
#ifdef TIME_MTP_OPERATIONS
|
|
qWarning() << "TOTAL connect:" <<totalTimer.elapsed();
|
|
#endif
|
|
return;
|
|
}
|
|
char *ser=LIBMTP_Get_Serialnumber(device);
|
|
if (ser) {
|
|
emit deviceDetails(QString::fromUtf8(ser));
|
|
delete ser;
|
|
} else {
|
|
emit deviceDetails(QString());
|
|
}
|
|
|
|
defaultMusicFolder=device->default_music_folder;
|
|
emit statusMessage(i18n("Connected to device"));
|
|
#ifdef TIME_MTP_OPERATIONS
|
|
qWarning() << "TOTAL connect:" <<totalTimer.elapsed();
|
|
#endif
|
|
}
|
|
|
|
void MtpConnection::disconnectFromDevice(bool showStatus)
|
|
{
|
|
if (device) {
|
|
destroyData();
|
|
LIBMTP_Release_Device(device);
|
|
device=0;
|
|
if (showStatus) {
|
|
emit statusMessage(i18n("Disconnected from device"));
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef MTP_FAKE_ALBUMARTIST_SUPPORT
|
|
struct MtpAlbum {
|
|
uint32_t folder;
|
|
QSet<QString> artists;
|
|
QList<MusicLibraryItemSong *> songs;
|
|
};
|
|
struct MtpFolder {
|
|
MtpFolder(const QString &ar=QString(), const QString &al=QString()) : artist(ar), album(al) { }
|
|
QString artist;
|
|
QString album;
|
|
};
|
|
#endif
|
|
|
|
struct Path {
|
|
Path() : storage(0), parent(0), id(0) { }
|
|
bool ok() const { return 0!=storage && 0!=parent && 0!=id; }
|
|
uint32_t storage;
|
|
uint32_t parent;
|
|
uint32_t id;
|
|
QString path;
|
|
};
|
|
|
|
static QString encodePath(LIBMTP_track_t *track, const QString &path, const QString &store)
|
|
{
|
|
return QChar('{')+QString::number(track->storage_id)+QChar('/')+QString::number(track->parent_id)+QChar('/')+QString::number(track->item_id)+
|
|
(store.isEmpty() ? QString() : (QChar('/')+store))+QChar('}')+path;
|
|
}
|
|
|
|
static Path decodePath(const QString &path)
|
|
{
|
|
Path p;
|
|
if (path.startsWith(QChar('{')) && path.contains(QChar('}'))) {
|
|
int end=path.indexOf(QChar('}'));
|
|
QStringList details=path.mid(1, end-1).split(QChar('/'));
|
|
if (details.count()>=3) {
|
|
p.storage=details.at(0).toUInt();
|
|
p.parent=details.at(1).toUInt();
|
|
p.id=details.at(2).toUInt();
|
|
}
|
|
p.path=path.mid(end+1);
|
|
} else {
|
|
p.path=path;
|
|
}
|
|
|
|
return p;
|
|
}
|
|
|
|
void MtpConnection::updateLibrary(const DeviceOptions &opts)
|
|
{
|
|
if (!isConnected()) {
|
|
connectToDevice();
|
|
}
|
|
|
|
destroyData();
|
|
|
|
if (!isConnected()) {
|
|
emit libraryUpdated();
|
|
return;
|
|
}
|
|
|
|
#ifdef TIME_MTP_OPERATIONS
|
|
QElapsedTimer timer;
|
|
QElapsedTimer totalTimer;
|
|
timer.start();
|
|
totalTimer.start();
|
|
#endif
|
|
|
|
library = new MusicLibraryItemRoot;
|
|
emit statusMessage(i18n("Updating folders..."));
|
|
#ifdef ENABLE_UNCACHED_MTP
|
|
updateFilesAndFolders();
|
|
if (abortRequested) {
|
|
return;
|
|
}
|
|
#else
|
|
updateFolders();
|
|
#endif
|
|
#ifdef TIME_MTP_OPERATIONS
|
|
qWarning() << "Folder update:" << timer.elapsed();
|
|
timer.restart();
|
|
#endif
|
|
if (folderMap.isEmpty()) {
|
|
destroyData();
|
|
emit libraryUpdated();
|
|
return;
|
|
}
|
|
#ifndef ENABLE_UNCACHED_MTP
|
|
emit statusMessage(i18n("Updating files..."));
|
|
updateFiles();
|
|
#ifdef TIME_MTP_OPERATIONS
|
|
qWarning() << "Files update:" << timer.elapsed();
|
|
timer.restart();
|
|
#endif
|
|
#endif
|
|
#ifdef MTP_CLEAN_ALBUMS
|
|
updateAlbums();
|
|
#ifdef TIME_MTP_OPERATIONS
|
|
qWarning() << "Clean albums:" << timer.elapsed();
|
|
timer.restart();
|
|
#endif
|
|
#endif
|
|
emit statusMessage(i18n("Updating tracks..."));
|
|
lastListPercent=-1;
|
|
LIBMTP_track_t *tracks=LIBMTP_Get_Tracklisting_With_Callback(device, &trackListMonitor, this);
|
|
QMap<uint32_t, Folder>::ConstIterator folderEnd=folderMap.constEnd();
|
|
QList<Storage>::Iterator store=storage.begin();
|
|
QList<Storage>::Iterator storeEnd=storage.end();
|
|
QMap<int, QString> storageNames;
|
|
for (; store!=storeEnd; ++store) {
|
|
setMusicFolder(*store);
|
|
storageNames[(*store).id]=(*store).description;
|
|
}
|
|
if (abortRequested) {
|
|
return;
|
|
}
|
|
|
|
MusicLibraryItemArtist *artistItem = 0;
|
|
MusicLibraryItemAlbum *albumItem = 0;
|
|
#ifdef MTP_FAKE_ALBUMARTIST_SUPPORT
|
|
QMap<QString, MtpAlbum> albumMap;
|
|
QMap<uint32_t, MtpFolder> folders;
|
|
bool getAlbumArtistFromPath=opts.scheme.startsWith(DeviceOptions::constAlbumArtist+QChar('/')+DeviceOptions::constAlbumTitle+QChar('/'));
|
|
#endif
|
|
#ifdef TIME_MTP_OPERATIONS
|
|
qWarning() << "Tracks update:" << timer.elapsed();
|
|
timer.restart();
|
|
#endif
|
|
while (tracks) {
|
|
if (abortRequested) {
|
|
return;
|
|
}
|
|
LIBMTP_track_t *track=tracks;
|
|
tracks=tracks->next;
|
|
|
|
QMap<uint32_t, Folder>::ConstIterator folder=folderMap.find(track->parent_id);
|
|
if (folder==folderEnd) {
|
|
// We only care about tracks in the music folder...
|
|
LIBMTP_destroy_track_t(track);
|
|
continue;
|
|
}
|
|
Song s;
|
|
QString trackFilename=QString::fromUtf8(track->filename);
|
|
s.id=track->item_id;
|
|
s.file=encodePath(track, folder.value().path+trackFilename, storageNames.count()>1 ? storageNames[track->storage_id] : QString());
|
|
s.album=QString::fromUtf8(track->album);
|
|
s.artist=QString::fromUtf8(track->artist);
|
|
s.albumartist=s.artist; // TODO: ALBUMARTIST: Read from 'track' when libMTP supports album artist!
|
|
s.composer=QString::fromUtf8(track->composer);
|
|
s.year=QString::fromUtf8(track->date).mid(0, 4).toUInt();
|
|
s.title=QString::fromUtf8(track->title);
|
|
s.genre=QString::fromUtf8(track->genre);
|
|
s.track=track->tracknumber;
|
|
s.time=(track->duration/1000.0)+0.5;
|
|
s.size=track->filesize;
|
|
s.fillEmptyFields();
|
|
#ifdef MTP_FAKE_ALBUMARTIST_SUPPORT
|
|
if (getAlbumArtistFromPath) {
|
|
QStringList folderParts=(*folder).path.split('/', QString::SkipEmptyParts);
|
|
if (folderParts.length()>=3) {
|
|
// Path should be "Music/${AlbumArtist}/${Album}"
|
|
int artistPath=1;
|
|
int albumPath=2;
|
|
// BubbleUPNP will download to "Music/Artist/${AlbumArtist}/${Album}" if it is configured to
|
|
// download to "Music" and "Preserve folder structure" (At least this is the folder structure
|
|
// from MiniDLNA...)
|
|
if (folderParts.length()>=4 && QLatin1String("Artist")==folderParts.at(1)) {
|
|
artistPath++;
|
|
albumPath++;
|
|
}
|
|
MtpFolder folder(folderParts.at(artistPath), folderParts.at(albumPath));
|
|
folders.insert(track->parent_id, folder);
|
|
if (folder.album==s.album && Song::isVariousArtists(folder.artist)) {
|
|
s.albumartist=folder.artist;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
#ifdef MTP_TRACKNUMBER_FROM_FILENAME
|
|
if (0==s.track) {
|
|
int space=trackFilename.indexOf(' ');
|
|
if (space>0 && space<=3) {
|
|
s.track=trackFilename.mid(0, space).toInt();
|
|
}
|
|
}
|
|
#endif
|
|
if (!artistItem || (supprtAlbumArtistTag ? s.artistOrComposer()!=artistItem->data() : s.artist!=artistItem->data())) {
|
|
artistItem = library->artist(s);
|
|
}
|
|
if (!albumItem || albumItem->parentItem()!=artistItem || s.albumName()!=albumItem->data()) {
|
|
albumItem = artistItem->album(s);
|
|
}
|
|
MusicLibraryItemSong *songItem = new MusicLibraryItemSong(s, albumItem);
|
|
const QSet<QString> &songGenres=songItem->allGenres();
|
|
albumItem->append(songItem);
|
|
albumItem->addGenres(songGenres);
|
|
artistItem->addGenres(songGenres);
|
|
library->addGenres(songGenres);
|
|
|
|
#ifdef MTP_FAKE_ALBUMARTIST_SUPPORT
|
|
// Store AlbumName->Artists/Songs mapping
|
|
MtpAlbum &al=albumMap[s.album];
|
|
al.artists.insert(s.artist);
|
|
al.songs.append(songItem);
|
|
al.folder=track->parent_id;
|
|
#endif
|
|
LIBMTP_destroy_track_t(track);
|
|
}
|
|
|
|
while (tracks) {
|
|
LIBMTP_track_t *track=tracks;
|
|
tracks=tracks->next;
|
|
LIBMTP_destroy_track_t(track);
|
|
}
|
|
|
|
#ifdef TIME_MTP_OPERATIONS
|
|
qWarning() << "Tracks parse:" << timer.elapsed();
|
|
timer.restart();
|
|
#endif
|
|
#ifdef MTP_FAKE_ALBUMARTIST_SUPPORT
|
|
// Use Album map to determine 'AlbumArtist' tag for various artist albums, and
|
|
// albums that have tracks where artist is set to '${artist} and somebodyelse'
|
|
QMap<QString, MtpAlbum>::ConstIterator it=albumMap.constBegin();
|
|
QMap<QString, MtpAlbum>::ConstIterator end=albumMap.constEnd();
|
|
for (; it!=end; ++it) {
|
|
if (abortRequested) {
|
|
return;
|
|
}
|
|
if ((*it).artists.count()>1) {
|
|
QSet<quint16> tracks;
|
|
QString shortestArtist;
|
|
bool duplicateTrackNumbers=false;
|
|
foreach (MusicLibraryItemSong *s, (*it).songs) {
|
|
if (tracks.contains(s->track())) {
|
|
duplicateTrackNumbers=true;
|
|
break;
|
|
} else {
|
|
if (shortestArtist.isEmpty() || s->song().artist.length()<shortestArtist.length()) {
|
|
shortestArtist=s->song().artist;
|
|
}
|
|
tracks.insert(s->track());
|
|
}
|
|
}
|
|
// If an album has mutiple tracks with the same track number, then we probably have X albums
|
|
// by X artists - in which case we proceeed no further.
|
|
if (!duplicateTrackNumbers) {
|
|
MtpFolder &f=folders[(*it).folder];
|
|
// Now, check to see if all artists contain 'shortestArtist'. If so then use 'shortestArtist' as the album
|
|
// artist. This is probably due to songs which have artist set to '${artist} and somebodyelse'
|
|
QString albumArtist=shortestArtist;
|
|
foreach (const QString &artist, (*it).artists) {
|
|
if (!artist.contains(shortestArtist)) {
|
|
// Found an artist that did not contain 'shortestArtist', so use 'Various Artists' for album artist
|
|
albumArtist=!f.artist.isEmpty() && f.album==it.key() ? f.artist : Song::variousArtists();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Now move songs to correct artist/album...
|
|
foreach (MusicLibraryItemSong *s, (*it).songs) {
|
|
if (s->song().albumartist==albumArtist) {
|
|
continue;
|
|
}
|
|
Song song=s->song();
|
|
song.albumartist=albumArtist;
|
|
artistItem=library->artist(song);
|
|
albumItem=artistItem->album(song);
|
|
MusicLibraryItemSong *songItem = new MusicLibraryItemSong(song, albumItem);
|
|
albumItem->append(songItem);
|
|
albumItem->updateGenres();
|
|
artistItem->updateGenres();
|
|
MusicLibraryItemAlbum *prevAlbum=(MusicLibraryItemAlbum *)s->parentItem();
|
|
prevAlbum->remove(s);
|
|
if (0==prevAlbum->childCount()) {
|
|
// Album no longer has any songs, so remove!
|
|
MusicLibraryItemArtist *prevArtist=(MusicLibraryItemArtist *)prevAlbum->parentItem();
|
|
prevArtist->remove(prevAlbum);
|
|
if (0==prevArtist->childCount()) {
|
|
// Artist no longer has any albums, so remove!
|
|
library->remove(prevArtist);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#ifdef TIME_MTP_OPERATIONS
|
|
qWarning() << "AlbumArtist detection:" << timer.elapsed();
|
|
timer.restart();
|
|
#endif
|
|
#endif
|
|
if (abortRequested) {
|
|
return;
|
|
}
|
|
library->applyGrouping();
|
|
#ifdef TIME_MTP_OPERATIONS
|
|
qWarning() << "Grouping:" << timer.elapsed();
|
|
qWarning() << "TOTAL update:" <<totalTimer.elapsed();
|
|
#endif
|
|
emit libraryUpdated();
|
|
}
|
|
|
|
void MtpConnection::setMusicFolder(Storage &store)
|
|
{
|
|
if (0==store.musicFolderId) {
|
|
store.musicFolderId=getFolder(constMusicFolder+QLatin1Char('/'), store.id);
|
|
if (0==store.musicFolderId) {
|
|
store.musicFolderId=createFolder(constMusicFolder, constMusicFolder+QLatin1Char('/'), 0, store.id);
|
|
}
|
|
if (0!=store.musicFolderId) {
|
|
store.musicPath=getPath(store.musicFolderId);
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef ENABLE_UNCACHED_MTP
|
|
void MtpConnection::updateFilesAndFolders()
|
|
{
|
|
folderMap.clear();
|
|
foreach (const Storage st, storage) {
|
|
if (abortRequested) {
|
|
return;
|
|
}
|
|
listFolder(st.id, constRootFolder, 0);
|
|
}
|
|
}
|
|
|
|
void MtpConnection::listFolder(uint32_t storage, uint32_t parentDir, Folder *f)
|
|
{
|
|
LIBMTP_file_t *files=LIBMTP_Get_Files_And_Folders(device, storage, parentDir);
|
|
|
|
while (files && !abortRequested) {
|
|
LIBMTP_file_t *file=files;
|
|
files = files->next;
|
|
|
|
QString name=QString::fromUtf8(file->filename);
|
|
if (LIBMTP_FILETYPE_FOLDER==file->filetype) {
|
|
bool isMusic=constRootFolder!=parentDir || 0==name.compare(constMusicFolder, Qt::CaseInsensitive);
|
|
if (isMusic) {
|
|
QMap<uint32_t, Folder>::ConstIterator it=folderMap.find(file->parent_id);
|
|
QString path;
|
|
if (it!=folderMap.constEnd()) {
|
|
path=it.value().path+name+QLatin1Char('/');
|
|
} else {
|
|
path=name+QLatin1Char('/');
|
|
}
|
|
|
|
QMap<uint32_t, Folder>::iterator entry=folderMap.insert(file->item_id, Folder(path, file->item_id, file->parent_id, file->storage_id));
|
|
if (folderMap.contains(file->parent_id)) {
|
|
folderMap[file->parent_id].folders.insert(file->item_id);
|
|
}
|
|
listFolder(storage, file->item_id, &(entry.value()));
|
|
}
|
|
} else if (f && constRootFolder!=parentDir) {
|
|
if (name.endsWith(".jpg", Qt::CaseInsensitive) || name.endsWith(".png", Qt::CaseInsensitive) || QLatin1String("albumart.pamp")==name) {
|
|
f->covers.insert(file->item_id, File(name, file->filesize, file->item_id));
|
|
} else {
|
|
f->files.insert(file->item_id, File(name, file->filesize, file->item_id));
|
|
}
|
|
}
|
|
LIBMTP_destroy_file_t(file);
|
|
}
|
|
}
|
|
#else
|
|
void MtpConnection::updateFolders()
|
|
{
|
|
folderMap.clear();
|
|
LIBMTP_folder_t *folders=LIBMTP_Get_Folder_List(device);
|
|
parseFolder(folders);
|
|
if (folders) {
|
|
// LIBMTP_destroy_folder_t is recursive
|
|
LIBMTP_destroy_folder_t(folders);
|
|
}
|
|
}
|
|
|
|
void MtpConnection::updateFiles()
|
|
{
|
|
LIBMTP_file_t *files=LIBMTP_Get_Filelisting_With_Callback(device, 0, 0);
|
|
QSet<uint32_t> folders=folderMap.keys().toSet();
|
|
while (files) {
|
|
LIBMTP_file_t *file=files;
|
|
files=files->next;
|
|
|
|
if (folders.contains(file->parent_id)) {
|
|
Folder &folder=folderMap[file->parent_id];
|
|
if (LIBMTP_FILETYPE_FOLDER==file->filetype) {
|
|
folder.folders.insert(file->item_id);
|
|
} else {
|
|
QString name=QString::fromUtf8(file->filename);
|
|
if (name.endsWith(".jpg", Qt::CaseInsensitive) || name.endsWith(".png", Qt::CaseInsensitive) || QLatin1String("albumart.pamp")==name) {
|
|
folder.covers.insert(file->item_id, File(name, file->filesize, file->item_id));
|
|
} else {
|
|
folder.files.insert(file->item_id, File(name, file->filesize, file->item_id));
|
|
}
|
|
}
|
|
}
|
|
LIBMTP_destroy_file_t(file);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void MtpConnection::updateStorage()
|
|
{
|
|
uint64_t sizeCalc=0;
|
|
uint64_t usedCalc=0;
|
|
if (device && LIBMTP_ERROR_NONE==LIBMTP_Get_Storage(device, LIBMTP_STORAGE_SORTBY_MAXSPACE)) {
|
|
LIBMTP_devicestorage_struct *s=device->storage;
|
|
while (s) {
|
|
QString volumeIdentifier=QString::fromUtf8(s->VolumeIdentifier);
|
|
QList<Storage>::Iterator it=storage.begin();
|
|
QList<Storage>::Iterator end=storage.end();
|
|
for ( ;it!=end; ++it) {
|
|
if ((*it).volumeIdentifier==volumeIdentifier) {
|
|
// We know about this storage ID, so update its size...
|
|
(*it).size=s->MaxCapacity;
|
|
(*it).used=s->MaxCapacity-s->FreeSpaceInBytes;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (it==end) {
|
|
// Unknown storage ID, so add to list!
|
|
Storage store;
|
|
store.id=s->id;
|
|
store.description=QString::fromUtf8(s->StorageDescription);
|
|
store.volumeIdentifier=QString::fromUtf8(s->VolumeIdentifier);
|
|
store.size=s->MaxCapacity;
|
|
store.used=s->MaxCapacity-s->FreeSpaceInBytes;
|
|
storage.append(store);
|
|
}
|
|
sizeCalc+=s->MaxCapacity;
|
|
usedCalc+=s->MaxCapacity-s->FreeSpaceInBytes;
|
|
s=s->next;
|
|
}
|
|
}
|
|
size=sizeCalc;
|
|
used=usedCalc;
|
|
}
|
|
|
|
QList<DeviceStorage> MtpConnection::getStorageList() const
|
|
{
|
|
QList<DeviceStorage> s;
|
|
QList<Storage>::ConstIterator it=storage.constBegin();
|
|
QList<Storage>::ConstIterator end=storage.constEnd();
|
|
for ( ;it!=end; ++it) {
|
|
DeviceStorage store;
|
|
store.size=(*it).size;
|
|
store.used=(*it).used;
|
|
store.description=(*it).description;
|
|
store.volumeIdentifier=(*it).volumeIdentifier;
|
|
s.append(store);
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
MtpConnection::Storage & MtpConnection::getStorage(const QString &volumeIdentifier)
|
|
{
|
|
QList<Storage>::Iterator first=storage.begin();
|
|
if (!volumeIdentifier.isEmpty()) {
|
|
QList<Storage>::Iterator it=first;
|
|
QList<Storage>::Iterator end=storage.end();
|
|
for ( ;it!=end; ++it) {
|
|
if ((*it).volumeIdentifier==volumeIdentifier) {
|
|
return *it;
|
|
}
|
|
}
|
|
}
|
|
|
|
return *first;
|
|
}
|
|
|
|
MtpConnection::Storage & MtpConnection::getStorage(uint32_t id)
|
|
{
|
|
QList<Storage>::Iterator first=storage.begin();
|
|
QList<Storage>::Iterator it=first;
|
|
QList<Storage>::Iterator end=storage.end();
|
|
for ( ;it!=end; ++it) {
|
|
if ((*it).id==id) {
|
|
return *it;
|
|
}
|
|
}
|
|
|
|
return *first;
|
|
}
|
|
|
|
uint32_t MtpConnection::createFolder(const QString &name, const QString &fullPath, uint32_t parentId, uint32_t storageId)
|
|
{
|
|
char *nameCopy = qstrdup(name.toUtf8().constData());
|
|
uint32_t newFolderId=LIBMTP_Create_Folder(device, nameCopy, parentId, storageId);
|
|
delete nameCopy;
|
|
if (0==newFolderId) {
|
|
return 0;
|
|
}
|
|
|
|
folderMap.insert(newFolderId, Folder(fullPath, newFolderId, parentId, storageId));
|
|
return newFolderId;
|
|
}
|
|
|
|
uint32_t MtpConnection::getFolder(const QString &path, uint32_t storageId)
|
|
{
|
|
QMap<uint32_t, Folder>::ConstIterator it=folderMap.constBegin();
|
|
QMap<uint32_t, Folder>::ConstIterator end=folderMap.constEnd();
|
|
|
|
for (; it!=end; ++it) {
|
|
if (storageId==(*it).storageId && 0==(*it).path.compare(path, Qt::CaseInsensitive)) {
|
|
return (*it).id;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
QString MtpConnection::getPath(uint32_t folderId)
|
|
{
|
|
return folderMap.contains(folderId) ? folderMap[folderId].path : QString();
|
|
}
|
|
|
|
uint32_t MtpConnection::checkFolderStructure(const QStringList &dirs, Storage &store)
|
|
{
|
|
setMusicFolder(store);
|
|
QString path;
|
|
uint32_t parentId=store.musicFolderId;
|
|
|
|
foreach (const QString &d, dirs) {
|
|
path+=d+QChar('/');
|
|
uint32_t folderId=getFolder(path, store.id);
|
|
if (0==folderId) {
|
|
folderId=createFolder(d, path, parentId, store.id);
|
|
if (0==folderId) {
|
|
return parentId;
|
|
} else {
|
|
parentId=folderId;
|
|
}
|
|
} else {
|
|
parentId=folderId;
|
|
}
|
|
}
|
|
return parentId;
|
|
}
|
|
|
|
#ifndef ENABLE_UNCACHED_MTP
|
|
void MtpConnection::parseFolder(LIBMTP_folder_t *folder)
|
|
{
|
|
if (!folder) {
|
|
return;
|
|
}
|
|
|
|
QMap<uint32_t, Folder>::ConstIterator it=folderMap.find(folder->parent_id);
|
|
QString path;
|
|
if (it!=folderMap.constEnd()) {
|
|
path=it.value().path+QString::fromUtf8(folder->name)+QChar('/');
|
|
} else {
|
|
path=QString::fromUtf8(folder->name)+QChar('/');
|
|
}
|
|
bool isMusic=path.startsWith(constMusicFolder+QLatin1Char('/'), Qt::CaseInsensitive);
|
|
if (isMusic) {
|
|
folderMap.insert(folder->folder_id, Folder(path, folder->folder_id, folder->parent_id, folder->storage_id));
|
|
if (folderMap.contains(folder->parent_id)) {
|
|
folderMap[folder->parent_id].folders.insert(folder->folder_id);
|
|
}
|
|
}
|
|
// Only recurse into music folder...
|
|
if (folder->child && isMusic) {
|
|
parseFolder(folder->child);
|
|
}
|
|
if (folder->sibling) {
|
|
parseFolder(folder->sibling);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
static char * createString(const QString &str)
|
|
{
|
|
return str.isEmpty() ? qstrdup("") : qstrdup(str.toUtf8());
|
|
}
|
|
|
|
static LIBMTP_filetype_t mtpFileType(const QString &f)
|
|
{
|
|
#ifdef ENABLE_KDE_SUPPORT
|
|
KMimeType::Ptr mime=KMimeType::findByPath(f);
|
|
|
|
if (mime->is("audio/mpeg")) {
|
|
return LIBMTP_FILETYPE_MP3;
|
|
}
|
|
if (mime->is("audio/ogg")) {
|
|
return LIBMTP_FILETYPE_OGG;
|
|
}
|
|
if (mime->is("audio/x-ms-wma")) {
|
|
return LIBMTP_FILETYPE_WMA;
|
|
}
|
|
if (mime->is("audio/mp4")) {
|
|
return LIBMTP_FILETYPE_M4A; // LIBMTP_FILETYPE_MP4
|
|
}
|
|
if (mime->is("audio/aac")) {
|
|
return LIBMTP_FILETYPE_AAC;
|
|
}
|
|
if (mime->is("audio/flac")) {
|
|
return LIBMTP_FILETYPE_FLAC;
|
|
}
|
|
if (mime->is("audio/x-wav")) {
|
|
return LIBMTP_FILETYPE_WAV;
|
|
}
|
|
if (mime->is("image/jpeg")) {
|
|
return LIBMTP_FILETYPE_JPEG;
|
|
}
|
|
if (mime->is("image/png")) {
|
|
return LIBMTP_FILETYPE_PNG;
|
|
}
|
|
#else
|
|
if (f.endsWith(".mp3", Qt::CaseInsensitive)) {
|
|
return LIBMTP_FILETYPE_MP3;
|
|
}
|
|
if (f.endsWith(".ogg", Qt::CaseInsensitive)) {
|
|
return LIBMTP_FILETYPE_OGG;
|
|
}
|
|
if (f.endsWith(".wma", Qt::CaseInsensitive)) {
|
|
return LIBMTP_FILETYPE_WMA;
|
|
}
|
|
if (f.endsWith(".m4a", Qt::CaseInsensitive)) {
|
|
return LIBMTP_FILETYPE_M4A; // LIBMTP_FILETYPE_MP4
|
|
}
|
|
if (f.endsWith(".aac", Qt::CaseInsensitive)) {
|
|
return LIBMTP_FILETYPE_AAC;
|
|
}
|
|
if (f.endsWith(".flac", Qt::CaseInsensitive)) {
|
|
return LIBMTP_FILETYPE_FLAC;
|
|
}
|
|
if (f.endsWith(".wav", Qt::CaseInsensitive)) {
|
|
return LIBMTP_FILETYPE_WAV;
|
|
}
|
|
if (f.endsWith(".jpg", Qt::CaseInsensitive)) {
|
|
return LIBMTP_FILETYPE_JPEG;
|
|
}
|
|
if (f.endsWith(".png", Qt::CaseInsensitive)) {
|
|
return LIBMTP_FILETYPE_PNG;
|
|
}
|
|
#endif
|
|
return LIBMTP_FILETYPE_UNDEF_AUDIO;
|
|
}
|
|
|
|
static QTemporaryFile * saveImageToTemp(const QImage &img, const QString &name)
|
|
{
|
|
QTemporaryFile *temp=new QTemporaryFile();
|
|
|
|
int index=name.lastIndexOf('.');
|
|
if (index>0) {
|
|
temp=new QTemporaryFile(QDir::tempPath()+"/cantata_XXXXXX"+name.mid(index));
|
|
} else {
|
|
temp=new QTemporaryFile(QDir::tempPath()+"/cantata_XXXXXX");
|
|
}
|
|
img.save(temp);
|
|
temp->close();
|
|
return temp;
|
|
}
|
|
|
|
void MtpConnection::putSong(const Song &s, bool fixVa, const DeviceOptions &opts, bool overwrite, bool copyCover)
|
|
{
|
|
int status=Device::Failed;
|
|
bool fixedVa=false;
|
|
bool embedCoverImage=Device::constEmbedCover==opts.coverName;
|
|
bool copiedCover=false;
|
|
LIBMTP_track_t *meta=0;
|
|
QString destName;
|
|
Storage store=getStorage(opts.volumeId);
|
|
uint32_t folderId=0;
|
|
|
|
copyCover=copyCover && !embedCoverImage && Device::constNoCover!=opts.coverName;
|
|
if (device) {
|
|
meta=LIBMTP_new_track_t();
|
|
meta->item_id=0;
|
|
Song song=s;
|
|
QString fileName=song.file;
|
|
QTemporaryFile *temp=0;
|
|
|
|
if (fixVa || embedCoverImage) {
|
|
// Need to 'workaround' broken various artists handling, and/or embedding cover, so write to a temporary file first...
|
|
temp=Device::copySongToTemp(song);
|
|
if (temp) {
|
|
if (embedCoverImage) {
|
|
Device::embedCover(song.file, song, opts.coverMaxSize);
|
|
}
|
|
if (fixVa && Device::fixVariousArtists(temp->fileName(), song, true)) {
|
|
fixedVa=true;
|
|
}
|
|
song.file=temp->fileName();
|
|
}
|
|
}
|
|
struct stat statBuf;
|
|
if (0==stat(QFile::encodeName(song.file).constData(), &statBuf)) {
|
|
meta->filesize=statBuf.st_size;
|
|
meta->modificationdate=statBuf.st_mtime;
|
|
}
|
|
|
|
// Check if storage has enough space, if not try to find one that does!
|
|
if (store.freeSpace()<meta->filesize) {
|
|
QList<Storage>::Iterator it=storage.begin();
|
|
QList<Storage>::Iterator end=storage.end();
|
|
for ( ;it!=end; ++it) {
|
|
if ((*it).freeSpace()>=meta->filesize) {
|
|
store=*it;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
meta->parent_id=folderId=store.musicFolderId;
|
|
meta->storage_id=store.id;
|
|
destName=store.musicPath+opts.createFilename(song);
|
|
QStringList dirs=destName.split('/', QString::SkipEmptyParts);
|
|
if (dirs.count()>1) {
|
|
destName=dirs.takeLast();
|
|
meta->parent_id=folderId=checkFolderStructure(dirs, store);
|
|
}
|
|
Folder &folder=folderMap[folderId];
|
|
QMap<uint32_t, File>::ConstIterator it=folder.files.constBegin();
|
|
QMap<uint32_t, File>::ConstIterator end=folder.files.constEnd();
|
|
for (; it!=end; ++it) {
|
|
if ((*it).name==destName) {
|
|
if (!overwrite || 0!=LIBMTP_Delete_Object(device, (*it).id)) {
|
|
status=Device::SongExists;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (status!=Device::SongExists) {
|
|
meta->title=createString(song.title);
|
|
meta->artist=createString(song.artist);
|
|
meta->composer=createString(song.composer);
|
|
meta->genre=createString(song.genre);
|
|
meta->album=createString(song.album);
|
|
meta->date=createString(QString().sprintf("%4d0101T0000.0", song.year));
|
|
meta->filename=createString(destName);
|
|
meta->tracknumber=song.track;
|
|
meta->duration=song.time*1000;
|
|
meta->rating=0;
|
|
meta->usecount=0;
|
|
meta->filetype=mtpFileType(song.file);
|
|
meta->next=0;
|
|
switch (LIBMTP_Send_Track_From_File(device, fileName.toUtf8(), meta, &progressMonitor, this)) {
|
|
case LIBMTP_ERROR_NONE: status=Device::Ok; break;
|
|
case LIBMTP_ERROR_STORAGE_FULL: status=Device::NoSpace; break;
|
|
default: status=Device::Failed; break;
|
|
}
|
|
}
|
|
if (temp) {
|
|
// Delete the temp file...
|
|
temp->remove();
|
|
delete temp;
|
|
}
|
|
}
|
|
|
|
if (Device::Ok==status) {
|
|
Folder &folder=folderMap[folderId];
|
|
// LibMTP seems to reset parent_id to 0 - but we NEED the correct value for encodePath
|
|
meta->parent_id=folderId;
|
|
if (copyCover) {
|
|
QMap<uint32_t, File>::ConstIterator it=folder.covers.constBegin();
|
|
QMap<uint32_t, File>::ConstIterator end=folder.covers.constEnd();
|
|
|
|
for (; it!=end; ++it) {
|
|
if (it.value().name==opts.coverName) {
|
|
copiedCover=true;
|
|
copyCover=false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Send cover, as a plain file...
|
|
if (copyCover) {
|
|
QString srcFile;
|
|
Covers::Image coverImage=Covers::self()->getImage(s);
|
|
QTemporaryFile *temp=0;
|
|
if (!coverImage.img.isNull() && !coverImage.fileName.isEmpty()) {
|
|
if (opts.coverMaxSize && (coverImage.img.width()>(int)opts.coverMaxSize || coverImage.img.height()>(int)opts.coverMaxSize)) {
|
|
temp=saveImageToTemp(coverImage.img.scaled(QSize(opts.coverMaxSize, opts.coverMaxSize), Qt::KeepAspectRatio, Qt::SmoothTransformation), opts.coverName);
|
|
} else if (!coverImage.fileName.endsWith(".jpg", Qt::CaseInsensitive) || !QFile::exists(coverImage.fileName)) {
|
|
temp=saveImageToTemp(coverImage.img, opts.coverName);
|
|
} else {
|
|
srcFile=coverImage.fileName;
|
|
}
|
|
if (temp) {
|
|
srcFile=temp->fileName();
|
|
}
|
|
}
|
|
|
|
if (!srcFile.isEmpty()) {
|
|
LIBMTP_file_t *fileMeta=LIBMTP_new_file_t();
|
|
fileMeta->item_id=0;
|
|
fileMeta->parent_id=folderId;
|
|
fileMeta->storage_id=store.id;
|
|
fileMeta->filename=createString(opts.coverName);
|
|
struct stat statBuf;
|
|
if (0==stat(QFile::encodeName(srcFile).constData(), &statBuf)) {
|
|
fileMeta->filesize=statBuf.st_size;
|
|
fileMeta->modificationdate=statBuf.st_mtime;
|
|
}
|
|
meta->filetype=mtpFileType(opts.coverName);
|
|
|
|
if (0==LIBMTP_Send_File_From_File(device, srcFile.toUtf8(), fileMeta, 0, 0)) {
|
|
folder.covers.insert(fileMeta->item_id, File(opts.coverName, statBuf.st_size, fileMeta->item_id));
|
|
copiedCover=true;
|
|
}
|
|
LIBMTP_destroy_file_t(fileMeta);
|
|
}
|
|
if (temp) {
|
|
temp->remove();
|
|
delete temp;
|
|
}
|
|
}
|
|
folder.files.insert(meta->item_id, File(destName, meta->filesize, meta->item_id));
|
|
updateStorage();
|
|
}
|
|
emit putSongStatus(status,
|
|
meta ? encodePath(meta, destName, storage.count()>1 ? store.description : QString()) : QString(),
|
|
fixedVa, copiedCover);
|
|
if (meta) {
|
|
LIBMTP_destroy_track_t(meta);
|
|
}
|
|
}
|
|
|
|
MtpConnection::File MtpConnection::getCoverDetils(const Song &s)
|
|
{
|
|
File cover;
|
|
Path path=decodePath(s.file);
|
|
if (path.ok() && folderMap.contains(path.parent) && !folderMap[path.parent].covers.isEmpty()) {
|
|
QMap<uint32_t, File> &covers=folderMap[path.parent].covers;
|
|
QMap<uint32_t, File>::ConstIterator it=covers.constBegin();
|
|
QMap<uint32_t, File>::ConstIterator end=covers.constEnd();
|
|
|
|
for (; it!=end; ++it) {
|
|
if (it.value().size>cover.size) {
|
|
cover=it.value();
|
|
}
|
|
}
|
|
}
|
|
|
|
return cover;
|
|
}
|
|
|
|
void MtpConnection::getSong(const Song &song, const QString &dest, bool fixVa, bool copyCover)
|
|
{
|
|
bool copiedSong=device && 0==LIBMTP_Get_File_To_File(device, song.id, dest.toUtf8(), &progressMonitor, this);
|
|
bool copiedCover=false;
|
|
|
|
if (copiedSong && !abortRequested && copyCover) {
|
|
QString destDir=Utils::getDir(dest);
|
|
|
|
if (QDir(destDir).entryList(QStringList() << QLatin1String("*.jpg") << QLatin1String("*.png"), QDir::Files|QDir::Readable).isEmpty()) {
|
|
File cover=getCoverDetils(song);
|
|
|
|
if (0!=cover.id) {
|
|
QString mpdCover=MPDConnection::self()->getDetails().coverName;
|
|
if (mpdCover.isEmpty()) {
|
|
mpdCover="cover";
|
|
}
|
|
QString fileName=QString(destDir+mpdCover+(cover.name.endsWith(".jpg", Qt::CaseInsensitive) ? ".jpg" : ".png"));
|
|
QByteArray fileNameUtf8=fileName.toUtf8();
|
|
copiedCover=0==LIBMTP_Get_File_To_File(device, cover.id, fileNameUtf8.constData(), 0, 0);
|
|
if (copiedCover) {
|
|
Utils::setFilePerms(fileName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (copiedSong && fixVa && !abortRequested) {
|
|
Song s(song);
|
|
Device::fixVariousArtists(dest, s, false);
|
|
}
|
|
emit getSongStatus(copiedSong, copiedCover);
|
|
}
|
|
|
|
void MtpConnection::delSong(const Song &song)
|
|
{
|
|
Path path=decodePath(song.file);
|
|
bool deletedSong=device && path.ok() && 0==LIBMTP_Delete_Object(device, path.id);
|
|
if (deletedSong) {
|
|
folderMap[path.parent].files.remove(path.id);
|
|
#ifdef MTP_CLEAN_ALBUMS
|
|
// Remove track from album. Remove album (and cover) if no tracks.
|
|
LIBMTP_album_t *album=getAlbum(song);
|
|
if (album) {
|
|
if (0==album->no_tracks || (1==album->no_tracks && album->tracks[0]==(uint32_t)song.id)) {
|
|
LIBMTP_Delete_Object(device, album->album_id);
|
|
updateAlbums();
|
|
} else if (album->no_tracks>1) {
|
|
// Remove track from album...
|
|
uint32_t *tracks = (uint32_t *)malloc((album->no_tracks-1)*sizeof(uint32_t));
|
|
if (tracks) {
|
|
bool found=false;
|
|
for (uint32_t i=0, j=0; i<album->no_tracks && j<(album->no_tracks-1); ++i) {
|
|
if (album->tracks[i]!=(uint32_t)song.id) {
|
|
tracks[j++]=album->tracks[i];
|
|
} else {
|
|
found=true;
|
|
}
|
|
}
|
|
if (found) {
|
|
album->no_tracks--;
|
|
free(album->tracks);
|
|
album->tracks = tracks;
|
|
LIBMTP_Update_Album(device, album);
|
|
} else {
|
|
free(tracks);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
updateStorage();
|
|
}
|
|
emit delSongStatus(deletedSong);
|
|
}
|
|
|
|
bool MtpConnection::removeFolder(uint32_t folderId)
|
|
{
|
|
QMap<uint32_t, Folder>::iterator folder=folderMap.find(folderId);
|
|
if (folderMap.end()!=folder && (*folder).folders.isEmpty() && (*folder).files.isEmpty()) {
|
|
// Delete any cover files...
|
|
QList<uint32_t> toRemove=(*folder).covers.keys();
|
|
foreach (uint32_t cover, toRemove) {
|
|
if (0==LIBMTP_Delete_Object(device, cover)) {
|
|
(*folder).covers.remove(cover);
|
|
}
|
|
}
|
|
|
|
// Delete folder, if it is now empty...
|
|
if ((*folder).covers.isEmpty() && 0==LIBMTP_Delete_Object(device, folderId)) {
|
|
if (folderMap.contains((*folder).parentId)) {
|
|
folderMap[(*folder).parentId].folders.remove(folderId);
|
|
}
|
|
folderMap.remove(folderId);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void MtpConnection::cleanDirs(const QSet<QString> &dirs)
|
|
{
|
|
foreach (const QString &d, dirs) {
|
|
Path path=decodePath(d);
|
|
Storage &store=getStorage(path.storage);
|
|
if (0==store.musicFolderId) {
|
|
continue;
|
|
}
|
|
uint32_t folderId=path.parent;
|
|
while (0!=folderId && folderId!=store.musicFolderId) {
|
|
QMap<uint32_t, Folder>::iterator it=folderMap.find(folderId);
|
|
if (it!=folderMap.end()) {
|
|
if (removeFolder(folderId)) {
|
|
folderId=(*it).parentId;
|
|
folderMap.erase(it);
|
|
} else {
|
|
break;
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
updateStorage();
|
|
emit cleanDirsStatus(true);
|
|
}
|
|
|
|
void MtpConnection::getCover(const Song &song)
|
|
{
|
|
File c=getCoverDetils(song);
|
|
|
|
if (0!=c.id) {
|
|
QByteArray data;
|
|
if (0==LIBMTP_Get_File_To_Handler(device, c.id, fileReceiver, &data, 0, 0)) {
|
|
QImage img;
|
|
if (img.loadFromData(data)) {
|
|
emit cover(song, img);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void MtpConnection::stop()
|
|
{
|
|
if (thread) {
|
|
disconnectFromDevice(false);
|
|
thread->stop();
|
|
thread=0;
|
|
}
|
|
}
|
|
|
|
#ifdef MTP_CLEAN_ALBUMS
|
|
void MtpConnection::updateAlbums()
|
|
{
|
|
while (albums) {
|
|
LIBMTP_album_t *album=albums;
|
|
albums=albums->next;
|
|
LIBMTP_destroy_album_t(album);
|
|
}
|
|
albums=LIBMTP_Get_Album_List(device);
|
|
}
|
|
|
|
LIBMTP_album_t * MtpConnection::getAlbum(const Song &song)
|
|
{
|
|
LIBMTP_album_t *al=albums;
|
|
LIBMTP_album_t *possible=0;
|
|
|
|
while (al) {
|
|
if (QString::fromUtf8(al->name)==song.album) {
|
|
// For some reason, MTP sometimes leaves blank albums behind.
|
|
// So, when looking for an album if we find one with the same name, but no tracks - then save this as a possibility...
|
|
if (0==al->no_tracks) {
|
|
QString aa(QString::fromUtf8(al->artist));
|
|
if (aa.isEmpty() || aa==song.albumArtist()) {
|
|
possible=al;
|
|
}
|
|
} else {
|
|
for (uint32_t i=0; i<al->no_tracks; ++i) {
|
|
if (al->tracks[i]==(uint32_t)song.id) {
|
|
return al;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
al=al->next;
|
|
}
|
|
|
|
return possible;
|
|
}
|
|
#endif
|
|
|
|
void MtpConnection::destroyData()
|
|
{
|
|
folderMap.clear();
|
|
if (library) {
|
|
delete library;
|
|
library=0;
|
|
}
|
|
|
|
#ifdef MTP_CLEAN_ALBUMS
|
|
while (albums) {
|
|
LIBMTP_album_t *album=albums;
|
|
albums=albums->next;
|
|
LIBMTP_destroy_album_t(album);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
QString cfgKey(Solid::Device &dev, const QString &serial)
|
|
{
|
|
QString key=QLatin1String("MTP-")+dev.vendor()+QChar('-')+dev.product()+QChar('-')+serial;
|
|
key.replace('/', '_');
|
|
return key;
|
|
}
|
|
|
|
MtpDevice::MtpDevice(MusicModel *m, Solid::Device &dev)
|
|
: Device(m, dev,
|
|
#ifdef MTP_FAKE_ALBUMARTIST_SUPPORT
|
|
true
|
|
#else
|
|
false
|
|
#endif
|
|
)
|
|
, pmp(dev.as<Solid::PortableMediaPlayer>())
|
|
, tempFile(0)
|
|
, mtpUpdating(false)
|
|
{
|
|
static bool registeredTypes=false;
|
|
if (!registeredTypes) {
|
|
qRegisterMetaType<QSet<QString> >("QSet<QString>");
|
|
qRegisterMetaType<DeviceOptions >("DeviceOptions");
|
|
registeredTypes=true;
|
|
}
|
|
|
|
Solid::GenericInterface *iface = dev.as<Solid::GenericInterface>();
|
|
unsigned int busNum(0);
|
|
unsigned int devNum(0);
|
|
if (iface) {
|
|
QMap<QString, QVariant> properties = iface->allProperties();
|
|
busNum = properties.value(QLatin1String("BUSNUM")).toInt();
|
|
devNum = properties.value(QLatin1String("DEVNUM")).toInt();
|
|
}
|
|
|
|
connection=new MtpConnection(busNum, devNum, supportsAlbumArtistTag());
|
|
connect(this, SIGNAL(updateLibrary(const DeviceOptions &)), connection, SLOT(updateLibrary(const DeviceOptions &)));
|
|
connect(connection, SIGNAL(libraryUpdated()), this, SLOT(libraryUpdated()));
|
|
connect(connection, SIGNAL(progress(int)), this, SLOT(emitProgress(int)));
|
|
connect(this, SIGNAL(putSong(const Song &, bool, const DeviceOptions &, bool, bool)), connection, SLOT(putSong(const Song &, bool, const DeviceOptions &, bool, bool)));
|
|
connect(connection, SIGNAL(putSongStatus(int, const QString &, bool, bool)), this, SLOT(putSongStatus(int, const QString &, bool, bool)));
|
|
connect(this, SIGNAL(getSong(const Song &, const QString &, bool, bool)), connection, SLOT(getSong(const Song &, const QString &, bool, bool)));
|
|
connect(connection, SIGNAL(getSongStatus(bool, bool)), this, SLOT(getSongStatus(bool, bool)));
|
|
connect(this, SIGNAL(delSong(const Song &)), connection, SLOT(delSong(const Song &)));
|
|
connect(connection, SIGNAL(delSongStatus(bool)), this, SLOT(delSongStatus(bool)));
|
|
connect(this, SIGNAL(cleanMusicDirs(const QSet<QString> &)), connection, SLOT(cleanDirs(const QSet<QString> &)));
|
|
connect(this, SIGNAL(getCover(const Song &)), connection, SLOT(getCover(const Song &)));
|
|
connect(connection, SIGNAL(cleanDirsStatus(bool)), this, SLOT(cleanDirsStatus(bool)));
|
|
connect(connection, SIGNAL(statusMessage(const QString &)), this, SLOT(setStatusMessage(const QString &)));
|
|
connect(connection, SIGNAL(deviceDetails(const QString &)), this, SLOT(deviceDetails(const QString &)));
|
|
connect(connection, SIGNAL(updatePercentage(int)), this, SLOT(updatePercentage(int)));
|
|
connect(connection, SIGNAL(cover(const Song &, const QImage &)), this, SIGNAL(cover(const Song &, const QImage &)));
|
|
opts.fixVariousArtists=false;
|
|
opts.coverName=constMtpDefaultCover;
|
|
QTimer::singleShot(0, this, SLOT(rescan(bool)));
|
|
defaultName=data();
|
|
if (!opts.name.isEmpty()) {
|
|
setData(opts.name);
|
|
}
|
|
}
|
|
|
|
MtpDevice::~MtpDevice()
|
|
{
|
|
stop();
|
|
}
|
|
|
|
void MtpDevice::deviceDetails(const QString &s)
|
|
{
|
|
if ((s!=serial || serial.isEmpty()) && solidDev.isValid()) {
|
|
serial=s;
|
|
QString configKey=cfgKey(solidDev, serial);
|
|
opts.load(configKey);
|
|
configured=Configuration().hasGroup(configKey);
|
|
if (!opts.name.isEmpty() && opts.name!=defaultName) {
|
|
setData(opts.name);
|
|
emit renamed();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool MtpDevice::isConnected() const
|
|
{
|
|
return solidDev.isValid() && pmp && pmp->isValid() && connection->isConnected();
|
|
}
|
|
|
|
void MtpDevice::stop()
|
|
{
|
|
abortJob();
|
|
deleteTemp();
|
|
if (connection) {
|
|
disconnect(connection, SIGNAL(libraryUpdated()), this, SLOT(libraryUpdated()));
|
|
disconnect(connection, SIGNAL(progress(int)), this, SLOT(emitProgress(int)));
|
|
disconnect(connection, SIGNAL(putSongStatus(int, const QString &, bool, bool)), this, SLOT(putSongStatus(int, const QString &, bool, bool)));
|
|
disconnect(connection, SIGNAL(getSongStatus(bool, bool)), this, SLOT(getSongStatus(bool, bool)));
|
|
disconnect(connection, SIGNAL(delSongStatus(bool)), this, SLOT(delSongStatus(bool)));
|
|
disconnect(connection, SIGNAL(cleanDirsStatus(bool)), this, SLOT(cleanDirsStatus(bool)));
|
|
disconnect(connection, SIGNAL(statusMessage(const QString &)), this, SLOT(setStatusMessage(const QString &)));
|
|
disconnect(connection, SIGNAL(deviceDetails(const QString &)), this, SLOT(deviceDetails(const QString &)));
|
|
disconnect(connection, SIGNAL(updatePercentage(int)), this, SLOT(updatePercentage(int)));
|
|
disconnect(connection, SIGNAL(cover(const Song &, const QImage &)), this, SIGNAL(cover(const Song &, const QImage &)));
|
|
metaObject()->invokeMethod(connection, "stop", Qt::QueuedConnection);
|
|
connection->deleteLater();
|
|
connection=0;
|
|
}
|
|
}
|
|
|
|
void MtpDevice::configure(QWidget *parent)
|
|
{
|
|
if (!isIdle()) {
|
|
return;
|
|
}
|
|
|
|
DevicePropertiesDialog *dlg=new DevicePropertiesDialog(parent);
|
|
connect(dlg, SIGNAL(updatedSettings(const QString &, const DeviceOptions &)), SLOT(saveProperties(const QString &, const DeviceOptions &)));
|
|
if (!configured) {
|
|
connect(dlg, SIGNAL(cancelled()), SLOT(saveProperties()));
|
|
}
|
|
DeviceOptions o=opts;
|
|
if (o.name.isEmpty()) {
|
|
o.name=data();
|
|
}
|
|
dlg->show(QString(), o, connection->getStorageList(), DevicePropertiesWidget::Prop_Name|DevicePropertiesWidget::Prop_CoversAll|DevicePropertiesWidget::Prop_Va|DevicePropertiesWidget::Prop_Transcoder);
|
|
}
|
|
|
|
void MtpDevice::rescan(bool full)
|
|
{
|
|
Q_UNUSED(full)
|
|
if (mtpUpdating || !solidDev.isValid()) {
|
|
return;
|
|
}
|
|
if (childCount()) {
|
|
update=new MusicLibraryItemRoot();
|
|
applyUpdate();
|
|
}
|
|
mtpUpdating=true;
|
|
emit updating(solidDev.udi(), true);
|
|
emit updateLibrary(opts);
|
|
}
|
|
|
|
void MtpDevice::addSong(const Song &s, bool overwrite, bool copyCover)
|
|
{
|
|
requestAbort(false);
|
|
if (!isConnected()) {
|
|
emit actionStatus(NotConnected);
|
|
return;
|
|
}
|
|
|
|
needToFixVa=opts.fixVariousArtists && s.isVariousArtists();
|
|
|
|
if (!overwrite) {
|
|
Song check=s;
|
|
|
|
if (needToFixVa) {
|
|
Device::fixVariousArtists(QString(), check, true);
|
|
}
|
|
if (songExists(check)) {
|
|
emit actionStatus(SongExists);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!QFile::exists(s.file)) {
|
|
emit actionStatus(SourceFileDoesNotExist);
|
|
return;
|
|
}
|
|
currentSong=s;
|
|
if (!opts.transcoderCodec.isEmpty()) {
|
|
Encoders::Encoder encoder=Encoders::getEncoder(opts.transcoderCodec);
|
|
if (encoder.codec.isEmpty()) {
|
|
emit actionStatus(CodecNotAvailable);
|
|
return;
|
|
}
|
|
|
|
if (!opts.transcoderWhenDifferent || encoder.isDifferent(s.file)) {
|
|
deleteTemp();
|
|
tempFile=new QTemporaryFile(QDir::tempPath()+"/cantata_XXXXXX"+encoder.extension);
|
|
tempFile->setAutoRemove(false);
|
|
|
|
if (!tempFile->open()) {
|
|
deleteTemp();
|
|
emit actionStatus(FailedToCreateTempFile);
|
|
return;
|
|
}
|
|
QString destFile=tempFile->fileName();
|
|
tempFile->close();
|
|
if (QFile::exists(destFile)) {
|
|
QFile::remove(destFile);
|
|
}
|
|
transcoding=true;
|
|
TranscodingJob *job=new TranscodingJob(encoder, opts.transcoderValue, s.file, destFile);
|
|
job->setProperty("overwrite", overwrite);
|
|
job->setProperty("copyCover", copyCover);
|
|
connect(job, SIGNAL(result(int)), SLOT(transcodeSongResult(int)));
|
|
connect(job, SIGNAL(percent(int)), SLOT(transcodePercent(int)));
|
|
job->start();
|
|
currentSong.file=destFile;
|
|
return;
|
|
}
|
|
}
|
|
transcoding=false;
|
|
emit putSong(currentSong, needToFixVa, opts, overwrite, copyCover);
|
|
}
|
|
|
|
void MtpDevice::copySongTo(const Song &s, const QString &musicPath, bool overwrite, bool copyCover)
|
|
{
|
|
requestAbort(false);
|
|
transcoding=false;
|
|
if (!isConnected()) {
|
|
emit actionStatus(NotConnected);
|
|
return;
|
|
}
|
|
|
|
needToFixVa=opts.fixVariousArtists && s.isVariousArtists();
|
|
|
|
if (!overwrite) {
|
|
Song check=s;
|
|
|
|
if (needToFixVa) {
|
|
Device::fixVariousArtists(QString(), check, false);
|
|
}
|
|
if (MusicLibraryModel::self()->songExists(check)) {
|
|
emit actionStatus(SongExists);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!songExists(s)) {
|
|
emit actionStatus(SongDoesNotExist);
|
|
return;
|
|
}
|
|
|
|
QString baseDir=MPDConnection::self()->getDetails().dir;
|
|
currentDestFile=baseDir+musicPath;
|
|
QDir dir(Utils::getDir(currentDestFile));
|
|
if (!dir.exists() && !Utils::createWorldReadableDir(dir.absolutePath(), baseDir)) {
|
|
emit actionStatus(DirCreationFaild);
|
|
return;
|
|
}
|
|
|
|
currentSong=s;
|
|
emit getSong(s, currentDestFile, needToFixVa, copyCover);
|
|
}
|
|
|
|
void MtpDevice::removeSong(const Song &s)
|
|
{
|
|
requestAbort(false);
|
|
if (!isConnected()) {
|
|
emit actionStatus(NotConnected);
|
|
return;
|
|
}
|
|
|
|
if (!songExists(s)) {
|
|
emit actionStatus(SongDoesNotExist);
|
|
return;
|
|
}
|
|
|
|
currentSong=s;
|
|
emit delSong(s);
|
|
}
|
|
|
|
void MtpDevice::cleanDirs(const QSet<QString> &dirs)
|
|
{
|
|
requestAbort(false);
|
|
if (!isConnected()) {
|
|
emit actionStatus(NotConnected);
|
|
return;
|
|
}
|
|
emit cleanMusicDirs(dirs);
|
|
}
|
|
|
|
void MtpDevice::requestCover(const Song &song)
|
|
{
|
|
requestAbort(false);
|
|
if (isConnected()) {
|
|
emit getCover(song);
|
|
}
|
|
}
|
|
|
|
void MtpDevice::putSongStatus(int status, const QString &file, bool fixedVa, bool copiedCover)
|
|
{
|
|
deleteTemp();
|
|
if (jobAbortRequested) {
|
|
return;
|
|
}
|
|
if (Ok!=status) {
|
|
emit actionStatus(status);
|
|
} else {
|
|
currentSong.file=file;
|
|
if (needToFixVa && fixedVa) {
|
|
currentSong.fixVariousArtists();
|
|
}
|
|
#ifndef MTP_FAKE_ALBUMARTIST_SUPPORT
|
|
else if (!opts.fixVariousArtists) { // TODO: ALBUMARTIST: Remove when libMTP supports album artist!
|
|
currentSong.albumartist=currentSong.artist;
|
|
}
|
|
#endif
|
|
addSongToList(currentSong);
|
|
emit actionStatus(Ok, copiedCover);
|
|
}
|
|
}
|
|
|
|
void MtpDevice::transcodeSongResult(int status)
|
|
{
|
|
TranscodingJob *job=qobject_cast<TranscodingJob *>(sender());
|
|
if (!job) {
|
|
return;
|
|
}
|
|
FileJob::finished(job);
|
|
if (jobAbortRequested) {
|
|
deleteTemp();
|
|
return;
|
|
}
|
|
if (Ok!=status) {
|
|
emit actionStatus(status);
|
|
} else {
|
|
emit putSong(currentSong, needToFixVa, opts, job->property("overwrite").toBool(), job->property("copyCover").toBool());
|
|
}
|
|
}
|
|
|
|
void MtpDevice::transcodePercent(int percent)
|
|
{
|
|
if (jobAbortRequested) {
|
|
FileJob *job=qobject_cast<FileJob *>(sender());
|
|
if (job) {
|
|
job->stop();
|
|
}
|
|
return;
|
|
}
|
|
emit progress(percent/2);
|
|
}
|
|
|
|
void MtpDevice::emitProgress(int percent)
|
|
{
|
|
if (jobAbortRequested) {
|
|
return;
|
|
}
|
|
emit progress(transcoding ? (50+(percent/2)) : percent);
|
|
}
|
|
|
|
void MtpDevice::getSongStatus(bool ok, bool copiedCover)
|
|
{
|
|
if (jobAbortRequested) {
|
|
return;
|
|
}
|
|
if (!ok) {
|
|
emit actionStatus(Failed);
|
|
} else {
|
|
currentSong.file=currentDestFile.mid(MPDConnection::self()->getDetails().dir.length());
|
|
QString origPath;
|
|
if (MPDConnection::self()->isMopdidy()) {
|
|
origPath=currentSong.file;
|
|
currentSong.file=Song::encodePath(currentSong.file);
|
|
}
|
|
if (needToFixVa) {
|
|
currentSong.revertVariousArtists();
|
|
}
|
|
Utils::setFilePerms(currentDestFile);
|
|
MusicLibraryModel::self()->addSongToList(currentSong);
|
|
DirViewModel::self()->addFileToList(origPath.isEmpty() ? currentSong.file : origPath,
|
|
origPath.isEmpty() ? QString() : currentSong.file);
|
|
emit actionStatus(Ok, copiedCover);
|
|
}
|
|
}
|
|
|
|
void MtpDevice::delSongStatus(bool ok)
|
|
{
|
|
if (jobAbortRequested) {
|
|
return;
|
|
}
|
|
if (!ok) {
|
|
emit actionStatus(Failed);
|
|
} else {
|
|
removeSongFromList(currentSong);
|
|
emit actionStatus(Ok);
|
|
}
|
|
}
|
|
|
|
void MtpDevice::cleanDirsStatus(bool ok)
|
|
{
|
|
emit actionStatus(ok ? Ok : Failed);
|
|
}
|
|
|
|
double MtpDevice::usedCapacity()
|
|
{
|
|
if (!isConnected()) {
|
|
return -1.0;
|
|
}
|
|
|
|
return connection->capacity()>0 ? (connection->usedSpace()*1.0)/(connection->capacity()*1.0) : -1.0;
|
|
}
|
|
|
|
QString MtpDevice::capacityString()
|
|
{
|
|
if (!isConnected()) {
|
|
return i18n("Not Connected");
|
|
}
|
|
|
|
return i18n("%1 free", Utils::formatByteSize(connection->capacity()-connection->usedSpace()));
|
|
}
|
|
|
|
qint64 MtpDevice::freeSpace()
|
|
{
|
|
if (!isConnected()) {
|
|
return 0;
|
|
}
|
|
|
|
return connection->capacity()-connection->usedSpace();
|
|
}
|
|
|
|
void MtpDevice::libraryUpdated()
|
|
{
|
|
if (update) {
|
|
delete update;
|
|
}
|
|
update=connection->takeLibrary();
|
|
setStatusMessage(QString());
|
|
emit updating(id(), false);
|
|
mtpUpdating=false;
|
|
}
|
|
|
|
void MtpDevice::saveProperties(const QString &, const DeviceOptions &newOpts)
|
|
{
|
|
if (configured && opts==newOpts) {
|
|
return;
|
|
}
|
|
|
|
QString newName=newOpts.name.isEmpty() ? defaultName : newOpts.name;
|
|
bool diffName=opts.name!=newName;
|
|
opts=newOpts;
|
|
if (diffName) {
|
|
setData(newName);
|
|
}
|
|
saveProperties();
|
|
if (diffName) {
|
|
emit renamed();
|
|
}
|
|
}
|
|
|
|
void MtpDevice::saveProperties()
|
|
{
|
|
if (solidDev.isValid()) {
|
|
configured=true;
|
|
opts.save(cfgKey(solidDev, serial), false, true, false); // Dont save fileame scheme - cant be changed!
|
|
emit configurationChanged();
|
|
}
|
|
}
|
|
|
|
void MtpDevice::saveOptions()
|
|
{
|
|
if (solidDev.isValid()) {
|
|
opts.save(cfgKey(solidDev, serial), false, true, false); // Dont save fileame scheme - cant be changed!
|
|
}
|
|
}
|
|
|
|
void MtpDevice::deleteTemp()
|
|
{
|
|
if (tempFile) {
|
|
tempFile->remove();
|
|
delete tempFile;
|
|
tempFile=0;
|
|
}
|
|
}
|