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 "application.h"
25 #include <QTranslator>
26 #include <QTextCodec>
27 #include <QLibraryInfo>
28 #include <QDir>
29 #include <QFile>
30 #include <QSettings>
31 #include "support/utils.h"
32 #include "config.h"
33 #include "settings.h"
34 #include "initialsettingswizard.h"
35 #include "mainwindow.h"
36 #include "mpd-interface/song.h"
37 #include "support/thread.h"
38 #include "db/librarydb.h"
39
40 // To enable debug...
41 #include "mpd-interface/mpdconnection.h"
42 #include "mpd-interface/mpdparseutils.h"
43 #include "mpd-interface/cuefile.h"
44 #include "covers.h"
45 #include "context/wikipediaengine.h"
46 #include "context/lastfmengine.h"
47 #include "context/metaengine.h"
48 #include "playlists/dynamicplaylists.h"
49 #ifdef ENABLE_DEVICES_SUPPORT
50 #include "models/devicesmodel.h"
51 #endif
52 #include "streams/streamfetcher.h"
53 #include "http/httpserver.h"
54 #include "widgets/songdialog.h"
55 #include "network/networkaccessmanager.h"
56 #include "context/ultimatelyricsprovider.h"
57 #include "tags/taghelperiface.h"
58 #include "context/contextwidget.h"
59 #include "scrobbling/scrobbler.h"
60 #include "gui/mediakeys.h"
61 #ifdef ENABLE_HTTP_STREAM_PLAYBACK
62 #include "mpd-interface/httpstream.h"
63 #endif
64 #ifdef AVAHI_FOUND
65 #include "avahidiscovery.h"
66 #endif
67 #include "customactions.h"
68
69 #include <QMutex>
70 #include <QMutexLocker>
71 #include <QTextStream>
72 #include <QDateTime>
73 #include <QByteArray>
74 #include <QCommandLineParser>
75 #include <iostream>
76
77 static QMutex msgMutex;
78 static bool firstMsg=true;
79 static bool debugToFile=false;
cantataQtMsgHandler(QtMsgType,const QMessageLogContext &,const QString & msg)80 static void cantataQtMsgHandler(QtMsgType, const QMessageLogContext &, const QString &msg)
81 {
82 if (debugToFile) {
83 QMutexLocker locker(&msgMutex);
84 QFile f(Utils::cacheDir(QString(), true)+"cantata.log");
85 if (f.open(QIODevice::WriteOnly|QIODevice::Append|QIODevice::Text)) {
86 QTextStream stream(&f);
87 if (firstMsg) {
88 stream << "------------START------------" << endl;
89 firstMsg=false;
90 }
91 stream << QDateTime::currentDateTime().toString(Qt::ISODate).replace("T", " ") << " - " << msg << endl;
92 }
93 } else {
94 std::cout << QDateTime::currentDateTime().toString(Qt::ISODate).replace("T", " ").toLatin1().constData()
95 << " - " << msg.toLocal8Bit().constData() << std::endl;
96 }
97 }
98
loadTranslation(const QString & prefix,const QString & path,const QString & overrideLanguage=QString ())99 static void loadTranslation(const QString &prefix, const QString &path, const QString &overrideLanguage = QString())
100 {
101 QString language = overrideLanguage.isEmpty() ? QLocale::system().name() : overrideLanguage;
102 QTranslator *t = new QTranslator;
103 if (t->load(prefix+"_"+language, path)) {
104 QCoreApplication::installTranslator(t);
105 } else {
106 delete t;
107 }
108 }
109
removeOldFiles(const QString & d,const QStringList & types)110 static void removeOldFiles(const QString &d, const QStringList &types)
111 {
112 if (!d.isEmpty()) {
113 QDir dir(d);
114 if (dir.exists()) {
115 QFileInfoList files=dir.entryInfoList(types, QDir::Files|QDir::NoDotAndDotDot);
116 for (const QFileInfo &file: files) {
117 QFile::remove(file.absoluteFilePath());
118 }
119 QString dirName=dir.dirName();
120 if (!dirName.isEmpty()) {
121 dir.cdUp();
122 dir.rmdir(dirName);
123 }
124 }
125 }
126 }
127
removeOldFiles()128 static void removeOldFiles()
129 {
130 // Remove Cantata 1.x XML cache files
131 removeOldFiles(Utils::cacheDir("library"), QStringList() << "*.xml" << "*.xml.gz");
132 removeOldFiles(Utils::cacheDir("jamendo"), QStringList() << "*.xml.gz");
133 removeOldFiles(Utils::cacheDir("magnatune"), QStringList() << "*.xml.gz");
134 }
135
debugAreas()136 static QString debugAreas()
137 {
138 return QObject::tr("mpd - MPD communication")+QLatin1Char('\n')
139 +QObject::tr("mpdparse - Parsing of MPD response")+QLatin1Char('\n')
140 +QObject::tr("cue - Cue file parsing")+QLatin1Char('\n')
141 +QObject::tr("covers - Cover fetching, and loading")+QLatin1Char('\n')
142 +QObject::tr("covers-verbose - Cover fetching, and loading (verbose) ")+QLatin1Char('\n')
143 +QObject::tr("context-wikipedia - Wikipedia info in context view")+QLatin1Char('\n')
144 +QObject::tr("context-lastfm - Last.fm info in context view")+QLatin1Char('\n')
145 +QObject::tr("context-widget - General debug in context view")+QLatin1Char('\n')
146 +QObject::tr("context-lyrics - Lyrics in context view")+QLatin1Char('\n')
147 +QObject::tr("dynamic - Dynamic playlists")+QLatin1Char('\n')
148 +QObject::tr("stream-fetcher - Fetching of stream URLs")+QLatin1Char('\n')
149 +QObject::tr("http-server - Built-in HTTP server")+QLatin1Char('\n')
150 +QObject::tr("song-dialog - Song dialogs (tags, replaygain, organiser)")+QLatin1Char('\n')
151 +QObject::tr("network-access - Network access")+QLatin1Char('\n')
152 +QObject::tr("threads - Threads")+QLatin1Char('\n')
153 +QObject::tr("scrobbler - Scrobbling")+QLatin1Char('\n')
154 +QObject::tr("sql - SQL access")+QLatin1Char('\n')
155 +QObject::tr("media-keys - Media-keys")+QLatin1Char('\n')
156 +QObject::tr("custom-actions - Custom actions")+QLatin1Char('\n')
157 #ifdef TAGLIB_FOUND
158 +QObject::tr("tags - Tag reading/writing")+QLatin1Char('\n')
159 #endif
160 #ifdef ENABLE_DEVICES_SUPPORT
161 +QObject::tr("devices - Device support")+QLatin1Char('\n')
162 #endif
163 #ifdef ENABLE_HTTP_STREAM_PLAYBACK
164 +QObject::tr("http-stream - Playback of MPD output streams")+QLatin1Char('\n')
165 #endif
166 #ifdef AVAHI_FOUND
167 +QObject::tr("avahi - Auto-discovery of MPD servers")+QLatin1Char('\n')
168 #endif
169 +QObject::tr("all - Enable all debug")+QLatin1Char('\n')
170 ;
171 }
172
installDebugMessageHandler(const QString & cmdLine)173 static void installDebugMessageHandler(const QString &cmdLine)
174 {
175 QStringList items=cmdLine.split(",", QString::SkipEmptyParts);
176
177 for (const auto &area: items) {
178 bool all = QLatin1String("all")==area;
179 if (all || QLatin1String("mpd")==area) {
180 MPDConnection::enableDebug();
181 }
182 if (all || QLatin1String("mpdparse")==area) {
183 MPDParseUtils::enableDebug();
184 }
185 if (all || QLatin1String("cue")==area) {
186 CueFile::enableDebug();
187 }
188 if (all || QLatin1String("covers")==area) {
189 Covers::enableDebug(false);
190 }
191 if (all || QLatin1String("covers-verbose")==area) {
192 Covers::enableDebug(true);
193 }
194 if (all || QLatin1String("context-wikipedia")==area) {
195 WikipediaEngine::enableDebug();
196 }
197 if (all || QLatin1String("context-lastfm")==area) {
198 LastFmEngine::enableDebug();
199 }
200 if (all || QLatin1String("context-info")==area) {
201 MetaEngine::enableDebug();
202 }
203 if (all || QLatin1String("context-widget")==area) {
204 ContextWidget::enableDebug();
205 }
206 if (all || QLatin1String("dynamic")==area) {
207 DynamicPlaylists::enableDebug();
208 }
209 if (all || QLatin1String("stream-fetcher")==area) {
210 StreamFetcher::enableDebug();
211 }
212 if (all || QLatin1String("http-server")==area) {
213 HttpServer::enableDebug();
214 }
215 if (all || QLatin1String("song-dialog")==area) {
216 SongDialog::enableDebug();
217 }
218 if (all || QLatin1String("network-access")==area) {
219 NetworkAccessManager::enableDebug();
220 }
221 if (all || QLatin1String("context-lyrics")==area) {
222 UltimateLyricsProvider::enableDebug();
223 }
224 if (all || QLatin1String("threads")==area) {
225 ThreadCleaner::enableDebug();
226 }
227 if (all || QLatin1String("scrobbler")==area) {
228 Scrobbler::enableDebug();
229 }
230 if (all || QLatin1String("sql")==area) {
231 LibraryDb::enableDebug();
232 }
233 if (all || QLatin1String("media-keys")==area) {
234 MediaKeys::enableDebug();
235 }
236 if (all || QLatin1String("custom-actions")==area) {
237 CustomActions::enableDebug();
238 }
239 #ifdef TAGLIB_FOUND
240 if (all || QLatin1String("tags")==area) {
241 TagHelperIface::enableDebug();
242 }
243 #endif
244 #ifdef ENABLE_DEVICES_SUPPORT
245 if (all || QLatin1String("devices")==area) {
246 DevicesModel::enableDebug();
247 }
248 #endif
249 #ifdef ENABLE_HTTP_STREAM_PLAYBACK
250 if (all || QLatin1String("http-stream")==area) {
251 HttpStream::enableDebug();
252 }
253 #endif
254 #ifdef AVAHI_FOUND
255 if (all || QLatin1String("avahi")==area) {
256 AvahiDiscovery::enableDebug();
257 }
258 #endif
259 }
260 qInstallMessageHandler(cantataQtMsgHandler);
261 }
262
263 #if defined Q_OS_LINUX && defined __GNUC__
264 #include <execinfo.h>
265 #include <unistd.h>
266 #include <signal.h>
267 #include <cxxabi.h>
268
sigHandler(int i)269 static void sigHandler(int i)
270 {
271 // Adapted from: https://panthema.net/2008/0901-stacktrace-demangled/
272
273 // stacktrace.h (c) 2008, Timo Bingmann from http://idlebox.net/
274 // published under the WTFPL v2.0
275 static const int constMaxFuncNameLen = 256;
276 static const int constNumLevels = 15;
277 void *addrlist[constNumLevels+1];
278
279 fprintf(stderr, "Unfortunately Cantata has crashed. Please report a bug at \n"
280 "https://github.com/CDrummond/cantata/issues/ and include the following stack trace:\n\n");
281 // retrieve current stack addresses
282 int addrlen = backtrace(addrlist, sizeof(addrlist) / sizeof(void*));
283 if (!addrlen) {
284 fprintf(stderr, "Failed to produce stack trace!\n");
285 _exit(0);
286 }
287
288 char ** symbolList = backtrace_symbols(addrlist, addrlen);
289 char funcName[constMaxFuncNameLen];
290
291 // iterate over the returned symbol lines. skip the first, it is the
292 // address of this function.
293 for (int i = 1; i < addrlen; i++) {
294 char *beginName = nullptr;
295 char *beginOffset = nullptr;
296 char *endOffset = nullptr;
297
298 // find parentheses and +address offset surrounding the mangled name:
299 // ./module(function+0x15c) [0x8048a6d]
300 for (char *p = symbolList[i]; *p; ++p) {
301 if (*p == '(') {
302 beginName = p;
303 } else if (*p == '+') {
304 beginOffset = p;
305 } else if (*p == ')' && beginOffset) {
306 endOffset = p;
307 break;
308 }
309 }
310
311 if (beginName && beginOffset && endOffset && beginName < beginOffset) {
312 *beginName++ = '\0';
313 *beginOffset++ = '\0';
314 *endOffset = '\0';
315
316 // mangled name is now in [begin_name, begin_offset) and caller
317 // offset in [begin_offset, end_offset). now apply
318 // __cxa_demangle():
319
320 int status = 0;
321 size_t nameLen = constMaxFuncNameLen;
322 char * ret = abi::__cxa_demangle(beginName, funcName, &nameLen, &status);
323 if (!status) {
324 fprintf(stderr, " %s : %s+%s\n", symbolList[i], ret, beginOffset);
325 } else {
326 // demangling failed. Output function name as a C function with
327 // no arguments.
328 fprintf(stderr, " %s : %s()+%s\n", symbolList[i], beginName, beginOffset);
329 }
330 } else {
331 // couldn't parse the line? print the whole line.
332 fprintf(stderr, " %s\n", symbolList[i]);
333 }
334 }
335
336 free(symbolList);
337 _exit(1);
338 }
339 #endif
340
main(int argc,char * argv[])341 int main(int argc, char *argv[])
342 {
343 #if defined Q_OS_LINUX && defined __GNUC__
344 signal(SIGSEGV, sigHandler);
345 #endif
346 QThread::currentThread()->setObjectName("GUI");
347 QCoreApplication::setApplicationName(PACKAGE_NAME);
348 QCoreApplication::setOrganizationName(ORGANIZATION_NAME);
349
350 QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
351 // Dont enable AA_EnableHighDpiScaling - messes up fractional scaling? Issue #1257
352 //#if QT_VERSION >= 0x050600 && defined Q_OS_WIN
353 //QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
354 //#endif
355
356 Application app(argc, argv);
357 app.setApplicationVersion(PACKAGE_VERSION_STRING);
358
359 QCommandLineParser cmdLineParser;
360 cmdLineParser.setApplicationDescription(QObject::tr("MPD Client"));
361 cmdLineParser.addHelpOption();
362 cmdLineParser.addVersionOption();
363 QCommandLineOption debugOption(QStringList() << "d" << "debug", QObject::tr("Comma-separated list of debug areas - possible values:\n")+debugAreas(), "debug", "");
364 QCommandLineOption debugToFileOption(QStringList() << "f" << "debug-to-file", QObject::tr("Log debug messages to %1").arg(Utils::cacheDir(QString(), true)+"cantata.log"), "", "false");
365 QCommandLineOption noNetworkOption(QStringList() << "n" << "no-network", QObject::tr("Disable network access"), "", "false");
366 QCommandLineOption collectionOption(QStringList() << "c" << "collection", QObject::tr("Collection name"), "collection", "");
367 QCommandLineOption fullscreenOption(QStringList() << "F" << "fullscreen", QObject::tr("Start full screen"), "", "false");
368 cmdLineParser.addOption(debugOption);
369 cmdLineParser.addOption(debugToFileOption);
370 cmdLineParser.addOption(noNetworkOption);
371 cmdLineParser.addOption(collectionOption);
372 cmdLineParser.addOption(fullscreenOption);
373 cmdLineParser.process(app);
374 QStringList files = cmdLineParser.positionalArguments();
375
376 if (!app.start(files)) {
377 return 0;
378 }
379
380 if (cmdLineParser.isSet(noNetworkOption)) {
381 NetworkAccessManager::disableNetworkAccess();
382 }
383
384 // Set the permissions on the config file on Unix - it can contain passwords
385 // for internet services so it's important that other users can't read it.
386 // On Windows these are stored in the registry instead.
387 #ifdef Q_OS_UNIX
388 QSettings s;
389
390 // Create the file if it doesn't exist already
391 if (!QFile::exists(s.fileName())) {
392 QFile file(s.fileName());
393 file.open(QIODevice::WriteOnly);
394 }
395
396 // Set -rw-------
397 QFile::setPermissions(s.fileName(), QFile::ReadOwner | QFile::WriteOwner);
398 #endif
399
400 removeOldFiles();
401 if (cmdLineParser.isSet(debugOption)) {
402 installDebugMessageHandler(cmdLineParser.value(debugOption));
403 debugToFile=cmdLineParser.isSet(debugToFileOption);
404 }
405
406 // Translations
407 QString lang=Settings::self()->lang();
408 #if defined Q_OS_WIN || defined Q_OS_MAC
409 loadTranslation("qt", CANTATA_SYS_TRANS_DIR, lang);
410 #else
411 loadTranslation("qt", QLibraryInfo::location(QLibraryInfo::TranslationsPath), lang);
412 #endif
413 QString local = Utils::fixPath(QCoreApplication::applicationDirPath())+"translations";
414 loadTranslation("cantata", QDir(local).exists() ? local : CANTATA_SYS_TRANS_DIR, lang);
415
416 Application::init();
417
418 if (Settings::self()->firstRun()) {
419 InitialSettingsWizard wz;
420 if (QDialog::Rejected==wz.exec()) {
421 return 0;
422 }
423 } else if (cmdLineParser.isSet(collectionOption)) {
424 QString col = cmdLineParser.value(collectionOption);
425 if (!col.isEmpty() && col!=Settings::self()->currentConnection()) {
426 auto collections = Settings::self()->allConnections();
427 for (const auto &c: collections) {
428 if (c.name==col) {
429 Settings::self()->saveCurrentConnection(col);
430 break;
431 }
432 }
433 }
434 }
435 MainWindow mw;
436 #if defined Q_OS_WIN || defined Q_OS_MAC
437 app.setActivationWindow(&mw);
438 #endif // !defined Q_OS_MAC
439 app.loadFiles(files);
440
441 if (cmdLineParser.isSet(fullscreenOption)) {
442 mw.fullScreen();
443 }
444
445 return app.exec();
446 }
447