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 "umsdevice.h"
25 #include "support/utils.h"
26 #include "support/monoicon.h"
27 #include "devicepropertiesdialog.h"
28 #include "devicepropertieswidget.h"
29 #include "actiondialog.h"
30 #include <QDir>
31 #include <QFile>
32 #include <QFileInfo>
33 #include <QTextStream>
34 #include "solid-lite/storagedrive.h"
35 
36 static const QLatin1String constSettingsFile("/.is_audio_player");
37 static const QLatin1String constMusicFolderKey("audio_folder");
38 static const QLatin1String constCollectionNameKey("collection_name");
39 
UmsDevice(MusicLibraryModel * m,Solid::Device & dev)40 UmsDevice::UmsDevice(MusicLibraryModel *m, Solid::Device &dev)
41     : FsDevice(m, dev)
42     , access(dev.as<Solid::StorageAccess>())
43 {
44     spaceInfo.setPath(access->filePath());
45 
46     QString details=QLatin1String(" (")+Utils::formatByteSize(spaceInfo.size());
47 
48     QStringList udiParts=dev.udi().split(QLatin1Char('/'), QString::SkipEmptyParts);
49     if (udiParts.length()>1) {
50         details+=QLatin1String(" - ")+udiParts.last();
51     }
52 
53     if (!details.isEmpty()) {
54         details+=QLatin1Char(')');
55     }
56 
57     defaultName=data()+details;
58     setData(defaultName);
59     setup();
60     icn=MonoIcon::icon(FontAwesome::usb, Utils::monoIconColor());
61 }
62 
~UmsDevice()63 UmsDevice::~UmsDevice()
64 {
65 }
66 
connectionStateChanged()67 void UmsDevice::connectionStateChanged()
68 {
69     if (isConnected()) {
70         spaceInfo.setPath(access->filePath());
71         setup();
72         if (opts.autoScan || scanned){ // Only scan if we are set to auto scan, or we have already scanned before...
73             rescan(false); // Read from cache if we have it!
74         } else {
75             setStatusMessage(tr("Not Scanned"));
76         }
77     } else {
78         clear();
79     }
80 }
81 
toggle()82 void UmsDevice::toggle()
83 {
84     if (solidDev.isValid() && access && access->isValid()) {
85         if (access->isAccessible()) {
86             stopScanner();
87             access->teardown();
88         } else {
89             access->setup();
90         }
91     }
92 }
93 
isConnected() const94 bool UmsDevice::isConnected() const
95 {
96     return solidDev.isValid() && access && access->isValid() && access->isAccessible();
97 }
98 
usedCapacity()99 double UmsDevice::usedCapacity()
100 {
101     if (cacheProgress>-1) {
102         return (cacheProgress*1.0)/100.0;
103     }
104     if (!isConnected()) {
105         return -1.0;
106     }
107 
108     return spaceInfo.size()>0 ? (spaceInfo.used()*1.0)/(spaceInfo.size()*1.0) : -1.0;
109 }
110 
capacityString()111 QString UmsDevice::capacityString()
112 {
113     if (cacheProgress>-1) {
114         return statusMessage();
115     }
116     if (!isConnected()) {
117         return tr("Not Connected");
118     }
119 
120     return tr("%1 free").arg(Utils::formatByteSize(spaceInfo.size()-spaceInfo.used()));
121 }
122 
freeSpace()123 qint64 UmsDevice::freeSpace()
124 {
125     if (!isConnected()) {
126         return 0;
127     }
128 
129     return spaceInfo.size()-spaceInfo.used();
130 }
131 
setup()132 void UmsDevice::setup()
133 {
134     if (!isConnected()) {
135         return;
136     }
137 
138     QString path=spaceInfo.path();
139     audioFolder = path;
140 
141     QFile file(path+constSettingsFile);
142     QString audioFolderSetting;
143     QString n=data();
144     bool haveOpts=FsDevice::readOpts(path+constCantataSettingsFile, opts, false);
145 
146     if (file.open(QIODevice::ReadOnly|QIODevice::Text)) {
147         configured=true;
148         QTextStream in(&file);
149         while (!in.atEnd()) {
150             QString line = in.readLine();
151             if (line.startsWith(constMusicFolderKey+"=")) {
152                 audioFolderSetting=audioFolder=Utils::cleanPath(path+'/'+line.section('=', 1, 1));
153                 if (!QDir(audioFolder).exists()) {
154                     audioFolder = path;
155                 }
156             } else if (line.startsWith(constMusicFilenameSchemeKey+"=")) {
157                 QString scheme = line.section('=', 1, 1);
158                 //protect against empty setting.
159                 if( !scheme.isEmpty() ) {
160                     opts.scheme = scheme;
161                 }
162             } else if (line.startsWith(constVfatSafeKey+"="))  {
163                 opts.vfatSafe = QLatin1String("true")==line.section('=', 1, 1);
164             } else if (line.startsWith(constAsciiOnlyKey+"=")) {
165                 opts.asciiOnly = QLatin1String("true")==line.section('=', 1, 1);
166             } else if (line.startsWith(constIgnoreTheKey+"=")) {
167                 opts.ignoreThe = QLatin1String("true")==line.section('=', 1, 1);
168             } else if (line.startsWith(constReplaceSpacesKey+"="))  {
169                 opts.replaceSpaces = QLatin1String("true")==line.section('=', 1, 1);
170             } else if (line.startsWith(constCollectionNameKey+"="))  {
171                 opts.name = line.section('=', 1, 1).trimmed();
172             } else {
173                 unusedParams+=line;
174             }
175         }
176     }
177 
178     if (!configured) {
179         configured=haveOpts;
180     }
181 
182     if (opts.coverName.isEmpty()) {
183         opts.coverName=constDefCoverFileName;
184     }
185 
186     // No setting, see if any standard dirs exist in path...
187     if (audioFolderSetting.isEmpty() || audioFolderSetting!=audioFolder) {
188         QStringList dirs;
189         dirs << QLatin1String("Music") << QLatin1String("MUSIC")
190              << QLatin1String("Albums") << QLatin1String("ALBUMS");
191 
192         for (const QString &d: dirs) {
193             if (QDir(path+d).exists()) {
194                 audioFolder=path+d;
195                 break;
196             }
197         }
198     }
199 
200     if (!audioFolder.endsWith('/')) {
201         audioFolder+='/';
202     }
203 
204     if (opts.autoScan || scanned){ // Only scan if we are set to auto scan, or we have already scanned before...
205         rescan(false); // Read from cache if we have it!
206     } else {
207         setStatusMessage(tr("Not Scanned"));
208     }
209     if (!opts.name.isEmpty() && opts.name!=n) {
210         setData(opts.name);
211         emit renamed();
212     }
213 }
214 
configure(QWidget * parent)215 void UmsDevice::configure(QWidget *parent)
216 {
217     if (!isIdle()) {
218         return;
219     }
220 
221     DevicePropertiesDialog *dlg=new DevicePropertiesDialog(parent);
222     connect(dlg, SIGNAL(updatedSettings(const QString &, const DeviceOptions &)), SLOT(saveProperties(const QString &, const DeviceOptions &)));
223     if (!configured) {
224         connect(dlg, SIGNAL(cancelled()), SLOT(saveProperties()));
225     }
226     DeviceOptions o=opts;
227     if (o.name.isEmpty()) {
228         o.name=data();
229     }
230     dlg->show(audioFolder, o, DevicePropertiesWidget::Prop_All,
231               qobject_cast<ActionDialog *>(parent) ? DevicePropertiesWidget::Prop_Folder : 0);
232 }
233 
saveProperties()234 void UmsDevice::saveProperties()
235 {
236     saveProperties(audioFolder, opts);
237 }
238 
toString(bool b)239 static inline QString toString(bool b)
240 {
241     return b ? QLatin1String("true") : QLatin1String("false");
242 }
243 
saveOptions()244 void UmsDevice::saveOptions()
245 {
246     if (!isConnected()) {
247         return;
248     }
249 
250     QString path=spaceInfo.path();
251     QFile file(path+constSettingsFile);
252     QString fixedPath(audioFolder);
253 
254     if (fixedPath.startsWith(path)) {
255         fixedPath=QLatin1String("./")+fixedPath.mid(path.length());
256     }
257 
258     DeviceOptions def;
259     if (file.open(QIODevice::WriteOnly|QIODevice::Text)) {
260         QTextStream out(&file);
261         if (!fixedPath.isEmpty()) {
262             out << constMusicFolderKey << '=' << fixedPath << '\n';
263         }
264         if (opts.scheme!=def.scheme) {
265             out << constMusicFilenameSchemeKey << '=' << opts.scheme << '\n';
266         }
267         if (opts.scheme!=def.scheme) {
268             out << constVfatSafeKey << '=' << toString(opts.vfatSafe) << '\n';
269         }
270         if (opts.asciiOnly!=def.asciiOnly) {
271             out << constAsciiOnlyKey << '=' << toString(opts.asciiOnly) << '\n';
272         }
273         if (opts.ignoreThe!=def.ignoreThe) {
274             out << constIgnoreTheKey << '=' << toString(opts.ignoreThe) << '\n';
275         }
276         if (opts.replaceSpaces!=def.replaceSpaces) {
277             out << constReplaceSpacesKey << '=' << toString(opts.replaceSpaces) << '\n';
278         }
279         if (!opts.name.isEmpty() && opts.name!=defaultName) {
280             out << constCollectionNameKey << '=' << opts.name << '\n';
281         }
282 
283         for (const QString &u: unusedParams) {
284             out << u << '\n';
285         }
286     }
287 }
288 
saveProperties(const QString & newPath,const DeviceOptions & newOpts)289 void UmsDevice::saveProperties(const QString &newPath, const DeviceOptions &newOpts)
290 {
291     QString nPath=Utils::fixPath(newPath);
292     if (configured && opts==newOpts && nPath==audioFolder) {
293         return;
294     }
295 
296     configured=true;
297     QString newName=newOpts.name.isEmpty() ? defaultName : newOpts.name;
298     bool diffName=opts.name!=newName;
299     bool diffCacheSettings=opts.useCache!=newOpts.useCache;
300     opts=newOpts;
301     if (diffName) {
302         setData(newName);
303     }
304     if (diffCacheSettings) {
305         if (opts.useCache) {
306             saveCache();
307         } else {
308             removeCache();
309         }
310     }
311     emit configurationChanged();
312 
313     QString oldPath=audioFolder;
314     if (!isConnected()) {
315         return;
316     }
317 
318     audioFolder=nPath;
319     saveOptions();
320 
321     FsDevice::writeOpts(spaceInfo.path()+constCantataSettingsFile, opts, false);
322 
323     if (oldPath!=audioFolder) {
324         rescan(); // Path changed, so we can ignore cache...
325     }
326     if (diffName) {
327         emit renamed();
328     }
329 }
330 
331 #include "moc_umsdevice.cpp"
332