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 det.
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 "mpduser.h"
25 #include "config.h"
26 #include "support/utils.h"
27 #include "gui/settings.h"
28 #include "support/globalstatic.h"
29 #include <QTextStream>
30 #include <QProcess>
31 #include <QDir>
32 #include <QFile>
33 #include <QFileInfo>
34 #include <QSet>
35 #include <QCoreApplication>
36 #include <signal.h>
37 
38 const QString MPDUser::constName=QLatin1String("-");
39 
40 static const QString constDir=QLatin1String("mpd");
41 static const QString constConfigFile=QLatin1String("mpd.conf");
42 static const QString constMusicFolderKey=QLatin1String("music_directory");
43 static const QString constSocketKey=QLatin1String("bind_to_address");
44 static const QString constPlaylistsKey=QLatin1String("playlist_directory");
45 static const QString constPidKey=QLatin1String("pid_file");
46 
translatedName()47 QString MPDUser::translatedName()
48 {
49     return QObject::tr("Personal");
50 }
51 
GLOBAL_STATIC(MPDUser,instance)52 GLOBAL_STATIC(MPDUser, instance)
53 
54 #if !defined Q_OS_WIN && !defined Q_OS_MAC
55 static void moveConfig()
56 {
57     QString oldName=QDir::homePath()+"/.config/cantata/"+constDir+"/"+constConfigFile;
58 
59     if (QFile::exists(oldName)) {
60         QString newName=Utils::dataDir(constDir, true)+constConfigFile;
61         if (QFile::exists(newName)) {
62             QFile::remove(oldName);
63         } else {
64             QFile::rename(oldName, newName);
65         }
66     }
67 }
68 #endif
69 
MPDUser()70 MPDUser::MPDUser()
71 {
72     #if !defined Q_OS_WIN && !defined Q_OS_MAC
73     moveConfig();
74     #endif
75     // For now, per-user MPD support is disabled for windows builds
76     // - as I'm unsure how/if MPD works in windows!!!
77     // - If enable, also need to fix isRunning!!
78     #ifndef Q_OS_WIN
79     mpdExe=Utils::findExe("mpd");
80     #endif
81     det.name=constName;
82     det.allowLocalStreaming=true;
83 }
84 
isSupported()85 bool MPDUser::isSupported()
86 {
87     return !mpdExe.isEmpty();
88 }
89 
isRunning()90 bool MPDUser::isRunning()
91 {
92     #ifdef Q_OS_WIN
93     return false;
94     #else
95     int pid=getPid();
96     return pid ? 0==::kill(pid, 0) : false;
97     #endif
98 }
99 
readValue(const QString & line)100 static QString readValue(const QString &line)
101 {
102     int start=line.indexOf("\"");
103     int end=-1==start ? -1 : line.indexOf("\"", start+1);
104     return -1==end ? QString() : line.mid(start+1, (end-start)-1);
105 }
106 
start()107 void MPDUser::start()
108 {
109     if (isRunning() || mpdExe.isEmpty()) {
110         return;
111     }
112 
113     init(true);
114 
115     if (!det.dir.isEmpty() && !det.hostname.isEmpty()) {
116         controlMpd(false);
117     }
118 }
119 
stop()120 void MPDUser::stop()
121 {
122     if (!isRunning() || mpdExe.isEmpty()) {
123         return;
124     }
125 
126     init(false);
127     controlMpd(true);
128 }
129 
setMusicFolder(const QString & folder)130 void MPDUser::setMusicFolder(const QString &folder)
131 {
132     if (folder==det.dir) {
133         return;
134     }
135     init(true);
136 
137     bool mpdRunning = isRunning();
138     if (mpdRunning) {
139         controlMpd(true);
140     }
141 
142     QFile cfgFile(Utils::dataDir(constDir, true)+constConfigFile);
143     QStringList lines;
144     if (cfgFile.open(QIODevice::ReadOnly|QIODevice::Text)) {
145         while (!cfgFile.atEnd()) {
146             QString line = QString::fromUtf8(cfgFile.readLine());
147             if (line.startsWith(constMusicFolderKey)) {
148                 lines.append(constMusicFolderKey+" \""+folder+"\"\n");
149             } else {
150                 lines.append(line);
151             }
152         }
153         cfgFile.close();
154     }
155 
156     if (!lines.isEmpty()) {
157         if (cfgFile.open(QIODevice::WriteOnly|QIODevice::Text)) {
158             QTextStream out(&cfgFile);
159             for (const QString &line: lines) {
160                 out << line;
161             }
162             cfgFile.close();
163         }
164     }
165     det.dir=folder;
166     det.setDirReadable();
167     if (mpdRunning) {
168         controlMpd(false);
169     }
170 }
171 
setDetails(const MPDConnectionDetails & d)172 void MPDUser::setDetails(const MPDConnectionDetails &d)
173 {
174     setMusicFolder(d.dir);
175     bool dirReadable=det.dirReadable;
176     det=d;
177     det.dirReadable=dirReadable;
178 }
179 
removeDir(const QString & d)180 static void removeDir(const QString &d)
181 {
182     if (d.isEmpty()) {
183         return;
184     }
185     QDir dir(d);
186     if (dir.exists()) {
187         QString dirName=dir.dirName();
188         if (!dirName.isEmpty()) {
189             dir.cdUp();
190             dir.rmdir(dirName);
191         }
192     }
193 }
194 
cleanup()195 void MPDUser::cleanup()
196 {
197     QString cfgFileName(Utils::dataDir(constDir, false)+constConfigFile);
198     QFile cfgFile(cfgFileName);
199     QSet<QString> files;
200     QSet<QString> dirs;
201     QString playlistDir;
202 
203     if (cfgFile.open(QIODevice::ReadOnly|QIODevice::Text)) {
204         QStringList fileKeys=QStringList() << constPidKey << constSocketKey << QLatin1String("db_file")
205                                            << QLatin1String("state_file") << QLatin1String("sticker_file");
206         QStringList dirKeys=QStringList() << constPlaylistsKey;
207         while (!cfgFile.atEnd()) {
208             QString line = QString::fromUtf8(cfgFile.readLine());
209             for (const QString &key: fileKeys) {
210                 if (line.startsWith(key)) {
211                     QString file=readValue(line);
212                     if (!file.isEmpty()) {
213                         QString dir=Utils::getDir(file);
214                         if (!dir.isEmpty()) {
215                             dirs.insert(dir);
216                         }
217                         files.insert(file);
218                         fileKeys.removeAll(key);
219                     }
220                 }
221             }
222             if (playlistDir.isEmpty() && line.startsWith(constPlaylistsKey)) {
223                 playlistDir=readValue(line);
224             }
225         }
226         files.insert(cfgFileName);
227         dirs.insert(Utils::getDir(cfgFileName));
228     }
229 
230     if (!dirs.isEmpty() && !files.isEmpty()) {
231         for (const QString &f: files) {
232             QFile::remove(f);
233         }
234 
235         if (!playlistDir.isEmpty()) {
236             QFileInfoList files=QDir(playlistDir).entryInfoList(QStringList() << "*.m3u", QDir::Files|QDir::NoDotAndDotDot);
237             for (const QFileInfo &file: files) {
238                 QFile::remove(file.absoluteFilePath());
239             }
240             removeDir(playlistDir);
241         }
242 
243         for (const QString &d: dirs) {
244             removeDir(d);
245         }
246         removeDir(Utils::dataDir(constDir, false));
247         removeDir(Utils::cacheDir(constDir, false));
248     }
249 }
250 
init(bool create)251 void MPDUser::init(bool create)
252 {
253     if (create || det.dir.isEmpty() || det.hostname.isEmpty() || pidFileName.isEmpty()) {
254         // Read coverFileName from Cantata settings...
255         det.dirReadable=false;
256 
257         // Read music folder and socket from MPD conf file...
258         QString cfgDir=Utils::dataDir(constDir, create);
259         QString cfgName(cfgDir+constConfigFile);
260         QString playlists;
261         if (create && !QFile::exists(cfgName)) {
262             // Conf file does not exist, so we need to create one...
263             QFile cfgTemplate(":"+constConfigFile+".template");
264 
265             if (cfgTemplate.open(QIODevice::ReadOnly|QIODevice::Text)) {
266                 QFile cfgFile(cfgName);
267                 if (cfgFile.open(QIODevice::WriteOnly|QIODevice::Text)) {
268                     QString homeDir=QDir::homePath();
269                     QString cacheDir=Utils::cacheDir(constDir, create);
270                     QString dataDir=Utils::dataDir(constDir, create);
271                     QTextStream out(&cfgFile);
272                     while (!cfgTemplate.atEnd()) {
273                         QString line = cfgTemplate.readLine();
274                         line=line.replace(QLatin1String("${HOME}"), homeDir);
275                         line=line.replace(QLatin1String("${CONFIG_DIR}"), cfgDir);
276                         line=line.replace(QLatin1String("${DATA_DIR}"), dataDir);
277                         line=line.replace(QLatin1String("${CACHE_DIR}"), cacheDir);
278                         line=line.replace("//", "/");
279                         out << line;
280 
281                         if (det.dir.isEmpty() && line.startsWith(constMusicFolderKey)) {
282                             det.dir=Utils::fixPath(readValue(line));
283                         }
284                         if (det.hostname.isEmpty() && line.startsWith(constSocketKey)) {
285                             det.hostname=readValue(line);
286                         }
287                         if (pidFileName.isEmpty() && line.startsWith(constPidKey)) {
288                             pidFileName=readValue(line);
289                         }
290                         // Create playlists dir...
291                         if (playlists.isEmpty() && line.startsWith(constPlaylistsKey)) {
292                             playlists=readValue(line);
293                             if (!playlists.isEmpty()) {
294                                 Utils::createWorldReadableDir(playlists, QString());
295                             }
296                         }
297                     }
298                 }
299             }
300         }
301 
302         if (det.dir.isEmpty() || det.hostname.isEmpty() || pidFileName.isEmpty()) {
303             QFile cfgFile(cfgName);
304             if (cfgFile.open(QIODevice::ReadOnly|QIODevice::Text)) {
305                 while (!cfgFile.atEnd() && (det.dir.isEmpty() || det.hostname.isEmpty() || pidFileName.isEmpty())) {
306                     QString line = QString::fromUtf8(cfgFile.readLine());
307                     if (det.dir.isEmpty() && line.startsWith(constMusicFolderKey)) {
308                         det.dir=Utils::fixPath(readValue(line));
309                     }
310                     if (det.hostname.isEmpty() && line.startsWith(constSocketKey)) {
311                         det.hostname=readValue(line);
312                     }
313                     if (pidFileName.isEmpty() && line.startsWith(constPidKey)) {
314                         pidFileName=readValue(line);
315                     }
316                 }
317             }
318             det.setDirReadable();
319         }
320         det.name=constName;
321     }
322 }
323 
getPid()324 int MPDUser::getPid()
325 {
326     int pid=0;
327 
328     init(false);
329     if (!pidFileName.isEmpty()) {
330         QFile pidFile(pidFileName);
331 
332         if (pidFile.open(QIODevice::ReadOnly|QIODevice::Text)) {
333             QTextStream str(&pidFile);
334             str >> pid;
335         }
336     }
337     return pid;
338 }
339 
controlMpd(bool stop)340 bool MPDUser::controlMpd(bool stop)
341 {
342     if (stop) {
343         int pid = getPid();
344         ::kill(pid, SIGKILL);
345         return !isRunning();
346     }
347 
348     QString confFile=Utils::dataDir(constDir, true)+constConfigFile;
349     if (!QFile::exists(confFile)) {
350         return false;
351     }
352     QStringList args=QStringList() << confFile;
353     /*if (stop) {
354         args+="--kill";
355     } else*/ {
356         // Ensure cache dir exists before starting MPD
357         Utils::cacheDir(constDir, true);
358         if (!pidFileName.isEmpty() && QFile::exists(pidFileName)) {
359             QFile::remove(pidFileName);
360         }
361     }
362     bool started=QProcess::startDetached(mpdExe, args);
363     if (started && !stop) {
364         for (int i=0; i<8; ++i) {
365             Utils::msleep(250);
366             if (0!=getPid()) {
367                 return true;
368             }
369         }
370     }
371     return started;
372 }
373