1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2011 Dennis Nienhüser <nienhueser@kde.org>
4 //
5 
6 /**
7   * Takes an .sqlite database with metadata about voice guidance speakers
8   * and associated .zip files containing 64.ogg files and produces four files
9   * for each speaker:
10   * - a .tar.gz to be used by KDE Marble via a GHNS dialog
11   * - a .zip to be downloaded by Marble users via edu.kde.org
12   * - a .zip to be downloaded by TomTom users via edu.kde.org
13   * - an .ogg speaker preview file
14   * The archives contain the speaker files and some additional stuff (license, authors, ...)
15   *
16   * The structure of the .sqlite database is expected as follows:
17   * One table called speakers with the following layout:
18   * - id PRIMARY KEY
19   * - name, email, nickname, gender, language, description, token VARCHAR
20   * - created DATETIME
21   * Additionally, the field gender is expected to be either "male" or "female" and language
22   * to have the format "NAME (langcode)"
23   *
24   * Also creates a knewstuff .xml file with the metadata.
25   *
26   * Some processing is done by calling other tools, namely tar, unzip, zip, vorbisgain, viftool.
27   * Make sure they're found in $PATH
28   *
29   */
30 
31 #include <QCoreApplication>
32 #include <QString>
33 #include <QDebug>
34 #include <QFileInfo>
35 #include <QDir>
36 #include <QTemporaryFile>
37 #include <QProcess>
38 
39 #include <QSqlDatabase>
40 #include <QSqlQuery>
41 #include <QSqlError>
42 
tomTomFiles()43 QStringList tomTomFiles()
44 {
45     QStringList result;
46     result << "100.ogg";
47     result << "200.ogg";
48     result << "2ndLeft.ogg";
49     result << "2ndRight.ogg";
50     result << "300.ogg";
51     result << "3rdLeft.ogg";
52     result << "3rdRight.ogg";
53     result << "400.ogg";
54     result << "500.ogg";
55     result << "50.ogg";
56     result << "600.ogg";
57     result << "700.ogg";
58     result << "800.ogg";
59     result << "80.ogg";
60     result << "After.ogg";
61     result << "AhExitLeft.ogg";
62     result << "AhExit.ogg";
63     result << "AhExitRight.ogg";
64     result << "AhFerry.ogg";
65     result << "AhKeepLeft.ogg";
66     result << "AhKeepRight.ogg";
67     result << "AhLeftTurn.ogg";
68     result << "AhRightTurn.ogg";
69     result << "AhUTurn.ogg";
70     result << "Arrive.ogg";
71     result << "BearLeft.ogg";
72     result << "BearRight.ogg";
73     result << "Charge.ogg";
74     result << "Depart.ogg";
75     result << "KeepLeft.ogg";
76     result << "KeepRight.ogg";
77     result << "LnLeft.ogg";
78     result << "LnRight.ogg";
79     result << "Meters.ogg";
80     result << "MwEnter.ogg";
81     result << "MwExitLeft.ogg";
82     result << "MwExit.ogg";
83     result << "MwExitRight.ogg";
84     result << "RbBack.ogg";
85     result << "RbCross.ogg";
86     result << "RbExit1.ogg";
87     result << "RbExit2.ogg";
88     result << "RbExit3.ogg";
89     result << "RbExit4.ogg";
90     result << "RbExit5.ogg";
91     result << "RbExit6.ogg";
92     result << "RbLeft.ogg";
93     result << "RbRight.ogg";
94     result << "RoadEnd.ogg";
95     result << "SharpLeft.ogg";
96     result << "SharpRight.ogg";
97     result << "Straight.ogg";
98     result << "TakeFerry.ogg";
99     result << "Then.ogg";
100     result << "TryUTurn.ogg";
101     result << "TurnLeft.ogg";
102     result << "TurnRight.ogg";
103     result << "UTurn.ogg";
104     result << "Yards.ogg";
105     return result;
106 }
107 
marbleFiles()108 QStringList marbleFiles()
109 {
110     QStringList result;
111     result << "Marble.ogg";
112     result << "RouteCalculated.ogg";
113     result << "RouteDeviated.ogg";
114     result << "GpsFound.ogg";
115     result << "GpsLost.ogg";
116     return result;
117 }
118 
usage(const QString & application)119 void usage( const QString &application )
120 {
121     qDebug() << "Usage: " << application << " /path/to/input/directory /path/to/output/directory /path/to/newstuff.xml";
122 }
123 
extract(const QString & zip,const QString & output)124 void extract( const QString &zip, const QString &output )
125 {
126     QProcess::execute( "unzip", QStringList() << "-q" << "-j" << "-d" << output << zip );
127 }
128 
normalize(const QString & output)129 void normalize( const QString &output )
130 {
131     QProcess vorbisgain;
132     vorbisgain.setWorkingDirectory( output );
133     vorbisgain.start( "vorbisgain", QStringList() << "-a" << tomTomFiles() << marbleFiles() );
134     vorbisgain.waitForFinished();
135 }
136 
createLegalFiles(const QString & directory,const QString & name,const QString & email)137 void createLegalFiles( const QString &directory, const QString &name, const QString &email )
138 {
139     QDir input( directory );
140     QFile authorsFile( input.filePath( "AUTHORS.txt" ) );
141     if ( authorsFile.open( QFile::WriteOnly | QFile::Truncate ) ) {
142         QTextStream stream( &authorsFile );
143         stream << name << " <" << email << ">";
144     }
145     authorsFile.close();
146 
147     QFile licenseFile( input.filePath( "LICENSE.txt" ) );
148     if ( licenseFile.open( QFile::WriteOnly | QFile::Truncate ) ) {
149         QTextStream stream( &licenseFile );
150         stream << "The ogg files in this directory are licensed under the creative commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0) license. ";
151         stream << "See https://creativecommons.org/licenses/by-sa/3.0/ and the file CC-BY-SA-3.0 in this directory.";
152     }
153     licenseFile.close();
154 
155     QFile installFile( input.filePath( "INSTALL.txt" ) );
156     if ( installFile.open( QFile::WriteOnly | QFile::Truncate ) ) {
157         QTextStream stream( &installFile );
158         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";
159         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";
160         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";
161         stream << "Check https://edu.kde.org/marble/speakers.php for updates and more speakers.";
162     }
163     installFile.close();
164 }
165 
convertToNewStuffFormat(const QString & input,const QString & output)166 void convertToNewStuffFormat( const QString &input, const QString &output )
167 {
168     QDir inputDirectory( input );
169     QStringList files;
170     files << tomTomFiles() << marbleFiles();
171     files << "AUTHORS.txt" << "INSTALL.txt" << "LICENSE.txt";
172     QStringList arguments;
173     arguments << "-czf" << output;
174     for( const QString &file: files ) {
175         arguments << inputDirectory.filePath( file );
176     }
177     arguments << "/usr/share/common-licenses/CC-BY-SA-3.0";
178 
179     QProcess::execute( "tar", arguments );
180 }
181 
convertToMarbleFormat(const QString & input,const QString & output)182 void convertToMarbleFormat( const QString &input, const QString &output )
183 {
184     QDir inputDirectory( input );
185     QStringList files;
186     files << tomTomFiles() << marbleFiles();
187     files << "AUTHORS.txt" << "INSTALL.txt" << "LICENSE.txt";
188     QStringList arguments;
189     arguments << "-q" << "-j" << output;
190     for( const QString &file: files ) {
191         arguments << inputDirectory.filePath( file );
192     }
193     arguments << "/usr/share/common-licenses/CC-BY-SA-3.0";
194 
195     QProcess::execute( "zip", arguments );
196 }
197 
convertToTomTomFormat(const QString & input,const QString & output,const QString & nick,const QString & simpleNick,int index,bool male,const QString & lang)198 void convertToTomTomFormat( const QString &input, const QString &output, const QString &nick, const QString &simpleNick, int index, bool male, const QString &lang )
199 {
200     QStringList arguments;
201     QString const prefix = input + QLatin1String("/data") + QString::number( index );
202     QString const vif = prefix + QLatin1String(".vif");
203     QString const chk = prefix + QLatin1String(".chk");
204     arguments << "join" << QString::number( index ) << nick << vif;
205     QProcess viftool;
206     viftool.setWorkingDirectory( input );
207     viftool.execute( "viftool", arguments );
208 
209     QFile vifFile( vif );
210     if ( vifFile.open( QFile::WriteOnly | QFile::Truncate ) ) {
211         QTextStream stream( &vifFile );
212         stream << nick << "\n"; // Name
213         stream << ( male ? 2 : 1 ) << "\n"; // gender index
214         /** @todo: flag, language index */
215         stream << 2 << "\n"; // Language index
216         stream << 114 << "\n"; // Flag index
217         stream << "1\n"; // Version number
218     }
219     vifFile.close();
220 
221     QDir inputDirectory( input );
222     QStringList files;
223     files << vif << chk;
224     files << "AUTHORS.txt" << "LICENSE.txt";
225     QStringList zipArguments;
226     zipArguments << "-q" << "-j" << ( output + QLatin1Char('/') + lang + QLatin1Char('-') + simpleNick + QLatin1String("-TomTom.zip") );
227     for( const QString &file: files ) {
228         QString const filePath = inputDirectory.filePath( file );
229         zipArguments <<  filePath;
230     }
231     zipArguments << "/usr/share/common-licenses/CC-BY-SA-3.0";
232 
233     QProcess::execute( "zip", zipArguments );
234 }
235 
process(const QDir & input,const QDir & output,const QString & xml)236 int process( const QDir &input, const QDir &output, const QString &xml )
237 {
238     QSqlDatabase database = QSqlDatabase::addDatabase( "QSQLITE" );
239     database.setDatabaseName( input.filePath( "speakers.db" ) );
240     if ( !database.open() ) {
241         qDebug() << "Failed to connect to database " << input.filePath( "speakers.db" );
242         return 3;
243     }
244 
245     output.mkdir( "files.kde.org" );
246     QSqlQuery query( "SELECT * FROM speakers ORDER BY Id" );
247 
248     QFile xmlFile( xml );
249     if ( !xmlFile.open( QFile::WriteOnly | QFile::Truncate ) ) {
250         qDebug() << "Failed to write to " << xmlFile.fileName();
251         return 3;
252     }
253 
254     QTextStream xmlOut( &xmlFile );
255     xmlOut << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
256     xmlOut << "<!DOCTYPE knewstuff SYSTEM \"knewstuff.dtd\">\n";
257     xmlOut << "<?xml-stylesheet type=\"text/xsl\" href=\"speakers.xsl\" ?>\n";
258     xmlOut << "<knewstuff>\n";
259 
260     int index = 71;
261     while (query.next()) {
262         QString const name = query.value(1).toString();
263         QString const email = query.value(2).toString();
264         QString const nick = query.value(3).toString();
265         QString const gender = query.value(4).toString();
266         QString const language = query.value(5).toString();
267         QString const lang = language.mid(0, language.indexOf(QLatin1Char('(')) - 1).replace(QLatin1Char(' '), QLatin1Char('-'));
268         QString const description = query.value(6).toString();
269         QString const token = query.value(7).toString();
270         QString const date = query.value(8).toString();
271         QString const zip = input.filePath( token );
272         QTemporaryFile tmpFile;
273         tmpFile.open();
274         QString const extracted = tmpFile.fileName();
275         tmpFile.remove();
276         QDir::root().mkdir( extracted );
277         qDebug() << "Name: " << name;
278 
279         QString const simpleNick = QString( nick ).replace( QLatin1Char(' '), QLatin1Char('-') );
280         QString const nickDir = output.filePath("files.kde.org") + QLatin1Char('/') + simpleNick;
281         QDir::root().mkdir( nickDir );
282         extract( zip, extracted );
283         normalize( extracted );
284         createLegalFiles( extracted, name, email );
285         QFile::copy(extracted + QLatin1String("/Marble.ogg"), nickDir + QLatin1Char('/') + lang + QLatin1Char('-') + simpleNick + QLatin1String(".ogg"));
286         convertToMarbleFormat(extracted, nickDir + QLatin1Char('/') + lang + QLatin1Char('-') + simpleNick + QLatin1String(".zip"));
287         convertToTomTomFormat(extracted, nickDir, nick, simpleNick, index, gender == QLatin1String("male"), lang);
288         convertToNewStuffFormat(extracted, nickDir + QLatin1Char('/') + lang + QLatin1Char('-') + simpleNick + QLatin1String(".tar.gz"));
289 
290         xmlOut << "  <stuff category=\"marble/data/audio\">\n";
291         xmlOut << "    <name lang=\"en\">" << language << " - " << nick << " (" <<  gender << ")" << "</name>\n";
292         xmlOut << "    <author>" << name << "</author>\n";
293         xmlOut << "    <licence>CC-By-SA 3.0</licence>\n";
294         xmlOut << "    <summary lang=\"en\">" << description << "</summary>\n";
295         xmlOut << "    <version>0.1</version>\n";
296         xmlOut << "    <releasedate>" << date << "</releasedate>\n";
297         xmlOut << "    <preview lang=\"en\">http://edu.kde.org/marble/speaker-" << gender << ".png</preview>\n";
298         xmlOut << "    <payload lang=\"en\">http://files.kde.org/marble/audio/speakers/" << simpleNick << "/" << lang << "-" << simpleNick << ".tar.gz</payload>\n";
299         xmlOut << "    <payload lang=\"ogg\">http://files.kde.org/marble/audio/speakers/" << simpleNick << "/" << lang << "-" << simpleNick << ".ogg</payload>\n";
300         xmlOut << "    <payload lang=\"zip\">http://files.kde.org/marble/audio/speakers/" << simpleNick << "/" << lang << "-" << simpleNick << ".zip</payload>\n";
301         xmlOut << "    <payload lang=\"tomtom\">http://files.kde.org/marble/audio/speakers/" << simpleNick << "/" << lang << "-" << simpleNick << "-TomTom.zip</payload>\n";
302         xmlOut << "  </stuff>\n";
303 
304         ++index;
305     }
306 
307     xmlOut << "</knewstuff>\n";
308     xmlFile.close();
309     return 0;
310 }
311 
main(int argc,char * argv[])312 int main(int argc, char *argv[])
313 {
314     QCoreApplication a(argc, argv);
315 
316     if ( argc < 4 ) {
317         usage( argv[0] );
318         return 1;
319     }
320 
321     QFileInfo input( argv[1] );
322     if ( !input.exists() || !input.isDir() ) {
323         qDebug() << "Incorrect input directory " << argv[1];
324         usage( argv[0] );
325         return 1;
326     }
327 
328     QFileInfo output( argv[2] );
329     if ( !output.exists() || !output.isWritable() ) {
330         qDebug() << "Incorrect output directory " << argv[1];
331         usage( argv[0] );
332         return 1;
333     }
334 
335     QFileInfo xmlFile( argv[3] );
336     return process( QDir( input.absoluteFilePath() ), QDir( output.absoluteFilePath() ), xmlFile.absoluteFilePath() );
337 }
338