/* * Cantata * * Copyright (c) 2011-2020 Craig Drummond * */ /* Copyright (C) 2005-2007 Richard Lärkäng This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "musicbrainz.h" #include "network/networkproxyfactory.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config.h" #include "support/thread.h" #include #include #include #if defined(__FreeBSD__) || defined(__FreeBSD_kernel__) #include #include #elif defined(__linux__) #include #endif #include #define DBUG qDebug() static const int constFramesPerSecond=75; static const int constDataTrackAdjust=11400; static inline int secondsToFrames(int s) { return constFramesPerSecond *s; } static inline int framesToSeconds(int f) { return (f/(constFramesPerSecond*1.0))+0.5; } struct Track { Track(int o=0, bool d=false) : offset(o), isData(d) {} int offset; bool isData; }; static QString calculateDiscId(const QList &tracks) { if (tracks.isEmpty()) { return QString(); } // Code based on libmusicbrainz/lib/diskid.cpp int numTracks = tracks.count()-1; QCryptographicHash sha(QCryptographicHash::Sha1); QString temp; temp = QStringLiteral("%1").arg(1, 2, 16, QLatin1Char('0')); sha.addData(temp.toUpper().toLatin1()); temp = QStringLiteral("%1").arg(numTracks, 2, 16, QLatin1Char('0')); sha.addData(temp.toUpper().toLatin1()); for(int i = 0; i < 100; i++) { int offset; if (0==i) { offset = tracks[numTracks].offset; } else if (i <= numTracks) { offset = tracks[i-1].offset; } else { offset = 0; } temp = QStringLiteral("%1").arg(offset, 8, 16, QLatin1Char('0')); sha.addData(temp.toUpper().toLatin1()); } QByteArray base64 = sha.result().toBase64(); // '/' '+' and '=' replaced for MusicBrainz return QString::fromLatin1(base64).replace(QLatin1Char( '/' ), QLatin1String( "_" )) .replace(QLatin1Char( '+' ), QLatin1String( "." )) .replace(QLatin1Char( '=' ), QLatin1String( "-" )); } static QString artistFromCreditList(MusicBrainz5::CArtistCredit *artistCredit ) { QString artistName; MusicBrainz5::CNameCreditList *artistList=artistCredit->NameCreditList(); if (artistList) { for (int i=0; i < artistList->NumItems(); i++) { MusicBrainz5::CNameCredit* name=artistList->Item(i); MusicBrainz5::CArtist* artist = name->Artist(); if (!name->Name().empty()) { artistName += QString::fromUtf8(name->Name().c_str()); } else { artistName += QString::fromUtf8(artist->Name().c_str()); } artistName += QString::fromUtf8(name->JoinPhrase().c_str()); } } return artistName; } MusicBrainz::MusicBrainz(const QString &device) : dev(device) { thread=new Thread(metaObject()->className()); moveToThread(thread); thread->start(); } MusicBrainz::~MusicBrainz() { thread->stop(); } void MusicBrainz::readDisc() { int fd=open(dev.toLocal8Bit(), O_RDONLY | O_NONBLOCK); if (fd < 0) { emit error(tr("Failed to open CD device")); return; } QList tracks; #if defined(__FreeBSD__) || defined(__FreeBSD_kernel__) struct ioc_toc_header th; struct ioc_read_toc_single_entry te; struct ioc_read_subchannel cdsc; struct cd_sub_channel_info data; bzero(&cdsc,sizeof(cdsc)); cdsc.data = &data; cdsc.data_len = sizeof(data); cdsc.data_format = CD_CURRENT_POSITION; cdsc.address_format = CD_MSF_FORMAT; if (ioctl(fd, CDIOCREADSUBCHANNEL, (char *)&cdsc) >= 0 && 0==ioctl(fd, CDIOREADTOCHEADER, &th)) { te.address_format = CD_LBA_FORMAT; for (int i=th.starting_track; i<=th.ending_track; i++) { te.track = i; if (0==ioctl(fd, CDIOREADTOCENTRY, &te)) { tracks.append(Track(te.entry.addr.lba + secondsToFrames(2), te.entry.control&0x04)); } } te.track = 0xAA; if (0==ioctl(fd, CDIOREADTOCENTRY, &te)) { tracks.append((ntohl(te.entry.addr.lba)+secondsToFrames(2))/secondsToFrames(1)); } } #elif defined(__linux__) struct cdrom_tochdr th; struct cdrom_tocentry te; int status = ioctl(fd, CDROM_DISC_STATUS, CDSL_CURRENT); if ( (CDS_AUDIO==status || CDS_MIXED==status) && 0==ioctl(fd, CDROMREADTOCHDR, &th)) { te.cdte_format = CDROM_LBA; for (int i=th.cdth_trk0; i<=th.cdth_trk1; i++) { te.cdte_track = i; if (0==ioctl(fd, CDROMREADTOCENTRY, &te)) { tracks.append(Track(te.cdte_addr.lba + secondsToFrames(2), te.cdte_ctrl&CDROM_DATA_TRACK)); } } te.cdte_track = CDROM_LEADOUT; if (0==ioctl(fd, CDROMREADTOCENTRY, &te)) { tracks.append((te.cdte_addr.lba+secondsToFrames(2))); } } #endif close(fd); initial.name=Song::unknown(); initial.artist=Song::unknown(); initial.genre=Song::unknown(); initial.isDefault=true; if (tracks.count()>1) { for (int i=0; i=3 && tracks.at(tracks.count()-2).isData) { tracks.takeLast(); Track last=tracks.takeLast(); last.offset-=constDataTrackAdjust; tracks.append(last); } } discId = calculateDiscId(tracks); emit initialDetails(initial); } void MusicBrainz::lookup(bool full) { bool isInitial=discId.isEmpty(); if (isInitial) { readDisc(); } if (!full) { return; } DBUG << "Should lookup " << discId; MusicBrainz5::CQuery Query("cantata-" PACKAGE_VERSION_STRING); QList m; QList proxies=NetworkProxyFactory::self()->queryProxy(QNetworkProxyQuery(QUrl("http://musicbrainz.org"))); for (const QNetworkProxy &p: proxies) { if (QNetworkProxy::HttpProxy==p.type() && 0!=p.port()) { Query.SetProxyHost(p.hostName().toLatin1().constData()); Query.SetProxyPort(p.port()); break; } } // Code adapted from libmusicbrainz/examples/cdlookup.cc try { MusicBrainz5::CMetadata Metadata=Query.Query("discid", discId.toLatin1().constData()); if (Metadata.Disc() && Metadata.Disc()->ReleaseList()) { MusicBrainz5::CReleaseList *releaseList=Metadata.Disc()->ReleaseList(); DBUG << "Found " << releaseList->NumItems() << " release(s)"; for (int i = 0; i < releaseList->NumItems(); i++) { MusicBrainz5::CRelease* release=releaseList->Item(i); //The releases returned from LookupDiscID don't contain full information MusicBrainz5::CQuery::tParamMap params; params["inc"]="artists labels recordings release-groups url-rels discids artist-credits"; std::string releaseId=release->ID(); MusicBrainz5::CMetadata Metadata2=Query.Query("release", releaseId, "", params); if (Metadata2.Release()) { MusicBrainz5::CRelease *fullRelease=Metadata2.Release(); //However, these releases will include information for all media in the release //So we need to filter out the only the media we want. MusicBrainz5::CMediumList mediaList=fullRelease->MediaMatchingDiscID(discId.toLatin1().constData()); if (mediaList.NumItems() > 0) { DBUG << "Found " << mediaList.NumItems() << " media item(s)"; for (int i=0; i < mediaList.NumItems(); i++) { MusicBrainz5::CMedium* medium= mediaList.Item(i); /*DBUG << "Found media: '" << medium.Title() << "', position " << medium.Position();*/ CdAlbum album; album.name=QString::fromUtf8(fullRelease->Title().c_str()); if (fullRelease->MediumList()->NumItems() > 1) { album.name = tr("%1 (Disc %2)").arg(album.name).arg(medium->Position()); album.disc=medium->Position(); } album.artist=artistFromCreditList(fullRelease->ArtistCredit()); album.genre=Song::unknown(); QString date = QString::fromUtf8(fullRelease->Date().c_str()); QRegExp yearRe("^(\\d{4,4})(-\\d{1,2}-\\d{1,2})?$"); if (yearRe.indexIn(date) > -1) { QString yearString = yearRe.cap(1); bool ok; album.year=yearString.toInt(&ok); if (!ok) { album.year = 0; } } MusicBrainz5::CTrackList *trackList=medium->TrackList(); if (trackList) { for (int i=0; i < trackList->NumItems(); i++) { // Ensure we have the same number of tracks are read from disc! if (album.tracks.count()>=initial.tracks.count()) { break; } MusicBrainz5::CTrack *track=trackList->Item(i); MusicBrainz5::CRecording *recording=track->Recording(); Song song; song.albumartist=album.artist; song.album=album.name; song.genres[0]=album.genre; song.id=song.track=track->Position(); song.time=track->Length()/1000; song.disc=album.disc; song.file=QString("%1.wav").arg(song.track); // Prefer title and artist from the track credits, but it appears to be empty if same as in Recording // Noticable in the musicbrainztest-fulldate test, where the title on the credits of track 18 are // "Bara om min älskade väntar", but the recording has title "Men bara om min älskade" if (recording && 0==track->ArtistCredit()) { song.artist=artistFromCreditList(recording->ArtistCredit()); } else { song.artist=artistFromCreditList(track->ArtistCredit()); } if (recording && track->Title().empty()) { song.title=QString::fromUtf8(recording->Title().c_str()); } else { song.title=QString::fromUtf8(track->Title().c_str()); } album.tracks.append(song); } } // Ensure we have the same number of tracks as read from disc! if (album.tracks.count()