update changelog
[debian/teleschorsch.git] / main.cpp
1 /*
2 # Copyright and License:
3 #
4 # Copyright (C) 2007-2009
5 # gregor herrmann <gregor+debian@comodo.priv.at>,
6 # Philipp Spitzer <philipp@spitzer.priv.at>
7 #
8 # This program is free software; you can redistribute it and/or modify it   
9 # under the terms of the GNU General Public License version 2 as published
10 # by the Free Software Foundation.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA, or point
18 # your browser to http://www.gnu.org/licenses/gpl.html
19 */
20
21 #include <QtGlobal>
22 #include <QtGui>
23
24 #include <iostream> // for help output
25 #include <string>   // for help output
26 #include <sys/stat.h> // glibc for umask and mkfifo
27 #include <errno.h>
28
29 #include "main.h"
30 #include "options.h"
31
32
33 // Types
34 // =====
35
36 VlcPlayer::VlcPlayer() {
37         name = "vlc";
38 }
39
40
41 bool VlcPlayer::play(QString urlToPlay, QTime offset, bool saveOnly, QString saveFileName, QString& errorMsg) {
42         if (saveOnly && saveFileName.isEmpty()) return true;
43         QStringList arguments;
44         arguments.append(urlToPlay);
45         arguments.append("--start-time");
46         arguments.append(QString::number(QTime(0, 0, 0, 0).secsTo(offset)));
47         if (!saveFileName.isEmpty()) {
48                 arguments.append("--sout");
49                 arguments.append(QString("#transcode{vcodec=mp4v,vb=384,scale=1,acodec=mpga,ab=128,channels=2}:duplicate{%1dst=std{access=file,mux=mp4,dst=\"%2\"}}").arg(saveOnly ? "" : "dst=display,").arg(saveFileName));
50         }
51         QProcess player(0);
52         qDebug() << "vlc" << arguments;
53         if (!player.startDetached("vlc", arguments)) {
54                 errorMsg = QObject::tr("Could not start vlc.");
55                 return false;
56         }
57         return true;
58 }
59
60
61 MPlayer::MPlayer() {
62         name = "mplayer";
63         download = 0;
64         #ifndef QT_4_1_COMPATIBLE
65         tee = 0;
66         player = 0;
67         #else
68         shell = 0;
69         #endif
70         tempFile = QDir::tempPath() + "teleschorschfifo." + qApp->sessionId();
71 }
72
73
74 MPlayer::~MPlayer() {
75         stopProcesses();
76         if (QFile::exists(tempFile)) QFile::remove(tempFile);
77 }
78
79
80 void MPlayer::stopProcesses() {
81         if (download) {download->terminate(); download = 0;}
82         #ifndef QT_4_1_COMPATIBLE
83         if (player) {player->terminate(); player = 0;}
84         if (tee) {tee->terminate(); tee = 0;}
85         #else
86         if (shell) {shell->terminate(); shell = 0;}
87         #endif
88         // If the QProcesses are deleted, a relatively long timeout occurs
89         /*
90         delete player;
91         delete tee;
92         delete download;
93         delete shell;
94         */
95
96 }
97
98
99 // Play & save with mplayer in the bash:
100 // mkfifo zibpipe
101 // mplayer -dumpstream -dumpfile zibpipe "rtsp://81.189.213.1:554/orf2/zb/zib070423l.rm?URL=/ramgen/orf2/zb/zib070423l.rm&cloakport=8088,554,7070" >/dev/null 2>&1 | tee pipeout < zibpipe | mplayer -cache 512 -
102 bool MPlayer::play(QString urlToPlay, QTime offset, bool saveOnly, QString saveFileName, QString& errorMsg) {
103         stopProcesses();
104         if (saveOnly && saveFileName.isEmpty()) return true;
105         QByteArray pipeFileName = tempFile.toLocal8Bit();
106         static const char* pipeFile = pipeFileName.data();
107         static const QString notStartError = QObject::tr("Could not start %1.");
108         QStringList arguments;
109         arguments.append(urlToPlay);
110         arguments.append("-prefer-ipv4");    // -"- avoids IPV6 error message
111         arguments.append("-ss"); 
112         arguments.append(QString::number(QTime(0, 0, 0, 0).secsTo(offset)));
113         if (!saveFileName.isEmpty()) {
114                 arguments.append("-dumpstream");
115                 arguments.append("-dumpfile");
116                 if (saveOnly) {
117                         arguments.append(saveFileName);
118                 } else {
119                         // Create fifo file ("named pipe") if it does not exist
120                         if (!QFile::exists(pipeFile)) {
121                                 int result = mkfifo(pipeFile, 0664);
122                                 if (result) {
123                                         switch (errno) {
124                                                 case EACCES: errorMsg = "EACCES"; break;
125                                                 case ENAMETOOLONG: errorMsg = "ENAMETOOLONG"; break;
126                                                 case ENOENT: errorMsg = "ENOENT"; break;
127                                                 case ENOTDIR: errorMsg = "ENOTDIR"; break;
128                                                 case ELOOP: errorMsg = "ELOOP"; break;
129                                                 case EEXIST: errorMsg = "EACCESS"; break;
130                                                 case ENOSPC: errorMsg = "ENOSPC"; break;
131                                                 case EROFS: errorMsg = "EROFS"; break;
132                                                 default: errorMsg = QObject::tr("unknown error code");
133                                         }
134                                         errorMsg = QObject::tr("The pipe %1 cannot be created (%2)").arg(pipeFile).arg(errorMsg);
135                                         return false;
136                                 }
137                         }
138                         arguments.append(pipeFile);
139                 }
140                 qDebug() << "mplayer" << arguments;
141                 download = new QProcess(0);
142                 download->start("mplayer", arguments);
143                 if (!download->waitForStarted()) {
144                         errorMsg = notStartError.arg("mplayer");
145                         return false;
146                 }
147                 if (saveOnly) return true;
148                 arguments.clear();
149                 arguments.append("-");
150         }
151         arguments.append("-cache");
152         arguments.append("512");
153         if (!saveOnly && !saveFileName.isEmpty()) {
154                 #ifndef QT_4_1_COMPATIBLE
155                 tee = new QProcess(0);
156                 QStringList teeArgs;
157                 teeArgs.append(saveFileName);
158                 tee->setStandardInputFile(pipeFile); // QT 4.2 function
159                 player = new QProcess(0);
160                 tee->setStandardOutputProcess(player); // QT 4.2 function
161                 qDebug() << "tee" << teeArgs;
162                 qDebug() << "mplayer" << arguments;
163                 tee->start("tee", teeArgs);
164                 if (!tee->waitForStarted()) {
165                         errorMsg = notStartError.arg("tee");
166                         return false;
167                 }
168                 player->start("mplayer", arguments); // _g_mplayer doesn't like - (/dev/stdin)
169                 if (!player->waitForStarted()) {
170                         errorMsg = notStartError.arg("mplayer");
171                         return false;
172                 }
173
174                 #else
175                 shell = new QProcess(0);
176                 QStringList shellArgs;
177                 shellArgs << "-c" << "tee " + saveFileName + " < " + pipeFile + " | mplayer " + arguments.join(" ");
178                 qDebug() << "/bin/sh" << shellArgs;
179                 shell->start("/bin/sh", shellArgs);
180                 if (!shell->waitForStarted()) {
181                         errorMsg = notStartError.arg("/bin/sh");
182                         return false;
183                 }
184                 #endif
185
186                 return true;
187         }
188         qDebug() << "gmplayer" << arguments;
189         QProcess player(0);
190         if (!player.startDetached("gmplayer", arguments)) {
191                 errorMsg = notStartError.arg("gmplayer");
192                 return false;
193         }
194         return true;
195 }
196
197
198
199 // Functions
200 // =========
201
202 void initConfigInfo(ConfigInfo& configInfo) {
203         configInfo.userConfigFile = QDir::homePath() + "/.teleschorschrc";
204         configInfo.systemConfigFile = "/etc/teleschorschrc";
205
206         // User config file
207         if (!configInfo.userConfigFile.isEmpty()) {
208                 QFile configFileUser(configInfo.userConfigFile);
209                 if (configFileUser.exists()) {configInfo.usedConfigFile  = configInfo.userConfigFile;}
210         }
211
212         // System config file
213         if (configInfo.usedConfigFile.isEmpty()) {
214                 QFile configFileSystem(configInfo.systemConfigFile);
215                 if (configFileSystem.exists()) configInfo.usedConfigFile = configInfo.systemConfigFile;
216         }
217 }
218
219
220 bool addConfig(const QString& fileName, ChannelVec& cv, QString& error) {
221         QFile file(fileName);
222         if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {error = QObject::tr("Could not open file %1").arg(fileName); return false;}
223         Channel channel; // current channel
224         bool sectionOccured = false;
225         while (!file.atEnd()) {
226                 QString line = QString(file.readLine()).trimmed();
227                 if (line.startsWith("#") || line.startsWith(";") || line.isEmpty()) continue; // strip comments
228                 
229                 // Is the line a [section]?
230                 bool section = line.startsWith("[") && line.endsWith("]") && line.size() >= 3;
231                 if (section) {
232                         if (sectionOccured) cv.push_back(channel);
233                         sectionOccured = true;
234                         channel = Channel();
235                         channel.name = line.mid(1, line.size()-2);
236                 }
237
238                 // Read properties
239                 int eqPos = line.indexOf("=");
240                 bool property = !section && eqPos >= 1;
241                 if (property) {
242                         QString propKey = line.left(eqPos);
243                         QString propVal = line.right(line.size()-eqPos-1);
244                         if (!sectionOccured) {error = QObject::tr("Property %1 is only allowed in a [section].").arg(propKey); return false;}
245
246                         if (propKey == "FULLNAME") channel.fullName = propVal;
247                         else if (propKey == "STATICURL") channel.staticUrl = propVal;
248                         else if (propKey == "PLAYER") channel.player = propVal;
249                         else {error = QObject::tr("Unknown key in ini file: %1").arg(propKey); return false;}
250                 }
251
252                 if (!section && !property) {error = QObject::tr("Line %1 is not valid.").arg(line); return false;}
253         }
254         if (sectionOccured) cv.push_back(channel);
255         return true;
256 }
257
258
259 QString readChannelVec(const ConfigInfo& configInfo, ChannelVec& channelVec) {
260         if (configInfo.usedConfigFile.isEmpty()) return QObject::tr("Neither %1 nor %2 found.").arg(configInfo.systemConfigFile).arg(configInfo.userConfigFile);
261         QString error;
262         if (!addConfig(configInfo.usedConfigFile, channelVec, error)) return QObject::tr("Error: %1.").arg(error);
263         return error;
264 }
265
266
267 /// \brief Finds a value for a specified variable and appends it to a string.
268 ///
269 /// \param[in]  var    variable to substitute. These are the possibilities:
270 ///                    - d  day of month (01-31)
271 ///                    - m  month (01-12)
272 ///                    - y  year (last two digits)
273 ///                    - Y  year (4 digits)
274 ///                    - dow_DE  day of week in German (Montag, ...)
275 /// \param[in]  date   date that should be used when replacing the date dependend variables
276 /// \param[out] result The determined value of the variable is _appended_.
277 /// \param[out] error  error message in error cases
278 /// \returns true in case of success.
279 bool substituteVar(const QString& var, QDate date, QString& result, QString& errorMsg) {
280         if (var == "d") {result += date.toString("dd"); return true;}
281         if (var == "m") {result += date.toString("MM"); return true;}
282         if (var == "y") {result += date.toString("yy"); return true;}
283         if (var == "Y") {result += date.toString("yyyy"); return true;}
284         if (var == "dow_DE") {
285                 int dow = date.dayOfWeek() - 1;
286                 static const char dow_de[][16] = {"Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"};
287                 if (dow >= 0 && dow < 7) {result += dow_de[dow]; return true;}
288         }
289         errorMsg = QObject::tr("No match for variable %1").arg(var);
290         return false;
291 }
292
293
294 /// \brief Finds a string, where variables of the form <code>${var}</code> are substituted in input (normally a staticUrl).
295 ///
296 /// - ${d}  day of month (01-31)
297 /// - ${m}  month (01-12)
298 /// - ${y}  year (last two digits)
299 /// - ${Y}  year (4 digits)
300 /// - ${dow_DE}  day of week in German (Montag, ...)
301 ///
302 /// \param[in]  input  string where the substitution should be done.
303 /// \param[in]  date   date that should be used when replacing the date dependend variables
304 /// \param[out] result The result string with the variables substituted is appended here
305 /// \param[out] error  error message in error cases
306 /// \returns true in case of success.
307 bool substituteVars(const QString& input, QDate date, QString& result, QString& errorMsg) {
308         int pos = 0;
309         int lastPos = 0;
310         while (pos != -1) {
311                 pos = input.indexOf("${", lastPos);
312                 if (pos == -1) result += input.mid(lastPos);
313                 else result += input.mid(lastPos, pos-lastPos);
314                 lastPos = pos;
315                 
316                 // Match?
317                 if (pos != -1) {
318                         pos = input.indexOf("}", lastPos);
319                         if (pos == -1) {
320                                 errorMsg = QObject::tr("${ not closed with }.");
321                                 return false;
322                         }
323                         QString var = input.mid(lastPos+2, pos-lastPos-2);
324                         if (!substituteVar(var, date, result, errorMsg)) return false;
325                         lastPos = pos+1;
326                 }
327         }
328         return true;
329 }
330
331
332 /// \brief Calls the /bin/sh shell with the specified command and appends the result to a string variable.
333 ///
334 /// \param[in]  command command to execute with <code>/bin/sh -c "command"</code>
335 /// \param[out] result  The result string where the output is appended at.
336 /// \param[out] error   error message in error cases
337 /// \returns true in case of success.
338 bool executeShellCommand(const QString& command, QString& result, QString& errorMsg) {
339         QProcess evalUrl(0);
340         QString cmd = "/bin/sh -c \"" + command + "\"";
341         evalUrl.start(cmd);
342         if (evalUrl.waitForFinished(3000)) {
343                 QByteArray newResult = evalUrl.readAllStandardOutput();
344                 if (result != newResult) {result += newResult.trimmed();}
345                 return true;
346         } 
347         errorMsg = QObject::tr("Shell command executed when substituting URL (%1) timed out.").arg(command);
348         return false;
349 }
350
351
352 /// \brief Evaluates the staticUrl
353 ///
354 /// - Variables of the form ${var} are substituted according the the function ::substituteVars
355 /// - If the staticUrl is enclosed by backticks it is evaluated by a shell command (after the substitution mentioned above)
356 ///
357 /// \param[in]  staticUrl string where the substitution should be done.
358 /// \param[in]  date      date that should be used when replacing the date dependend variables
359 /// \param[out] result    The result string is appended here
360 /// \param[out] error     error message in error cases
361 /// \returns true in case of success.
362 bool evaluateStaticUrl(const QString& staticUrl, QDate date, QString& result, QString& errorMsg) {
363         QString subst;
364         bool success = substituteVars(staticUrl, date, subst, errorMsg);
365         if (!success) return false;
366
367         // Evaluate staticUrl - it might be dynamic despite its name :-)
368         QString cmdres;
369         if (subst.left(1) == "`" && subst.right(1) == "`") {
370                 success = executeShellCommand(subst.mid(1, subst.size()-2), cmdres, errorMsg);
371                 if (!success) return false;
372                 result = cmdres;
373         } else result = subst;
374         return true;
375 }
376
377
378 void showHelp() {
379         // Isn't there a way to output QStrings directly?
380         std::cout << QObject::tr("Usage: qteleschorsch [options]").toStdString() << std::endl
381         << QObject::tr("Options: --help: Shows this message at the command line and exits qteleschorsch").toStdString() << std::endl
382         << QObject::tr("More help: man qteleschorsch").toStdString() << std::endl;
383 }
384
385
386
387 // MainDialog
388 // ==========
389
390 MainDialog::MainDialog(QWidget *parent): QDialog(parent) {
391         player = 0;
392         
393         // User interface
394         setupUi(this);
395         QObject::connect(btnOptions, SIGNAL(clicked()), this, SLOT(editOptions()));
396         QObject::connect(btnStart, SIGNAL(clicked()), this, SLOT(startAction()));
397         QObject::connect(btnStartSave, SIGNAL(clicked()), this, SLOT(startSaveAction()));
398         QObject::connect(btnSave, SIGNAL(clicked()), this, SLOT(saveAction()));
399
400         // Init configInfo
401         initConfigInfo(configInfo);
402
403         // Read config
404         QString error = readChannelVec(configInfo, channelVec);
405         if (!error.isEmpty()) QMessageBox::warning(this, tr("Problem when reading the configuration file"), error);
406
407         // Fill in channels
408         updateLwChannels();
409
410         // Default date
411         QDateTime dateTime = QDateTime::currentDateTime(); // set the default date to today if it is past midday
412         if (dateTime.time().hour() < 12) dateTime = dateTime.addDays(-1);
413         #ifndef QT_4_1_COMPATIBLE
414         calDate->setSelectedDate(dateTime.date());
415         #else
416         calDateEdit->setDate(dateTime.date());
417         #endif
418 }
419
420
421 MainDialog::~MainDialog() {
422         delete player;
423 }
424
425
426 bool MainDialog::play(bool play, bool save) {
427         // Read data from form
428         int row = lwChannels->currentRow();
429         if (row < 0) return false;
430
431         Channel channel = channelVec[row];
432         #ifndef QT_4_1_COMPATIBLE
433         QDate date = calDate->selectedDate();
434         #else
435         QDate date = calDateEdit->date();
436         #endif
437         // Substitude URL
438         QString substUrl;
439         QString errorMsg;
440         if (!evaluateStaticUrl(channel.staticUrl, date, substUrl, errorMsg)) {
441                 QMessageBox::warning(this, tr("Problem when substituting URL"), errorMsg);
442                 return false;
443         }
444
445         // Determine player to use
446         delete player; 
447         player = 0;
448         if (channel.player.contains("vlc")) player = new VlcPlayer();
449         if (channel.player.contains("mplayer")) player = new MPlayer();
450         if (!player) {
451                 QMessageBox::warning(this, tr("Unknown player"), tr("player %1 not known").arg(channel.player));
452                 return false;
453         }
454
455         // Ask filename
456         QString fileName;
457         if (save) {
458                 fileName = QFileDialog::getSaveFileName(this, tr("Save as ..."), QDir::homePath() + "/" + channel.name + "_" + date.toString(Qt::ISODate) +  (channel.player.contains("vlc") ? ".mpeg" : ""), (channel.player.contains("vlc") ? tr("Videos (*.mpeg)") : tr("Videos (*.*)")));
459                 if (fileName.isEmpty()) return true;
460         }
461
462         // Play or Save
463         if (!player->play(substUrl, teOffset->time(), !play, fileName, errorMsg)) {
464                 QMessageBox::warning(this, tr("Error while playing"), errorMsg);
465                 return false;
466         }
467         return true;
468 }
469
470
471
472 void MainDialog::editOptions() {
473         OptionsDialog *od = new OptionsDialog();
474         if (od->exec(configInfo.userConfigFile)) {
475                 channelVec.clear();
476                 QString error = readChannelVec(configInfo, channelVec);
477                 if (!error.isEmpty()) QMessageBox::warning(this, tr("Problem when reading the configuration file"), error);
478                 updateLwChannels();
479         }
480         delete od;
481 }
482
483
484 void MainDialog::updateLwChannels() {
485         int row = lwChannels->currentRow(); // remember selected row
486         lwChannels->clear();
487         for (int i = 0; i != channelVec.size(); ++i) lwChannels->addItem(channelVec[i].fullName);
488         if (row > lwChannels->count()) row = lwChannels->count()-1; // set to last row. if count==0, row gets -1.
489         if (row == -1 && lwChannels->count() > 0) row = 0; // set to first if nothing was selected previously.
490         if (row != -1) lwChannels->setCurrentRow(row);
491 }
492
493
494 bool MainDialog::startAction() {
495                 return play(true, false);
496 }
497
498
499 bool MainDialog::startSaveAction() {
500                 return play(true, true);
501 }
502
503
504 bool MainDialog::saveAction() {
505                 return play(false, true);
506 }
507
508
509
510 // Main function
511 // =============
512
513 int main(int argc, char *argv[]) {
514         QApplication app(argc, argv);
515
516         // Initialize translation
517         QString locale = QLocale::system().name();
518         QTranslator translator;
519         translator.load(QString(":/qteleschorsch_") + locale);
520         app.installTranslator(&translator);
521
522         // Parse command line options
523         if (app.arguments().size() > 1) {
524                 showHelp();
525                 return 0;
526         }
527
528         MainDialog *mainDialog = new MainDialog();
529         mainDialog->setWindowFlags(Qt::Window);
530         mainDialog->show();
531         int status = app.exec();
532         delete mainDialog;
533         return status;
534