1 /*
2  * Cantata
3  *
4  * Copyright (c) 2011-2014 Craig Drummond <craig.p.drummond@gmail.com>
5  *
6  * ----
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  * General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; see the file COPYING.  If not, write to
20  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21  * Boston, MA 02110-1301, USA.
22  */
23 
24 #include "syncdialog.h"
25 #include "synccollectionwidget.h"
26 #include "actiondialog.h"
27 #include "devicepropertiesdialog.h"
28 #include "devicepropertieswidget.h"
29 #include "mpd-interface/song.h"
30 #include "mpd-interface/mpdconnection.h"
31 #include "models/mpdlibrarymodel.h"
32 #include "models/devicesmodel.h"
33 #include "support/messagebox.h"
34 #include "support/squeezedtextlabel.h"
35 #include "widgets/icons.h"
36 #include <QSplitter>
37 #include <QFileInfo>
38 
39 struct SyncSong : public Song
40 {
SyncSongSyncSong41     SyncSong(const Song &o)
42         : Song(o) {
43     }
operator ==SyncSong44     bool operator==(const SyncSong &o) const {
45         return title==o.title && album==o.album && albumArtist()==o.albumArtist();
46     }
operator <SyncSong47     bool operator<(const SyncSong &o) const {
48         int compare=albumArtist().compare(o.albumArtist());
49 
50         if (0!=compare) {
51             return compare<0;
52         }
53         compare=album.compare(o.album);
54         if (0!=compare) {
55             return compare<0;
56         }
57         return title.compare(o.title)<0;
58     }
59 };
60 
qHash(const SyncSong & key)61 inline uint qHash(const SyncSong &key)
62 {
63     return qHash(key.albumArtist()+key.artist+key.title);
64 }
65 
getDiffs(const QSet<Song> & s1,const QSet<Song> & s2,QSet<Song> & in1,QSet<Song> & in2)66 static void getDiffs(const QSet<Song> &s1, const QSet<Song> &s2, QSet<Song> &in1, QSet<Song> &in2)
67 {
68     QSet<SyncSong> a;
69     QSet<SyncSong> b;
70 
71     for (const Song &s: s1) {
72         a.insert(s);
73     }
74 
75     for (const Song &s: s2) {
76         b.insert(s);
77     }
78 
79     QSet<SyncSong> r=a-b;
80 
81     for (const Song &s: r) {
82         in1.insert(s);
83     }
84 
85     r=b-a;
86 
87     for (const Song &s: r) {
88         in2.insert(s);
89     }
90 }
91 
92 static int iCount=0;
93 
instanceCount()94 int SyncDialog::instanceCount()
95 {
96     return iCount;
97 }
98 
SyncDialog(QWidget * parent)99 SyncDialog::SyncDialog(QWidget *parent)
100     : Dialog(parent, "SyncDialog", QSize(680, 680))
101     , state(State_Lists)
102     , currentDev(nullptr)
103 {
104     iCount++;
105 
106     QWidget *mw=new QWidget(this);
107     QVBoxLayout *l=new QVBoxLayout(mw);
108     QSplitter *splitter=new QSplitter(mw);
109     libWidget=new SyncCollectionWidget(splitter, tr("Library:"));
110     devWidget=new SyncCollectionWidget(splitter, tr("Device:"));
111     statusLabel=new SqueezedTextLabel(this);
112     statusLabel->setText(tr("Loading all songs from library, please wait..."));
113     splitter->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
114     NoteLabel *noteLabel=new NoteLabel(this);
115     noteLabel->setText(tr("<code>Library</code> lists only songs that are in your library, but not on the device. Likewise <code>Device</code> lists "
116                             "songs that are only on the device.<br/>"
117                             "Select songs from <code>Library</code> that you would like to copy to <code>Device</code>, "
118                             "and select songs from <code>Device</code> that you would like to copy to <code>Library</code>. "
119                             "Then press the <code>Synchronize</code> button."));
120     l->addWidget(splitter);
121     l->addWidget(statusLabel);
122     l->addWidget(noteLabel);
123     libWidget->setEnabled(false);
124     devWidget->setEnabled(false);
125     setMainWidget(mw);
126     setButtons(Cancel|Ok);
127     setButtonText(Ok, tr("Synchronize"));
128     enableButtonOk(false);
129     setAttribute(Qt::WA_DeleteOnClose);
130     setCaption(tr("Synchronize"));
131     connect(libWidget, SIGNAL(selectionChanged()), SLOT(selectionChanged()));
132     connect(devWidget, SIGNAL(selectionChanged()), SLOT(selectionChanged()));
133     connect(libWidget, SIGNAL(configure()), SLOT(configure()));
134     connect(devWidget, SIGNAL(configure()), SLOT(configure()));
135     libOptions.save(MPDConnectionDetails::configGroupName(MPDConnection::self()->getDetails().name), true);
136 }
137 
~SyncDialog()138 SyncDialog::~SyncDialog()
139 {
140     iCount--;
141 }
142 
sync(const QString & udi)143 void SyncDialog::sync(const QString &udi)
144 {
145     devUdi=udi;
146     connect(MpdLibraryModel::self(), SIGNAL(songListing(QList<Song>,double)), this, SLOT(librarySongs(QList<Song>,double)));
147     MpdLibraryModel::self()->listSongs();
148     show();
149 }
150 
copy(const QList<Song> & songs)151 void SyncDialog::copy(const QList<Song> &songs)
152 {
153     Device *dev=getDevice();
154 
155     if (!dev) {
156         return;
157     }
158 
159     bool fromDev=sender()==devWidget;
160     ActionDialog *dlg=new ActionDialog(this);
161     connect(dlg, SIGNAL(completed()), SLOT(updateSongs()));
162     dlg->copy(fromDev ? dev->id() : QString(), fromDev ? QString() : dev->id(), songs);
163 }
164 
updateSongs()165 void SyncDialog::updateSongs()
166 {
167     Device *dev=getDevice();
168 
169     if (!dev) {
170         deleteLater();
171         hide();
172         return;
173     }
174 
175     QSet<Song> devSongs=dev->allSongs(dev->options().fixVariousArtists);
176     QSet<Song> inDev;
177     QSet<Song> inLib;
178     getDiffs(devSongs, libSongs, inDev, inLib);
179 
180     if (0==inDev.count() && 0==inLib.count()) {
181         MessageBox::information(isVisible() ? this : parentWidget(), tr("Device and library are in sync."));
182         deleteLater();
183         hide();
184         return;
185     }
186     devWidget->setSupportsAlbumArtistTag(dev->supportsAlbumArtistTag());
187     devWidget->update(inDev);
188     libWidget->update(inLib);
189     libWidget->setEnabled(true);
190     devWidget->setEnabled(true);
191     libSongs.clear();
192 }
193 
librarySongs(const QList<Song> & songs,double pc)194 void SyncDialog::librarySongs(const QList<Song> &songs, double pc)
195 {
196     if (songs.isEmpty()) {
197         statusLabel->hide();
198         disconnect(MpdLibraryModel::self(), SIGNAL(songListing(QList<Song>,double)), this, SLOT(librarySongs(QList<Song>,double)));
199         updateSongs();
200     } else {
201         libSongs+=songs.toSet();
202         statusLabel->setText(tr("Loading all songs from library, please wait...%1%...").arg(pc));
203     }
204 }
205 
selectionChanged()206 void SyncDialog::selectionChanged()
207 {
208     enableButtonOk(libWidget->numCheckedSongs() || devWidget->numCheckedSongs());
209 }
210 
configure()211 void SyncDialog::configure()
212 {
213     if (libWidget==sender()) {
214         DevicePropertiesDialog *dlg=new DevicePropertiesDialog(this);
215         connect(dlg, SIGNAL(updatedSettings(const QString &, const DeviceOptions &)), SLOT(saveProperties(const QString &, const DeviceOptions &)));
216         dlg->setCaption(tr("Local Music Library Properties"));
217         dlg->show(MPDConnection::self()->getDetails().dir, libOptions, DevicePropertiesWidget::Prop_Basic|DevicePropertiesWidget::Prop_FileName);
218     } else {
219         Device *dev=getDevice();
220         if (dev) {
221             dev->configure(this);
222         }
223     }
224 }
225 
saveProperties(const QString & path,const DeviceOptions & opts)226 void SyncDialog::saveProperties(const QString &path, const DeviceOptions &opts)
227 {
228     Q_UNUSED(path)
229     libOptions=opts;
230     libOptions.save(MPDConnectionDetails::configGroupName(MPDConnection::self()->getDetails().name), true, false);
231 }
232 
slotButtonClicked(int button)233 void SyncDialog::slotButtonClicked(int button)
234 {
235     switch(button) {
236     case Ok: {
237         Device *dev=getDevice();
238         if (dev) {
239             setVisible(false);
240             ActionDialog *dlg=new ActionDialog(parentWidget());
241             QList<Song> songs=libWidget->checkedSongs();
242             QString devId;
243             devId=dev->id();
244             dlg->sync(devId, libWidget->checkedSongs(), devWidget->checkedSongs());
245             Dialog::slotButtonClicked(button);
246         }
247         break;
248     }
249     case Cancel:
250         MpdLibraryModel::self()->cancelListing();
251         Dialog::slotButtonClicked(button);
252         break;
253     default:
254         break;
255     }
256 }
257 
getDevice()258 Device * SyncDialog::getDevice()
259 {
260     Device *dev=DevicesModel::self()->device(devUdi);
261     if (!dev) {
262         MessageBox::error(isVisible() ? this : parentWidget(), tr("Device has been removed!"));
263         return nullptr;
264     }
265 
266     if (currentDev && dev!=currentDev) {
267         MessageBox::error(isVisible() ? this : parentWidget(), tr("Device has been changed?"));
268         return nullptr;
269     }
270 
271     if (dev->isIdle()) {
272         return dev;
273     }
274 
275     MessageBox::error(isVisible() ? this : parentWidget(), tr("Device is busy?"));
276     return nullptr;
277 }
278 
279 #include "moc_syncdialog.cpp"
280