2 * Copyright (c) 2019, De Rais <derais@cock.li>
4 * Permission to use, copy, modify, and/or distribute this software for
5 * any purpose with or without fee is hereby granted, provided that the
6 * above copyright notice and this permission notice appear in all
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
10 * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
11 * WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
12 * AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
13 * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
14 * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
15 * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
16 * PERFORMANCE OF THIS SOFTWARE.
23 #include <yajl/yajl_tree.h>
28 #define xmin(a, b) ((a) < (b) ? (a) : (b))
30 /* For state while in all the yajl callbacks */
34 struct cytube_message
*m
;
37 /* Various paths for accessing JSON */
38 const char *changemedia_id_path
[] = { "id", 0 };
39 const char *changemedia_title_path
[] = { "title", 0 };
40 const char *changemedia_runtime_path
[] = { "duration", 0 };
41 const char *changemedia_seconds_path
[] = { "seconds", 0 };
42 const char *changemedia_type_path
[] = { "type", 0 };
43 const char *changemedia_currenttime_path
[] = { "currentTime", 0 };
44 const char *changemedia_paused_path
[] = { "paused", 0 };
45 const char *chatmsg_msg_path
[] = { "msg", 0 };
46 const char *chatmsg_username_path
[] = { "username", 0 };
47 const char *movemedia_from_path
[] = { "from", 0 };
48 const char *movemedia_after_path
[] = { "after", 0 };
49 const char *playlist_id_path
[] = { "media", "id", 0 };
50 const char *playlist_runtime_path
[] = { "media", "duration", 0 };
51 const char *playlist_title_path
[] = { "media", "title", 0 };
52 const char *playlist_type_path
[] = { "media", "type", 0 };
53 const char *playlist_uid_path
[] = { "uid", 0 };
54 const char *queue_id_path
[] = { "item", "media", "id", 0 };
55 const char *queue_runtime_path
[] = { "item", "media", "duration", 0 };
56 const char *queue_title_path
[] = { "item", "media", "title", 0 };
57 const char *queue_type_path
[] = { "item", "media", "type", 0 };
58 const char *queue_uid_path
[] = { "item", "uid", 0 };
59 const char *queue_after_path
[] = { "after", 0 };
62 static char * make_uri(const char *type
, const char *id
);
64 /* Add a message to the end of s's messages, maybe rolling over */
65 #define CHAT_LOG_LEN 70
67 append_chat_entry(struct state
volatile *s
, const char *username
, const
68 char *msg
, int special
)
71 size_t mlen
= msg
? strlen(msg
) : 0;
73 if (s
->chat_log
.msgs_len
>= CHAT_LOG_LEN
) {
74 /* Just slide them over */
75 char *old_user
= s
->chat_log
.msgs
[0].user
;
76 char *old_text
= s
->chat_log
.msgs
[0].text
;
79 memmove(s
->chat_log
.msgs
, s
->chat_log
.msgs
+ 1, (CHAT_LOG_LEN
-
81 sizeof(*s
->chat_log
.msgs
));
82 s
->chat_log
.msgs
[CHAT_LOG_LEN
- 1] = (struct chat_msg
) { /* */
83 .user
= strdup(username
), .text
= strdup(msg
),
86 duptext
= s
->chat_log
.msgs
[CHAT_LOG_LEN
- 1].text
;
87 mlen
= new_text
? strlen(new_text
) : 0;
91 size_t new_msgs_len
= xmin(s
->chat_log
.msgs_len
+ 1,
93 struct chat_msg
*new_msgs
= calloc(sizeof(*new_msgs
),
95 struct chat_msg
*old_msgs
= s
->chat_log
.msgs
;
96 size_t k
= new_msgs_len
- 1;
102 new_msgs
[k
] = (struct chat_msg
) { /* */
103 .user
= strdup(username
), .text
= strdup(msg
),
106 duptext
= new_msgs
[k
].text
;
107 memcpy(new_msgs
, old_msgs
, s
->chat_log
.msgs_len
*
108 sizeof(*s
->chat_log
.msgs
));
109 s
->chat_log
.msgs
= new_msgs
;
110 s
->chat_log
.msgs_len
++;
114 for (size_t j
= 0; duptext
&&
116 if ((unsigned char) duptext
[j
] < 0x20) {
126 /* Make sure a certain entry is in s's playlist */
128 ensure_playlist_entry(struct state
volatile *s
, const char *id
, const
129 char *title
, const char *runtime
, const char *type
, int
131 maybe_uid
, int *out_uid
)
133 char *uri
= make_uri(type
, id
);
136 size_t where_this_entry_is
= (size_t) -1;
137 size_t where_prev_entry_is
= (size_t) -1;
141 * What we'd like to do is use the UIDs to insert into the
142 * playlist. However, we might not have a UID. In that case,
143 * fall back to the uri.
147 * This is bogus -- they don't give us a UID, but
148 * they might move it around later based on the
149 * UID, etc. Fixing this properly takes way too
150 * much computation, so let's just give this a fake
151 * UID. If the playlist actually matters, the next
152 * playlist ping will pick it up correctly within
153 * 2 minutes. If the playlist doesn't matter, then
154 * it's probably hidden and we'll just get a list
155 * of currently-playing stuff.
157 for (size_t k
= 0; k
< s
->playlist
.entries_len
; ++k
) {
158 struct playlist_entry
*e
= &s
->playlist
.entries
[k
];
162 if (!strcmp(e
->uri
, uri
)) {
175 * Now, just try and stick this thing in place. If there's
176 * already an entry with this UID, we need to updated it
177 * and move it to the right place (if maybe_after_uid is
180 for (size_t k
= 0; k
< s
->playlist
.entries_len
; ++k
) {
181 struct playlist_entry
*e
= &s
->playlist
.entries
[k
];
184 where_this_entry_is
= k
;
187 if (e
->uid
== maybe_after_uid
) {
188 where_prev_entry_is
= k
;
193 if (where_prev_entry_is
== (size_t) -1 &&
194 s
->playlist
.entries_len
> 0 &&
195 where_this_entry_is
!= 0) {
196 where_prev_entry_is
= s
->playlist
.entries_len
- 1;
199 if (where_this_entry_is
== (size_t) -1) {
201 /* We have no spot for this entry. So insert it */
203 size_t new_size
= (s
->playlist
.entries_len
+ 1) *
204 sizeof *(s
->playlist
.entries
);
205 struct playlist_entry
*new_e
= 0;
207 if (s
->playlist
.entries_len
> ((size_t) -1) >> 2) {
208 ERROR_MESSAGE("overflow");
212 if (!(newmem
= realloc(s
->playlist
.entries
, new_size
))) {
213 PERROR_MESSAGE("realloc");
217 s
->playlist
.entries
= newmem
;
218 s
->playlist
.entries_len
= s
->playlist
.entries_len
+ 1;
219 new_e
= &(s
->playlist
.entries
[s
->playlist
.entries_len
- 1]);
220 new_e
->title
= strdup(title
);
221 duptitle
= new_e
->title
;
222 new_e
->title_width
= strwidth(title
);
223 new_e
->runtime
= strdup(runtime
);
224 new_e
->uri
= strdup(uri
);
226 where_this_entry_is
= s
->playlist
.entries_len
- 1;
229 * We actually have a spot for this entry. Just
232 struct playlist_entry
*e
=
233 &s
->playlist
.entries
[where_this_entry_is
];
236 e
->title
= strdup(title
);
238 e
->title_width
= strwidth(title
);
240 e
->runtime
= strdup(runtime
);
242 e
->uri
= strdup(uri
);
247 * At this point, s->p.e[where_this_entry_is] actually holds
248 * the data we want. The only problem imight be that it
249 * might not be directly after where_prev_entry_is.
251 if (maybe_after_uid
< 0 ||
252 where_prev_entry_is
== (size_t) -1 ||
253 where_prev_entry_is
+ 1 == where_this_entry_is
) {
257 /* Now we actually need to rearrange things, sadly. */
258 if (where_prev_entry_is
+ 1 < where_this_entry_is
) {
259 struct playlist_entry temp_e
= (struct playlist_entry
) { 0 };
260 size_t to_move_num
= where_this_entry_is
-
261 (where_prev_entry_is
+ 1);
262 size_t to_move_len
= to_move_num
* sizeof *s
->playlist
.entries
;
264 memcpy(&temp_e
, &(s
->playlist
.entries
[where_this_entry_is
]),
266 memmove(&(s
->playlist
.entries
[where_prev_entry_is
+ 2]),
267 &(s
->playlist
.entries
[where_prev_entry_is
+ 1]),
269 memcpy(&(s
->playlist
.entries
[where_prev_entry_is
+ 1]), &temp_e
,
272 struct playlist_entry temp_e
= (struct playlist_entry
) { 0 };
273 size_t to_move_num
= where_prev_entry_is
- where_this_entry_is
;
274 size_t to_move_len
= to_move_num
* sizeof *s
->playlist
.entries
;
276 memcpy(&temp_e
, &(s
->playlist
.entries
[where_this_entry_is
]),
278 memmove(&(s
->playlist
.entries
[where_this_entry_is
]),
279 &(s
->playlist
.entries
[where_this_entry_is
+ 1]),
281 memcpy(&(s
->playlist
.entries
[where_prev_entry_is
]), &temp_e
,
287 for (size_t k
= 0; duptitle
[k
]; ++k
) {
288 if ((unsigned char) duptitle
[k
] < 0x20) {
300 /* Free all memory associated with a chat_log */
302 clean_chat_log(struct chat_log
volatile *c
)
304 for (size_t k
= 0; k
< c
->msgs_len
; ++k
) {
305 free(c
->msgs
[k
].user
);
306 free(c
->msgs
[k
].text
);
314 /* Free all memory associated with a playlist */
316 clean_playlist(struct playlist
volatile *p
)
318 for (size_t k
= 0; k
< p
->entries_len
; ++k
) {
319 struct playlist_entry
*e
= &p
->entries
[k
];
331 /* yajl_tree_get, returning strings */
333 get_string_from(yajl_val root
, const char **path
)
335 yajl_val v
= yajl_tree_get(root
, path
, yajl_t_string
);
337 return YAJL_GET_STRING(v
);
340 /* yajl_tree_get, returning ints */
342 get_integer_from(yajl_val root
, const char **path
)
344 yajl_val v
= yajl_tree_get(root
, path
, yajl_t_number
);
346 if (YAJL_IS_INTEGER(v
)) {
347 return YAJL_GET_INTEGER(v
);
353 /* yajl_tree_get, returning floating-point */
355 get_double_from(yajl_val root
, const char **path
)
357 yajl_val v
= yajl_tree_get(root
, path
, yajl_t_number
);
359 if (YAJL_IS_DOUBLE(v
)) {
360 return YAJL_GET_DOUBLE(v
);
366 /* "yt", "9NACc7DBRh0" -> {an actual URI} */
368 make_uri(const char *type
, const char *id
)
370 if (!strcmp(type
, "yt")) {
371 return aprintf("https://youtube.com/watch?v=%s", id
);
372 } else if (!strcmp(type
, "vi")) {
373 return aprintf("https://vimeo.com/%s", id
);
374 } else if (!strcmp(type
, "dm")) {
375 return aprintf("https://dailymotion.com/video/%s", id
);
376 } else if (!strcmp(type
, "sc")) {
378 } else if (!strcmp(type
, "li")) {
379 return aprintf("https://livestream.com/%s", id
);
380 } else if (!strcmp(type
, "tw")) {
381 return aprintf("https://twitch.tv/%s", id
);
382 } else if (!strcmp(type
, "rt")) {
384 } else if (!strcmp(type
, "im")) {
385 return aprintf("https://imgur.com/a/%s", id
);
386 } else if (!strcmp(type
, "us")) {
387 return aprintf("https://ustream.tv/channel/%s", id
);
388 } else if (!strcmp(type
, "gd")) {
389 return aprintf("https://docs.google.com/file/d/%s", id
);
390 } else if (!strcmp(type
, "fi")) {
392 } else if (!strcmp(type
, "hb")) {
393 return aprintf("https://www.smashcast.tv/%s", id
);
394 } else if (!strcmp(type
, "hl")) {
396 } else if (!strcmp(type
, "sb")) {
397 return aprintf("https://www.streamable.com/%s", id
);
398 } else if (!strcmp(type
, "tc")) {
399 return aprintf("https://clips.twitch.tv/%s", id
);
400 } else if (!strcmp(type
, "cm")) {
407 /* Make valgrind happy */
409 state_clean(struct state
volatile *s
)
412 * Note this function only affects the "application state"
413 * and "connection info" parts of state. The UI stuff is
416 clean_playlist(&s
->playlist
);
417 clean_chat_log(&s
->chat_log
);
418 free(s
->socket_host
);
420 *s
= (struct state
) { 0 };
423 /* Handle a message. */
425 state_handle(struct state
volatile *s
, const char *msg
, size_t len
)
428 * The boilerplate for passing errors back out of the LWS
429 * thread isn't worth it, so just do nothing on parse errors.
432 char eb
[1024] = { 0 };
433 char *null_terminated
= 0;
434 const char *command
= 0;
438 * YAJL has some stupid memory leaks when seeing invalid
439 * JSON. Pull request #168 is one, there's another in string
440 * handling, etc. Let's try to block a few of them.
442 if (msg
[len
- 1] != ']') {
446 if (!(null_terminated
= malloc(len
+ 1))) {
450 memcpy(null_terminated
, msg
, len
);
451 null_terminated
[len
] = 0;
452 tree
= yajl_tree_parse(null_terminated
, eb
, 1024);
454 if ((ret
= -!!eb
[0])) {
459 !YAJL_IS_ARRAY(tree
) ||
460 YAJL_GET_ARRAY(tree
)->len
< 1 ||
461 !((command
= YAJL_GET_STRING(YAJL_GET_ARRAY(tree
)->values
[0])))) {
465 if (!strcmp(command
, "setPermissions")) {
466 if (!s
->playlist
.entries_len
) {
467 s
->must_ask_for_playlist
= 1;
469 } else if (!strcmp(command
, "changeMedia") &&
470 YAJL_GET_ARRAY(tree
)->len
>= 2) {
472 yajl_val e
= YAJL_GET_ARRAY(tree
)->values
[1];
473 char *id
= get_string_from(e
, changemedia_id_path
);
474 char *title
= get_string_from(e
, changemedia_title_path
);
475 char *runtime
= get_string_from(e
, changemedia_runtime_path
);
476 char *type
= get_string_from(e
, changemedia_type_path
);
477 int runtime_s
= get_integer_from(e
, changemedia_seconds_path
);
478 double currenttime
= get_double_from(e
,
479 changemedia_currenttime_path
);
480 char *paused
= get_string_from(e
, changemedia_paused_path
);
482 s
->playlist
.paused
= (paused
&&
483 !strcmp(paused
, "true"));
484 s
->playlist
.current_playtime
= (int) currenttime
;
485 s
->playlist
.current_length
= runtime_s
;
486 s
->playlist
.zero_clocktime
= time(0);
487 s
->playlist
.zero_playtime
= s
->playlist
.current_playtime
;
490 ensure_playlist_entry(s
, id
, title
, runtime
, type
, -1, -1,
492 s
->playlist
.current_playing_uid
= uid
;
493 s
->dirty_playlist
= 1;
494 } else if (!strcmp(command
, "moveMedia") &&
495 YAJL_GET_ARRAY(tree
)->len
>= 2) {
496 yajl_val e
= YAJL_GET_ARRAY(tree
)->values
[1];
497 char *from
= get_string_from(e
, movemedia_from_path
);
498 char *after
= get_string_from(e
, movemedia_after_path
);
500 /* TODO: handle this */
501 LOG("moveMedia: \u00ab%s\u00bb", null_terminated
);
502 LOG("from = \u00ab%s\u00bb", from
);
503 LOG("after = \u00ab%s\u00bb", after
);
504 } else if (!strcmp(command
, "playlist") &&
505 YAJL_GET_ARRAY(tree
)->len
>= 2 &&
506 YAJL_IS_ARRAY(YAJL_GET_ARRAY(tree
)->values
[1])) {
508 size_t these_len
= YAJL_GET_ARRAY(YAJL_GET_ARRAY(
509 tree
)->values
[1])->len
;
511 for (size_t k
= 0; k
< these_len
; ++k
) {
512 yajl_val e
= YAJL_GET_ARRAY(YAJL_GET_ARRAY(
515 char *id
= get_string_from(e
, playlist_id_path
);
516 char *title
= get_string_from(e
, playlist_title_path
);
517 char *runtime
= get_string_from(e
,
518 playlist_runtime_path
);
519 char *type
= get_string_from(e
, playlist_type_path
);
520 int uid
= get_integer_from(e
, playlist_uid_path
);
522 ensure_playlist_entry(s
, id
, title
, runtime
, type
,
523 last_uid
, uid
, &uid
);
528 s
->dirty_playlist
= 1;
529 } else if (!strcmp(command
, "chatMsg") &&
530 YAJL_GET_ARRAY(tree
)->len
>= 2) {
531 yajl_val e
= YAJL_GET_ARRAY(tree
)->values
[1];
532 char *username
= get_string_from(e
, chatmsg_username_path
);
533 char *msg
= get_string_from(e
, chatmsg_msg_path
);
535 append_chat_entry(s
, username
, msg
, 0);
537 if (!s
->uri_or_chat
) {
540 } else if (!strcmp(command
, "queue") &&
541 YAJL_GET_ARRAY(tree
)->len
>= 2) {
542 yajl_val e
= YAJL_GET_ARRAY(tree
)->values
[1];
543 char *id
= get_string_from(e
, queue_id_path
);
544 char *title
= get_string_from(e
, queue_title_path
);
545 char *runtime
= get_string_from(e
, queue_runtime_path
);
546 char *type
= get_string_from(e
, queue_type_path
);
547 int uid
= get_integer_from(e
, queue_uid_path
);
548 int after
= get_integer_from(e
, queue_after_path
);
550 ensure_playlist_entry(s
, id
, title
, runtime
, type
, after
, uid
,
552 s
->dirty_playlist
= 1;
556 yajl_tree_free(tree
);
557 free(null_terminated
);