You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
kaffeine/kaffeine/src/input/disc/cddb.cpp

573 lines
14 KiB

/*
* cddb.cpp
*
* Copyright (C) 2000 Michael Matz <matz@kde.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <unistd.h>
#include <tqdir.h>
#include <tqtextstream.h>
#include <tqregexp.h>
#include <tqapplication.h>
#include <tqstring.h>
#include <tqcursor.h>
#include <kdebug.h>
#include <ksock.h>
#include <kextsock.h>
#include <tdelocale.h>
#include <kinputdialog.h>
#include "cddb.h"
#include "cddb.moc"
CDDB::CDDB() : ks(0), port(80), remote(false), save_local(false)
{
TQString s = TQDir::homeDirPath()+"/.cddb";
cddb_dirs +=s;
}
CDDB::~CDDB()
{
deinit();
}
bool CDDB::set_server(const char *hostname, unsigned short int _port)
{
if (ks)
{
if (h_name == hostname && port == _port)
return true;
deinit();
}
remote = (hostname != 0) && (*hostname != 0);
kdDebug(7101) << "CDDB: set_server, host=" << hostname << "port=" << _port << endl;
if (remote)
{
ks = new KExtendedSocket(hostname, _port);
if (ks->connect() < 0)
{
kdDebug(7101) << "CDDB: Can't connect!" << endl;
delete ks;
ks = 0;
return false;
}
h_name = hostname;
port = _port;
TQCString r;
readLine(r); // the server greeting
writeLine("cddb hello kde-user blubb tdeio_audiocd 0.4");
readLine(r);
}
return true;
}
bool CDDB::deinit()
{
if (ks)
{
writeLine("quit");
TQCString r;
readLine(r);
ks->close();
}
h_name.resize(0);
port = 0;
remote = false;
ks = 0;
return true;
}
bool CDDB::readLine(TQCString& ret)
{
int read_length = 0;
char small_b[128];
//fd_set set;
ret.resize(0);
while (read_length < 40000)
{
// Look for a \n in buf
int ni = buf.find('\n');
if (ni >= 0)
{
// Nice, so return this substring (without the \n),
// and truncate buf accordingly
ret = buf.left(ni);
if (ret.length() && ret[ret.length()-1] == '\r')
ret.resize(ret.length());
buf.remove(0, ni+1);
kdDebug(7101) << "CDDB: got `" << ret << "'" << endl;
return true;
}
// Try to refill the buffer
ks->waitForMore(60 * 1000);
ssize_t l = ks->readBlock(small_b, sizeof(small_b)-1);
if (l <= 0)
{
// l==0 normally means fd got closed, but we really need a lineend
return false;
}
small_b[l] = 0;
read_length += l;
buf += small_b;
}
return false;
}
bool CDDB::writeLine(const TQCString& line)
{
const char *b = line.data();
int l = line.length();
kdDebug(7101) << "CDDB: send `" << line << "'" << endl;
while (l)
{
ssize_t wl = ks->writeBlock(b, l);
if (wl < 0 && errno != EINTR)
return false;
if (wl < 0)
wl = 0;
l -= wl;
b += wl;
}
l = line.length();
if (l && line.data()[l-1] != '\n')
{
char c = '\n';
ssize_t wl;
do {
wl = ks->writeBlock(&c, 1);
} while (wl <= 0 && errno == EINTR);
if (wl<=0 && errno != EINTR)
return false;
}
return true;
}
unsigned int CDDB::get_discid(TQValueList<int>& track_ofs)
{
unsigned int id = 0;
int num_tracks = track_ofs.count() - 2;
// the last two track_ofs[] are disc begin and disc end
for (int i = num_tracks - 1; i >= 0; i--)
{
int n = track_ofs[i];
n /= 75;
while (n > 0)
{
id += n % 10;
n /= 10;
}
}
unsigned int l = track_ofs[num_tracks + 1];
l -= track_ofs[num_tracks];
l /= 75;
id = ((id % 255) << 24) | (l << 8) | num_tracks;
return id;
}
static int get_code (const TQCString &s)
{
bool ok;
int code = s.left(3).toInt(&ok);
if (!ok)
code = -1;
return code;
}
static void parse_query_resp (const TQCString& _r, TQCString& catg, TQCString& d_id, TQCString& title)
{
TQCString r = _r.stripWhiteSpace();
int i = r.find(' ');
if (i)
{
catg = r.left(i).stripWhiteSpace();
r.remove(0, i+1);
r = r.stripWhiteSpace();
}
i = r.find(' ');
if (i)
{
d_id = r.left(i).stripWhiteSpace();
r.remove(0, i+1);
r = r.stripWhiteSpace();
}
title = r;
}
TQString CDDB::track(int i) const
{
if (i < 0 || i >= int(m_names.count()))
return TQString();
return m_names[i].utf8();
}
bool CDDB::parse_read_resp(TQTextStream *stream, TQTextStream *write_stream)
{
/* Note, that m_names and m_title should be empty */
TQCString end = ".";
/* Fill table, so we can index it below. */
for (int i = 0; i < m_tracks; i++)
{
m_names.append("");
}
while (1)
{
TQCString r;
if (stream)
{
if (stream->atEnd())
break;
r = stream->readLine().latin1();
}
else
{
if (!readLine(r))
return false;
}
/* Normally the "." is not saved into the local files, but be
tolerant about this. */
if (r == end)
break;
if (write_stream)
*write_stream << r << endl;
r = r.stripWhiteSpace();
if (r.isEmpty() || r[0] == '#')
continue;
if (r.left(7) == "DTITLE=")
{
r.remove(0, 7);
m_title += TQString::fromLocal8Bit(r.stripWhiteSpace());
}
else if (r.left(6) == "TTITLE")
{
r.remove(0, 6);
int e = r.find('=');
if (e)
{
bool ok;
int i = r.left(e).toInt(&ok);
if (ok && i >= 0 && i < m_tracks)
{
r.remove(0, e+1);
m_names[i] += TQString::fromLocal8Bit(r);
}
}
}
}
/* XXX We should canonicalize the strings ("\n" --> '\n' e.g.) */
int si = m_title.find(" / ");
if (si > 0)
{
m_artist = m_title.left(si).stripWhiteSpace();
m_title.remove(0, si+3);
m_title = m_title.stripWhiteSpace();
}
if (m_title.isEmpty())
m_title = i18n("No Title");
/*else
m_title.replace(TQRegExp("/"), "%2f");*/
if (m_artist.isEmpty())
m_artist = i18n("Unknown");
/*else
m_artist.replace(TQRegExp("/"), "%2f");*/
kdDebug(7101) << "CDDB: found Title: `" << m_title << "'" << endl;
for (int i = 0; i < m_tracks; i++)
{
if (m_names[i].isEmpty())
m_names[i] += i18n("Track %1").arg(i);
m_names[i].replace(TQRegExp("/"), "%2f");
kdDebug(7101) << "CDDB: found Track " << i+1 << ": `" << m_names[i]
<< "'" << endl;
}
return true;
}
void CDDB::add_cddb_dirs(const TQStringList& list)
{
TQString s = TQDir::homeDirPath()+"/.cddb";
cddb_dirs = list;
if (cddb_dirs.isEmpty())
cddb_dirs += s;
}
/* Locates and opens the local file corresponding to that discid.
Returns TRUE, if file is found and ready for reading.
Returns FALSE, if file isn't found. In this case ret_file is initialized
with a TQFile which resides in the first cddb_dir, and has a temp name
(the ID + getpid()). You can open it for writing. */
bool CDDB::searchLocal(unsigned int id, TQFile *ret_file)
{
TQDir dir;
TQString filename;
filename = TQString("%1").arg(id, 0, 16).rightJustify(8, '0');
TQStringList::ConstIterator it;
for (it = cddb_dirs.begin(); it != cddb_dirs.end(); ++it)
{
dir.setPath(*it);
if (!dir.exists())
continue;
/* First look in dir directly. */
ret_file->setName (*it + "/" + filename);
if (ret_file->exists() && ret_file->open(IO_ReadOnly))
return true;
/* And then in the subdirs of dir (representing the categories normally).
*/
const TQFileInfoList *subdirs = dir.entryInfoList (TQDir::Dirs);
TQFileInfoListIterator fiit(*subdirs);
TQFileInfo *fi;
while ((fi = fiit.current()) != 0)
{
ret_file->setName (*it + "/" + fi->fileName() + "/" + filename);
if (ret_file->exists() && ret_file->open(IO_ReadOnly))
return true;
++fiit;
}
}
TQString pid;
pid.setNum(::getpid());
ret_file->setName (cddb_dirs[0] + "/" + filename + "." + pid);
/* Try to create the save location. */
dir.setPath(cddb_dirs[0]);
if (save_local && !dir.exists())
{
//dir = TQDir::current();
dir.mkdir(cddb_dirs[0]);
}
return false;
}
bool CDDB::queryCD(TQValueList<int>& track_ofs)
{
int num_tracks = track_ofs.count() - 2;
if (num_tracks < 1)
return false;
unsigned int id = get_discid(track_ofs);
TQFile file;
bool local;
/* Already read this ID. */
if (id == m_discid)
return true;
emit cddbMessage(i18n("Searching local cddb entry ..."));
tqApp->processEvents();
/* First look for a local file. */
local = searchLocal (id, &file);
/* If we have no local file, and no remote connection, barf. */
if (!local && (!remote || ks == 0))
return false;
m_tracks = num_tracks;
m_title = "";
m_artist = "";
m_names.clear();
m_discid = id;
if (local)
{
TQTextStream stream(&file);
/* XXX Hmm, what encoding is used by CDDB files? local? Unicode?
Nothing? */
//stream.setEncoding(TQTextStream::Locale);
parse_read_resp(&stream, 0);
file.close();
return true;
}
emit cddbMessage(i18n("Searching remote cddb entry ..."));
tqApp->processEvents();
/* Remote CDDB query. */
unsigned int length = track_ofs[num_tracks+1] - track_ofs[num_tracks];
TQCString q;
q.sprintf("cddb query %08x %d", id, num_tracks);
TQCString num;
for (int i = 0; i < num_tracks; i++)
q += " " + num.setNum(track_ofs[i]);
q += " " + num.setNum(length / 75);
if (!writeLine(q))
return false;
TQCString r;
if (!readLine(r))
return false;
r = r.stripWhiteSpace();
int code = get_code(r);
if (code == 200)
{
TQCString catg, d_id, title;
TQDir dir;
TQCString s, pid;
emit cddbMessage(i18n("Found exact match cddb entry ..."));
tqApp->processEvents();
/* an exact match */
r.remove(0, 3);
parse_query_resp(r, catg, d_id, title);
kdDebug(7101) << "CDDB: found exact CD: category=" << catg << " DiscId="
<< d_id << " Title=`" << title << "'" << endl;
q = "cddb read " + catg + " " + d_id;
if (!writeLine(q))
return false;
if (!readLine(r))
return false;
r = r.stripWhiteSpace();
code = get_code(r);
if (code != 210)
return false;
pid.setNum(::getpid());
s = cddb_dirs[0].latin1();
//s = s + "/" +catg; // xine seems to not search in local subdirs
dir.setPath( s );
if ( !dir.exists() ) dir.mkdir( s );
s = s+"/"+ d_id;
file.setName( s );
if (save_local && file.open(IO_WriteOnly))
{
kdDebug(7101) << "CDDB: file name to save =" << file.name() << endl;
TQTextStream stream(&file);
if (!parse_read_resp(0, &stream))
{
file.remove();
return false;
}
file.close();
/*TQString newname (file.name());
newname.truncate(newname.findRev('.'));
if (TQDir::current().rename(file.name(), newname)) {
kdDebug(7101) << "CDDB: rename failed" << endl;
file.remove();
} */
}
else if (!parse_read_resp(0, 0))
return false;
}
else if (code == 211)
{
// Found some close matches. We'll read the query response and ask the user
// which one should be fetched from the server.
TQCString end = ".";
TQCString catg, d_id, title;
TQDir dir;
TQCString s, pid, first_match;
TQStringList disc_ids;
/* some close matches */
//XXX may be try to find marker based on r
emit cddbMessage(i18n("Found close cddb entry ..."));
tqApp->processEvents();
int i=0;
while (1)
{
if (!readLine(r))
return false;
r = r.stripWhiteSpace();
if (r == end)
break;
disc_ids.append(r);
if (i == 0)
first_match = r;
i++;
}
bool ok = false;
// We don't want to be thinking too much, do we?
TQApplication::restoreOverrideCursor();
// Oh, mylord, which match should I serve you?
TQString _answer = KInputDialog::getItem(i18n("CDDB Matches"), i18n("Several close CDDB entries found. Choose one:"),
disc_ids, 0, false, &ok );
TQCString answer = _answer.utf8();
if (ok){ // Get user selected match
parse_query_resp(answer, catg, d_id, title);
}
else{ // Get first match
parse_query_resp(first_match, catg, d_id, title);
}
// Now we can continue thinking...
TQApplication::setOverrideCursor( TQCursor(TQt::WaitCursor) );
kdDebug(7101) << "CDDB: found close CD: category=" << catg << " DiscId="
<< d_id << " Title=`" << title << "'" << endl;
// ... and forth we go as usual
q = "cddb read " + catg + " " + d_id;
if (!writeLine(q))
return false;
if (!readLine(r))
return false;
r = r.stripWhiteSpace();
code = get_code(r);
if (code != 210)
return false;
pid.setNum(::getpid());
s = cddb_dirs[0].latin1();
dir.setPath( s );
if ( !dir.exists() ) dir.mkdir( s );
s = s+"/"+ d_id;
file.setName( s );
if (save_local && file.open(IO_WriteOnly))
{
kdDebug(7101) << "CDDB: file name to save =" << file.name() << endl;
TQTextStream stream(&file);
if (!parse_read_resp(0, &stream))
{
file.remove();
return false;
}
file.close();
}
else if (!parse_read_resp(0, 0))
return false;
}
else
{
/* 202 - no match found
403 - Database entry corrupt
409 - no handshake */
kdDebug(7101) << "CDDB: query returned code " << code << endl;
return false;
}
return true;
}