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