summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--.gitmodules3
-rw-r--r--CMakeLists.txt22
-rw-r--r--cmake/FindFFMPEG.cmake144
-rw-r--r--sap.cpp473
m---------vendor/rawr-ebooks0
5 files changed, 642 insertions, 0 deletions
diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a58f64c --- /dev/null +++ b/.gitmodules
@@ -0,0 +1,3 @@
1[submodule "vendor/rawr-ebooks"]
2 path = vendor/rawr-ebooks
3 url = https://github.com/hatkirby/rawr-ebooks
diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..70a9572 --- /dev/null +++ b/CMakeLists.txt
@@ -0,0 +1,22 @@
1cmake_minimum_required (VERSION 3.1)
2project (sap)
3
4set(CMAKE_BUILD_TYPE Debug)
5
6set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
7
8find_package(FFMPEG REQUIRED)
9include_directories(${FFMPEG_INCLUDE_DIRS})
10
11find_package(PkgConfig)
12pkg_check_modules(GraphicsMagick GraphicsMagick++ REQUIRED)
13include_directories(${GraphicsMagick_INCLUDE_DIRS})
14link_directories(${GraphicsMagick_LIBRARY_DIRS})
15
16add_subdirectory(vendor/rawr-ebooks EXCLUDE_FROM_ALL)
17include_directories(vendor/rawr-ebooks vendor/rawr-ebooks/vendor/libtwittercpp/src vendor/rawr-ebooks/vendor/yaml-cpp/include)
18
19add_executable(sap sap.cpp)
20set_property(TARGET sap PROPERTY CXX_STANDARD 11)
21set_property(TARGET sap PROPERTY CXX_STANDARD_REQUIRED ON)
22target_link_libraries(sap rawr twitter++ ${FFMPEG_LIBRARIES} ${GraphicsMagick_LIBRARIES} yaml-cpp)
diff --git a/cmake/FindFFMPEG.cmake b/cmake/FindFFMPEG.cmake new file mode 100644 index 0000000..d7f972e --- /dev/null +++ b/cmake/FindFFMPEG.cmake
@@ -0,0 +1,144 @@
1# - Try to find FFMPEG
2# Once done this will define
3# FFMPEG_FOUND - System has FFMPEG
4# FFMPEG_INCLUDE_DIRS - The FFMPEG include directories
5# FFMPEG_LIBRARIES - The libraries needed to use FFMPEG
6# FFMPEG_LIBRARY_DIRS - The directory to find FFMPEG libraries
7#
8# written by Roy Shilkrot 2013 http://www.morethantechnical.com/
9#
10
11find_package(PkgConfig)
12
13
14MACRO(FFMPEG_FIND varname shortname headername)
15
16 IF(NOT WIN32)
17 PKG_CHECK_MODULES(PC_${varname} ${shortname})
18
19 FIND_PATH(${varname}_INCLUDE_DIR "${shortname}/${headername}"
20 HINTS ${PC_${varname}_INCLUDEDIR} ${PC_${varname}_INCLUDE_DIRS}
21 NO_DEFAULT_PATH
22 )
23 ELSE()
24 FIND_PATH(${varname}_INCLUDE_DIR "${shortname}/${headername}")
25 ENDIF()
26
27 IF(${varname}_INCLUDE_DIR STREQUAL "${varname}_INCLUDE_DIR-NOTFOUND")
28 message(STATUS "look for newer strcture")
29 IF(NOT WIN32)
30 PKG_CHECK_MODULES(PC_${varname} "lib${shortname}")
31
32 FIND_PATH(${varname}_INCLUDE_DIR "lib${shortname}/${headername}"
33 HINTS ${PC_${varname}_INCLUDEDIR} ${PC_${varname}_INCLUDE_DIRS}
34 NO_DEFAULT_PATH
35 )
36 ELSE()
37 FIND_PATH(${varname}_INCLUDE_DIR "lib${shortname}/${headername}")
38 IF(${${varname}_INCLUDE_DIR} STREQUAL "${varname}_INCLUDE_DIR-NOTFOUND")
39 #Desperate times call for desperate measures
40 MESSAGE(STATUS "globbing...")
41 FILE(GLOB_RECURSE ${varname}_INCLUDE_DIR "/ffmpeg*/${headername}")
42 MESSAGE(STATUS "found: ${${varname}_INCLUDE_DIR}")
43 IF(${varname}_INCLUDE_DIR)
44 GET_FILENAME_COMPONENT(${varname}_INCLUDE_DIR "${${varname}_INCLUDE_DIR}" PATH)
45 GET_FILENAME_COMPONENT(${varname}_INCLUDE_DIR "${${varname}_INCLUDE_DIR}" PATH)
46 ELSE()
47 SET(${varname}_INCLUDE_DIR "${varname}_INCLUDE_DIR-NOTFOUND")
48 ENDIF()
49 ENDIF()
50 ENDIF()
51 ENDIF()
52
53
54 IF(${${varname}_INCLUDE_DIR} STREQUAL "${varname}_INCLUDE_DIR-NOTFOUND")
55 MESSAGE(STATUS "Can't find includes for ${shortname}...")
56 ELSE()
57 MESSAGE(STATUS "Found ${shortname} include dirs: ${${varname}_INCLUDE_DIR}")
58
59# GET_DIRECTORY_PROPERTY(FFMPEG_PARENT DIRECTORY ${${varname}_INCLUDE_DIR} PARENT_DIRECTORY)
60 GET_FILENAME_COMPONENT(FFMPEG_PARENT ${${varname}_INCLUDE_DIR} PATH)
61 MESSAGE(STATUS "Using FFMpeg dir parent as hint: ${FFMPEG_PARENT}")
62
63 IF(NOT WIN32)
64 FIND_LIBRARY(${varname}_LIBRARIES NAMES ${shortname}
65 HINTS ${PC_${varname}_LIBDIR} ${PC_${varname}_LIBRARY_DIR} ${FFMPEG_PARENT})
66 ELSE()
67# FIND_PATH(${varname}_LIBRARIES "${shortname}.dll.a" HINTS ${FFMPEG_PARENT})
68 FILE(GLOB_RECURSE ${varname}_LIBRARIES "${FFMPEG_PARENT}/*${shortname}.lib")
69 # GLOBing is very bad... but windows sux, this is the only thing that works
70 ENDIF()
71
72 IF(${varname}_LIBRARIES STREQUAL "${varname}_LIBRARIES-NOTFOUND")
73 MESSAGE(STATUS "look for newer structure for library")
74 FIND_LIBRARY(${varname}_LIBRARIES NAMES lib${shortname}
75 HINTS ${PC_${varname}_LIBDIR} ${PC_${varname}_LIBRARY_DIR} ${FFMPEG_PARENT})
76 ENDIF()
77
78
79 IF(${varname}_LIBRARIES STREQUAL "${varname}_LIBRARIES-NOTFOUND")
80 MESSAGE(STATUS "Can't find lib for ${shortname}...")
81 ELSE()
82 MESSAGE(STATUS "Found ${shortname} libs: ${${varname}_LIBRARIES}")
83 ENDIF()
84
85
86 IF(NOT ${varname}_INCLUDE_DIR STREQUAL "${varname}_INCLUDE_DIR-NOTFOUND"
87 AND NOT ${varname}_LIBRARIES STREQUAL ${varname}_LIBRARIES-NOTFOUND)
88
89 MESSAGE(STATUS "found ${shortname}: include ${${varname}_INCLUDE_DIR} lib ${${varname}_LIBRARIES}")
90 SET(FFMPEG_${varname}_FOUND 1)
91 SET(FFMPEG_${varname}_INCLUDE_DIRS ${${varname}_INCLUDE_DIR})
92 SET(FFMPEG_${varname}_LIBS ${${varname}_LIBRARIES})
93 ELSE()
94 MESSAGE(STATUS "Can't find ${shortname}")
95 ENDIF()
96
97 ENDIF()
98
99ENDMACRO(FFMPEG_FIND)
100
101FFMPEG_FIND(LIBAVFORMAT avformat avformat.h)
102FFMPEG_FIND(LIBAVDEVICE avdevice avdevice.h)
103FFMPEG_FIND(LIBAVCODEC avcodec avcodec.h)
104FFMPEG_FIND(LIBAVUTIL avutil avutil.h)
105FFMPEG_FIND(LIBSWSCALE swscale swscale.h)
106
107SET(FFMPEG_FOUND "NO")
108IF (FFMPEG_LIBAVFORMAT_FOUND AND
109 FFMPEG_LIBAVDEVICE_FOUND AND
110 FFMPEG_LIBAVCODEC_FOUND AND
111 FFMPEG_LIBAVUTIL_FOUND AND
112 FFMPEG_LIBSWSCALE_FOUND
113 )
114
115
116 SET(FFMPEG_FOUND "YES")
117
118 SET(FFMPEG_INCLUDE_DIRS ${FFMPEG_LIBAVFORMAT_INCLUDE_DIRS})
119
120 SET(FFMPEG_LIBRARY_DIRS ${FFMPEG_LIBAVFORMAT_LIBRARY_DIRS})
121
122 SET(FFMPEG_LIBRARIES
123 ${FFMPEG_LIBAVFORMAT_LIBS}
124 ${FFMPEG_LIBAVDEVICE_LIBS}
125 ${FFMPEG_LIBAVCODEC_LIBS}
126 ${FFMPEG_LIBAVUTIL_LIBS}
127 ${FFMPEG_LIBSWSCALE_LIBS}
128 )
129
130ELSE ()
131
132 MESSAGE(STATUS "Could not find FFMPEG")
133
134ENDIF()
135
136message(STATUS ${FFMPEG_LIBRARIES} ${FFMPEG_LIBAVFORMAT_LIBRARIES})
137
138include(FindPackageHandleStandardArgs)
139# handle the QUIETLY and REQUIRED arguments and set FFMPEG_FOUND to TRUE
140# if all listed variables are TRUE
141find_package_handle_standard_args(FFMPEG DEFAULT_MSG
142 FFMPEG_LIBRARIES FFMPEG_INCLUDE_DIRS)
143
144mark_as_advanced(FFMPEG_INCLUDE_DIRS FFMPEG_LIBRARY_DIRS FFMPEG_LIBRARIES) \ No newline at end of file
diff --git a/sap.cpp b/sap.cpp new file mode 100644 index 0000000..4e2f66c --- /dev/null +++ b/sap.cpp
@@ -0,0 +1,473 @@
1extern "C" {
2#include <libavformat/avformat.h>
3#include <libavcodec/avcodec.h>
4#include <libavutil/imgutils.h>
5#include <libswscale/swscale.h>
6}
7
8#include <Magick++.h>
9#include <iostream>
10#include <rawr.h>
11#include <vector>
12#include <list>
13#include <fstream>
14#include <dirent.h>
15#include <sstream>
16#include <twitter.h>
17#include <yaml-cpp/yaml.h>
18#include <thread>
19#include <chrono>
20
21template <class Container>
22Container split(std::string input, std::string delimiter)
23{
24 Container result;
25
26 while (!input.empty())
27 {
28 int divider = input.find(delimiter);
29 if (divider == std::string::npos)
30 {
31 result.push_back(input);
32
33 input = "";
34 } else {
35 result.push_back(input.substr(0, divider));
36
37 input = input.substr(divider+delimiter.length());
38 }
39 }
40
41 return result;
42}
43
44template <class InputIterator>
45std::string implode(InputIterator first, InputIterator last, std::string delimiter)
46{
47 std::stringstream result;
48
49 for (InputIterator it = first; it != last; it++)
50 {
51 if (it != first)
52 {
53 result << delimiter;
54 }
55
56 result << *it;
57 }
58
59 return result.str();
60}
61
62int maxWordsInLine(std::vector<std::string> words, Magick::Image& textimage)
63{
64 int result = 0;
65
66 std::string curline = "";
67 Magick::TypeMetric metric;
68 for (auto word : words)
69 {
70 curline += " " + word;
71
72 textimage.fontTypeMetrics(curline, &metric);
73 if (metric.textWidth() > ((textimage.columns()/10)*9))
74 {
75 break;
76 } else {
77 result++;
78 }
79 }
80
81 return result;
82}
83
84int minHeightRequired(std::vector<std::string> words, Magick::Image& textimage)
85{
86 int result = 0;
87 while (!words.empty())
88 {
89 int prefixlen = maxWordsInLine(words, textimage);
90 std::string prefixText = implode(std::begin(words), std::begin(words) + prefixlen, " ");
91 std::vector<std::string> suffix(std::begin(words) + prefixlen, std::end(words));
92 Magick::TypeMetric metric;
93 textimage.fontTypeMetrics(prefixText, &metric);
94 result += metric.textHeight() + 5;
95
96 words = suffix;
97 }
98
99 return result - 5;
100}
101
102void layoutText(Magick::Image& textimage, Magick::Image& shadowimage, int width, int height, std::string text)
103{
104 DIR* fontdir;
105 struct dirent* ent;
106 if ((fontdir = opendir("fonts")) == nullptr)
107 {
108 std::cout << "Couldn't find fonts." << std::endl;
109 return;
110 }
111
112 std::vector<std::string> fonts;
113 while ((ent = readdir(fontdir)) != nullptr)
114 {
115 std::string dname(ent->d_name);
116 if ((dname.find(".otf") != std::string::npos) || (dname.find(".ttf") != std::string::npos))
117 {
118 fonts.push_back(dname);
119 }
120 }
121
122 closedir(fontdir);
123
124 textimage.fillColor(Magick::Color(MaxRGB, MaxRGB, MaxRGB, MaxRGB * 0.0));
125 shadowimage.fillColor(Magick::Color(0, 0, 0, 0));
126 shadowimage.strokeColor("black");
127
128 int minSize = 48;
129 int realMaxSize = 96;
130 int maxSize = realMaxSize;
131 Magick::TypeMetric metric;
132 std::string font;
133 auto words = split<std::vector<std::string>>(text, " ");
134 int top = 5;
135 int minWords = 1;
136 while (!words.empty())
137 {
138 if (font.empty() || (rand() % 10 == 0))
139 {
140 font = fonts[rand() % fonts.size()];
141 textimage.font("fonts/" + font);
142 shadowimage.font("fonts/" + font);
143 }
144
145 int size = rand() % (maxSize - minSize + 1) + minSize;
146 textimage.fontPointsize(size);
147 int maxWords = maxWordsInLine(words, textimage);
148 int touse;
149 if (minWords > maxWords)
150 {
151 touse = maxWords;
152 } else {
153 touse = rand() % (maxWords - minWords + 1) + minWords;
154 }
155 std::string prefixText = implode(std::begin(words), std::begin(words) + touse, " ");
156 std::vector<std::string> suffix(std::begin(words) + touse, std::end(words));
157 textimage.fontTypeMetrics(prefixText, &metric);
158
159 textimage.fontPointsize(minSize);
160 int lowpadding = minHeightRequired(suffix, textimage);
161 int freespace = height - 5 - top - lowpadding - metric.textHeight();
162 std::cout << "top of " << top << " with lowpad of " << lowpadding << " and textheight of " << metric.textHeight() << " with freespace=" << freespace << std::endl;
163 if (freespace < 0)
164 {
165 minWords = touse;
166
167 continue;
168 }
169
170 maxSize = realMaxSize;
171 minWords = 1;
172
173 int toppadding;
174 if (rand() % 2 == 0)
175 {
176 // Exponential distribution, biased toward top
177 toppadding = log(rand() % (int)exp(freespace + 1) + 1);
178 } else {
179 // Linear distribution, biased toward bottom
180 toppadding = rand() % (freespace + 1);
181 }
182
183 int leftx = rand() % (width - 10 - (int)metric.textWidth()) + 5;
184 std::cout << "printing at " << leftx << "," << (top + toppadding + metric.ascent()) << std::endl;
185 textimage.fontPointsize(size);
186 textimage.annotate(prefixText, Magick::Geometry(0, 0, leftx, top + toppadding + metric.ascent()));
187
188 shadowimage.fontPointsize(size);
189 shadowimage.strokeWidth(size / 10);
190 shadowimage.annotate(prefixText, Magick::Geometry(0, 0, leftx, top + toppadding + metric.ascent()));
191 //shadowimage.draw(Magick::DrawableRectangle(leftx - 5, top + toppadding, leftx + metric.textWidth() + 5, top + toppadding + metric.textHeight() + 10 + metric.descent()));
192
193 words = suffix;
194 top += toppadding + metric.textHeight();
195 }
196
197 Magick::PixelPacket* shadowpixels = shadowimage.getPixels(0, 0, width, height);
198 Magick::PixelPacket* textpixels = textimage.getPixels(0, 0, width, height);
199 for (int j=0; j<height; j++)
200 {
201 for (int i=0; i<width; i++)
202 {
203 int ind = j*width+i;
204 if (shadowpixels[ind].opacity != MaxRGB)
205 {
206 shadowpixels[ind].opacity = MaxRGB * 0.25;
207 }
208
209 if (textpixels[ind].opacity != MaxRGB)
210 {
211 //textpixels[ind].opacity = MaxRGB * 0.05;
212 }
213 }
214 }
215
216 shadowimage.syncPixels();
217 textimage.syncPixels();
218
219 shadowimage.blur(10.0, 20.0);
220 textimage.blur(0.0, 0.5);
221}
222
223static int open_codec_context(int *stream_idx, AVFormatContext *fmt_ctx, enum AVMediaType type)
224{
225 int ret, stream_index;
226 AVStream *st;
227 AVCodecContext *dec_ctx = NULL;
228 AVCodec *dec = NULL;
229 AVDictionary *opts = NULL;
230 ret = av_find_best_stream(fmt_ctx, type, -1, -1, NULL, 0);
231 if (ret < 0)
232 {
233 //fprintf(stderr, "Could not find %s stream in input file '%s'\n", av_get_media_type_string(type), src_filename);
234 return ret;
235 } else {
236 stream_index = ret;
237 st = fmt_ctx->streams[stream_index];
238
239 // find decoder for the stream
240 dec_ctx = st->codec;
241 dec = avcodec_find_decoder(dec_ctx->codec_id);
242 if (!dec)
243 {
244 fprintf(stderr, "Failed to find %s codec\n", av_get_media_type_string(type));
245 return AVERROR(EINVAL);
246 }
247
248 // Init the decoders, with or without reference counting
249 av_dict_set(&opts, "refcounted_frames", "0", 0);
250 if ((ret = avcodec_open2(dec_ctx, dec, &opts)) < 0)
251 {
252 fprintf(stderr, "Failed to open %s codec\n", av_get_media_type_string(type));
253 return ret;
254 }
255
256 *stream_idx = stream_index;
257 }
258
259 return 0;
260}
261
262int main(int argc, char** argv)
263{
264 srand(time(NULL));
265 rand(); rand(); rand(); rand();
266
267 Magick::InitializeMagick(nullptr);
268 av_register_all();
269
270 YAML::Node config = YAML::LoadFile("config.yml");
271
272 twitter::auth auth;
273 auth.setConsumerKey(config["consumer_key"].as<std::string>());
274 auth.setConsumerSecret(config["consumer_secret"].as<std::string>());
275 auth.setAccessKey(config["access_key"].as<std::string>());
276 auth.setAccessSecret(config["access_secret"].as<std::string>());
277
278 twitter::client client(auth);
279
280 std::ifstream infile("corpus1.txt");
281 std::string corpus;
282 std::string line;
283 while (getline(infile, line))
284 {
285 if (line.back() == '\r')
286 {
287 line.pop_back();
288 }
289
290 corpus += line + " ";
291 }
292
293 infile.close();
294
295 std::ifstream infile2("corpus2.txt");
296 std::string corpus2;
297 while (getline(infile2, line))
298 {
299 if (line.back() == '\r')
300 {
301 line.pop_back();
302 }
303
304 corpus2 += line + " ";
305 }
306
307 infile2.close();
308
309 rawr kgramstats;
310 kgramstats.addCorpus(corpus);
311 kgramstats.addCorpus(corpus2);
312 kgramstats.compile(5);
313 kgramstats.setMinCorpora(2);
314
315 DIR* videodir;
316 struct dirent* ent;
317 if ((videodir = opendir("videos")) == nullptr)
318 {
319 std::cout << "Couldn't find videos." << std::endl;
320 return -1;
321 }
322
323 std::vector<std::string> videos;
324 while ((ent = readdir(videodir)) != nullptr)
325 {
326 std::string dname(ent->d_name);
327 if (dname.find(".mp4") != std::string::npos)
328 {
329 videos.push_back(dname);
330 }
331 }
332
333 closedir(videodir);
334
335 for (;;)
336 {
337 std::string video = "videos/" + videos[rand() % videos.size()];
338 std::cout << "Opening " << video << std::endl;
339
340 AVFormatContext* format = nullptr;
341 if (avformat_open_input(&format, video.c_str(), nullptr, nullptr))
342 {
343 std::cout << "could not open file" << std::endl;
344 return 1;
345 }
346
347 if (avformat_find_stream_info(format, nullptr))
348 {
349 std::cout << "could not read stream" << std::endl;
350 return 5;
351 }
352
353 int video_stream_idx = -1;
354 if (open_codec_context(&video_stream_idx, format, AVMEDIA_TYPE_VIDEO))
355 {
356 std::cout << "could not open codec" << std::endl;
357 return 6;
358 }
359
360 AVStream* stream = format->streams[video_stream_idx];
361 AVCodecContext* codec = stream->codec;
362 int codecw = codec->width;
363 int codech = codec->height;
364
365 int64_t seek = (rand() % format->duration) * codec->time_base.num / codec->time_base.den;
366 std::cout << seek << std::endl;
367 if (av_seek_frame(format, video_stream_idx, seek, 0))
368 {
369 std::cout << "could not seek" << std::endl;
370 return 4;
371 }
372
373 AVPacket packet;
374 av_init_packet(&packet);
375
376 AVFrame* frame = av_frame_alloc();
377 AVFrame* converted = av_frame_alloc();
378
379 int buffer_size = av_image_get_buffer_size(AV_PIX_FMT_RGB24, codecw, codech, 1);
380 uint8_t* buffer = new uint8_t[buffer_size];
381
382 av_image_alloc(converted->data, converted->linesize, codecw, codech, AV_PIX_FMT_RGB24, 1);
383
384 for (;;)
385 {
386 if (av_read_frame(format, &packet))
387 {
388 std::cout << "could not read frame" << std::endl;
389 return 2;
390 }
391
392 if (packet.stream_index != video_stream_idx)
393 {
394 continue;
395 }
396
397 int got_pic;
398 if (avcodec_decode_video2(codec, frame, &got_pic, &packet) < 0)
399 {
400 std::cout << "could not decode frame" << std::endl;
401 return 7;
402 }
403
404 if (!got_pic)
405 {
406 continue;
407 }
408
409 if (packet.flags && AV_PKT_FLAG_KEY)
410 {
411 SwsContext* sws = sws_getContext(codecw, codech, codec->pix_fmt, codecw, codech, AV_PIX_FMT_RGB24, 0, nullptr, nullptr, 0);
412 sws_scale(sws, frame->data, frame->linesize, 0, codech, converted->data, converted->linesize);
413 sws_freeContext(sws);
414
415 av_image_copy_to_buffer(buffer, buffer_size, converted->data, converted->linesize, AV_PIX_FMT_RGB24, codecw, codech, 1);
416 av_frame_free(&frame);
417 av_frame_free(&converted);
418 av_packet_unref(&packet);
419 avcodec_close(codec);
420 avformat_close_input(&format);
421
422 int width = 1024;
423 int height = codech * width / codecw;
424
425 Magick::Image image;
426 image.read(codecw, codech, "RGB", Magick::CharPixel, buffer);
427 image.zoom(Magick::Geometry(width, height));
428
429 std::string action = kgramstats.randomSentence(rand() % 15 + 5);
430 Magick::Image textimage(Magick::Geometry(width, height), "transparent");
431 Magick::Image shadowimage(Magick::Geometry(width, height), "transparent");
432 layoutText(textimage, shadowimage, width, height, action);
433 image.composite(shadowimage, 0, 0, Magick::OverCompositeOp);
434 image.composite(textimage, 0, 0, Magick::OverCompositeOp);
435
436 image.magick("jpeg");
437
438 Magick::Blob outputimg;
439 image.write(&outputimg);
440
441 delete[] buffer;
442
443 std::cout << "Generated image." << std::endl << "Tweeting..." << std::endl;
444
445 long media_id;
446 twitter::response resp = client.uploadMedia("image/jpeg", (const char*) outputimg.data(), outputimg.length(), media_id);
447 if (resp != twitter::response::ok)
448 {
449 std::cout << "Twitter error while uploading image: " << resp << std::endl;
450
451 break;
452 }
453
454 twitter::tweet tw;
455 resp = client.updateStatus("", tw, twitter::tweet(), {media_id});
456 if (resp != twitter::response::ok)
457 {
458 std::cout << "Twitter error while tweeting: " << resp << std::endl;
459
460 break;
461 }
462
463 std::cout << "Done!" << std::endl << "Waiting..." << std::endl << std::endl;
464
465 break;
466 }
467 }
468
469 std::this_thread::sleep_for(std::chrono::hours(1));
470 }
471
472 return 0;
473}
diff --git a/vendor/rawr-ebooks b/vendor/rawr-ebooks new file mode 160000
Subproject 5ce05b81520d06a78165c5c5039007c9f29d4b2