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