Upload analytics.
[dcpomatic.git] / src / lib / analytics.cc
1 /*
2     Copyright (C) 2018 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 #include "analytics.h"
22 #include "exceptions.h"
23 #include "job.h"
24 #include "cross.h"
25 #include <dcp/raw_convert.h>
26 #include <dcp/util.h>
27 #include <libcxml/cxml.h>
28 #include <curl/curl.h>
29 #include <libxml++/libxml++.h>
30 #include <boost/filesystem.hpp>
31 #include <boost/algorithm/string.hpp>
32 #include <boost/foreach.hpp>
33 #include <iostream>
34
35 #include "i18n.h"
36
37 using std::string;
38 using std::map;
39 using dcp::raw_convert;
40 using boost::algorithm::trim;
41 using boost::shared_ptr;
42
43 Analytics* Analytics::_instance;
44 int const Analytics::_current_version = 1;
45
46 Event::Event ()
47 {
48         gettimeofday (&_time, 0);
49 }
50
51 Event::Event (cxml::ConstNodePtr node)
52 {
53         _time.tv_sec = node->number_child<int64_t>("Time");
54         _time.tv_usec = 0;
55         BOOST_FOREACH (cxml::ConstNodePtr i, node->node_children()) {
56                 set(i->name(), i->content());
57         }
58 }
59
60 void
61 Event::set (string k, string v)
62 {
63         _data[k] = v;
64 }
65
66 string
67 Event::get (string k) const
68 {
69         map<string, string>::const_iterator i = _data.find (k);
70         if (i == _data.end()) {
71                 return "";
72         }
73         return i->second;
74 }
75
76 void
77 Event::as_xml (xmlpp::Element* parent) const
78 {
79         /* It would be nice if this had timezone */
80         parent->add_child("Time")->add_child_text(raw_convert<string>(_time.tv_sec));
81         for (map<string, string>::const_iterator i = _data.begin(); i != _data.end(); ++i) {
82                 parent->add_child(i->first)->add_child_text(i->second);
83         }
84 }
85
86 string
87 Event::dump () const
88 {
89         string d;
90         d += raw_convert<string>(_time.tv_sec) + "\n";
91         for (map<string, string>::const_iterator i = _data.begin(); i != _data.end(); ++i) {
92                 d += i->first + ": " + i->second + "\n";
93         }
94         return d;
95 }
96
97 Analytics::Analytics ()
98         : _id (dcp::make_uuid())
99         , _thread (0)
100 {
101
102 }
103
104 Analytics::~Analytics ()
105 {
106         if (!_thread) {
107                 return;
108         }
109
110         _thread->interrupt();
111         if (_thread->joinable()) {
112                 try {
113                         _thread->join();
114                 } catch (...) {
115                         /* Too late to do anything about this */
116                 }
117         }
118
119         delete _thread;
120 }
121
122 void
123 Analytics::start ()
124 {
125         _thread = new boost::thread (boost::bind(&Analytics::thread, this));
126 #ifdef DCPOMATIC_LINUX
127         pthread_setname_np (_thread->native_handle(), "update-checker");
128 #endif
129 }
130
131 void
132 Analytics::thread ()
133 try
134 {
135         while (true) {
136
137                 {
138                         boost::mutex::scoped_lock lm (_mutex);
139                         if (_events.empty ()) {
140                                 continue;
141                         }
142
143                         CURL* curl = curl_easy_init ();
144                         if (!curl) {
145                                 continue;
146                         }
147
148                         curl_easy_setopt (curl, CURLOPT_URL, "https://dcpomatic.com/analytics");
149                         xmlpp::Document doc;
150                         xmlpp_document (doc);
151                         curl_easy_setopt (curl, CURLOPT_POST, 1);
152                         curl_easy_setopt (curl, CURLOPT_COPYPOSTFIELDS, doc.write_to_string().c_str());
153                         CURLcode res = curl_easy_perform (curl);
154                         if (res == CURLE_OK) {
155                                 _events.clear ();
156                         }
157                         curl_easy_cleanup (curl);
158                         write ();
159
160                 }
161
162                 dcpomatic_sleep (60);
163         }
164 }
165 catch (...) {
166         /* Never mind */
167 }
168
169 int
170 Analytics::successful_dcp_encodes () const
171 {
172         boost::mutex::scoped_lock lm (_mutex);
173         int n = 0;
174         BOOST_FOREACH (Event e, _events) {
175                 if (e.get("type") == "job_state" && e.get("json_name") == "transcode" && e.get("status") == "finished_ok") {
176                         ++n;
177                 }
178         }
179         return n;
180 }
181
182 void
183 Analytics::job_state_changed (shared_ptr<Job> job)
184 {
185         Event ev;
186         ev.set ("type", "job_state");
187         ev.set ("json_name", job->json_name());
188         ev.set ("sub_name", job->sub_name());
189         ev.set ("error-summary", job->error_summary());
190         ev.set ("error-details", job->error_details());
191         ev.set ("status", job->json_status());
192
193         {
194                 boost::mutex::scoped_lock lm (_mutex);
195                 _events.push_back (ev);
196                 write ();
197         }
198
199         if (successful_dcp_encodes() == 3) {
200                 emit (
201                         boost::bind(
202                                 boost::ref(Message),
203                                 _("Congratulations!"),
204                                 _(
205                                         "<h2>You have made 3 DCPs with DCP-o-matic!</h2>"
206                                         "<img width=\"20%\" src=\"memory:me.jpg\" align=\"center\">"
207                                         "<p>Hello. I'm Carl and I'm the "
208                                         "developer of DCP-o-matic. I work on it in my spare time (with the help "
209                                         "of a fine volunteer team of testers and translators) and I release it "
210                                         "as free software."
211
212                                         "<p>If you find DCP-o-matic useful, please consider a donation to the "
213                                         "project. Financial support will help me to spend more "
214                                         "time developing DCP-o-matic and making it better!"
215
216                                         "<p><ul>"
217                                         "<li><a href=\"https://dcpomatic.com/donate_amount?amount=40\">Go to Paypal to donate £40</a>"
218                                         "<li><a href=\"https://dcpomatic.com/donate_amount?amount=20\">Go to Paypal to donate £20</a>"
219                                         "<li><a href=\"https://dcpomatic.com/donate_amount?amount=10\">Go to Paypal to donate £10</a>"
220                                         "</ul>"
221
222                                         "<p>Thank you!"
223                                         )
224                                 )
225                         );
226         }
227 }
228
229 /** Must be called with a lock held on _mutex*/
230 void
231 Analytics::xmlpp_document (xmlpp::Document& doc) const
232 {
233         xmlpp::Element* root = doc.create_root_node ("Analytics");
234
235         root->add_child("Version")->add_child_text(raw_convert<string>(_current_version));
236         root->add_child("Id")->add_child_text(_id);
237         BOOST_FOREACH (Event e, _events) {
238                 e.as_xml (root->add_child("Event"));
239         }
240 }
241
242 /** Must be called with a lock held on _mutex */
243 void
244 Analytics::write () const
245 {
246         try {
247                 xmlpp::Document doc;
248                 xmlpp_document (doc);
249                 doc.write_to_file_formatted(path("analytics.xml").string());
250         } catch (xmlpp::exception& e) {
251                 string s = e.what ();
252                 trim (s);
253                 throw FileError (s, path("analytics.xml"));
254         }
255 }
256
257 void
258 Analytics::read ()
259 try
260 {
261         cxml::Document f ("Analytics");
262         f.read_file (path("analytics.xml"));
263         boost::mutex::scoped_lock lm (_mutex);
264         _id = f.string_child("Id");
265         BOOST_FOREACH (cxml::ConstNodePtr i, f.node_children("Event")) {
266                 _events.push_back (Event(i));
267         }
268 } catch (...) {
269         /* Never mind */
270 }
271
272 Analytics*
273 Analytics::instance ()
274 {
275         if (!_instance) {
276                 _instance = new Analytics();
277                 _instance->read();
278         }
279
280         return _instance;
281 }