1 /*
2  * Cantata
3  *
4  * Copyright (c) 2011-2020 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 "serversettings.h"
25 #include "settings.h"
26 #include "support/inputdialog.h"
27 #include "support/messagebox.h"
28 #include "widgets/icons.h"
29 #include "db/mpdlibrarydb.h"
30 #ifdef ENABLE_SIMPLE_MPD_SUPPORT
31 #include "mpd-interface/mpduser.h"
32 #endif
33 #include "support/utils.h"
34 #ifdef AVAHI_FOUND
35 #include "findmpddialog.h"
36 #endif
37 #include <QDir>
38 #include <QComboBox>
39 #include <QPushButton>
40 #include <QValidator>
41 #include <QStyle>
42 #include <QStandardPaths>
43 #include <algorithm>
44 
45 #define REMOVE(w) \
46     w->setVisible(false); \
47     w->deleteLater(); \
48     w=0;
49 
50 class CollectionNameValidator : public QValidator
51 {
52     public:
53 
CollectionNameValidator(QObject * parent)54     CollectionNameValidator(QObject *parent) : QValidator(parent) { }
55 
validate(QString & input,int &) const56     State validate(QString &input, int &) const override
57     {
58         #ifdef ENABLE_SIMPLE_MPD_SUPPORT
59         if (input.startsWith(MPDUser::constName)) {
60             return Invalid;
61         }
62         #endif
63         if (-1!=input.indexOf("/") || -1!=input.indexOf("\\") || -1!=input.indexOf("*") || -1!=input.indexOf("?")) {
64             return Invalid;
65         }
66         return Acceptable;
67     }
68 };
69 
ServerSettings(QWidget * p)70 ServerSettings::ServerSettings(QWidget *p)
71     : QWidget(p)
72     , haveBasicCollection(false)
73     , isCurrentConnection(false)
74     , allOptions(true) // will be toggled
75     , prevIndex(0)
76 {
77     setupUi(this);
78     #if defined ENABLE_DEVICES_SUPPORT
79     musicFolderNoteLabel->appendText(QLatin1String("<i> ")+tr("This folder will also be used to locate music files "
80                                      "for tag-editing, replay gain, and transferring to (and from) devices.")+QLatin1String("</i>"));
81     #else
82     musicFolderNoteLabel->appendText(QLatin1String("<i> ")+tr("This folder will also be used to locate music files "
83                                      "for tag-editing, replay gain, etc.")+QLatin1String("</i>"));
84     #endif
85     musicFolderNoteLabel->appendText(QLatin1String("<br/><i><a href=\"https://github.com/CDrummond/cantata/wiki/Accessing-music-files,-and-covers\">")+
86                                      tr("More information")+QLatin1String("</a></i>"));
87 
88     connect(combo, SIGNAL(activated(int)), SLOT(showDetails(int)));
89     connect(removeButton, SIGNAL(clicked(bool)), SLOT(remove()));
90     connect(addButton, SIGNAL(clicked(bool)), SLOT(add()));
91     connect(name, SIGNAL(textChanged(QString)), SLOT(nameChanged()));
92     connect(basicDir, SIGNAL(textChanged(QString)), SLOT(basicDirChanged()));
93     addButton->setIcon(Icons::self()->addIcon);
94     removeButton->setIcon(Icons::self()->minusIcon);
95     addButton->setAutoRaise(true);
96     removeButton->setAutoRaise(true);
97 
98     #if defined Q_OS_WIN
99     hostLabel->setText(tr("Host:"));
100     #endif
101 
102     name->setValidator(new CollectionNameValidator(this));
103     #ifndef ENABLE_HTTP_STREAM_PLAYBACK
104     REMOVE(streamUrlLabel)
105     REMOVE(streamUrl)
106     REMOVE(streamUrlNoteLabel)
107     #endif
108     #ifdef ENABLE_HTTP_SERVER
109     allowLocalStreamingLabel->setToolTip(allowLocalStreaming->toolTip());
110     #else
111     REMOVE(allowLocalStreaming)
112     REMOVE(allowLocalStreamingLabel)
113     REMOVE(allowLocalStreamingNoteLabel)
114     #endif
115     autoUpdate->setToolTip(autoUpdate->toolTip());
116 
117     #ifdef Q_OS_MAC
118     expandingSpacer->changeSize(0, 0, QSizePolicy::Fixed, QSizePolicy::Fixed);
119     #endif
120 
121     #ifdef AVAHI_FOUND
122     discoveryButton = new QPushButton(tr("Discover..."), this);
123     hostLayout->insertWidget(hostLayout->count(), discoveryButton);
124     connect(discoveryButton, &QPushButton::clicked, this, &ServerSettings::detectMPDs);
125     #endif
126 }
127 
load()128 void ServerSettings::load()
129 {
130     QList<MPDConnectionDetails> all=Settings::self()->allConnections();
131     QString currentCon=Settings::self()->currentConnection();
132 
133     std::sort(all.begin(), all.end());
134     combo->clear();
135     int idx=0;
136     haveBasicCollection=false;
137     for (MPDConnectionDetails d: all) {
138         combo->addItem(d.getName(), d.name);
139         if (d.name==currentCon) {
140             prevIndex=idx;
141         }
142         idx++;
143         #ifdef ENABLE_SIMPLE_MPD_SUPPORT
144         if (d.name==MPDUser::constName) {
145             d.dir=MPDUser::self()->details().dir;
146             haveBasicCollection=true;
147             prevBasic=d;
148         }
149         #endif
150         DeviceOptions opts;
151         opts.load(MPDConnectionDetails::configGroupName(d.name), true);
152         collections.append(Collection(d, opts));
153     }
154     combo->setCurrentIndex(prevIndex);
155     setDetails(collections.at(prevIndex).details);
156     removeButton->setEnabled(combo->count()>1);
157 }
158 
save()159 void ServerSettings::save()
160 {
161     if (combo->count()<1) {
162         return;
163     }
164 
165     Collection current=collections.at(combo->currentIndex());
166     current.details=getDetails();
167     collections.replace(combo->currentIndex(), current);
168 
169     QList<MPDConnectionDetails> existingInConfig=Settings::self()->allConnections();
170     QList<Collection> toAdd;
171 
172     for (const Collection &c: collections) {
173         bool found=false;
174         for (int i=0; i<existingInConfig.count(); ++i) {
175             MPDConnectionDetails e=existingInConfig.at(i);
176             if (e.name==c.details.name) {
177                 existingInConfig.removeAt(i);
178                 found=true;
179                 if (c.details.hostname!=e.hostname || c.details.port!=e.port || c.details.password!=e.password ||
180                     c.details.dir!=e.dir
181                     #ifdef ENABLE_HTTP_STREAM_PLAYBACK
182                     || c.details.streamUrl!=e.streamUrl
183                     #endif
184                     || c.details.allowLocalStreaming!=e.allowLocalStreaming
185                     || c.details.autoUpdate!=e.autoUpdate
186                     ) {
187                     toAdd.append(c);
188                 }
189             }
190         }
191         if (!found) {
192             toAdd.append(c);
193         }
194         #ifdef ENABLE_SIMPLE_MPD_SUPPORT
195         if (c.details.name==MPDUser::constName) {
196             prevBasic=c;
197         }
198         #endif
199     }
200 
201     for (const MPDConnectionDetails &c: existingInConfig) {
202         Settings::self()->removeConnectionDetails(c.name);
203     }
204 
205     for (const Collection &c: toAdd) {
206         Settings::self()->saveConnectionDetails(c.details);
207         c.namingOpts.save(MPDConnectionDetails::configGroupName(c.details.name), true);
208     }
209 
210     #ifdef ENABLE_SIMPLE_MPD_SUPPORT
211     if (!haveBasicCollection && MPDUser::self()->isSupported()) {
212         MPDUser::self()->cleanup();
213     }
214 
215     if (current.details.name==MPDUser::constName) {
216         MPDUser::self()->setDetails(current.details);
217     }
218     #endif
219     Settings::self()->saveCurrentConnection(current.details.name);
220     MpdLibraryDb::removeUnusedDbs();
221 }
222 
cancel()223 void ServerSettings::cancel()
224 {
225     #ifdef ENABLE_SIMPLE_MPD_SUPPORT
226     // If we are canceling any changes, then we need to restore user settings...
227     if (prevBasic.details.name==MPDUser::constName) {
228         MPDUser::self()->setDetails(prevBasic.details);
229     }
230     #endif
231 }
232 
showDetails(int index)233 void ServerSettings::showDetails(int index)
234 {
235     if (-1!=prevIndex && index!=prevIndex) {
236         MPDConnectionDetails details=getDetails();
237         if (details.name.isEmpty()) {
238             details.name=generateName(prevIndex);
239         }
240         collections.replace(prevIndex, details);
241         #ifdef ENABLE_SIMPLE_MPD_SUPPORT
242         if (details.name!=MPDUser::constName)
243         #endif
244         {
245             combo->setItemText(prevIndex, details.name);
246         }
247     }
248     setDetails(collections.at(index).details);
249     prevIndex=index;
250 }
251 
add()252 void ServerSettings::add()
253 {
254     #ifdef ENABLE_SIMPLE_MPD_SUPPORT
255     bool addStandard=true;
256 
257     if (!haveBasicCollection && MPDUser::self()->isSupported()) {
258         static const QChar constBullet(0x2022);
259 
260         switch (MessageBox::questionYesNoCancel(this,
261                                    QLatin1String("<p>")+
262                                    tr("Which type of collection do you wish to connect to?")+QLatin1String("<br/><br/>")+
263                                    constBullet+QLatin1Char(' ')+tr("Standard - music collection may be shared, is on another machine, is "
264                                                                      "already setup, or you wish to enable access from other clients (e.g. "
265                                                                      "MPDroid)")+QLatin1String("<br/><br/>")+
266                                    constBullet+QLatin1Char(' ')+tr("Basic - music collection is not shared with others, and Cantata will "
267                                                                      "configure and control the MPD instance. This setup will be exclusive "
268                                                                      "to Cantata, and will <b>not</b> be accessible to other MPD clients.")+
269                                                                      QLatin1String("<br/><br/>")+
270                                    tr("<i><b>NOTE:</b> %1</i>").arg(tr("If you wish to have an advanced MPD setup (e.g. multiple audio "
271                                         "outputs, full DSD support, etc) then you <b>must</b> choose 'Standard'")),
272                                    tr("Add Collection"), GuiItem(tr("Standard")), GuiItem(tr("Basic")))) {
273         case MessageBox::Yes: addStandard=true; break;
274         case MessageBox::No: addStandard=false; break;
275         default: return;
276         }
277     }
278     #endif
279     MPDConnectionDetails details;
280     #ifdef ENABLE_SIMPLE_MPD_SUPPORT
281     if (addStandard) {
282     #endif
283         details.name=generateName();
284         details.port=6600;
285         details.hostname=QLatin1String("localhost");
286         details.dir=QLatin1String("/var/lib/mpd/music/");
287         details.allowLocalStreaming=true;
288         details.autoUpdate=false;
289         combo->addItem(details.name);
290     #ifdef ENABLE_SIMPLE_MPD_SUPPORT
291     } else {
292         details=MPDUser::self()->details(true);
293         QString dir=QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
294         if (dir.isEmpty()) {
295             QString dir=QDir::homePath()+"/Music";
296             dir=dir.replace("//", "/");
297         }
298         dir=Utils::fixPath(dir);
299         basicDir->setText(dir);
300         MPDUser::self()->setMusicFolder(dir);
301         combo->addItem(MPDUser::translatedName());
302         haveBasicCollection = true;
303     }
304     #endif
305     removeButton->setEnabled(combo->count()>1);
306     collections.append(Collection(details));
307     combo->setCurrentIndex(combo->count()-1);
308     prevIndex=combo->currentIndex();
309     setDetails(details);
310 }
311 
remove()312 void ServerSettings::remove()
313 {
314     int index=combo->currentIndex();
315     #ifdef ENABLE_SIMPLE_MPD_SUPPORT
316     QString cName=1==stackedWidget->currentIndex() ? MPDUser::translatedName() : name->text();
317     #else
318     QString cName=name->text();
319     #endif
320     if (combo->count()>1 && MessageBox::Yes==MessageBox::questionYesNo(this, tr("Delete '%1'?").arg(cName),
321                                                                        tr("Delete"), StdGuiItem::del(), StdGuiItem::cancel())) {
322         bool isLast=index==(combo->count()-1);
323         combo->removeItem(index);
324         combo->setCurrentIndex(isLast ? index-1 : index);
325         prevIndex=combo->currentIndex();
326         collections.removeAt(index);
327         if (1==stackedWidget->currentIndex()) {
328             haveBasicCollection=false;
329         }
330         setDetails(collections.at(combo->currentIndex()).details);
331     }
332     removeButton->setEnabled(combo->count()>1);
333 }
334 
nameChanged()335 void ServerSettings::nameChanged()
336 {
337     combo->setItemText(combo->currentIndex(), name->text().trimmed());
338 }
339 
basicDirChanged()340 void ServerSettings::basicDirChanged()
341 {
342     if (!prevBasic.details.dir.isEmpty()) {
343         QString d=Utils::fixPath(basicDir->text().trimmed());
344         basicMusicFolderNoteLabel->setOn(d.isEmpty() || d!=prevBasic.details.dir);
345     }
346 }
347 
348 #ifdef AVAHI_FOUND
adoptServerSettings(QString ip,QString p)349 void ServerSettings::adoptServerSettings(QString ip, QString p)
350 {
351     host->setText(ip);
352     port->setValue(p.toInt());
353 }
354 
detectMPDs()355 void ServerSettings::detectMPDs()
356 {
357     FindMpdDialog findMpdDlg(this);
358     QObject::connect(&findMpdDlg, &FindMpdDialog::serverChosen, this, &ServerSettings::adoptServerSettings);
359     findMpdDlg.exec();
360 }
361 #endif
362 
generateName(int ignore) const363 QString ServerSettings::generateName(int ignore) const
364 {
365     QString n;
366     QSet<QString> collectionNames;
367     for (int i=0; i<collections.size(); ++i) {
368         if (i!=ignore) {
369             collectionNames.insert(collections.at(i).details.name);
370         }
371     }
372 
373     for (int i=1; i<512; ++i) {
374         n=tr("New Collection %1").arg(i);
375         if (!collectionNames.contains(n)) {
376             break;
377         }
378     }
379 
380     return n;
381 }
382 
setDetails(const MPDConnectionDetails & details)383 void ServerSettings::setDetails(const MPDConnectionDetails &details)
384 {
385     #ifdef ENABLE_SIMPLE_MPD_SUPPORT
386     if (details.name==MPDUser::constName) {
387         basicDir->setText(Utils::convertPathForDisplay(details.dir));
388         stackedWidget->setCurrentIndex(1);
389     } else {
390     #endif
391         name->setText(details.name.isEmpty() ? tr("Default") : details.name);
392         host->setText(details.hostname);
393         port->setValue(details.port);
394         password->setText(details.password);
395         dir->setText(Utils::convertPathForDisplay(details.dir));
396         #ifdef ENABLE_HTTP_STREAM_PLAYBACK
397         streamUrl->setText(details.streamUrl);
398         #endif
399         #ifdef ENABLE_HTTP_SERVER
400         allowLocalStreaming->setChecked(details.allowLocalStreaming);
401         #endif
402         autoUpdate->setChecked(details.autoUpdate);
403         stackedWidget->setCurrentIndex(0);
404     #ifdef ENABLE_SIMPLE_MPD_SUPPORT
405     }
406     #endif
407 }
408 
getDetails() const409 MPDConnectionDetails ServerSettings::getDetails() const
410 {
411     MPDConnectionDetails details;
412     if (0==stackedWidget->currentIndex()) {
413         details.name=name->text().trimmed();
414         #ifdef ENABLE_SIMPLE_MPD_SUPPORT
415         if (details.name==MPDUser::constName) {
416             details.name=QString();
417         }
418         #endif
419         details.hostname=host->text().trimmed();
420         details.port=port->value();
421         details.password=password->text();
422         details.dir=Utils::convertPathFromDisplay(dir->text());
423         #ifdef ENABLE_HTTP_STREAM_PLAYBACK
424         details.streamUrl=streamUrl->text().trimmed();
425         #endif
426         #ifdef ENABLE_HTTP_SERVER
427         details.allowLocalStreaming=allowLocalStreaming->isChecked();
428         #endif
429         details.autoUpdate=autoUpdate->isChecked();
430     }
431     #ifdef ENABLE_SIMPLE_MPD_SUPPORT
432     else {
433         details=MPDUser::self()->details(true);
434         details.dir=Utils::convertPathFromDisplay(basicDir->text());
435         MPDUser::self()->setMusicFolder(details.dir);
436     }
437     #endif
438     details.setDirReadable();
439     return details;
440 }
441 
442 #include "moc_serversettings.cpp"
443