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