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