1e383b39a5ecdafc516532308c85fc0638f4acf0
[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/id.h"
26 #include "wx/job_manager_view.h"
27 #include "wx/servers_list_dialog.h"
28 #include "wx/wx_ptr.h"
29 #include "wx/wx_signal_manager.h"
30 #include "wx/wx_util.h"
31 #include "lib/compose.hpp"
32 #include "lib/config.h"
33 #include "lib/dcpomatic_socket.h"
34 #include "lib/film.h"
35 #ifdef DCPOMATIC_GROK
36 #include "lib/grok/context.h"
37 #endif
38 #include "lib/job.h"
39 #include "lib/job_manager.h"
40 #include "lib/make_dcp.h"
41 #include "lib/transcode_job.h"
42 #include "lib/util.h"
43 #include "lib/version.h"
44 #include <dcp/filesystem.h>
45 #include <dcp/warnings.h>
46 LIBDCP_DISABLE_WARNINGS
47 #include <wx/aboutdlg.h>
48 #include <wx/cmdline.h>
49 #include <wx/dnd.h>
50 #include <wx/preferences.h>
51 #include <wx/splash.h>
52 #include <wx/stdpaths.h>
53 #include <wx/wx.h>
54 LIBDCP_ENABLE_WARNINGS
55 #include <iostream>
56 #include <set>
57
58
59 using std::cout;
60 using std::dynamic_pointer_cast;
61 using std::exception;
62 using std::list;
63 using std::make_shared;
64 using std::set;
65 using std::shared_ptr;
66 using std::string;
67 using boost::scoped_array;
68 using boost::thread;
69 #if BOOST_VERSION >= 106100
70 using namespace boost::placeholders;
71 #endif
72
73
74 static list<boost::filesystem::path> films_to_load;
75
76
77 enum {
78         ID_file_add_film = DCPOMATIC_MAIN_MENU,
79         ID_tools_encoding_servers,
80         ID_help_about
81 };
82
83
84 void
85 setup_menu (wxMenuBar* m)
86 {
87         auto file = new wxMenu;
88         file->Append (ID_file_add_film, _("&Add Film...\tCtrl-A"));
89 #ifdef DCPOMATIC_OSX
90         file->Append (wxID_EXIT, _("&Exit"));
91 #else
92         file->Append (wxID_EXIT, _("&Quit"));
93 #endif
94
95 #ifdef DCPOMATIC_OSX
96         file->Append(wxID_PREFERENCES, _("&Preferences...\tCtrl-,"));
97 #else
98         auto edit = new wxMenu;
99         edit->Append (wxID_PREFERENCES, _("&Preferences...\tCtrl-P"));
100 #endif
101
102         auto tools = new wxMenu;
103         tools->Append (ID_tools_encoding_servers, _("Encoding servers..."));
104
105         auto help = new wxMenu;
106         help->Append (ID_help_about, _("About"));
107
108         m->Append (file, _("&File"));
109 #ifndef DCPOMATIC_OSX
110         m->Append (edit, _("&Edit"));
111 #endif
112         m->Append (tools, _("&Tools"));
113         m->Append (help, _("&Help"));
114 }
115
116
117 class DOMFrame : public wxFrame
118 {
119 public:
120         enum class Tool {
121                 ADD,
122                 PAUSE
123         };
124
125         class DCPDropTarget : public wxFileDropTarget
126         {
127         public:
128                 DCPDropTarget(DOMFrame* owner)
129                         : _frame(owner)
130                 {}
131
132                 bool OnDropFiles(wxCoord, wxCoord, wxArrayString const& filenames) override
133                 {
134                         if (filenames.GetCount() == 1) {
135                                 /* Try to load a directory */
136                                 auto path = boost::filesystem::path(wx_to_std(filenames[0]));
137                                 if (dcp::filesystem::is_directory(path)) {
138                                         _frame->start_job(wx_to_std(filenames[0]));
139                                         return true;
140                                 }
141                         }
142
143                         return false;
144                 }
145
146         private:
147                 DOMFrame* _frame;
148         };
149
150         explicit DOMFrame (wxString const & title)
151                 : wxFrame (nullptr, -1, title)
152                 , _sizer (new wxBoxSizer(wxVERTICAL))
153         {
154                 auto bar = new wxMenuBar;
155                 setup_menu (bar);
156                 SetMenuBar (bar);
157
158                 Config::instance()->Changed.connect (boost::bind (&DOMFrame::config_changed, this, _1));
159
160                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_add_film, this),    ID_file_add_film);
161                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::file_quit, this),        wxID_EXIT);
162                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::edit_preferences, this), wxID_PREFERENCES);
163                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::tools_encoding_servers, this), ID_tools_encoding_servers);
164                 Bind (wxEVT_MENU, boost::bind (&DOMFrame::help_about, this),       ID_help_about);
165
166                 auto panel = new wxPanel (this);
167                 auto s = new wxBoxSizer (wxHORIZONTAL);
168                 s->Add (panel, 1, wxEXPAND);
169                 SetSizer (s);
170
171                 wxBitmap add(icon_path("add"), wxBITMAP_TYPE_PNG);
172                 wxBitmap pause(icon_path("pause"), wxBITMAP_TYPE_PNG);
173
174                 auto toolbar = new wxToolBar(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTB_HORIZONTAL);
175                 toolbar->SetMargins(4, 4);
176                 toolbar->SetToolBitmapSize(wxSize(32, 32));
177                 toolbar->AddTool(static_cast<int>(Tool::ADD), _("Add film"), add, _("Add film for conversion"));
178                 toolbar->AddCheckTool(static_cast<int>(Tool::PAUSE), _("Pause/resume"), pause, wxNullBitmap, _("Pause or resume conversion"));
179                 toolbar->Realize();
180                 _sizer->Add(toolbar, 0, wxALL, 6);
181
182                 toolbar->Bind(wxEVT_TOOL, bind(&DOMFrame::tool_clicked, this, _1));
183
184                 auto job_manager_view = new JobManagerView (panel, true);
185                 _sizer->Add (job_manager_view, 1, wxALL | wxEXPAND, 6);
186
187                 panel->SetSizer (_sizer);
188
189                 Bind (wxEVT_CLOSE_WINDOW, boost::bind(&DOMFrame::close, this, _1));
190                 Bind (wxEVT_SIZE, boost::bind(&DOMFrame::sized, this, _1));
191
192                 SetDropTarget(new DCPDropTarget(this));
193         }
194
195         void tool_clicked(wxCommandEvent& ev)
196         {
197                 switch (static_cast<Tool>(ev.GetId())) {
198                 case Tool::ADD:
199                         add_film();
200                         break;
201                 case Tool::PAUSE:
202                 {
203                         auto jm = JobManager::instance();
204                         if (jm->paused()) {
205                                 jm->resume();
206                         } else {
207                                 jm->pause();
208                         }
209                         break;
210                 }
211                 }
212         }
213
214         void start_job (boost::filesystem::path path)
215         {
216                 try {
217                         auto film = make_shared<Film>(path);
218                         film->read_metadata ();
219
220                         double total_required;
221                         double available;
222                         bool can_hard_link;
223
224                         film->should_be_enough_disk_space (total_required, available, can_hard_link);
225
226                         set<shared_ptr<const Film>> films;
227
228                         for (auto i: JobManager::instance()->get()) {
229                                 films.insert (i->film());
230                         }
231
232                         for (auto i: films) {
233                                 double progress = 0;
234                                 for (auto j: JobManager::instance()->get()) {
235                                         if (i == j->film() && dynamic_pointer_cast<TranscodeJob>(j)) {
236                                                 progress = j->progress().get_value_or(0);
237                                         }
238                                 }
239
240                                 double required;
241                                 i->should_be_enough_disk_space (required, available, can_hard_link);
242                                 total_required += (1 - progress) * required;
243                         }
244
245                         if ((total_required - available) > 1) {
246                                 if (!confirm_dialog (
247                                             this,
248                                             wxString::Format(
249                                                     _("The DCPs for this film and the films already in the queue will take up about %.1f GB.  The "
250                                                       "disks that you are using only have %.1f GB available.  Do you want to add this film to the queue anyway?"),
251                                                     total_required, available))) {
252                                         return;
253                                 }
254                         }
255
256                         make_dcp (film, TranscodeJob::ChangedBehaviour::STOP);
257                 } catch (std::exception& e) {
258                         auto p = std_to_wx (path.string ());
259                         auto b = p.ToUTF8 ();
260                         error_dialog (this, wxString::Format(_("Could not open film at %s"), p.data()), std_to_wx(e.what()));
261                 }
262         }
263
264 private:
265         void sized (wxSizeEvent& ev)
266         {
267                 _sizer->Layout ();
268                 ev.Skip ();
269         }
270
271         bool should_close ()
272         {
273                 if (!JobManager::instance()->work_to_do()) {
274                         return true;
275                 }
276
277                 auto d = make_wx<wxMessageDialog>(
278                         nullptr,
279                         _("There are unfinished jobs; are you sure you want to quit?"),
280                         _("Unfinished jobs"),
281                         wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION
282                         );
283
284                 return d->ShowModal() == wxID_YES;
285         }
286
287         void close (wxCloseEvent& ev)
288         {
289                 if (!should_close()) {
290                         ev.Veto ();
291                         return;
292                 }
293
294                 ev.Skip ();
295                 JobManager::drop ();
296         }
297
298         void file_add_film ()
299         {
300                 add_film ();
301         }
302
303         void file_quit ()
304         {
305                 if (should_close()) {
306                         Close (true);
307                 }
308         }
309
310         void edit_preferences ()
311         {
312                 if (!_config_dialog) {
313                         _config_dialog = create_full_config_dialog ();
314                 }
315                 _config_dialog->Show (this);
316         }
317
318         void tools_encoding_servers ()
319         {
320                 if (!_servers_list_dialog) {
321                         _servers_list_dialog = new ServersListDialog (this);
322                 }
323
324                 _servers_list_dialog->Show ();
325         }
326
327         void help_about ()
328         {
329                 auto d = make_wx<AboutDialog>(this);
330                 d->ShowModal ();
331         }
332
333         void add_film ()
334         {
335                 auto dialog = make_wx<wxDirDialog>(this, _("Select film to open"), wxStandardPaths::Get().GetDocumentsDir(), wxDEFAULT_DIALOG_STYLE | wxDD_DIR_MUST_EXIST);
336                 if (_last_parent) {
337                         dialog->SetPath(std_to_wx(_last_parent.get().string()));
338                 }
339
340                 int r;
341                 while (true) {
342                         r = dialog->ShowModal();
343                         if (r == wxID_OK && dialog->GetPath() == wxStandardPaths::Get().GetDocumentsDir()) {
344                                 error_dialog (this, _("You did not select a folder.  Make sure that you select a folder before clicking Open."));
345                         } else {
346                                 break;
347                         }
348                 }
349
350                 if (r == wxID_OK) {
351                         start_job(wx_to_std(dialog->GetPath()));
352                 }
353
354                 _last_parent = boost::filesystem::path(wx_to_std(dialog->GetPath())).parent_path();
355         }
356
357         void config_changed (Config::Property what)
358         {
359                 /* Instantly save any config changes when using the DCP-o-matic GUI */
360                 if (what == Config::CINEMAS) {
361                         try {
362                                 Config::instance()->write_cinemas();
363                         } catch (exception& e) {
364                                 error_dialog (
365                                         this,
366                                         wxString::Format(
367                                                 _("Could not write to cinemas file at %s.  Your changes have not been saved."),
368                                                 std_to_wx (Config::instance()->cinemas_file().string()).data()
369                                                 )
370                                         );
371                         }
372                 } else {
373                         try {
374                                 Config::instance()->write_config();
375                         } catch (exception& e) {
376                                 error_dialog (
377                                         this,
378                                         wxString::Format(
379                                                 _("Could not write to config file at %s.  Your changes have not been saved."),
380                                                 std_to_wx (Config::instance()->cinemas_file().string()).data()
381                                                 )
382                                         );
383                         }
384                 }
385
386 #ifdef DCPOMATIC_GROK
387                 if (what == Config::GROK) {
388                         setup_grok_library_path();
389                 }
390 #endif
391         }
392
393         boost::optional<boost::filesystem::path> _last_parent;
394         wxSizer* _sizer;
395         wxPreferencesEditor* _config_dialog = nullptr;
396         ServersListDialog* _servers_list_dialog = nullptr;
397 };
398
399
400 static const wxCmdLineEntryDesc command_line_description[] = {
401         { wxCMD_LINE_PARAM, 0, 0, "film to load", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_MULTIPLE | wxCMD_LINE_PARAM_OPTIONAL },
402         { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 }
403 };
404
405
406 class JobServer : public Server, public Signaller
407 {
408 public:
409         JobServer()
410                 : Server (BATCH_JOB_PORT)
411         {}
412
413         void handle (shared_ptr<Socket> socket) override
414         {
415                 try {
416                         auto const length = socket->read_uint32();
417                         if (length < 65536) {
418                                 scoped_array<char> buffer(new char[length]);
419                                 socket->read(reinterpret_cast<uint8_t*>(buffer.get()), length);
420                                 string s(buffer.get());
421                                 emit(boost::bind(boost::ref(StartJob), s));
422                                 socket->write (reinterpret_cast<uint8_t const *>("OK"), 3);
423                         }
424                 } catch (...) {
425
426                 }
427         }
428
429         boost::signals2::signal<void(std::string)> StartJob;
430 };
431
432
433 class App : public wxApp
434 {
435         bool OnInit () override
436         {
437                 wxInitAllImageHandlers ();
438
439                 SetAppName (_("DCP-o-matic Batch Converter"));
440                 is_batch_converter = true;
441
442                 Config::FailedToLoad.connect(boost::bind(&App::config_failed_to_load, this, _1));
443                 Config::Warning.connect (boost::bind(&App::config_warning, this, _1));
444
445                 auto splash = maybe_show_splash ();
446
447                 if (!wxApp::OnInit()) {
448                         return false;
449                 }
450
451 #ifdef DCPOMATIC_LINUX
452                 unsetenv ("UBUNTU_MENUPROXY");
453 #endif
454
455                 dcpomatic_setup_path_encoding ();
456
457                 /* Enable i18n; this will create a Config object
458                    to look for a force-configured language.  This Config
459                    object will be wrong, however, because dcpomatic_setup
460                    hasn't yet been called and there aren't any filters etc.
461                    set up yet.
462                 */
463                 dcpomatic_setup_i18n ();
464
465                 /* Set things up, including filters etc.
466                    which will now be internationalised correctly.
467                 */
468                 dcpomatic_setup ();
469
470                 /* Force the configuration to be re-loaded correctly next
471                    time it is needed.
472                 */
473                 Config::drop ();
474
475                 _frame = new DOMFrame (_("DCP-o-matic Batch Converter"));
476                 SetTopWindow (_frame);
477                 _frame->Maximize ();
478                 if (splash) {
479                         splash->Destroy ();
480                 }
481                 _frame->Show ();
482
483                 try {
484                         auto server = new JobServer();
485                         server->StartJob.connect(bind(&DOMFrame::start_job, _frame, _1));
486                         new thread (boost::bind (&JobServer::run, server));
487                 } catch (boost::system::system_error& e) {
488                         error_dialog(_frame, _("Could not listen for new batch jobs.  Perhaps another instance of the DCP-o-matic Batch Converter is running."));
489                 }
490
491                 signal_manager = new wxSignalManager (this);
492                 this->Bind (wxEVT_IDLE, boost::bind (&App::idle, this));
493
494                 shared_ptr<Film> film;
495                 for (auto i: films_to_load) {
496                         if (dcp::filesystem::is_directory(i)) {
497                                 try {
498                                         film = make_shared<Film>(i);
499                                         film->read_metadata ();
500                                         make_dcp (film, TranscodeJob::ChangedBehaviour::EXAMINE_THEN_STOP);
501                                 } catch (exception& e) {
502                                         error_dialog (
503                                                 0,
504                                                 std_to_wx(String::compose(wx_to_std(_("Could not load film %1")), i.string())),
505                                                 std_to_wx(e.what())
506                                                 );
507                                 }
508                         }
509                 }
510
511 #ifdef DCPOMATIC_GROK
512                 grk_plugin::setMessengerLogger(new grk_plugin::GrokLogger("[GROK] "));
513                 setup_grok_library_path();
514 #endif
515
516                 return true;
517         }
518
519         void idle ()
520         {
521                 signal_manager->ui_idle ();
522         }
523
524         void OnInitCmdLine (wxCmdLineParser& parser) override
525         {
526                 parser.SetDesc (command_line_description);
527                 parser.SetSwitchChars (wxT ("-"));
528         }
529
530         bool OnCmdLineParsed (wxCmdLineParser& parser) override
531         {
532                 for (size_t i = 0; i < parser.GetParamCount(); ++i) {
533                         films_to_load.push_back (wx_to_std(parser.GetParam(i)));
534                 }
535
536                 return true;
537         }
538
539         void config_failed_to_load(Config::LoadFailure what)
540         {
541                 report_config_load_failure(_frame, what);
542         }
543
544         void config_warning (string m)
545         {
546                 message_dialog (_frame, std_to_wx(m));
547         }
548
549         DOMFrame* _frame;
550 };
551
552
553 IMPLEMENT_APP (App)