1 #include "preferences/upgrade.h"
2 
3 #include <QMessageBox>
4 #include <QPixmap>
5 #include <QPushButton>
6 #include <QScopedPointer>
7 #include <QTranslator>
8 
9 #include "config.h"
10 #include "controllers/defs_controllers.h"
11 #include "database/mixxxdb.h"
12 #include "library/library_preferences.h"
13 #include "library/trackcollection.h"
14 #include "preferences/beatdetectionsettings.h"
15 #include "preferences/usersettings.h"
16 #include "util/cmdlineargs.h"
17 #include "util/db/dbconnectionpooled.h"
18 #include "util/db/dbconnectionpooler.h"
19 #include "util/math.h"
20 #include "util/versionstore.h"
21 
Upgrade()22 Upgrade::Upgrade()
23         : m_bFirstRun(false),
24           m_bRescanLibrary(false) {
25 }
26 
~Upgrade()27 Upgrade::~Upgrade() {
28 }
29 
30 // We return the UserSettings here because we have to make changes to the
31 // configuration and the location of the file may change between releases.
versionUpgrade(const QString & settingsPath)32 UserSettingsPointer Upgrade::versionUpgrade(const QString& settingsPath) {
33 
34 /*  Pre-1.7.0:
35 *
36 *   Since we didn't store version numbers in the config file prior to 1.7.0,
37 *   we check to see if the user is upgrading if his config files are in the old location,
38 *   since we moved them in 1.7.0. This code takes care of moving them.
39 */
40 
41     QDir oldLocation = QDir(QDir::homePath());
42 #ifdef __WINDOWS__
43     QFileInfo* pre170Config = new QFileInfo(oldLocation.filePath("mixxx.cfg"));
44 #else
45     QFileInfo* pre170Config = new QFileInfo(oldLocation.filePath(".mixxx.cfg"));
46 #endif
47 
48     if (pre170Config->exists()) {
49 
50         // Move the files to their new location
51         QDir newLocation = QDir(settingsPath);
52 
53         if (!newLocation.exists()) {
54             qDebug() << "Creating new settings directory" << newLocation.absolutePath();
55             newLocation.mkpath(".");
56         }
57 
58         QString errorText = "Error moving your %1 file %2 to the new location %3: \n";
59 
60 #ifdef __WINDOWS__
61         QString oldFilePath = oldLocation.filePath("mixxxtrack.xml");
62 #else
63         QString oldFilePath = oldLocation.filePath(".mixxxtrack.xml");
64 #endif
65 
66         QString newFilePath = newLocation.filePath("mixxxtrack.xml");
67         QFile* oldFile = new QFile(oldFilePath);
68         if (oldFile->exists()) {
69             if (oldFile->copy(newFilePath)) {
70                 oldFile->remove();
71             }
72             else {
73                 if (oldFile->error() == 14) {
74                     qDebug() << errorText.arg(
75                                         "library", oldFilePath, newFilePath)
76                              << "The destination file already exists.";
77                 } else {
78                     qDebug() << errorText.arg(
79                                         "library", oldFilePath, newFilePath)
80                              << "Error #" << oldFile->error();
81                 }
82             }
83         }
84         delete oldFile;
85 
86 #ifdef __WINDOWS__
87         oldFilePath = oldLocation.filePath("mixxxbpmschemes.xml");
88 #else
89         oldFilePath = oldLocation.filePath(".mixxxbpmscheme.xml");
90 #endif
91         newFilePath = newLocation.filePath("mixxxbpmscheme.xml");
92         oldFile = new QFile(oldFilePath);
93         if (oldFile->exists()) {
94             if (oldFile->copy(newFilePath)) {
95                 oldFile->remove();
96             } else {
97                 if (oldFile->error() == 14) {
98                     qDebug() << errorText.arg(
99                                         "settings", oldFilePath, newFilePath)
100                              << "The destination file already exists.";
101                 } else {
102                     qDebug() << errorText.arg(
103                                         "settings", oldFilePath, newFilePath)
104                              << "Error #" << oldFile->error();
105                 }
106             }
107         }
108         delete oldFile;
109 #ifdef __WINDOWS__
110         oldFilePath = oldLocation.filePath("MixxxMIDIBindings.xml");
111 #else
112         oldFilePath = oldLocation.filePath(".MixxxMIDIBindings.xml");
113 #endif
114         newFilePath = newLocation.filePath("MixxxMIDIBindings.xml");
115         oldFile = new QFile(oldFilePath);
116         if (oldFile->exists()) {
117             qWarning() << "The MIDI mapping file format has changed in this version of Mixxx. You will need to reconfigure your MIDI controller. See the Wiki for full details on the new format.";
118             if (oldFile->copy(newFilePath)) {
119                 oldFile->remove();
120             } else {
121                 if (oldFile->error() == 14) {
122                     qDebug()
123                             << errorText.arg(
124                                        "MIDI mapping", oldFilePath, newFilePath)
125                             << "The destination file already exists.";
126                 } else {
127                     qDebug()
128                             << errorText.arg(
129                                        "MIDI mapping", oldFilePath, newFilePath)
130                             << "Error #" << oldFile->error();
131                 }
132             }
133         }
134         // Tidy up
135         delete oldFile;
136 #ifdef __WINDOWS__
137         QFile::remove(oldLocation.filePath("MixxxMIDIDevice.xml")); // Obsolete file, so just delete it
138 #else
139         QFile::remove(oldLocation.filePath(".MixxxMIDIDevice.xml")); // Obsolete file, so just delete it
140 #endif
141 
142 #ifdef __WINDOWS__
143         oldFilePath = oldLocation.filePath("mixxx.cfg");
144 #else
145         oldFilePath = oldLocation.filePath(".mixxx.cfg");
146 #endif
147         newFilePath = newLocation.filePath(MIXXX_SETTINGS_FILE);
148         oldFile = new QFile(oldFilePath);
149         if (oldFile->copy(newFilePath)) {
150             oldFile->remove();
151         } else {
152             if (oldFile->error() == 14) {
153                 qDebug() << errorText.arg(
154                                     "configuration", oldFilePath, newFilePath)
155                          << "The destination file already exists.";
156             } else {
157                 qDebug() << errorText.arg(
158                                     "configuration", oldFilePath, newFilePath)
159                          << "Error #" << oldFile->error();
160             }
161         }
162         delete oldFile;
163 
164     }
165     // Tidy up
166     delete pre170Config;
167     // End pre-1.7.0 code
168 
169 
170 /***************************************************************************
171 *                           Post-1.7.0 upgrade code
172 *
173 *   Add entries to the IF ladder below if anything needs to change from the
174 *   previous to the current version. This allows for incremental upgrades
175 *   in case a user upgrades from a few versions prior.
176 ****************************************************************************/
177 
178     // Read the config file from home directory
179     UserSettingsPointer config(new ConfigObject<ConfigValue>(
180             QDir(settingsPath).filePath(MIXXX_SETTINGS_FILE)));
181 
182     QString configVersion = config->getValueString(ConfigKey("[Config]","Version"));
183 
184     if (configVersion.isEmpty()) {
185 
186 #ifdef __APPLE__
187         qDebug() << "Config version is empty, trying to read pre-1.9.0 config";
188         // Try to read the config from the pre-1.9.0 final directory on OS X (we moved it in 1.9.0 final)
189         QScopedPointer<QFile> oldConfigFile(new QFile(QDir::homePath().append("/").append(".mixxx/mixxx.cfg")));
190         if (oldConfigFile->exists() && ! CmdlineArgs::Instance().getSettingsPathSet()) {
191             qDebug() << "Found pre-1.9.0 config for OS X";
192             // Note: We changed MIXXX_SETTINGS_PATH in 1.9.0 final on OS X so
193             // it must be hardcoded to ".mixxx" here for legacy.
194             config = UserSettingsPointer(new ConfigObject<ConfigValue>(
195                 QDir::homePath().append("/.mixxx/mixxx.cfg")));
196             // Just to be sure all files like logs and soundconfig go with mixxx.cfg
197             // TODO(XXX) Trailing slash not needed anymore as we switches from String::append
198             // to QDir::filePath elsewhere in the code. This is candidate for removal.
199             CmdlineArgs::Instance().setSettingsPath(QDir::homePath().append("/.mixxx/"));
200             configVersion = config->getValueString(ConfigKey("[Config]","Version"));
201         }
202         else {
203 #elif __WINDOWS__
204         qDebug() << "Config version is empty, trying to read pre-1.12.0 config";
205         // Try to read the config from the pre-1.12.0 final directory on Windows (we moved it in 1.12.0 final)
206         QScopedPointer<QFile> oldConfigFile(new QFile(QDir::homePath().append("/Local Settings/Application Data/Mixxx/mixxx.cfg")));
207         if (oldConfigFile->exists() && ! CmdlineArgs::Instance().getSettingsPathSet()) {
208             qDebug() << "Found pre-1.12.0 config for Windows";
209             // Note: We changed MIXXX_SETTINGS_PATH in 1.12.0 final on Windows
210             // so it must be hardcoded to "Local Settings/Application
211             // Data/Mixxx/" here for legacy.
212             config = UserSettingsPointer(new ConfigObject<ConfigValue>(
213                 QDir::homePath().append("/Local Settings/Application Data/Mixxx/mixxx.cfg")));
214             // Just to be sure all files like logs and soundconfig go with mixxx.cfg
215             // TODO(XXX) Trailing slash not needed anymore as we switches from String::append
216             // to QDir::filePath elsewhere in the code. This is candidate for removal.
217             CmdlineArgs::Instance().setSettingsPath(QDir::homePath().append("/Local Settings/Application Data/Mixxx/"));
218             configVersion = config->getValueString(ConfigKey("[Config]","Version"));
219         }
220         else {
221 #endif
222             // This must have been the first run... right? :)
223             qDebug() << "No version number in configuration file. Setting to"
224                      << VersionStore::version();
225             config->set(ConfigKey("[Config]", "Version"), ConfigValue(VersionStore::version()));
226             m_bFirstRun = true;
227             return config;
228 #ifdef __APPLE__
229         }
230 #elif __WINDOWS__
231         }
232 #endif
233     }
234 
235     // If it's already current, stop here
236     if (configVersion == VersionStore::version()) {
237         qDebug() << "Configuration file is at the current version" << VersionStore::version();
238         return config;
239     }
240 
241     // Allows for incremental upgrades in case someone upgrades from a few versions prior
242     // (I wish we could do a switch on a QString.)
243     /*
244     // Examples, since we didn't store the version number prior to v1.7.0
245     if (configVersion.startsWith("1.6.0")) {
246         qDebug() << "Upgrading from v1.6.0 to 1.6.1...";
247         // Upgrade tasks go here
248         configVersion = "1.6.1";
249         config->set(ConfigKey("[Config]","Version"), ConfigValue("1.6.1"));
250     }
251     if (configVersion.startsWith("1.6.1")) {
252         qDebug() << "Upgrading from v1.6.1 to 1.7.0...";
253         // Upgrade tasks go here
254         configVersion = "1.7.0";
255         config->set(ConfigKey("[Config]","Version"), ConfigValue("1.7.0"));
256     }
257     */
258 
259     // We use the following blocks to detect if this is the first time
260     // you've run the latest version of Mixxx. This lets us show
261     // the promo tracks stats agreement stuff for all users that are
262     // upgrading Mixxx.
263 
264     if (configVersion.startsWith("1.7")) {
265         qDebug() << "Upgrading from v1.7.x...";
266         // Upgrade tasks go here
267         // Nothing to change, really
268         configVersion = "1.8.0";
269         config->set(ConfigKey("[Config]","Version"), ConfigValue("1.8.0"));
270     }
271 
272     if (configVersion.startsWith("1.8.0~beta1") ||
273         configVersion.startsWith("1.8.0~beta2")) {
274         qDebug() << "Upgrading from v1.8.0~beta...";
275         // Upgrade tasks go here
276         configVersion = "1.8.0";
277         config->set(ConfigKey("[Config]","Version"), ConfigValue("1.8.0"));
278     }
279     if (configVersion.startsWith("1.8") || configVersion.startsWith("1.9.0beta1")) {
280         qDebug() << "Upgrading from" << configVersion << "...";
281         // Upgrade tasks go here
282 #ifdef __APPLE__
283         QString OSXLocation180 = QDir::homePath().append("/").append(".mixxx");
284         QString OSXLocation190 = settingsPath;
285         QDir newOSXDir(OSXLocation190);
286         newOSXDir.mkpath(OSXLocation190);
287 
288         QList<QPair<QString, QString> > dirsToMove;
289         dirsToMove.push_back(QPair<QString, QString>(OSXLocation180, OSXLocation190));
290         dirsToMove.push_back(QPair<QString, QString>(OSXLocation180 + "/midi", OSXLocation190 + "midi"));
291         dirsToMove.push_back(QPair<QString, QString>(OSXLocation180 + "/presets", OSXLocation190 + "presets"));
292 
293         QListIterator<QPair<QString, QString> > dirIt(dirsToMove);
294         QPair<QString, QString> curPair;
295         while (dirIt.hasNext())
296         {
297             curPair = dirIt.next();
298             qDebug() << "Moving" << curPair.first << "to" << curPair.second;
299             QDir oldSubDir(curPair.first);
300             QDir newSubDir(curPair.second);
301             newSubDir.mkpath(curPair.second); // Create the new destination directory
302 
303             QStringList contents = oldSubDir.entryList(QDir::Files | QDir::NoDotAndDotDot);
304             QStringListIterator it(contents);
305             QString cur;
306             // Iterate over all the files in the source directory and copy them to the dest dir.
307             while (it.hasNext())
308             {
309                 cur = it.next();
310                 QString src = curPair.first + "/" + cur;
311                 QString dest = curPair.second + "/" + cur;
312                 qDebug() << "Copying" << src << "to" << dest;
313                 if (!QFile::copy(src, dest))
314                 {
315                     qDebug() << "Failed to move file during upgrade.";
316                 }
317             }
318 
319             // Rename the old directory.
320             newOSXDir.rename(OSXLocation180, OSXLocation180 + "-1.8");
321         }
322         // Reload the configuration file from the new location.
323         // (We want to make sure we save to the new location...)
324         config = UserSettingsPointer(new ConfigObject<ConfigValue>(
325                 QDir(settingsPath).filePath(MIXXX_SETTINGS_FILE)));
326 #endif
327         configVersion = "1.9.0";
328         config->set(ConfigKey("[Config]","Version"), ConfigValue("1.9.0"));
329     }
330     if (configVersion.startsWith("1.9") || configVersion.startsWith("1.10")) {
331         qDebug() << "Upgrading from v1.9.x/1.10.x...";
332 
333         bool successful = true;
334 
335         qDebug() << "Copying midi/ to controllers/";
336         QString midiPath = legacyUserPresetsPath(config);
337         QString controllerPath = userPresetsPath(config);
338         QDir oldDir(midiPath);
339         QDir newDir(controllerPath);
340         newDir.mkpath(controllerPath);  // create the new directory
341 
342         QStringList contents = oldDir.entryList(QDir::Files | QDir::NoDotAndDotDot);
343         QStringListIterator it(contents);
344         QString cur;
345         // Iterate over all the files in the source directory and copy them to the dest dir.
346         while (it.hasNext()) {
347             cur = it.next();
348             if (newDir.exists(cur)) {
349                 qDebug() << cur << "already exists in"
350                          << controllerPath << "Skipping.";
351                 continue;
352             }
353             QString src = oldDir.absoluteFilePath(cur);
354             QString dest = newDir.absoluteFilePath(cur);
355             qDebug() << "Copying" << src << "to" << dest;
356             if (!QFile::copy(src, dest)) {
357                 qDebug() << "Failed to copy file during upgrade.";
358                 successful = false;
359             }
360         }
361 
362         bool reanalyze_choice = askReanalyzeBeats();
363         BeatDetectionSettings bpmSettings(config);
364         bpmSettings.setReanalyzeWhenSettingsChange(reanalyze_choice);
365 
366         if (successful) {
367             qDebug() << "Upgrade Successful";
368             configVersion = "1.11.0";
369             config->set(ConfigKey("[Config]","Version"),
370                         ConfigValue(configVersion));
371         } else {
372             qDebug() << "Upgrade Failed";
373         }
374     }
375 
376     if (configVersion.startsWith("1.11")) {
377         qDebug() << "Upgrading from v1.11.x...";
378         bool successful = false;
379         {
380             MixxxDb mixxxDb(config);
381             const mixxx::DbConnectionPooler dbConnectionPooler(
382                     mixxxDb.connectionPool());
383             if (dbConnectionPooler.isPooling()) {
384                 QSqlDatabase dbConnection = mixxx::DbConnectionPooled(mixxxDb.connectionPool());
385                 DEBUG_ASSERT(dbConnection.isOpen());
386                 if (MixxxDb::initDatabaseSchema(dbConnection)) {
387                     TrackCollection tc(config);
388                     tc.connectDatabase(dbConnection);
389 
390                     // upgrade to the multi library folder settings
391                     QString currentFolder = config->getValueString(PREF_LEGACY_LIBRARY_DIR);
392                     // to migrate the DB just add the current directory to the new
393                     // directories table
394                     // NOTE(rryan): We don't have to ask for sandbox permission to this
395                     // directory because the normal startup integrity check in Library will
396                     // notice if we don't have permission and ask for access. Also, the
397                     // Sandbox isn't setup yet at this point in startup because it relies on
398                     // the config settings path and this function is what loads the config
399                     // so it's not ready yet.
400                     successful = tc.addDirectory(currentFolder);
401 
402                     tc.disconnectDatabase();
403                 }
404             }
405         }
406 
407         // ask for library rescan to activate cover art. We can later ask for
408         // this variable when the library scanner is constructed.
409         m_bRescanLibrary = askReScanLibrary();
410 
411         // Versions of mixxx until 1.11 had a hack that multiplied gain by 1/2,
412         // which was compensation for another hack that set replaygain to a
413         // default of 6.  We've now removed all of the hacks, so subtracting
414         // 6 from everyone's replay gain should keep things consistent for
415         // all users.
416         int oldReplayGain = config->getValue(
417                 ConfigKey("[ReplayGain]", "InitialReplayGainBoost"), 6);
418         int newReplayGain = math_max(-6, oldReplayGain - 6);
419         config->set(ConfigKey("[ReplayGain]", "InitialReplayGainBoost"),
420                     ConfigValue(newReplayGain));
421 
422         // if everything until here worked fine we can mark the configuration as
423         // updated
424         if (successful) {
425             configVersion = VersionStore::version();
426             config->set(ConfigKey("[Config]", "Version"), ConfigValue(VersionStore::version()));
427         }
428         else {
429             qDebug() << "Upgrade failed!\n";
430         }
431     }
432 
433     if (configVersion.startsWith("1.12") ||
434         configVersion.startsWith("2.0") ||
435         configVersion.startsWith("2.1.0")) {
436         // No special upgrade required, just update the value.
437         configVersion = VersionStore::version();
438         config->set(ConfigKey("[Config]", "Version"), ConfigValue(VersionStore::version()));
439     }
440 
441     if (configVersion == VersionStore::version()) {
442         qDebug() << "Configuration file is now at the current version" << VersionStore::version();
443     } else {
444         qWarning() << "Configuration file is at version" << configVersion
445                    << "instead of the current" << VersionStore::version();
446     }
447 
448     return config;
449 }
450 
askReScanLibrary()451 bool Upgrade::askReScanLibrary() {
452     QMessageBox msgBox;
453     msgBox.setIconPixmap(QPixmap(":/images/icons/mixxx.svg"));
454     msgBox.setWindowTitle(QMessageBox::tr("Upgrading Mixxx"));
455     msgBox.setText(QMessageBox::tr("Mixxx now supports displaying cover art.\n"
456                       "Do you want to scan your library for cover files now?"));
457     QPushButton* rescanButton = msgBox.addButton(
458         QMessageBox::tr("Scan"), QMessageBox::AcceptRole);
459     msgBox.addButton(QMessageBox::tr("Later"), QMessageBox::RejectRole);
460     msgBox.setDefaultButton(rescanButton);
461     msgBox.exec();
462 
463     return msgBox.clickedButton() == rescanButton;
464 }
465 
askReanalyzeBeats()466 bool Upgrade::askReanalyzeBeats() {
467     QString windowTitle =
468             QMessageBox::tr("Upgrading Mixxx from v1.9.x/1.10.x.");
469     QString mainHeading =
470             QMessageBox::tr("Mixxx has a new and improved beat detector.");
471     QString paragraph1 = QMessageBox::tr(
472         "When you load tracks, Mixxx can re-analyze them "
473         "and generate new, more accurate beatgrids. This will make "
474         "automatic beatsync and looping more reliable.");
475     QString paragraph2 = QMessageBox::tr(
476         "This does not affect saved cues, hotcues, playlists, or crates.");
477     QString paragraph3 = QMessageBox::tr(
478         "If you do not want Mixxx to re-analyze your tracks, choose "
479         "\"Keep Current Beatgrids\". You can change this setting at any time "
480         "from the \"Beat Detection\" section of the Preferences.");
481     QString keepCurrent = QMessageBox::tr("Keep Current Beatgrids");
482     QString generateNew = QMessageBox::tr("Generate New Beatgrids");
483 
484     QMessageBox msgBox;
485     msgBox.setIconPixmap(QPixmap(":/images/icons/mixxx.svg"));
486     msgBox.setWindowTitle(windowTitle);
487     msgBox.setText(QString("<html><h2>%1</h2><p>%2</p><p>%3</p><p>%4</p></html>")
488                    .arg(mainHeading, paragraph1, paragraph2, paragraph3));
489     msgBox.addButton(keepCurrent, QMessageBox::NoRole);
490     QPushButton* OverwriteButton = msgBox.addButton(
491         generateNew, QMessageBox::YesRole);
492     msgBox.setDefaultButton(OverwriteButton);
493     msgBox.exec();
494 
495     if (msgBox.clickedButton() == (QAbstractButton*)OverwriteButton) {
496         return true;
497     }
498     return false;
499 }
500