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