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 "audiocddevice.h"
25 #ifdef CDDB_FOUND
26 #include "cddbinterface.h"
27 #endif
28 #ifdef MUSICBRAINZ5_FOUND
29 #include "musicbrainz.h"
30 #endif
31 #include "models/musiclibraryitemsong.h"
32 #include "models/mpdlibrarymodel.h"
33 #include "models/playqueuemodel.h"
34 #include "support/utils.h"
35 #include "extractjob.h"
36 #include "mpd-interface/mpdconnection.h"
37 #include "gui/covers.h"
38 #include "gui/settings.h"
39 #include "widgets/icons.h"
40 #include <QDir>
41 #include <QUrl>
42 #include <QUrlQuery>
43 #include "solid-lite/block.h"
44 
45 const QLatin1String AudioCdDevice::constAnyDev("-");
46 
coverUrl(QString udi)47 QString AudioCdDevice::coverUrl(QString udi)
48 {
49     udi.replace(" ", "_");
50     udi.replace("\n", "_");
51     udi.replace("\t", "_");
52     udi.replace("/", "_");
53     udi.replace(":", "_");
54     return Song::constCddaProtocol+udi;
55 }
56 
getDevice(const QUrl & url)57 QString AudioCdDevice::getDevice(const QUrl &url)
58 {
59     if (QLatin1String("cdda")==url.scheme()) {
60         QUrlQuery q(url);
61         if (q.hasQueryItem("dev")) {
62             return q.queryItemValue("dev");
63         }
64         return constAnyDev;
65     }
66 
67     QString path=url.path();
68     if (path.startsWith("/run/user/")) {
69         const QString marker=QLatin1String("/gvfs/cdda:host=");
70         int pos=path.lastIndexOf(marker);
71         if (-1!=pos) {
72             return QLatin1String("/dev/")+path.mid(pos+marker.length());
73         }
74     }
75     return QString();
76 }
77 
AudioCdDevice(MusicLibraryModel * m,Solid::Device & dev)78 AudioCdDevice::AudioCdDevice(MusicLibraryModel *m, Solid::Device &dev)
79     : Device(m, dev, false, true)
80     #ifdef CDDB_FOUND
81     , cddb(0)
82     #endif
83     #ifdef MUSICBRAINZ5_FOUND
84     , mb(0)
85     #endif
86     , year(0)
87     , disc(0)
88     , time(0xFFFFFFFF)
89     , lookupInProcess(false)
90     , autoPlay(false)
91 {
92     icn=Icons::self()->albumMonoIcon;
93     drive=dev.parent().as<Solid::OpticalDrive>();
94     Solid::Block *block=dev.as<Solid::Block>();
95     if (block) {
96         device=block->device();
97     } else { // With UDisks2 we cannot get block from device :-(
98         QStringList parts=dev.udi().split("/", QString::SkipEmptyParts);
99         if (!parts.isEmpty()) {
100             parts=parts.last().split(":");
101             if (!parts.isEmpty()) {
102                 device="/dev/"+parts.first();
103             }
104         }
105     }
106     if (!device.isEmpty()) {
107         static bool registeredTypes=false;
108         if (!registeredTypes) {
109             qRegisterMetaType<CdAlbum >("CdAlbum");
110             qRegisterMetaType<QList<CdAlbum> >("QList<CdAlbum>");
111             registeredTypes=true;
112         }
113         devPath=Song::constCddaProtocol+device+QChar('/');
114         #if defined CDDB_FOUND && defined MUSICBRAINZ5_FOUND
115         connectService(Settings::self()->useCddb());
116         #else
117         connectService(true);
118         #endif
119         detailsString=tr("Reading disc");
120         setStatusMessage(detailsString);
121         lookupInProcess=true;
122         connect(Covers::self(), SIGNAL(cover(const Song &, const QImage &, const QString &)),
123                 this, SLOT(setCover(const Song &, const QImage &, const QString &)));
124         emit lookup(Settings::self()->cdAuto());
125     }
126 }
127 
~AudioCdDevice()128 AudioCdDevice::~AudioCdDevice()
129 {
130     #ifdef CDDB_FOUND
131     if (cddb) {
132         cddb->deleteLater();
133         cddb=0;
134     }
135     #endif
136     #ifdef MUSICBRAINZ5_FOUND
137     if (mb) {
138         mb->deleteLater();
139         mb=0;
140     }
141     #endif
142     // Remove any downloaded cover image...
143     if (!coverImage.fileName.isEmpty() && coverImage.fileName.startsWith(Utils::cacheDir(Covers::constCddaCoverDir, false))) {
144         QFile::remove(coverImage.fileName);
145     }
146 }
147 
dequeue()148 void AudioCdDevice::dequeue()
149 {
150     QList<Song> tracks;
151     for (const MusicLibraryItem *item: childItems()) {
152         if (MusicLibraryItem::Type_Song==item->itemType()) {
153             Song song=static_cast<const MusicLibraryItemSong *>(item)->song();
154             song.file=path()+song.file;
155             tracks.append(song);
156         }
157     }
158 
159     if (!tracks.isEmpty()) {
160         PlayQueueModel::self()->remove(tracks);
161     }
162 }
163 
isAudioDevice(const QString & dev) const164 bool AudioCdDevice::isAudioDevice(const QString &dev) const
165 {
166     return constAnyDev==dev || device==dev;
167 }
168 
connectService(bool useCddb)169 void AudioCdDevice::connectService(bool useCddb)
170 {
171     #if defined CDDB_FOUND && defined MUSICBRAINZ5_FOUND
172     if (cddb && !useCddb) {
173         cddb->deleteLater();
174         cddb=0;
175     }
176     if (mb && useCddb) {
177         mb->deleteLater();
178         mb=0;
179     }
180     #else
181     Q_UNUSED(useCddb)
182     #endif
183 
184     #ifdef CDDB_FOUND
185     if (!cddb
186             #ifdef MUSICBRAINZ5_FOUND
187             && useCddb
188             #endif
189             ) {
190         cddb=new CddbInterface(device);
191         connect(cddb, SIGNAL(error(QString)), this, SIGNAL(error(QString)));
192         connect(cddb, SIGNAL(initialDetails(CdAlbum)), this, SLOT(setDetails(CdAlbum)));
193         connect(cddb, SIGNAL(matches(const QList<CdAlbum> &)), SLOT(cdMatches(const QList<CdAlbum> &)));
194         connect(this, SIGNAL(lookup(bool)), cddb, SLOT(lookup(bool)));
195     }
196     #endif
197 
198     #ifdef MUSICBRAINZ5_FOUND
199     if (!mb
200             #ifdef CDDB_FOUND
201             && !useCddb
202             #endif
203             ) {
204         mb=new MusicBrainz(device);
205         connect(mb, SIGNAL(error(QString)), this, SIGNAL(error(QString)));
206         connect(mb, SIGNAL(initialDetails(CdAlbum)), this, SLOT(setDetails(CdAlbum)));
207         connect(mb, SIGNAL(matches(const QList<CdAlbum> &)), SLOT(cdMatches(const QList<CdAlbum> &)));
208         connect(this, SIGNAL(lookup(bool)), mb, SLOT(lookup(bool)));
209     }
210     #endif
211 }
212 
rescan(bool useCddb)213 void AudioCdDevice::rescan(bool useCddb)
214 {
215     if (!device.isEmpty()) {
216         connectService(useCddb);
217         lookupInProcess=true;
218         emit lookup(true);
219     }
220 }
221 
toggle()222 void AudioCdDevice::toggle()
223 {
224     if (drive) {
225         stop();
226         drive->eject();
227         PlayQueueModel::self()->removeCantataStreams(true);
228     }
229 }
230 
stop()231 void AudioCdDevice::stop()
232 {
233 }
234 
copySongTo(const Song & s,const QString & musicPath,bool overwrite,bool copyCover)235 void AudioCdDevice::copySongTo(const Song &s, const QString &musicPath, bool overwrite, bool copyCover)
236 {
237     jobAbortRequested=false;
238     if (!isConnected()) {
239         emit actionStatus(NotConnected);
240         return;
241     }
242 
243     needToFixVa=opts.fixVariousArtists && s.isVariousArtists();
244 
245     if (!overwrite) {
246         Song check=s;
247 
248         if (needToFixVa) {
249             Device::fixVariousArtists(QString(), check, false);
250         }
251         if (MpdLibraryModel::self()->songExists(check)) {
252             emit actionStatus(SongExists);
253             return;
254         }
255     }
256 
257     DeviceOptions mpdOpts;
258     mpdOpts.load(MPDConnectionDetails::configGroupName(MPDConnection::self()->getDetails().name), true);
259 
260     Encoders::Encoder encoder=Encoders::getEncoder(mpdOpts.transcoderCodec);
261     if (encoder.codec.isEmpty()) {
262         emit actionStatus(CodecNotAvailable);
263         return;
264     }
265 
266     QString source=device;
267     QString baseDir=MPDConnection::self()->getDetails().dir;
268     currentDestFile=encoder.changeExtension(baseDir+musicPath);
269     QDir dir(Utils::getDir(currentDestFile));
270     if (!dir.exists() && !Utils::createWorldReadableDir(dir.absolutePath(), baseDir)) {
271         emit actionStatus(DirCreationFaild);
272         return;
273     }
274 
275     currentSong=s;
276     ExtractJob *job=new ExtractJob(encoder, mpdOpts.transcoderValue, source, currentDestFile, currentSong, copyCover ? coverImage.fileName : QString());
277     connect(job, SIGNAL(result(int)), SLOT(copySongToResult(int)));
278     connect(job, SIGNAL(percent(int)), SLOT(percent(int)));
279     job->start();
280 }
281 
totalTime()282 quint32 AudioCdDevice::totalTime()
283 {
284     if (0xFFFFFFFF==time) {
285         time=0;
286         for (MusicLibraryItem *i: childItems()) {
287             time+=static_cast<MusicLibraryItemSong *>(i)->song().time;
288         }
289     }
290 
291     return time;
292 }
293 
percent(int pc)294 void AudioCdDevice::percent(int pc)
295 {
296     if (jobAbortRequested && 100!=pc) {
297         FileJob *job=qobject_cast<FileJob *>(sender());
298         if (job) {
299             job->stop();
300         }
301         return;
302     }
303     emit progress(pc);
304 }
305 
copySongToResult(int status)306 void AudioCdDevice::copySongToResult(int status)
307 {
308     ExtractJob *job=qobject_cast<ExtractJob *>(sender());
309     FileJob::finished(job);
310     if (jobAbortRequested) {
311         if (job && job->wasStarted() && QFile::exists(currentDestFile)) {
312             QFile::remove(currentDestFile);
313         }
314         return;
315     }
316     if (Ok!=status) {
317         emit actionStatus(status);
318     } else {
319         currentSong.file=currentDestFile.mid(MPDConnection::self()->getDetails().dir.length());
320         QString origPath;
321         if (MPDConnection::self()->isMopidy()) {
322             origPath=currentSong.file;
323             currentSong.file=Song::encodePath(currentSong.file);
324         }
325         if (needToFixVa) {
326             currentSong.revertVariousArtists();
327         }
328         Utils::setFilePerms(currentDestFile);
329 //        MusicLibraryModel::self()->addSongToList(currentSong);
330 //        DirViewModel::self()->addFileToList(origPath.isEmpty() ? currentSong.file : origPath,
331 //                                            origPath.isEmpty() ? QString() : currentSong.file);
332         emit actionStatus(Ok, job && job->coverCopied());
333     }
334 }
335 
336 static const int constBytesPerSecond=44100*4;
337 
setDetails(const CdAlbum & a)338 void AudioCdDevice::setDetails(const CdAlbum &a)
339 {
340     bool differentAlbum=album!=a.name || artist!=a.artist;
341     lookupInProcess=false;
342     setData(a.artist);
343     album=a.name;
344     artist=a.artist;
345     composer=a.composer;
346     genre=a.genre;
347     year=a.year;
348     disc=a.disc;
349     update=new MusicLibraryItemRoot();
350     int totalDuration=0;
351     for (Song s: a.tracks) {
352         totalDuration+=s.time;
353         s.size=s.time*constBytesPerSecond;
354         update->append(new MusicLibraryItemSong(s, update));
355     }
356     setStatusMessage(QString());
357     detailsString=tr("%n Tracks (%1)", "", a.tracks.count()).arg(Utils::formatTime(totalDuration));
358     emit updating(id(), false);
359     if (differentAlbum && !a.isDefault) {
360         Song s;
361         s.artist=s.albumartist=artist;
362         s.album=album;
363         s.file=AudioCdDevice::coverUrl(id());
364         s.title=id();
365         s.type=Song::Cdda;
366         Covers::Image img=Covers::self()->requestImage(s, true);
367         if (!img.img.isNull()) {
368             setCover(img);
369         }
370     }
371 
372     if (autoPlay) {
373         autoPlay=false;
374         playTracks();
375     } else {
376         updateDetails();
377     }
378 }
379 
cdMatches(const QList<CdAlbum> & albums)380 void AudioCdDevice::cdMatches(const QList<CdAlbum> &albums)
381 {
382     lookupInProcess=false;
383     if (1==albums.count()) {
384         setDetails(albums.at(0));
385     } else if (albums.count()>1) {
386         // More than 1 match, so prompt user!
387         emit matches(id(), albums);
388     }
389 }
390 
setCover(const Covers::Image & img)391 void AudioCdDevice::setCover(const Covers::Image &img)
392 {
393     coverImage=img;
394     updateStatus();
395 }
396 
scaleCoverPix(int size) const397 void AudioCdDevice::scaleCoverPix(int size) const
398 {
399     if (!coverImage.img.isNull()) {
400         if (scaledCover.width()!=size && scaledCover.height()!=size) {
401             scaledCover=QPixmap::fromImage(coverImage.img.scaled(size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation));
402         }
403     }
404 }
405 
setCover(const Song & song,const QImage & img,const QString & file)406 void AudioCdDevice::setCover(const Song &song, const QImage &img, const QString &file)
407 {
408     if (song.isCdda() && song.albumartist==artist && song.album==album) {
409         setCover(Covers::Image(img, file));
410     }
411 }
412 
autoplay()413 void AudioCdDevice::autoplay()
414 {
415     if (childCount()) {
416         playTracks();
417     } else {
418         autoPlay=true;
419     }
420 }
421 
playTracks()422 void AudioCdDevice::playTracks()
423 {
424     QList<Song> tracks;
425     for (const MusicLibraryItem *item: childItems()) {
426         if (MusicLibraryItem::Type_Song==item->itemType()) {
427             Song song=static_cast<const MusicLibraryItemSong *>(item)->song();
428             song.file=path()+song.file;
429             tracks.append(song);
430         }
431     }
432 
433     if (!tracks.isEmpty()) {
434         emit play(tracks);
435     }
436 }
437 
updateDetails()438 void AudioCdDevice::updateDetails()
439 {
440     QList<Song> tracks;
441     for (const MusicLibraryItem *item: childItems()) {
442         if (MusicLibraryItem::Type_Song==item->itemType()) {
443             Song song=static_cast<const MusicLibraryItemSong *>(item)->song();
444             song.file=path()+song.file;
445             tracks.append(song);
446         }
447     }
448 
449     if (!tracks.isEmpty()) {
450         emit updatedDetails(tracks);
451     }
452 }
453 
454 #include "moc_audiocddevice.cpp"
455