ef8816708bbdd4017ef267c5c490fe00a889a2eb
[dcpomatic.git] / src / tools / dcpomatic_batch.cc
1 /*
2     Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21
22 #include "wx/about_dialog.h"
23 #include "wx/dcpomatic_button.h"
24 #include "wx/full_config_dialog.h"
25 #include "wx/job_manager_view.h"
26 #include "wx/servers_list_dialog.h"
27 #include "wx/wx_signal_manager.h"
28 #include "wx/wx_util.h"
29 #include "lib/compose.hpp"
30 #include "lib/config.h"
31 #include "lib/dcpomatic_socket.h"
32 #include "lib/film.h"
33 #include "lib/job.h"
34 #include "lib/job_manager.h"
35 #include "lib/make_dcp.h"
36 #include "lib/transcode_job.h"
37 #include "lib/util.h"
38 #include "lib/version.h"
39 #include <wx/aboutdlg.h>
40 #include <wx/cmdline.h>
41 #include <wx/preferences.h>
42 #include <wx/splash.h>
43 #include <wx/stdpaths.h>
44 #include <wx/wx.h>
45 #include <iostream>
46 #include <set>
47
48
49 using std::cout;
50 using std::dynamic_pointer_cast;
51 using std::exception;
52 using std::list;
53 using std::make_shared;
54 using std::set;
55 using std::shared_ptr;
56 using std::string;
57 using boost::scoped_array;
58 using boost::thread;
59 #if BOOST_VERSION >= 106100
60 using namespace boost::placeholders;
61 #endif
62
63
64 static list<boost::filesystem::path> films_to_load;
65
66
67 enum {
68         ID_file_add_film = 1,
69         ID_tools_encoding_servers,
70         ID_help_about
71 };
72
73
74 void
75 setup_menu (wxMenuBar* m)
76 {
77         auto file = new wxMenu;
78         file->Append (ID_file_add_film, _("&Add Film...\tCtrl-A"));
79 #ifdef DCPOMATIC_OSX
80         file->Append (wxID_EXIT, _("&Exit"));
81 #else
82         file->Append (wxID_EXIT, _("&Quit"));
83 #endif
84
85 #ifdef DCPOMATIC_OSX
86         file->Append (wxID_PREFERENCES, _("&Preferences...\tCtrl-P"));
87 #else
88         auto edit = new wxMenu;
89         edit->Append (wxID_PREFERENCES, _("&Preferences...\tCtrl-P"));
90 #endif
91
92         auto tools = new wxMenu;
93         tools->Append (ID_tools_encoding_servers, _("Encoding servers..."));
94
95         auto help = new wxMenu;
96         help->Append (ID_help_about, _("About"));
97
98         m->Append (file, _("&File"));
99 #ifndef DCPOMATIC_OSX
100         m->Append (edit, _("&Edit"));
101 #endif
102         m->Append (tools, _("&Tools"));
103         m->Append (help, _("&Help"));
104 }
105
106
107 class DOMFrame : public wxFrame
108 {
109 public:
110         explicit DOMFrame (wxString const & title)
111                 : wxFrame (nullptr, -1, title)
112                 , _sizer (new wxBoxSizer(wxVERTICAL))
113         {
114                 auto bar = new wxMenuBar;
115                 setup_menu (bar);
116                 SetMenuBar (bar);
117
118                 Config::instance()->Changed.connect (boost::bind (&DOMFrame::config_changed, this, _1));
119
120                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_add_film, this),    ID_file_add_film);
121                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_quit, this),        wxID_EXIT);
122                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::edit_preferences, this), wxID_PREFERENCES);
123                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_encoding_servers, this), ID_tools_encoding_servers);
124                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::help_about, this),       ID_help_about);
125
126                 auto panel = new wxPanel (this);
127                 auto s = new wxBoxSizer (wxHORIZONTAL);
128                 s->Add (panel, 1, wxEXPAND);
129                 SetSizer (s);
130
131                 auto job_manager_view = new JobManagerView (panel, true);
132                 _sizer->Add (job_manager_view, 1, wxALL | wxEXPAND, 6);
133
134                 auto buttons = new wxBoxSizer (wxHORIZONTAL);
135                 auto add = new Button (panel, _("Add Film..."));
136                 add->Bind (wxEVT_BUTTON, boost::bind(&DOMFrame::add_film, this));
137                 buttons->Add (add, 1, wxALL, 6);
138                 _pause = new Button (panel, _("Pause"));
139                 _pause->Bind (wxEVT_BUTTON, boost::bind(&DOMFrame::pause, this));
140                 buttons->Add (_pause, 1, wxALL, 6);
141                 _resume = new Button (panel, _("Resume"));
142                 _resume->Bind (wxEVT_BUTTON, boost::bind(&DOMFrame::resume, this));
143                 buttons->Add (_resume, 1, wxALL, 6);
144
145                 setup_sensitivity ();
146
147                 _sizer->Add (buttons, 0, wxALL, 6);
148
149                 panel->SetSizer (_sizer);
150
151                 Bind (wxEVT_CLOSE_WINDOW, boost::bind(&DOMFrame::close, this, _1));
152                 Bind (wxEVT_SIZE, boost::bind(&DOMFrame::sized, this, _1));
153         }
154
155         void setup_sensitivity ()
156         {
157                 _pause->Enable (!JobManager::instance()->paused());
158                 _resume->Enable (JobManager::instance()->paused());
159         }
160
161         void pause ()
162         {
163                 JobManager::instance()->pause();
164                 setup_sensitivity ();
165         }
166
167         void resume ()
168         {
169                 JobManager::instance()->resume();
170                 setup_sensitivity ();
171         }
172
173         void start_job (boost::filesystem::path path)
174         {
175                 try {
176                         auto film = make_shared<Film>(path);
177                         film->read_metadata ();
178
179                         double total_required;
180                         double available;
181                         bool can_hard_link;
182
183                         film->should_be_enough_disk_space (total_required, available, can_hard_link);
184
185                         set<shared_ptr<const Film>> films;
186
187                         for (auto i: JobManager::instance()->get()) {
188                                 films.insert (i->film());
189                         }
190
191                         for (auto i: films) {
192                                 double progress = 0;
193                                 for (auto j: JobManager::instance()->get()) {
194                                         if (i == j->film() && dynamic_pointer_cast<TranscodeJob>(j)) {
195                                                 progress = j->progress().get_value_or(0);
196                                         }
197                                 }
198
199                                 double required;
200                                 i->should_be_enough_disk_space (required, available, can_hard_link);
201                                 total_required += (1 - progress) * required;
202                         }
203
204                         if ((total_required - available) > 1) {
205                                 if (!confirm_dialog (
206                                             this,
207                                             wxString::Format(
208                                                     _("The DCPs for this film and the films already in the queue will take up about %.1f GB.  The "
209                                                       "disks that you are using only have %.1f GB available.  Do you want to add this film to the queue anyway?"),
210                                                     total_required, available))) {
211                                         return;
212                                 }
213                         }
214
215                         make_dcp (film, TranscodeJob::ChangedBehaviour::STOP);
216                 } catch (std::exception& e) {
217                         auto p = std_to_wx (path.string ());
218                         auto b = p.ToUTF8 ();
219                         error_dialog (this, wxString::Format(_("Could not open film at %s"), p.data()), std_to_wx(e.what()));
220                 }
221         }
222
223 private:
224         void sized (wxSizeEvent& ev)
225         {
226                 _sizer->Layout ();
227                 ev.Skip ();
228         }
229
230         bool should_close ()
231         {
232                 if (!JobManager::instance()->work_to_do()) {
233                         return true;
234                 }
235
236                 auto d = new wxMessageDialog (
237                         0,
238                         _("There are unfinished jobs; are you sure you want to quit?"),
239                         _("Unfinished jobs"),
240                         wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION
241                         );
242
243                 bool const r = d->ShowModal() == wxID_YES;
244                 d->Destroy ();
245                 return r;
246         }
247
248         void close (wxCloseEvent& ev)
249         {
250                 if (!should_close()) {
251                         ev.Veto ();
252                         return;
253                 }
254
255                 ev.Skip ();
256         }
257
258         void file_add_film ()
259         {
260                 add_film ();
261         }
262
263         void file_quit ()
264         {
265                 if (should_close()) {
266                         Close (true);
267                 }
268         }
269
270         void edit_preferences ()
271         {
272                 if (!_config_dialog) {
273                         _config_dialog = create_full_config_dialog ();
274                 }
275                 _config_dialog->Show (this);
276         }
277
278         void tools_encoding_servers ()
279         {
280                 if (!_servers_list_dialog) {
281                         _servers_list_dialog = new ServersListDialog (this);
282                 }
283
284                 _servers_list_dialog->Show ();
285         }
286
287         void help_about ()
288         {
289                 auto d = new AboutDialog (this);
290                 d->ShowModal ();
291                 d->Destroy ();
292         }
293
294         void add_film ()
295         {
296                 auto c = new wxDirDialog (this, _("Select film to open"), wxStandardPaths::Get().GetDocumentsDir(), wxDEFAULT_DIALOG_STYLE | wxDD_DIR_MUST_EXIST);
297                 if (_last_parent) {
298                         c->SetPath (std_to_wx(_last_parent.get().string()));
299                 }
300
301                 int r;
302                 while (true) {
303                         r = c->ShowModal ();
304                         if (r == wxID_OK && c->GetPath() == wxStandardPaths::Get().GetDocumentsDir()) {
305                                 error_dialog (this, _("You did not select a folder.  Make sure that you select a folder before clicking Open."));
306                         } else {
307                                 break;
308                         }
309                 }
310
311                 if (r == wxID_OK) {
312                         start_job (wx_to_std (c->GetPath ()));
313                 }
314
315                 _last_parent = boost::filesystem::path (wx_to_std (c->GetPath ())).parent_path ();
316
317                 c->Destroy ();
318         }
319
320         void config_changed (Config::Property what)
321         {
322                 /* Instantly save any config changes when using the DCP-o-matic GUI */
323                 if (what == Config::CINEMAS) {
324                         try {
325                                 Config::instance()->write_cinemas();
326                         } catch (exception& e) {
327                                 error_dialog (
328                                         this,
329                                         wxString::Format(
330                                                 _("Could not write to cinemas file at %s.  Your changes have not been saved."),
331                                                 std_to_wx (Config::instance()->cinemas_file().string()).data()
332                                                 )
333                                         );
334                         }
335                 } else {
336                         try {
337                                 Config::instance()->write_config();
338                         } catch (exception& e) {
339                                 error_dialog (
340                                         this,
341                                         wxString::Format(
342                                                 _("Could not write to config file at %s.  Your changes have not been saved."),
343                                                 std_to_wx (Config::instance()->cinemas_file().string()).data()
344                                                 )
345                                         );
346                         }
347                 }
348         }
349
350         boost::optional<boost::filesystem::path> _last_parent;
351         wxSizer* _sizer;
352         wxPreferencesEditor* _config_dialog = nullptr;
353         ServersListDialog* _servers_list_dialog = nullptr;
354         wxButton* _pause;
355         wxButton* _resume;
356 };
357
358
359 static const wxCmdLineEntryDesc command_line_description[] = {
360         { wxCMD_LINE_PARAM, 0, 0, "film to load", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_MULTIPLE | wxCMD_LINE_PARAM_OPTIONAL },
361         { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 }
362 };
363
364
365 class JobServer : public Server
366 {
367 public:
368         explicit JobServer (DOMFrame* frame)
369                 : Server (BATCH_JOB_PORT)
370                 , _frame (frame)
371         {}
372
373         void handle (shared_ptr<Socket> socket) override
374         {
375                 try {
376                         int const length = socket->read_uint32 ();
377                         scoped_array<char> buffer(new char[length]);
378                         socket->read (reinterpret_cast<uint8_t*>(buffer.get()), length);
379                         string s (buffer.get());
380                         _frame->start_job (s);
381                         socket->write (reinterpret_cast<uint8_t const *>("OK"), 3);
382                 } catch (...) {
383
384                 }
385         }
386
387 private:
388         DOMFrame* _frame;
389 };
390
391
392 class App : public wxApp
393 {
394         bool OnInit () override
395         {
396                 wxInitAllImageHandlers ();
397
398                 SetAppName (_("DCP-o-matic Batch Converter"));
399                 is_batch_converter = true;
400
401                 Config::FailedToLoad.connect (boost::bind(&App::config_failed_to_load, this));
402                 Config::Warning.connect (boost::bind(&App::config_warning, this, _1));
403
404                 auto splash = maybe_show_splash ();
405
406                 if (!wxApp::OnInit()) {
407                         return false;
408                 }
409
410 #ifdef DCPOMATIC_LINUX
411                 unsetenv ("UBUNTU_MENUPROXY");
412 #endif
413
414                 dcpomatic_setup_path_encoding ();
415
416                 /* Enable i18n; this will create a Config object
417                    to look for a force-configured language.  This Config
418                    object will be wrong, however, because dcpomatic_setup
419                    hasn't yet been called and there aren't any filters etc.
420                    set up yet.
421                 */
422                 dcpomatic_setup_i18n ();
423
424                 /* Set things up, including filters etc.
425                    which will now be internationalised correctly.
426                 */
427                 dcpomatic_setup ();
428
429                 /* Force the configuration to be re-loaded correctly next
430                    time it is needed.
431                 */
432                 Config::drop ();
433
434                 _frame = new DOMFrame (_("DCP-o-matic Batch Converter"));
435                 SetTopWindow (_frame);
436                 _frame->Maximize ();
437                 if (splash) {
438                         splash->Destroy ();
439                 }
440                 _frame->Show ();
441
442                 auto server = new JobServer (_frame);
443                 new thread (boost::bind (&JobServer::run, server));
444
445                 signal_manager = new wxSignalManager (this);
446                 this->Bind (wxEVT_IDLE, boost::bind (&App::idle, this));
447
448                 shared_ptr<Film> film;
449                 for (auto i: films_to_load) {
450                         if (boost::filesystem::is_directory(i)) {
451                                 try {
452                                         film = make_shared<Film>(i);
453                                         film->read_metadata ();
454                                         make_dcp (film, TranscodeJob::ChangedBehaviour::EXAMINE_THEN_STOP);
455                                 } catch (exception& e) {
456                                         error_dialog (
457                                                 0,
458                                                 std_to_wx(String::compose(wx_to_std(_("Could not load film %1")), i.string())),
459                                                 std_to_wx(e.what())
460                                                 );
461                                 }
462                         }
463                 }
464
465                 return true;
466         }
467
468         void idle ()
469         {
470                 signal_manager->ui_idle ();
471         }
472
473         void OnInitCmdLine (wxCmdLineParser& parser) override
474         {
475                 parser.SetDesc (command_line_description);
476                 parser.SetSwitchChars (wxT ("-"));
477         }
478
479         bool OnCmdLineParsed (wxCmdLineParser& parser) override
480         {
481                 for (size_t i = 0; i < parser.GetParamCount(); ++i) {
482                         films_to_load.push_back (wx_to_std(parser.GetParam(i)));
483                 }
484
485                 return true;
486         }
487
488         void config_failed_to_load ()
489         {
490                 message_dialog (_frame, _("The existing configuration failed to load.  Default values will be used instead.  These may take a short time to create."));
491         }
492
493         void config_warning (string m)
494         {
495                 message_dialog (_frame, std_to_wx(m));
496         }
497
498         DOMFrame* _frame;
499 };
500
501
502 IMPLEMENT_APP (App)