1 /*
2 * Cantata
3 *
4 * Copyright (c) 2011-2020 Craig Drummond <craig.p.drummond@gmail.com>
5 *
6 */
7 /*
8 Copyright (C) 2005-2007 Richard Lärkäng <nouseforaname@home.se>
9
10 This library is free software; you can redistribute it and/or
11 modify it under the terms of the GNU Library General Public
12 License as published by the Free Software Foundation; either
13 version 2 of the License, or (at your option) any later version.
14
15 This library is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 Library General Public License for more details.
19
20 You should have received a copy of the GNU Library General Public License
21 along with this library; see the file COPYING.LIB. If not, write to
22 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
23 Boston, MA 02110-1301, USA.
24 */
25
26 #include "musicbrainz.h"
27 #include "network/networkproxyfactory.h"
28 #include <QNetworkProxy>
29 #include <QCryptographicHash>
30 #include <musicbrainz5/Query.h>
31 #include <musicbrainz5/Medium.h>
32 #include <musicbrainz5/Release.h>
33 #include <musicbrainz5/ReleaseGroup.h>
34 #include <musicbrainz5/Track.h>
35 #include <musicbrainz5/Recording.h>
36 #include <musicbrainz5/Disc.h>
37 #include <musicbrainz5/HTTPFetch.h>
38 #include <musicbrainz5/ArtistCredit.h>
39 #include <musicbrainz5/Artist.h>
40 #include <musicbrainz5/NameCredit.h>
41 #include <QList>
42 #include <QRegExp>
43 #include "config.h"
44 #include "support/thread.h"
45 #include <fcntl.h>
46 #include <unistd.h>
47 #include <sys/ioctl.h>
48 #if defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
49 #include <sys/cdio.h>
50 #include <arpa/inet.h>
51 #elif defined(__linux__)
52 #include <linux/cdrom.h>
53 #endif
54
55 #include <QDebug>
56 #define DBUG qDebug()
57
58 static const int constFramesPerSecond=75;
59 static const int constDataTrackAdjust=11400;
60
secondsToFrames(int s)61 static inline int secondsToFrames(int s) {
62 return constFramesPerSecond *s;
63 }
64
framesToSeconds(int f)65 static inline int framesToSeconds(int f) {
66 return (f/(constFramesPerSecond*1.0))+0.5;
67 }
68
69 struct Track {
TrackTrack70 Track(int o=0, bool d=false) : offset(o), isData(d) {}
71 int offset;
72 bool isData;
73 };
74
calculateDiscId(const QList<Track> & tracks)75 static QString calculateDiscId(const QList<Track> &tracks)
76 {
77 if (tracks.isEmpty()) {
78 return QString();
79 }
80
81 // Code based on libmusicbrainz/lib/diskid.cpp
82 int numTracks = tracks.count()-1;
83 QCryptographicHash sha(QCryptographicHash::Sha1);
84 QString temp;
85
86 temp = QStringLiteral("%1").arg(1, 2, 16, QLatin1Char('0'));
87 sha.addData(temp.toUpper().toLatin1());
88 temp = QStringLiteral("%1").arg(numTracks, 2, 16, QLatin1Char('0'));
89 sha.addData(temp.toUpper().toLatin1());
90
91 for(int i = 0; i < 100; i++) {
92 int offset;
93 if (0==i) {
94 offset = tracks[numTracks].offset;
95 } else if (i <= numTracks) {
96 offset = tracks[i-1].offset;
97 } else {
98 offset = 0;
99 }
100
101 temp = QStringLiteral("%1").arg(offset, 8, 16, QLatin1Char('0'));
102 sha.addData(temp.toUpper().toLatin1());
103 }
104
105 QByteArray base64 = sha.result().toBase64();
106 // '/' '+' and '=' replaced for MusicBrainz
107 return QString::fromLatin1(base64).replace(QLatin1Char( '/' ), QLatin1String( "_" ))
108 .replace(QLatin1Char( '+' ), QLatin1String( "." ))
109 .replace(QLatin1Char( '=' ), QLatin1String( "-" ));
110 }
111
artistFromCreditList(MusicBrainz5::CArtistCredit * artistCredit)112 static QString artistFromCreditList(MusicBrainz5::CArtistCredit *artistCredit )
113 {
114 QString artistName;
115 MusicBrainz5::CNameCreditList *artistList=artistCredit->NameCreditList();
116
117 if (artistList) {
118 for (int i=0; i < artistList->NumItems(); i++) {
119 MusicBrainz5::CNameCredit* name=artistList->Item(i);
120 MusicBrainz5::CArtist* artist = name->Artist();
121
122 if (!name->Name().empty()) {
123 artistName += QString::fromUtf8(name->Name().c_str());
124 } else {
125 artistName += QString::fromUtf8(artist->Name().c_str());
126 }
127 artistName += QString::fromUtf8(name->JoinPhrase().c_str());
128 }
129 }
130
131 return artistName;
132 }
133
MusicBrainz(const QString & device)134 MusicBrainz::MusicBrainz(const QString &device)
135 : dev(device)
136 {
137 thread=new Thread(metaObject()->className());
138 moveToThread(thread);
139 thread->start();
140 }
141
~MusicBrainz()142 MusicBrainz::~MusicBrainz()
143 {
144 thread->stop();
145 }
146
readDisc()147 void MusicBrainz::readDisc()
148 {
149 int fd=open(dev.toLocal8Bit(), O_RDONLY | O_NONBLOCK);
150 if (fd < 0) {
151 emit error(tr("Failed to open CD device"));
152 return;
153 }
154 QList<Track> tracks;
155
156 #if defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
157 struct ioc_toc_header th;
158 struct ioc_read_toc_single_entry te;
159 struct ioc_read_subchannel cdsc;
160 struct cd_sub_channel_info data;
161 bzero(&cdsc,sizeof(cdsc));
162 cdsc.data = &data;
163 cdsc.data_len = sizeof(data);
164 cdsc.data_format = CD_CURRENT_POSITION;
165 cdsc.address_format = CD_MSF_FORMAT;
166 if (ioctl(fd, CDIOCREADSUBCHANNEL, (char *)&cdsc) >= 0 && 0==ioctl(fd, CDIOREADTOCHEADER, &th)) {
167 te.address_format = CD_LBA_FORMAT;
168 for (int i=th.starting_track; i<=th.ending_track; i++) {
169 te.track = i;
170 if (0==ioctl(fd, CDIOREADTOCENTRY, &te)) {
171 tracks.append(Track(te.entry.addr.lba + secondsToFrames(2), te.entry.control&0x04));
172 }
173 }
174 te.track = 0xAA;
175 if (0==ioctl(fd, CDIOREADTOCENTRY, &te)) {
176 tracks.append((ntohl(te.entry.addr.lba)+secondsToFrames(2))/secondsToFrames(1));
177 }
178 }
179 #elif defined(__linux__)
180 struct cdrom_tochdr th;
181 struct cdrom_tocentry te;
182 int status = ioctl(fd, CDROM_DISC_STATUS, CDSL_CURRENT);
183 if ( (CDS_AUDIO==status || CDS_MIXED==status) && 0==ioctl(fd, CDROMREADTOCHDR, &th)) {
184 te.cdte_format = CDROM_LBA;
185 for (int i=th.cdth_trk0; i<=th.cdth_trk1; i++) {
186 te.cdte_track = i;
187 if (0==ioctl(fd, CDROMREADTOCENTRY, &te)) {
188 tracks.append(Track(te.cdte_addr.lba + secondsToFrames(2), te.cdte_ctrl&CDROM_DATA_TRACK));
189 }
190 }
191 te.cdte_track = CDROM_LEADOUT;
192 if (0==ioctl(fd, CDROMREADTOCENTRY, &te)) {
193 tracks.append((te.cdte_addr.lba+secondsToFrames(2)));
194 }
195 }
196 #endif
197 close(fd);
198
199 initial.name=Song::unknown();
200 initial.artist=Song::unknown();
201 initial.genre=Song::unknown();
202 initial.isDefault=true;
203
204 if (tracks.count()>1) {
205 for (int i=0; i<tracks.count()-1; ++i) {
206 const Track &trk=tracks.at(i);
207 if (trk.isData) {
208 continue;
209 }
210 const Track &next=tracks.at(i+1);
211 Song s;
212 s.track=i+1;
213 s.title=tr("Track %1").arg(s.track).toUtf8();
214 s.artist=Song::unknown();
215 s.albumartist=initial.artist;
216 s.album=initial.name;
217 s.id=s.track;
218
219 s.time=framesToSeconds((next.offset-trk.offset)-(next.isData ? constDataTrackAdjust : 0));
220 s.file=QString("%1.wav").arg(s.track);
221 s.year=initial.year;
222 initial.tracks.append(s);
223 }
224
225 if (tracks.count()>=3 && tracks.at(tracks.count()-2).isData) {
226 tracks.takeLast();
227 Track last=tracks.takeLast();
228 last.offset-=constDataTrackAdjust;
229 tracks.append(last);
230 }
231 }
232
233 discId = calculateDiscId(tracks);
234 emit initialDetails(initial);
235 }
236
lookup(bool full)237 void MusicBrainz::lookup(bool full)
238 {
239 bool isInitial=discId.isEmpty();
240 if (isInitial) {
241 readDisc();
242 }
243
244 if (!full) {
245 return;
246 }
247 DBUG << "Should lookup " << discId;
248
249 MusicBrainz5::CQuery Query("cantata-" PACKAGE_VERSION_STRING);
250 QList<CdAlbum> m;
251 QList<QNetworkProxy> proxies=NetworkProxyFactory::self()->queryProxy(QNetworkProxyQuery(QUrl("http://musicbrainz.org")));
252 for (const QNetworkProxy &p: proxies) {
253 if (QNetworkProxy::HttpProxy==p.type() && 0!=p.port()) {
254 Query.SetProxyHost(p.hostName().toLatin1().constData());
255 Query.SetProxyPort(p.port());
256 break;
257 }
258 }
259
260 // Code adapted from libmusicbrainz/examples/cdlookup.cc
261
262 try {
263 MusicBrainz5::CMetadata Metadata=Query.Query("discid", discId.toLatin1().constData());
264
265 if (Metadata.Disc() && Metadata.Disc()->ReleaseList()) {
266 MusicBrainz5::CReleaseList *releaseList=Metadata.Disc()->ReleaseList();
267 DBUG << "Found " << releaseList->NumItems() << " release(s)";
268
269 for (int i = 0; i < releaseList->NumItems(); i++) {
270 MusicBrainz5::CRelease* release=releaseList->Item(i);
271
272 //The releases returned from LookupDiscID don't contain full information
273
274 MusicBrainz5::CQuery::tParamMap params;
275 params["inc"]="artists labels recordings release-groups url-rels discids artist-credits";
276
277 std::string releaseId=release->ID();
278 MusicBrainz5::CMetadata Metadata2=Query.Query("release", releaseId, "", params);
279
280 if (Metadata2.Release()) {
281 MusicBrainz5::CRelease *fullRelease=Metadata2.Release();
282
283 //However, these releases will include information for all media in the release
284 //So we need to filter out the only the media we want.
285 MusicBrainz5::CMediumList mediaList=fullRelease->MediaMatchingDiscID(discId.toLatin1().constData());
286
287 if (mediaList.NumItems() > 0) {
288 DBUG << "Found " << mediaList.NumItems() << " media item(s)";
289
290 for (int i=0; i < mediaList.NumItems(); i++) {
291 MusicBrainz5::CMedium* medium= mediaList.Item(i);
292
293 /*DBUG << "Found media: '" << medium.Title() << "', position " << medium.Position();*/
294 CdAlbum album;
295
296 album.name=QString::fromUtf8(fullRelease->Title().c_str());
297
298 if (fullRelease->MediumList()->NumItems() > 1) {
299 album.name = tr("%1 (Disc %2)").arg(album.name).arg(medium->Position());
300 album.disc=medium->Position();
301 }
302 album.artist=artistFromCreditList(fullRelease->ArtistCredit());
303 album.genre=Song::unknown();
304
305 QString date = QString::fromUtf8(fullRelease->Date().c_str());
306 QRegExp yearRe("^(\\d{4,4})(-\\d{1,2}-\\d{1,2})?$");
307 if (yearRe.indexIn(date) > -1) {
308 QString yearString = yearRe.cap(1);
309 bool ok;
310 album.year=yearString.toInt(&ok);
311 if (!ok) {
312 album.year = 0;
313 }
314 }
315
316 MusicBrainz5::CTrackList *trackList=medium->TrackList();
317 if (trackList) {
318 for (int i=0; i < trackList->NumItems(); i++) {
319 // Ensure we have the same number of tracks are read from disc!
320 if (album.tracks.count()>=initial.tracks.count()) {
321 break;
322 }
323 MusicBrainz5::CTrack *track=trackList->Item(i);
324 MusicBrainz5::CRecording *recording=track->Recording();
325 Song song;
326
327 song.albumartist=album.artist;
328 song.album=album.name;
329 song.genres[0]=album.genre;
330 song.id=song.track=track->Position();
331 song.time=track->Length()/1000;
332 song.disc=album.disc;
333 song.file=QString("%1.wav").arg(song.track);
334
335 // Prefer title and artist from the track credits, but it appears to be empty if same as in Recording
336 // Noticable in the musicbrainztest-fulldate test, where the title on the credits of track 18 are
337 // "Bara om min älskade väntar", but the recording has title "Men bara om min älskade"
338 if (recording && 0==track->ArtistCredit()) {
339 song.artist=artistFromCreditList(recording->ArtistCredit());
340 } else {
341 song.artist=artistFromCreditList(track->ArtistCredit());
342 }
343
344 if (recording && track->Title().empty()) {
345 song.title=QString::fromUtf8(recording->Title().c_str());
346 } else {
347 song.title=QString::fromUtf8(track->Title().c_str());
348 }
349 album.tracks.append(song);
350 }
351 }
352
353 // Ensure we have the same number of tracks as read from disc!
354 if (album.tracks.count()<initial.tracks.count()) {
355 for (int i=album.tracks.count(); i<initial.tracks.count(); ++i) {
356 album.tracks.append(initial.tracks.at(i));
357 }
358 }
359 m.append(album);
360 }
361 }
362 }
363 }
364 }
365 } catch (MusicBrainz5::CConnectionError &e) {
366 DBUG << "MusicBrainz error" << e.what();
367 } catch (MusicBrainz5::CTimeoutError &e) {
368 DBUG << "MusicBrainz error - %1" << e.what();
369 } catch (MusicBrainz5::CAuthenticationError &e) {
370 DBUG << "MusicBrainz error - %1" << e.what();
371 } catch (MusicBrainz5::CFetchError &e) {
372 DBUG << "MusicBrainz error - %1" << e.what();
373 } catch (MusicBrainz5::CRequestError &e) {
374 DBUG << "MusicBrainz error - %1" << e.what();
375 } catch (MusicBrainz5::CResourceNotFoundError &e) {
376 DBUG << "MusicBrainz error - %1" << e.what();
377 }
378
379 if (m.isEmpty()) {
380 if (!isInitial) {
381 emit error(tr("No matches found in MusicBrainz"));
382 }
383 } else if (isInitial) {
384 emit initialDetails(m.first());
385 } else {
386 emit matches(m);
387 }
388 }
389
390 #include "moc_musicbrainz.cpp"
391