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 "cddbinterface.h"
25 #include "gui/settings.h"
26 #include "network/networkproxyfactory.h"
27 #include "support/thread.h"
28 #include <QNetworkProxy>
29 #include <QSet>
30 #include <QUdpSocket>
31 #include <cddb/cddb.h>
32 #include <fcntl.h>
33 #include <unistd.h>
34 #include <sys/ioctl.h>
35 #if defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
36 #include <sys/cdio.h>
37 #include <arpa/inet.h>
38 #elif defined(__linux__)
39 #include <linux/cdrom.h>
40 #endif
41 
42 static struct CddbInterfaceCleanup
43 {
~CddbInterfaceCleanupCddbInterfaceCleanup44     ~CddbInterfaceCleanup() { libcddb_shutdown(); }
45 } cleanup;
46 
dataTrack()47 QString CddbInterface::dataTrack()
48 {
49     return tr("Data Track");
50 }
51 
CddbInterface(const QString & device)52 CddbInterface::CddbInterface(const QString &device)
53     : dev(device)
54     , disc(0)
55 {
56     thread=new Thread(metaObject()->className());
57     moveToThread(thread);
58     thread->start();
59 }
60 
~CddbInterface()61 CddbInterface::~CddbInterface()
62 {
63     thread->stop();
64     if (disc) {
65         cddb_disc_destroy(disc);
66     }
67 }
68 
toAlbum(cddb_disc_t * disc,const CdAlbum & initial=CdAlbum ())69 static CdAlbum toAlbum(cddb_disc_t *disc, const CdAlbum &initial=CdAlbum())
70 {
71     CdAlbum album;
72     if (!disc) {
73         return album;
74     }
75     album.name=QString::fromUtf8(cddb_disc_get_title(disc));
76     album.artist=QString::fromUtf8(cddb_disc_get_artist(disc));
77     album.genre=QString::fromUtf8(cddb_disc_get_genre(disc));
78     album.year=cddb_disc_get_year(disc);
79     int numTracks=cddb_disc_get_track_count(disc);
80     if (numTracks>0) {
81         for (int t=0; t<numTracks; ++t) {
82             cddb_track_t *trk=cddb_disc_get_track(disc, t);
83             if (!trk) {
84                 continue;
85             }
86 
87             Song track;
88             track.track=cddb_track_get_number(trk);
89             track.title=QString::fromUtf8(cddb_track_get_title(trk));
90             track.artist=QString::fromUtf8(cddb_track_get_artist(trk));
91             track.albumartist=album.artist;
92             track.album=album.name;
93             track.id=track.track;
94             track.file=QString("%1.wav").arg(track.track);
95             track.year=album.year;
96 
97             if (initial.isNull()) {
98                 track.time=cddb_track_get_length(trk);
99                 if (CddbInterface::dataTrack()==track.title) {
100                     // Adjust last track length...
101                     if (album.tracks.count()) {
102                         Song last=album.tracks.takeLast();
103                         last.time-=FRAMES_TO_SECONDS(11400);
104                         album.tracks.append(last);
105                     }
106                 } else {
107                     album.tracks.append(track);
108                 }
109             } else if (t>=initial.tracks.count()) {
110                 break;
111             } else {
112                 track.time=initial.tracks.at(t).time;
113                 album.tracks.append(track);
114             }
115         }
116     }
117 
118     // Ensure we always have same number of tracks...
119     if (!initial.isNull() && album.tracks.count()<initial.tracks.count()) {
120         for (int i=album.tracks.count(); i<initial.tracks.count(); ++i) {
121             album.tracks.append(initial.tracks.at(i));
122         }
123     }
124     return album;
125 }
126 
127 // Copied from asunder v2.2 - GPL v2
readDisc()128 void CddbInterface::readDisc()
129 {
130     if (disc) {
131         return;
132     }
133 
134     int fd=open(dev.toLocal8Bit(), O_RDONLY | O_NONBLOCK);
135     if (fd < 0) {
136         emit error(tr("Failed to open CD device"));
137         return;
138     }
139     QByteArray unknown=Song::unknown().toUtf8();
140 
141     #if defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
142     struct ioc_toc_header th;
143     struct ioc_read_toc_single_entry te;
144     struct ioc_read_subchannel cdsc;
145     struct cd_sub_channel_info data;
146     bzero(&cdsc, sizeof(cdsc));
147     cdsc.data = &data;
148     cdsc.data_len = sizeof(data);
149     cdsc.data_format = CD_CURRENT_POSITION;
150     cdsc.address_format = CD_MSF_FORMAT;
151     if (ioctl(fd, CDIOCREADSUBCHANNEL, (char *)&cdsc) >= 0 && 0==ioctl(fd, CDIOREADTOCHEADER, &th)) {
152         disc = cddb_disc_new();
153         if (disc) {
154             te.address_format = CD_LBA_FORMAT;
155             for (int i=th.starting_track; i<=th.ending_track; i++) {
156                 te.track = i;
157                 if (0==ioctl(fd, CDIOREADTOCENTRY, &te)) {
158                     cddb_track_t *track = cddb_track_new();
159                     if (track) {
160                         cddb_track_set_frame_offset(track, te.entry.addr.lba + SECONDS_TO_FRAMES(2));
161                         cddb_track_set_title(track, te.entry.control&0x04 ? dataTrack().toUtf8().constData() : tr("Track %1").arg(i).toUtf8().constData());
162                         cddb_track_set_artist(track, unknown.constData());
163                         cddb_disc_add_track(disc, track);
164                     }
165                 }
166             }
167             te.track = 0xAA;
168             if (0==ioctl(fd, CDIOREADTOCENTRY, &te))  {
169                 cddb_disc_set_length(disc, (ntohl(te.entry.addr.lba)+SECONDS_TO_FRAMES(2))/SECONDS_TO_FRAMES(1));
170             }
171         }
172     }
173     #elif defined(__linux__)
174     struct cdrom_tochdr th;
175     struct cdrom_tocentry te;
176     int status = ioctl(fd, CDROM_DISC_STATUS, CDSL_CURRENT);
177     if ((CDS_AUDIO==status || CDS_MIXED==status) && 0==ioctl(fd, CDROMREADTOCHDR, &th)) {
178         disc = cddb_disc_new();
179         if (disc) {
180             te.cdte_format = CDROM_LBA;
181             for (int i=th.cdth_trk0; i<=th.cdth_trk1; i++) {
182                 te.cdte_track = i;
183                 if (0==ioctl(fd, CDROMREADTOCENTRY, &te)) {
184                     cddb_track_t *track = cddb_track_new();
185                     if (track) {
186                         cddb_track_set_frame_offset(track, te.cdte_addr.lba + SECONDS_TO_FRAMES(2));
187                         cddb_track_set_title(track, te.cdte_ctrl&CDROM_DATA_TRACK ? dataTrack().toUtf8().constData() : tr("Track %1").arg(i).toUtf8().constData());
188                         cddb_track_set_artist(track, unknown.constData());
189                         cddb_disc_add_track(disc, track);
190                     }
191                 }
192             }
193 
194             te.cdte_track = CDROM_LEADOUT;
195             if (0==ioctl(fd, CDROMREADTOCENTRY, &te)) {
196                 cddb_disc_set_length(disc, (te.cdte_addr.lba+SECONDS_TO_FRAMES(2))/SECONDS_TO_FRAMES(1));
197             }
198         }
199     }
200     #endif
201 
202     if (disc) {
203         cddb_disc_set_artist(disc, unknown.constData());
204         cddb_disc_set_title(disc, unknown.constData());
205         cddb_disc_set_genre(disc, unknown.constData());
206         cddb_disc_calc_discid(disc);
207     }
208     close(fd);
209 
210     initial=toAlbum(disc);
211     initial.isDefault=true;
212     emit initialDetails(initial);
213 }
214 
215 class CddbConnection
216 {
217 public:
CddbConnection(cddb_disc_t * d)218     CddbConnection(cddb_disc_t *d) : disc(0) {
219         connection = cddb_new();
220         if (connection) {
221             cddb_cache_disable(connection);
222             cddb_set_server_name(connection, Settings::self()->cddbHost().toLatin1().constData());
223             cddb_set_server_port(connection, Settings::self()->cddbPort());
224             disc=cddb_disc_clone(d);
225             QUrl url;
226             url.setHost(Settings::self()->cddbHost());
227             url.setPort(Settings::self()->cddbPort());
228             QList<QNetworkProxy> proxies=NetworkProxyFactory::self()->queryProxy(QNetworkProxyQuery(url));
229 
230             for (const QNetworkProxy &p: proxies) {
231                 if (QNetworkProxy::HttpProxy==p.type() && 0!=p.port()) {
232                     cddb_set_http_proxy_server_name(connection, p.hostName().toLatin1().constData());
233                     cddb_set_http_proxy_server_port(connection, p.port());
234                     cddb_http_proxy_enable(connection);
235                     break;
236                 }
237             }
238         }
239     }
240 
~CddbConnection()241     ~CddbConnection() {
242         if (disc) {
243             cddb_disc_destroy(disc);
244         }
245         if (connection) {
246             cddb_destroy(connection);
247         }
248     }
249 
operator bool() const250     operator bool() const { return 0!=connection; }
query()251     int query() { return cddb_query(connection, disc); }
read()252     int read() { return cddb_read(connection, disc); }
error()253     const char * error() { return cddb_error_str(cddb_errno(connection)); }
trackCount()254     int trackCount() {  return cddb_disc_get_track_count(disc); }
next()255     int next() { return cddb_query_next(connection, disc); }
toAlbum(const CdAlbum & initial)256     CdAlbum toAlbum(const CdAlbum &initial) { return ::toAlbum(disc, initial); }
257 
258 private:
259     cddb_conn_t *connection;
260     cddb_disc_t *disc;
261 };
262 
lookup(bool full)263 void CddbInterface::lookup(bool full)
264 {
265     bool isInitial=!disc;
266     if (!disc) {
267         readDisc();
268     }
269 
270     if (!disc || !full) {
271         // Errors already logged in readDisc
272         return;
273     }
274 
275     CddbConnection cddb(disc);
276     if (!cddb) {
277         emit error(tr("Failed to create CDDB connection"));
278         return;
279     }
280 
281     if (!checkConnection()) {
282         emit error(tr("Failed to contact CDDB server, please check CDDB and network settings"));
283         return;
284     }
285 
286     if (cddb.query()<1) {
287         if (!isInitial) {
288             emit error(tr("No matches found in CDDB"));
289         }
290         return;
291     }
292 
293     QList<CdAlbum> m;
294     for (;;) {
295         if (!cddb.read()) {
296             emit error(tr("CDDB error: %1", cddb.error()));
297             return;
298         }
299         int numTracks=cddb.trackCount();
300         if (numTracks<=0) {
301             continue;
302         }
303 
304         CdAlbum album=cddb.toAlbum(initial);
305         if (!album.tracks.isEmpty()) {
306             m.append(album);
307         }
308         if (!cddb.next()) {
309             break;
310         }
311     }
312 
313     if (m.isEmpty()) {
314         if (!isInitial) {
315             emit error(tr("No matches found in CDDB"));
316         }
317     } else if (isInitial) {
318         emit initialDetails(m.first());
319     } else {
320         emit matches(m);
321     }
322 }
323 
checkConnection()324 bool CddbInterface::checkConnection()
325 {
326     QUdpSocket socket(this);
327     socket.connectToHost(Settings::self()->cddbHost(), Settings::self()->cddbPort(), QIODevice::ReadOnly);
328     bool ok=socket.waitForConnected(2000);
329     socket.close();
330     return ok;
331 }
332 
333 #include "moc_cddbinterface.cpp"
334