File indexing completed on 2024-05-12 16:46:26

0001 /***************************************************************************
0002     Copyright (C) 2006-2009 Robby Stephenson <robby@periapsis.org>
0003  ***************************************************************************/
0004 
0005 /***************************************************************************
0006  *                                                                         *
0007  *   This program is free software; you can redistribute it and/or         *
0008  *   modify it under the terms of the GNU General Public License as        *
0009  *   published by the Free Software Foundation; either version 2 of        *
0010  *   the License or (at your option) version 3 or any later version        *
0011  *   accepted by the membership of KDE e.V. (or its successor approved     *
0012  *   by the membership of KDE e.V.), which shall act as a proxy            *
0013  *   defined in Section 14 of version 3 of the license.                    *
0014  *                                                                         *
0015  *   This program is distributed in the hope that it will be useful,       *
0016  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0017  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0018  *   GNU General Public License for more details.                          *
0019  *                                                                         *
0020  *   You should have received a copy of the GNU General Public License     *
0021  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0022  *                                                                         *
0023  ***************************************************************************/
0024 
0025 // The information about the AMC file format was taken from the source code for
0026 // GCfilms, (GPL) (c) 2005 Tian
0027 // Monotheka, (GPL) (c) 2004, 2005 Michael Dominic K.
0028 //                      2005 Aurelien Mino
0029 
0030 #include "amcimporter.h"
0031 #include "../fieldformat.h"
0032 #include "../collections/videocollection.h"
0033 #include "../images/imagefactory.h"
0034 #include "../tellico_debug.h"
0035 
0036 #include <QImage>
0037 #include <QByteArray>
0038 #include <QApplication>
0039 
0040 #include <limits>
0041 #include <algorithm>
0042 
0043 #define AMC_FILE_ID " AMC_X.Y Ant Movie Catalog 3.5.x   www.buypin.com    www.antp.be "
0044 
0045 namespace {
0046   static const quint32 AMC_MAX_STRING_SIZE = 128 * 1024;
0047 }
0048 
0049 using Tellico::Import::AMCImporter;
0050 
0051 AMCImporter::AMCImporter(const QUrl& url_) : DataImporter(url_), m_cancelled(false), m_failed(false), m_majVersion(0), m_minVersion(0) {
0052 }
0053 
0054 AMCImporter::~AMCImporter() {
0055 }
0056 
0057 bool AMCImporter::canImport(int type) const {
0058   return type == Data::Collection::Video;
0059 }
0060 
0061 Tellico::Data::CollPtr AMCImporter::collection() {
0062   if(m_coll) {
0063     return m_coll;
0064   }
0065 
0066   if(!fileRef().open()) {
0067     return Data::CollPtr();
0068   }
0069 
0070   QIODevice* f = fileRef().file();
0071   m_ds.setDevice(f);
0072   // AMC is always little-endian? can't confirm
0073   m_ds.setByteOrder(QDataStream::LittleEndian);
0074   emit signalTotalSteps(this, f->size());
0075 
0076   const uint l = sizeof(AMC_FILE_ID)-1;
0077   QVector<char> buffer(l+1);
0078   m_ds.readRawData(buffer.data(), l);
0079   QString version = QString::fromLocal8Bit(buffer.data(), l);
0080   QRegularExpression versionRx(QLatin1String(".+AMC_(\\d+)\\.(\\d+).+"));
0081   QRegularExpressionMatch versionMatch = versionRx.match(version);
0082   if(!versionMatch.hasMatch()) {
0083     myDebug() << "no file id match";
0084     return Data::CollPtr();
0085   }
0086 
0087   m_coll = new Data::VideoCollection(true);
0088 
0089   m_majVersion = versionMatch.captured(1).toInt();
0090   m_minVersion = versionMatch.captured(2).toInt();
0091 //  myDebug() << m_majVersion << "-" << m_minVersion;
0092 
0093   readString(); // name
0094   readString(); // email
0095   if(m_majVersion <= 3 && m_minVersion < 5) {
0096     readString(); // icq
0097   }
0098   readString(); // webpage
0099   readString(); // description
0100 
0101   const bool showProgress = options() & ImportProgress;
0102 
0103   while(!m_cancelled && !m_failed && !f->atEnd()) {
0104     readEntry();
0105     if(showProgress) {
0106       emit signalProgress(this, f->pos());
0107       qApp->processEvents();
0108     }
0109   }
0110 
0111   return m_coll;
0112 }
0113 
0114 bool AMCImporter::readBool() {
0115   quint8 b;
0116   m_ds >> b;
0117   return b;
0118 }
0119 
0120 quint32 AMCImporter::readInt() {
0121   if(m_failed) {
0122     return 0;
0123   }
0124   quint32 i;
0125   m_ds >> i;
0126   if(i >= std::numeric_limits<uint>::max()) {
0127     i = 0;
0128   }
0129   return i;
0130 }
0131 
0132 QString AMCImporter::readString() {
0133   if(m_failed) {
0134     return QString();
0135   }
0136   // The serialization format is a length specifier first, then l bytes of data
0137   quint32 l = readInt();
0138   if(l == 0) {
0139     return QString();
0140   }
0141   if(l > AMC_MAX_STRING_SIZE) {
0142     myDebug() << "string is too long:" << l;
0143     m_failed = true;
0144     return QString();
0145   }
0146   QVector<char> buffer(l+1);
0147   m_ds.readRawData(buffer.data(), l);
0148   QString s = QString::fromLocal8Bit(buffer.data(), l);
0149 //  myDebug() << "string: " << s;
0150   return s;
0151 }
0152 
0153 QString AMCImporter::readImage(const QString& format_) {
0154   if(m_failed) {
0155     return QString();
0156   }
0157   quint32 l = readInt();
0158   if(l == 0) {
0159     return QString();
0160   }
0161   if(l > AMC_MAX_STRING_SIZE) {
0162     myDebug() << "string is too long:" << l;
0163     m_failed = true;
0164     return QString();
0165   }
0166   QVector<char> buffer(l+1);
0167   m_ds.readRawData(buffer.data(), l);
0168   QByteArray bytes;
0169   bytes.reserve(l);
0170   std::copy(buffer.data(), buffer.data() + l, bytes.begin());
0171   QImage img = QImage::fromData(bytes);
0172   if(img.isNull()) {
0173     static uint count = 0;
0174     ++count;
0175     if(count < 5) {
0176       myDebug() << "AMCImporter::readImage() - null image, expected" << format_ << "from" << l << "bytes";
0177     } else if(count == 6) {
0178       myDebug() << "AMCImporter::readImage() - skipping further errors for null images";
0179     }
0180     return QString();
0181   }
0182   QString format = QStringLiteral("PNG");
0183   if(format_ == QLatin1String(".jpg")) {
0184     format = QStringLiteral("JPEG");
0185   } else if(format_ == QLatin1String(".gif")) {
0186     format = QStringLiteral("GIF");
0187   }
0188   return ImageFactory::addImage(img, format);
0189 }
0190 
0191 void AMCImporter::readEntry() {
0192   Data::EntryPtr e(new Data::Entry(m_coll));
0193 
0194   quint32 id = readInt();
0195   if(id > 0) {
0196     e->setId(id);
0197   }
0198   readInt(); // add date
0199 
0200   quint32 rating = readInt();
0201   if(m_majVersion >= 3 && m_minVersion >= 5) {
0202     rating /= 10;
0203   }
0204   e->setField(QStringLiteral("rating"), QString::number(rating));
0205   quint32 year = readInt();
0206   if(year > 0) {
0207     e->setField(QStringLiteral("year"), QString::number(year));
0208   }
0209   quint32 time = readInt();
0210   if(time > 0) {
0211     e->setField(QStringLiteral("running-time"), QString::number(time));
0212   }
0213 
0214   readInt(); // video bitrate
0215   readInt(); // audio bitrate
0216   readInt(); // number of files
0217   readBool(); // checked
0218   readString(); // media label
0219   e->setField(QStringLiteral("medium"), readString());
0220   readString(); // source
0221   readString(); // borrower
0222   QString s = readString(); // title
0223   if(!s.isEmpty()) {
0224     e->setField(QStringLiteral("title"), s);
0225   }
0226   QString s2 = readString(); // translated title
0227   if(s.isEmpty()) {
0228     e->setField(QStringLiteral("title"), s2);
0229   }
0230 
0231   e->setField(QStringLiteral("director"), readString());
0232   s = readString();
0233   QRegularExpression roleRx(QLatin1String("(.+?) \\(([^(]+)\\)"));
0234   QRegularExpressionMatch roleMatch = roleRx.match(s);
0235   if(roleMatch.hasMatch()) {
0236     QString role = roleMatch.captured(2).toLower();
0237     if(role == QLatin1String("story") || role == QLatin1String("written by")) {
0238       e->setField(QStringLiteral("writer"), roleMatch.captured(1));
0239     } else {
0240       e->setField(QStringLiteral("producer"), s);
0241     }
0242   } else {
0243     e->setField(QStringLiteral("producer"), s);
0244   }
0245   e->setField(QStringLiteral("nationality"), readString());
0246   e->setField(QStringLiteral("genre"), readString().replace(QLatin1String(", "), FieldFormat::delimiterString()));
0247 
0248   e->setField(QStringLiteral("cast"), parseCast(readString()).join(FieldFormat::rowDelimiterString()));
0249 
0250   readString(); // url
0251   e->setField(QStringLiteral("plot"), readString());
0252   e->setField(QStringLiteral("comments"), readString());
0253   s = readString(); // video format
0254   QRegularExpression regionRx(QLatin1String("Region \\d"));
0255   QRegularExpressionMatch regionMatch = regionRx.match(s);
0256   if(regionMatch.hasMatch()) {
0257     e->setField(QStringLiteral("region"), regionMatch.captured());
0258   }
0259   e->setField(QStringLiteral("audio-track"), readString()); // audio format
0260   readString(); // resolution
0261   readString(); // frame rate
0262   e->setField(QStringLiteral("language"), readString()); // audio language
0263   e->setField(QStringLiteral("subtitle"), readString()); // subtitle
0264   readString(); // file size
0265   s = readString(); // picture extension
0266   s = readImage(s); // picture
0267   if(!s.isEmpty()) {
0268     e->setField(QStringLiteral("cover"), s);
0269   }
0270 
0271   m_coll->addEntries(e);
0272 }
0273 
0274 QStringList AMCImporter::parseCast(const QString& text_) {
0275   QStringList cast;
0276   int nPar = 0;
0277   QRegularExpression castRx(QLatin1String("[,()]"));
0278   QRegularExpressionMatch castMatch = castRx.match(text_);
0279   QString person, role;
0280   int oldPos = 0;
0281   for(int pos = castMatch.capturedStart(); pos > -1; pos = castMatch.capturedStart()) {
0282     if(text_.at(pos) == QLatin1Char(',') && nPar%2 == 0) {
0283       // we're done with this one
0284       person += text_.mid(oldPos, pos-oldPos).trimmed();
0285       QString all = person;
0286       if(!role.isEmpty()) {
0287         if(role.startsWith(QLatin1String("as "))) {
0288           role = role.mid(3);
0289         }
0290         all += FieldFormat::columnDelimiterString() + role;
0291       }
0292       cast << all;
0293       person.clear();
0294       role.clear();
0295       oldPos = pos+1; // add one to go past comma
0296     } else if(text_.at(pos) == QLatin1Char('(')) {
0297       if(nPar == 0) {
0298         person = text_.mid(oldPos, pos-oldPos).trimmed();
0299         oldPos = pos+1; // add one to go past parenthesis
0300       }
0301       ++nPar;
0302     } else if(text_.at(pos) == QLatin1Char(')')) {
0303       --nPar;
0304       if(nPar == 0) {
0305         role = text_.mid(oldPos, pos-oldPos).trimmed();
0306         oldPos = pos+1; // add one to go past parenthesis
0307       }
0308     }
0309     castMatch = castRx.match(text_, pos+1);
0310   }
0311   // grab the last one
0312   if(nPar%2 == 0) {
0313     int pos = text_.length();
0314     person += text_.mid(oldPos, pos-oldPos).trimmed();
0315     QString all = person;
0316     if(!role.isEmpty()) {
0317       if(role.startsWith(QLatin1String("as "))) {
0318         role = role.mid(3);
0319       }
0320       all += FieldFormat::columnDelimiterString() + role;
0321     }
0322     cast << all;
0323   }
0324   return cast;
0325 }
0326 
0327 void AMCImporter::slotCancel() {
0328   m_cancelled = true;
0329 }