File indexing completed on 2024-07-21 12:04:53

0001 // SPDX-License-Identifier: LGPL-2.1-or-later
0002 //
0003 // SPDX-FileCopyrightText: 2011 Dennis Nienhüser <nienhueser@kde.org>
0004 //
0005 
0006 /**
0007   * Takes an .sqlite database with metadata about voice guidance speakers
0008   * and associated .zip files containing 64.ogg files and produces four files
0009   * for each speaker:
0010   * - a .tar.gz to be used by KDE Marble via a GHNS dialog
0011   * - a .zip to be downloaded by Marble users via edu.kde.org
0012   * - a .zip to be downloaded by TomTom users via edu.kde.org
0013   * - an .ogg speaker preview file
0014   * The archives contain the speaker files and some additional stuff (license, authors, ...)
0015   *
0016   * The structure of the .sqlite database is expected as follows:
0017   * One table called speakers with the following layout:
0018   * - id PRIMARY KEY
0019   * - name, email, nickname, gender, language, description, token VARCHAR
0020   * - created DATETIME
0021   * Additionally, the field gender is expected to be either "male" or "female" and language
0022   * to have the format "NAME (langcode)"
0023   *
0024   * Also creates a knewstuff .xml file with the metadata.
0025   *
0026   * Some processing is done by calling other tools, namely tar, unzip, zip, vorbisgain, viftool.
0027   * Make sure they're found in $PATH
0028   *
0029   */
0030 
0031 #include <QCoreApplication>
0032 #include <QString>
0033 #include <QDebug>
0034 #include <QFileInfo>
0035 #include <QDir>
0036 #include <QTemporaryFile>
0037 #include <QProcess>
0038 
0039 #include <QSqlDatabase>
0040 #include <QSqlQuery>
0041 #include <QSqlError>
0042 
0043 QStringList tomTomFiles()
0044 {
0045     QStringList result;
0046     result << "100.ogg";
0047     result << "200.ogg";
0048     result << "2ndLeft.ogg";
0049     result << "2ndRight.ogg";
0050     result << "300.ogg";
0051     result << "3rdLeft.ogg";
0052     result << "3rdRight.ogg";
0053     result << "400.ogg";
0054     result << "500.ogg";
0055     result << "50.ogg";
0056     result << "600.ogg";
0057     result << "700.ogg";
0058     result << "800.ogg";
0059     result << "80.ogg";
0060     result << "After.ogg";
0061     result << "AhExitLeft.ogg";
0062     result << "AhExit.ogg";
0063     result << "AhExitRight.ogg";
0064     result << "AhFerry.ogg";
0065     result << "AhKeepLeft.ogg";
0066     result << "AhKeepRight.ogg";
0067     result << "AhLeftTurn.ogg";
0068     result << "AhRightTurn.ogg";
0069     result << "AhUTurn.ogg";
0070     result << "Arrive.ogg";
0071     result << "BearLeft.ogg";
0072     result << "BearRight.ogg";
0073     result << "Charge.ogg";
0074     result << "Depart.ogg";
0075     result << "KeepLeft.ogg";
0076     result << "KeepRight.ogg";
0077     result << "LnLeft.ogg";
0078     result << "LnRight.ogg";
0079     result << "Meters.ogg";
0080     result << "MwEnter.ogg";
0081     result << "MwExitLeft.ogg";
0082     result << "MwExit.ogg";
0083     result << "MwExitRight.ogg";
0084     result << "RbBack.ogg";
0085     result << "RbCross.ogg";
0086     result << "RbExit1.ogg";
0087     result << "RbExit2.ogg";
0088     result << "RbExit3.ogg";
0089     result << "RbExit4.ogg";
0090     result << "RbExit5.ogg";
0091     result << "RbExit6.ogg";
0092     result << "RbLeft.ogg";
0093     result << "RbRight.ogg";
0094     result << "RoadEnd.ogg";
0095     result << "SharpLeft.ogg";
0096     result << "SharpRight.ogg";
0097     result << "Straight.ogg";
0098     result << "TakeFerry.ogg";
0099     result << "Then.ogg";
0100     result << "TryUTurn.ogg";
0101     result << "TurnLeft.ogg";
0102     result << "TurnRight.ogg";
0103     result << "UTurn.ogg";
0104     result << "Yards.ogg";
0105     return result;
0106 }
0107 
0108 QStringList marbleFiles()
0109 {
0110     QStringList result;
0111     result << "Marble.ogg";
0112     result << "RouteCalculated.ogg";
0113     result << "RouteDeviated.ogg";
0114     result << "GpsFound.ogg";
0115     result << "GpsLost.ogg";
0116     return result;
0117 }
0118 
0119 void usage( const QString &application )
0120 {
0121     qDebug() << "Usage: " << application << " /path/to/input/directory /path/to/output/directory /path/to/newstuff.xml";
0122 }
0123 
0124 void extract( const QString &zip, const QString &output )
0125 {
0126     QProcess::execute( "unzip", QStringList() << "-q" << "-j" << "-d" << output << zip );
0127 }
0128 
0129 void normalize( const QString &output )
0130 {
0131     QProcess vorbisgain;
0132     vorbisgain.setWorkingDirectory( output );
0133     vorbisgain.start( "vorbisgain", QStringList() << "-a" << tomTomFiles() << marbleFiles() );
0134     vorbisgain.waitForFinished();
0135 }
0136 
0137 void createLegalFiles( const QString &directory, const QString &name, const QString &email )
0138 {
0139     QDir input( directory );
0140     QFile authorsFile( input.filePath( "AUTHORS.txt" ) );
0141     if ( authorsFile.open( QFile::WriteOnly | QFile::Truncate ) ) {
0142         QTextStream stream( &authorsFile );
0143         stream << name << " <" << email << ">";
0144     }
0145     authorsFile.close();
0146 
0147     QFile licenseFile( input.filePath( "LICENSE.txt" ) );
0148     if ( licenseFile.open( QFile::WriteOnly | QFile::Truncate ) ) {
0149         QTextStream stream( &licenseFile );
0150         stream << "The ogg files in this directory are licensed under the creative commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0) license. ";
0151         stream << "See https://creativecommons.org/licenses/by-sa/3.0/ and the file CC-BY-SA-3.0 in this directory.";
0152     }
0153     licenseFile.close();
0154 
0155     QFile installFile( input.filePath( "INSTALL.txt" ) );
0156     if ( installFile.open( QFile::WriteOnly | QFile::Truncate ) ) {
0157         QTextStream stream( &installFile );
0158         stream << "To install this voice guidance speaker in Marble, copy the entire directory to the audio/speakers/ directory in Marble's data path.\n\n";
0159         stream << "For example, if this directory is called 'MySpeaker' and you want to use it on the Nokia N900, copy the directory with all files to /home/user/MyDocs/.local/share/marble/audio/speakers/MySpeaker\n\n";
0160         stream << "Afterwards start Marble on the N900 and press the routing info box (four icons on the bottom) for two seconds with the pen. Enter the configuration dialog and choose the 'MySpeaker' speaker.\n\n";
0161         stream << "Check https://edu.kde.org/marble/speakers.php for updates and more speakers.";
0162     }
0163     installFile.close();
0164 }
0165 
0166 void convertToNewStuffFormat( const QString &input, const QString &output )
0167 {
0168     QDir inputDirectory( input );
0169     QStringList files;
0170     files << tomTomFiles() << marbleFiles();
0171     files << "AUTHORS.txt" << "INSTALL.txt" << "LICENSE.txt";
0172     QStringList arguments;
0173     arguments << "-czf" << output;
0174     for( const QString &file: files ) {
0175         arguments << inputDirectory.filePath( file );
0176     }
0177     arguments << "/usr/share/common-licenses/CC-BY-SA-3.0";
0178 
0179     QProcess::execute( "tar", arguments );
0180 }
0181 
0182 void convertToMarbleFormat( const QString &input, const QString &output )
0183 {
0184     QDir inputDirectory( input );
0185     QStringList files;
0186     files << tomTomFiles() << marbleFiles();
0187     files << "AUTHORS.txt" << "INSTALL.txt" << "LICENSE.txt";
0188     QStringList arguments;
0189     arguments << "-q" << "-j" << output;
0190     for( const QString &file: files ) {
0191         arguments << inputDirectory.filePath( file );
0192     }
0193     arguments << "/usr/share/common-licenses/CC-BY-SA-3.0";
0194 
0195     QProcess::execute( "zip", arguments );
0196 }
0197 
0198 void convertToTomTomFormat( const QString &input, const QString &output, const QString &nick, const QString &simpleNick, int index, bool male, const QString &lang )
0199 {
0200     QStringList arguments;
0201     QString const prefix = input + QLatin1String("/data") + QString::number( index );
0202     QString const vif = prefix + QLatin1String(".vif");
0203     QString const chk = prefix + QLatin1String(".chk");
0204     arguments << "join" << QString::number( index ) << nick << vif;
0205     QProcess viftool;
0206     viftool.setWorkingDirectory( input );
0207     viftool.execute( "viftool", arguments );
0208 
0209     QFile vifFile( vif );
0210     if ( vifFile.open( QFile::WriteOnly | QFile::Truncate ) ) {
0211         QTextStream stream( &vifFile );
0212         stream << nick << "\n"; // Name
0213         stream << ( male ? 2 : 1 ) << "\n"; // gender index
0214         /** @todo: flag, language index */
0215         stream << 2 << "\n"; // Language index
0216         stream << 114 << "\n"; // Flag index
0217         stream << "1\n"; // Version number
0218     }
0219     vifFile.close();
0220 
0221     QDir inputDirectory( input );
0222     QStringList files;
0223     files << vif << chk;
0224     files << "AUTHORS.txt" << "LICENSE.txt";
0225     QStringList zipArguments;
0226     zipArguments << "-q" << "-j" << ( output + QLatin1Char('/') + lang + QLatin1Char('-') + simpleNick + QLatin1String("-TomTom.zip") );
0227     for( const QString &file: files ) {
0228         QString const filePath = inputDirectory.filePath( file );
0229         zipArguments <<  filePath;
0230     }
0231     zipArguments << "/usr/share/common-licenses/CC-BY-SA-3.0";
0232 
0233     QProcess::execute( "zip", zipArguments );
0234 }
0235 
0236 int process( const QDir &input, const QDir &output, const QString &xml )
0237 {
0238     QSqlDatabase database = QSqlDatabase::addDatabase( "QSQLITE" );
0239     database.setDatabaseName( input.filePath( "speakers.db" ) );
0240     if ( !database.open() ) {
0241         qDebug() << "Failed to connect to database " << input.filePath( "speakers.db" );
0242         return 3;
0243     }
0244 
0245     output.mkdir( "files.kde.org" );
0246     QSqlQuery query( "SELECT * FROM speakers ORDER BY Id" );
0247 
0248     QFile xmlFile( xml );
0249     if ( !xmlFile.open( QFile::WriteOnly | QFile::Truncate ) ) {
0250         qDebug() << "Failed to write to " << xmlFile.fileName();
0251         return 3;
0252     }
0253 
0254     QTextStream xmlOut( &xmlFile );
0255     xmlOut << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
0256     xmlOut << "<!DOCTYPE knewstuff SYSTEM \"knewstuff.dtd\">\n";
0257     xmlOut << "<?xml-stylesheet type=\"text/xsl\" href=\"speakers.xsl\" ?>\n";
0258     xmlOut << "<knewstuff>\n";
0259 
0260     int index = 71;
0261     while (query.next()) {
0262         QString const name = query.value(1).toString();
0263         QString const email = query.value(2).toString();
0264         QString const nick = query.value(3).toString();
0265         QString const gender = query.value(4).toString();
0266         QString const language = query.value(5).toString();
0267         QString const lang = language.mid(0, language.indexOf(QLatin1Char('(')) - 1).replace(QLatin1Char(' '), QLatin1Char('-'));
0268         QString const description = query.value(6).toString();
0269         QString const token = query.value(7).toString();
0270         QString const date = query.value(8).toString();
0271         QString const zip = input.filePath( token );
0272         QTemporaryFile tmpFile;
0273         tmpFile.open();
0274         QString const extracted = tmpFile.fileName();
0275         tmpFile.remove();
0276         QDir::root().mkdir( extracted );
0277         qDebug() << "Name: " << name;
0278 
0279         QString const simpleNick = QString( nick ).replace( QLatin1Char(' '), QLatin1Char('-') );
0280         QString const nickDir = output.filePath("files.kde.org") + QLatin1Char('/') + simpleNick;
0281         QDir::root().mkdir( nickDir );
0282         extract( zip, extracted );
0283         normalize( extracted );
0284         createLegalFiles( extracted, name, email );
0285         QFile::copy(extracted + QLatin1String("/Marble.ogg"), nickDir + QLatin1Char('/') + lang + QLatin1Char('-') + simpleNick + QLatin1String(".ogg"));
0286         convertToMarbleFormat(extracted, nickDir + QLatin1Char('/') + lang + QLatin1Char('-') + simpleNick + QLatin1String(".zip"));
0287         convertToTomTomFormat(extracted, nickDir, nick, simpleNick, index, gender == QLatin1String("male"), lang);
0288         convertToNewStuffFormat(extracted, nickDir + QLatin1Char('/') + lang + QLatin1Char('-') + simpleNick + QLatin1String(".tar.gz"));
0289 
0290         xmlOut << "  <stuff category=\"marble/data/audio\">\n";
0291         xmlOut << "    <name lang=\"en\">" << language << " - " << nick << " (" <<  gender << ")" << "</name>\n";
0292         xmlOut << "    <author>" << name << "</author>\n";
0293         xmlOut << "    <licence>CC-By-SA 3.0</licence>\n";
0294         xmlOut << "    <summary lang=\"en\">" << description << "</summary>\n";
0295         xmlOut << "    <version>0.1</version>\n";
0296         xmlOut << "    <releasedate>" << date << "</releasedate>\n";
0297         xmlOut << "    <preview lang=\"en\">http://edu.kde.org/marble/speaker-" << gender << ".png</preview>\n";
0298         xmlOut << "    <payload lang=\"en\">http://files.kde.org/marble/audio/speakers/" << simpleNick << "/" << lang << "-" << simpleNick << ".tar.gz</payload>\n";
0299         xmlOut << "    <payload lang=\"ogg\">http://files.kde.org/marble/audio/speakers/" << simpleNick << "/" << lang << "-" << simpleNick << ".ogg</payload>\n";
0300         xmlOut << "    <payload lang=\"zip\">http://files.kde.org/marble/audio/speakers/" << simpleNick << "/" << lang << "-" << simpleNick << ".zip</payload>\n";
0301         xmlOut << "    <payload lang=\"tomtom\">http://files.kde.org/marble/audio/speakers/" << simpleNick << "/" << lang << "-" << simpleNick << "-TomTom.zip</payload>\n";
0302         xmlOut << "  </stuff>\n";
0303 
0304         ++index;
0305     }
0306 
0307     xmlOut << "</knewstuff>\n";
0308     xmlFile.close();
0309     return 0;
0310 }
0311 
0312 int main(int argc, char *argv[])
0313 {
0314     QCoreApplication a(argc, argv);
0315 
0316     if ( argc < 4 ) {
0317         usage( argv[0] );
0318         return 1;
0319     }
0320 
0321     QFileInfo input( argv[1] );
0322     if ( !input.exists() || !input.isDir() ) {
0323         qDebug() << "Incorrect input directory " << argv[1];
0324         usage( argv[0] );
0325         return 1;
0326     }
0327 
0328     QFileInfo output( argv[2] );
0329     if ( !output.exists() || !output.isWritable() ) {
0330         qDebug() << "Incorrect output directory " << argv[1];
0331         usage( argv[0] );
0332         return 1;
0333     }
0334 
0335     QFileInfo xmlFile( argv[3] );
0336     return process( QDir( input.absoluteFilePath() ), QDir( output.absoluteFilePath() ), xmlFile.absoluteFilePath() );
0337 }