1 #include "preferences/broadcastsettings.h"
2 
3 #include <QDir>
4 #include <QFileInfo>
5 #include <QFileInfoList>
6 #include <QStringList>
7 
8 #include "broadcast/defs_broadcast.h"
9 #include "defs_urls.h"
10 #include "moc_broadcastsettings.cpp"
11 #include "util/logger.h"
12 #include "util/memory.h"
13 
14 namespace {
15 const char* kProfilesSubfolder = "broadcast_profiles";
16 const char* kDefaultProfile = "Connection 1"; // Must be used only when initializing profiles
17 const mixxx::Logger kLogger("BroadcastSettings");
18 } // anonymous namespace
19 
BroadcastSettings(UserSettingsPointer pConfig,QObject * parent)20 BroadcastSettings::BroadcastSettings(
21         UserSettingsPointer pConfig, QObject* parent)
22     : QObject(parent),
23       m_pConfig(pConfig),
24       m_profiles() {
25     loadProfiles();
26 }
27 
loadProfiles()28 void BroadcastSettings::loadProfiles() {
29     QDir profilesFolder(getProfilesFolder());
30     if (!profilesFolder.exists()) {
31         kLogger.info() << "Profiles folder doesn't exist. Creating it.";
32         profilesFolder.mkpath(profilesFolder.absolutePath());
33     }
34 
35     QStringList nameFilters("*.bcp.xml");
36     QFileInfoList files =
37             profilesFolder.entryInfoList(nameFilters, QDir::Files, QDir::Name);
38 
39     // If *.bcp.xml files exist in the profiles subfolder, those will be loaded
40     // and instantiated in the class' internal profile list for other by it and
41     // Mixxx subsystems related to Live Broadcasting.
42     // If that directory is empty (common reasons: it has been created by the
43     // code at the beginning, or all profiles were deleted) then a default
44     // profile with default values is created. That case could also mean that
45     // Mixxx has just been upgraded to a new version, so "legacy format" values
46     // has fetched from mixxx.cfg and loaded into the fresh default profile.
47     // It's important to take into account that the "legacy" settings are left
48     // in mixxx.cfg for retro-compatibility during alpha and beta testing.
49 
50     if (files.size() > 0) {
51         kLogger.info() << "Found" << files.size() << "profile(s)";
52 
53         // Load profiles from filesystem
54         for (const QFileInfo& fileInfo : files) {
55             BroadcastProfilePtr profile =
56                     BroadcastProfile::loadFromFile(fileInfo.absoluteFilePath());
57 
58             if (profile) {
59                 addProfile(profile);
60             }
61         }
62     } else {
63         kLogger.info() << "No profiles found. Creating default profile.";
64 
65         BroadcastProfilePtr defaultProfile(
66                     new BroadcastProfile(kDefaultProfile));
67         // Upgrade from mixxx.cfg format to XML (if required)
68         loadLegacySettings(defaultProfile);
69 
70         addProfile(defaultProfile);
71         saveProfile(defaultProfile);
72     }
73 }
74 
addProfile(BroadcastProfilePtr profile)75 bool BroadcastSettings::addProfile(BroadcastProfilePtr profile) {
76     if (!profile) {
77         return false;
78     }
79 
80     if (m_profiles.size() >= BROADCAST_MAX_CONNECTIONS) {
81         kLogger.warning() << "addProfile: connection limit reached."
82                  << "can't add more than" << QString::number(BROADCAST_MAX_CONNECTIONS)
83                  << "connections.";
84         return false;
85     }
86 
87     // It is best to avoid using QSharedPointer::data(), especially when
88     // passing it to another function, as it puts the associated pointer
89     // at risk of being manually deleted.
90     // However it's fine with Qt's connect because it can be trusted that
91     // it won't delete the pointer.
92     connect(profile.data(),
93             &BroadcastProfile::profileNameChanged,
94             this,
95             &BroadcastSettings::onProfileNameChanged);
96     connect(profile.data(),
97             &BroadcastProfile::connectionStatusChanged,
98             this,
99             &BroadcastSettings::onConnectionStatusChanged);
100     m_profiles.insert(profile->getProfileName(), BroadcastProfilePtr(profile));
101 
102     emit profileAdded(profile);
103     return true;
104 }
105 
saveProfile(BroadcastProfilePtr profile)106 bool BroadcastSettings::saveProfile(BroadcastProfilePtr profile) {
107     if (!profile) {
108         return false;
109     }
110 
111     QString filename = profile->getLastFilename();
112     if (filename.isEmpty()) {
113         // there was no previous filename, find an unused filename
114         for (int index = 0;; ++index) {
115             if (index > 0) {
116                 // add an index to the file name to make it unique
117                 filename = filePathForProfile(profile->getProfileName() + QString::number(index));
118             } else {
119                 filename = filePathForProfile(profile->getProfileName());
120             }
121 
122             QFileInfo fileInfo(filename);
123             if (!fileInfo.exists()) {
124                 break;
125             }
126         }
127     }
128 
129     return profile->save(filename);
130 }
131 
filePathForProfile(const QString & profileName)132 QString BroadcastSettings::filePathForProfile(const QString& profileName) {
133     QString filename = profileName + ".bcp.xml";
134     filename = BroadcastProfile::stripForbiddenChars(filename);
135     return QDir(getProfilesFolder()).absoluteFilePath(filename);
136 }
137 
filePathForProfile(BroadcastProfilePtr profile)138 QString BroadcastSettings::filePathForProfile(BroadcastProfilePtr profile) {
139     if (!profile) {
140         return QString();
141     }
142 
143     return filePathForProfile(profile->getProfileName());
144 }
145 
deleteFileForProfile(BroadcastProfilePtr profile)146 bool BroadcastSettings::deleteFileForProfile(BroadcastProfilePtr profile) {
147     if (!profile) {
148         return false;
149     }
150 
151     QString filename = profile->getLastFilename();
152     if (filename.isEmpty()) {
153         // no file was saved, there is no file to delete
154         return false;
155     }
156 
157     QFileInfo xmlFile(filename);
158     if (xmlFile.exists()) {
159         return QFile::remove(xmlFile.absoluteFilePath());
160     }
161     return false;
162 }
163 
getProfilesFolder()164 QString BroadcastSettings::getProfilesFolder() {
165     QString profilesPath(m_pConfig->getSettingsPath());
166     profilesPath.append(QDir::separator() + QString(kProfilesSubfolder));
167     return profilesPath;
168 }
169 
saveAll()170 void BroadcastSettings::saveAll() {
171     for (const auto& kv : qAsConst(m_profiles)) {
172         saveProfile(kv);
173     }
174     emit profilesChanged();
175 }
176 
onProfileNameChanged(const QString & oldName,const QString & newName)177 void BroadcastSettings::onProfileNameChanged(const QString& oldName, const QString& newName) {
178     if (!m_profiles.contains(oldName)) {
179         return;
180     }
181 
182     BroadcastProfilePtr profile = m_profiles.take(oldName);
183     if (profile) {
184         m_profiles.insert(newName, profile);
185         emit profileRenamed(oldName, profile);
186 
187         deleteFileForProfile(profile);
188         saveProfile(profile);
189     }
190 }
191 
onConnectionStatusChanged(int newStatus)192 void BroadcastSettings::onConnectionStatusChanged(int newStatus) {
193     Q_UNUSED(newStatus);
194 }
195 
profileAt(int index)196 BroadcastProfilePtr BroadcastSettings::profileAt(int index) {
197     auto it = m_profiles.begin() + index;
198     return it != m_profiles.end() ? it.value() : BroadcastProfilePtr(nullptr);
199 }
200 
profiles()201 QList<BroadcastProfilePtr> BroadcastSettings::profiles() {
202     return m_profiles.values();
203 }
204 
applyModel(BroadcastSettingsModel * pModel)205 void BroadcastSettings::applyModel(BroadcastSettingsModel* pModel) {
206     if (!pModel) {
207         return;
208     }
209     // TODO(Palakis): lock both lists against modifications while syncing
210 
211     // Step 1: find profiles to delete from the settings
212     for (auto profileIter = m_profiles.begin(); profileIter != m_profiles.end();) {
213         QString profileName = (*profileIter)->getProfileName();
214         if (!pModel->getProfileByName(profileName)) {
215             // If profile exists in settings but not in the model,
216             // remove the profile from the settings
217             const auto removedProfile = *profileIter;
218             deleteFileForProfile(removedProfile);
219             profileIter = m_profiles.erase(profileIter);
220             emit profileRemoved(removedProfile);
221         } else {
222             ++profileIter;
223         }
224     }
225 
226     // Step 2: add new profiles
227     const QList<BroadcastProfilePtr> existingProfiles = pModel->profiles();
228     for (auto profileCopy : existingProfiles) {
229         // Check if profile already exists in settings
230         BroadcastProfilePtr existingProfile =
231                 m_profiles.value(profileCopy->getProfileName());
232         if (!existingProfile) {
233             // If no profile with the same name exists, add the new
234             // profile to the settings.
235             // The BroadcastProfile instance is a copy to separate it
236             // from its existence in the model
237             addProfile(profileCopy->valuesCopy());
238         }
239     }
240 
241     // Step 3: update existing profiles
242     const QList<BroadcastProfilePtr> allProfiles = pModel->profiles();
243     for (BroadcastProfilePtr profileCopy : allProfiles) {
244         BroadcastProfilePtr actualProfile =
245                 m_profiles.value(profileCopy->getProfileName());
246         if (actualProfile) {
247             profileCopy->copyValuesTo(actualProfile);
248         }
249     }
250 
251     saveAll();
252 }
253