diff options
53 files changed, 1323 insertions, 2 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c421d4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +desktop/icons/*_psd +extensions/chrome.crx +extensions/chrome.pem +extensions/chrome.zip +extensions/firefox.xpi +desktop/*.o +desktop/vkpc @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013 41P +Copyright (c) 2013 ch1p Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -1,4 +1,6 @@ -vkpc-linux +VK Player Controller ========== Application for Linux that allows you to control audio player on vk.com by media keys. + +Installation instructions: http://ch1p.com/vkpc/?linux diff --git a/desktop/Makefile b/desktop/Makefile new file mode 100644 index 0000000..49e6f7a --- /dev/null +++ b/desktop/Makefile @@ -0,0 +1,29 @@ +LIBS = gtk+-2.0 glib-2.0 cairo pango gdk-pixbuf-2.0 atk libwebsockets x11 +CC = gcc +CCFLAGS = -Wall -std=c99 -pthread `pkg-config --cflags --libs ${LIBS}` +LDFLAGS = -lm +BINARIES = vkpc + +all : vkpc + +vkpc : server.o grab.o vector.o main.o + ${CC} ${CCFLAGS} server.o grab.o vector.o main.o ${LDFLAGS} -o vkpc + +server.o : server.c + ${CC} ${CCFLAGS} -c server.c + +grab.o : grab.c + ${CC} ${CCFLAGS} -c grab.c + +vector.o : vector.c + ${CC} ${CCFLAGS} -c vector.c + +main.o : main.c + ${CC} ${CCFLAGS} -c main.c + +install: + cp vkpc /usr/bin + sh install_icons.sh + +clean: + rm -f $(BINARIES) *.o diff --git a/desktop/grab.c b/desktop/grab.c new file mode 100644 index 0000000..716b0c6 --- /dev/null +++ b/desktop/grab.c @@ -0,0 +1,51 @@ +#include <stdio.h> +#include <stdbool.h> +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <X11/XF86keysym.h> +#include "grab.h" + +static int error_handler(Display *dpy, XErrorEvent *err) { + fprintf(stderr, "Failed to grab key!\n"); + return 0; +} + +void grab_init(void (*handler)(enum HotkeyEvent e)) { + Display *dpy = XOpenDisplay(0); + Window root = DefaultRootWindow(dpy); + XEvent ev; + + struct Hotkey hotkeys[HOTKEYS_COUNT] = { + { HK_PAUSE, XKeysymToKeycode(dpy, XF86XK_AudioPause) }, + { HK_PLAY, XKeysymToKeycode(dpy, XF86XK_AudioPlay) }, + { HK_NEXT, XKeysymToKeycode(dpy, XF86XK_AudioNext) }, + { HK_PREV, XKeysymToKeycode(dpy, XF86XK_AudioPrev) } + }; + + XSetErrorHandler(error_handler); + + for (int i = 0; i < HOTKEYS_COUNT; i++) { + XGrabKey(dpy, hotkeys[i].keycode, 0, root, false, GrabModeAsync, GrabModeAsync); + } + + XSelectInput(dpy, root, KeyPressMask); + while (true) { + XNextEvent(dpy, &ev); + + switch (ev.type) { + case KeyPress: ; + for (int i = 0; i < HOTKEYS_COUNT; i++) { + if (ev.xkey.keycode == hotkeys[i].keycode) { + (*handler)(hotkeys[i].event); + break; + } + } + break; + + default: + break; + } + } + + XCloseDisplay(dpy); +} diff --git a/desktop/grab.h b/desktop/grab.h new file mode 100644 index 0000000..762f9d8 --- /dev/null +++ b/desktop/grab.h @@ -0,0 +1,15 @@ +#ifndef GRAB_H__ +#define GRAB_H__ + +enum HotkeyEvent { + HK_PREV, HK_NEXT, HK_PAUSE, HK_PLAY, + HOTKEYS_COUNT +}; +struct Hotkey { + enum HotkeyEvent event; + int keycode; +}; + +void grab_init(); + +#endif diff --git a/desktop/icons/Faenza-Radiance/apps/16/vkpc.png b/desktop/icons/Faenza-Radiance/apps/16/vkpc.png Binary files differnew file mode 100644 index 0000000..1eaf260 --- /dev/null +++ b/desktop/icons/Faenza-Radiance/apps/16/vkpc.png diff --git a/desktop/icons/Faenza-Radiance/apps/22/vkpc.png b/desktop/icons/Faenza-Radiance/apps/22/vkpc.png Binary files differnew file mode 100644 index 0000000..03ec012 --- /dev/null +++ b/desktop/icons/Faenza-Radiance/apps/22/vkpc.png diff --git a/desktop/icons/Faenza-Radiance/apps/24/vkpc.png b/desktop/icons/Faenza-Radiance/apps/24/vkpc.png Binary files differnew file mode 100644 index 0000000..ad1fc6c --- /dev/null +++ b/desktop/icons/Faenza-Radiance/apps/24/vkpc.png diff --git a/desktop/icons/Faenza-Radiance/apps/32/vkpc.png b/desktop/icons/Faenza-Radiance/apps/32/vkpc.png Binary files differnew file mode 100644 index 0000000..4107fc3 --- /dev/null +++ b/desktop/icons/Faenza-Radiance/apps/32/vkpc.png diff --git a/desktop/icons/Faenza-Radiance/apps/48/vkpc.png b/desktop/icons/Faenza-Radiance/apps/48/vkpc.png Binary files differnew file mode 100644 index 0000000..041f8a6 --- /dev/null +++ b/desktop/icons/Faenza-Radiance/apps/48/vkpc.png diff --git a/desktop/icons/Faenza-Radiance/apps/64/vkpc.png b/desktop/icons/Faenza-Radiance/apps/64/vkpc.png Binary files differnew file mode 100644 index 0000000..ba1980d --- /dev/null +++ b/desktop/icons/Faenza-Radiance/apps/64/vkpc.png diff --git a/desktop/icons/Faenza-Radiance/apps/96/vkpc.png b/desktop/icons/Faenza-Radiance/apps/96/vkpc.png Binary files differnew file mode 100644 index 0000000..853b46d --- /dev/null +++ b/desktop/icons/Faenza-Radiance/apps/96/vkpc.png diff --git a/desktop/icons/Faenza/apps/16/vkpc.png b/desktop/icons/Faenza/apps/16/vkpc.png Binary files differnew file mode 100644 index 0000000..29e3991 --- /dev/null +++ b/desktop/icons/Faenza/apps/16/vkpc.png diff --git a/desktop/icons/Faenza/apps/22/vkpc.png b/desktop/icons/Faenza/apps/22/vkpc.png Binary files differnew file mode 100644 index 0000000..73daaa8 --- /dev/null +++ b/desktop/icons/Faenza/apps/22/vkpc.png diff --git a/desktop/icons/Faenza/apps/24/vkpc.png b/desktop/icons/Faenza/apps/24/vkpc.png Binary files differnew file mode 100644 index 0000000..3f1ca5e --- /dev/null +++ b/desktop/icons/Faenza/apps/24/vkpc.png diff --git a/desktop/icons/Faenza/apps/32/vkpc.png b/desktop/icons/Faenza/apps/32/vkpc.png Binary files differnew file mode 100644 index 0000000..3502985 --- /dev/null +++ b/desktop/icons/Faenza/apps/32/vkpc.png diff --git a/desktop/icons/Faenza/apps/48/vkpc.png b/desktop/icons/Faenza/apps/48/vkpc.png Binary files differnew file mode 100644 index 0000000..3f9ffa5 --- /dev/null +++ b/desktop/icons/Faenza/apps/48/vkpc.png diff --git a/desktop/icons/Faenza/apps/64/vkpc.png b/desktop/icons/Faenza/apps/64/vkpc.png Binary files differnew file mode 100644 index 0000000..7278517 --- /dev/null +++ b/desktop/icons/Faenza/apps/64/vkpc.png diff --git a/desktop/icons/Faenza/apps/96/vkpc.png b/desktop/icons/Faenza/apps/96/vkpc.png Binary files differnew file mode 100644 index 0000000..d0e8b61 --- /dev/null +++ b/desktop/icons/Faenza/apps/96/vkpc.png diff --git a/desktop/icons/hicolor/128x128/apps/vkpc.png b/desktop/icons/hicolor/128x128/apps/vkpc.png Binary files differnew file mode 100644 index 0000000..7c5f49e --- /dev/null +++ b/desktop/icons/hicolor/128x128/apps/vkpc.png diff --git a/desktop/icons/hicolor/22x22/apps/vkpc.png b/desktop/icons/hicolor/22x22/apps/vkpc.png Binary files differnew file mode 100644 index 0000000..0f9186c --- /dev/null +++ b/desktop/icons/hicolor/22x22/apps/vkpc.png diff --git a/desktop/icons/hicolor/24x24/apps/vkpc.png b/desktop/icons/hicolor/24x24/apps/vkpc.png Binary files differnew file mode 100644 index 0000000..18b2b38 --- /dev/null +++ b/desktop/icons/hicolor/24x24/apps/vkpc.png diff --git a/desktop/icons/hicolor/32x32/apps/vkpc.png b/desktop/icons/hicolor/32x32/apps/vkpc.png Binary files differnew file mode 100644 index 0000000..8cad27c --- /dev/null +++ b/desktop/icons/hicolor/32x32/apps/vkpc.png diff --git a/desktop/icons/hicolor/36x36/apps/vkpc.png b/desktop/icons/hicolor/36x36/apps/vkpc.png Binary files differnew file mode 100644 index 0000000..6448800 --- /dev/null +++ b/desktop/icons/hicolor/36x36/apps/vkpc.png diff --git a/desktop/icons/hicolor/40x40/apps/vkpc.png b/desktop/icons/hicolor/40x40/apps/vkpc.png Binary files differnew file mode 100644 index 0000000..df33c09 --- /dev/null +++ b/desktop/icons/hicolor/40x40/apps/vkpc.png diff --git a/desktop/icons/hicolor/48x48/apps/vkpc.png b/desktop/icons/hicolor/48x48/apps/vkpc.png Binary files differnew file mode 100644 index 0000000..d34540e --- /dev/null +++ b/desktop/icons/hicolor/48x48/apps/vkpc.png diff --git a/desktop/icons/hicolor/64x64/apps/vkpc.png b/desktop/icons/hicolor/64x64/apps/vkpc.png Binary files differnew file mode 100644 index 0000000..f94329a --- /dev/null +++ b/desktop/icons/hicolor/64x64/apps/vkpc.png diff --git a/desktop/icons/hicolor/72x72/apps/vkpc.png b/desktop/icons/hicolor/72x72/apps/vkpc.png Binary files differnew file mode 100644 index 0000000..3f26601 --- /dev/null +++ b/desktop/icons/hicolor/72x72/apps/vkpc.png diff --git a/desktop/icons/hicolor/96x96/apps/vkpc.png b/desktop/icons/hicolor/96x96/apps/vkpc.png Binary files differnew file mode 100644 index 0000000..b05a855 --- /dev/null +++ b/desktop/icons/hicolor/96x96/apps/vkpc.png diff --git a/desktop/icons/ubuntu-mono-dark/16/vkpc.png b/desktop/icons/ubuntu-mono-dark/16/vkpc.png Binary files differnew file mode 100644 index 0000000..440e6b9 --- /dev/null +++ b/desktop/icons/ubuntu-mono-dark/16/vkpc.png diff --git a/desktop/icons/ubuntu-mono-dark/22/vkpc.png b/desktop/icons/ubuntu-mono-dark/22/vkpc.png Binary files differnew file mode 100644 index 0000000..6781df2 --- /dev/null +++ b/desktop/icons/ubuntu-mono-dark/22/vkpc.png diff --git a/desktop/icons/ubuntu-mono-dark/24/vkpc.png b/desktop/icons/ubuntu-mono-dark/24/vkpc.png Binary files differnew file mode 100644 index 0000000..e57de23 --- /dev/null +++ b/desktop/icons/ubuntu-mono-dark/24/vkpc.png diff --git a/desktop/icons/ubuntu-mono-light/16/vkpc.png b/desktop/icons/ubuntu-mono-light/16/vkpc.png Binary files differnew file mode 100644 index 0000000..dfb7295 --- /dev/null +++ b/desktop/icons/ubuntu-mono-light/16/vkpc.png diff --git a/desktop/icons/ubuntu-mono-light/22/vkpc.png b/desktop/icons/ubuntu-mono-light/22/vkpc.png Binary files differnew file mode 100644 index 0000000..adc4436 --- /dev/null +++ b/desktop/icons/ubuntu-mono-light/22/vkpc.png diff --git a/desktop/icons/ubuntu-mono-light/24/vkpc.png b/desktop/icons/ubuntu-mono-light/24/vkpc.png Binary files differnew file mode 100644 index 0000000..e04c84d --- /dev/null +++ b/desktop/icons/ubuntu-mono-light/24/vkpc.png diff --git a/desktop/info.h b/desktop/info.h new file mode 100644 index 0000000..f13bbdc --- /dev/null +++ b/desktop/info.h @@ -0,0 +1,13 @@ +#ifndef INFO_H__ +#define INFO_H__ + +#define APP_NAME "VK Player Controller" +#define APP_ABOUT "Use media buttons to switch between tracks." +#define APP_VERSION "0.1" +#define APP_AUTHOR "Eugene Z. <ch1p@ch1p.com>" +#define APP_URL "http://ch1p.com/vkpc/" + +#define SERVER_PORT 52178 +#define SERVER_HOST "localhost" + +#endif diff --git a/desktop/install_icons.sh b/desktop/install_icons.sh new file mode 100755 index 0000000..cbdb4e1 --- /dev/null +++ b/desktop/install_icons.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +ICONS_PATH=/usr/share/icons +ICONS=( "Faenza" "Faenza-Radiance" "hicolor" "ubuntu-mono-dark" "ubuntu-mono-light" ) +if [[ $EUID -ne 0 ]]; then + echo "This script must be run as root." 1>&2 + exit 1 +fi + +for i in "${ICONS[@]}" +do + if [ -d "${ICONS_PATH}/$i" ]; then + cp -r ${DIR}/icons/$1/* ${ICONS_PATH}/$1 + fi +done diff --git a/desktop/main.c b/desktop/main.c new file mode 100644 index 0000000..8703991 --- /dev/null +++ b/desktop/main.c @@ -0,0 +1,140 @@ +#include <stdio.h> +#include <stdlib.h> +#include <stdbool.h> +#include <pthread.h> +#include <gtk/gtk.h> + +#include "info.h" +#include "server.h" +#include "grab.h" + +static GtkStatusIcon *tray_icon; +static GtkWidget *menu; + +enum server_last_cmd_enum server_last_cmd = NONE; +static pthread_t grab_thread; +static pthread_t server_thread; + +pthread_mutex_t server_last_cmd_mutex; + +void tray_icon_on_click(GtkStatusIcon *status_icon, gpointer user_data) { + // left-click +} + +void tray_icon_on_menu(GtkStatusIcon *status_icon, guint button, guint activate_time, gpointer user_data) { + // right-click + gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, button, activate_time); +} + +void menu_about(GtkWidget *widget, gpointer data) { + GtkWidget *about_dialog; + + const gchar *authors[] = { + APP_AUTHOR, + NULL + }; + + about_dialog = gtk_about_dialog_new(); + gtk_about_dialog_set_version((GtkAboutDialog *)about_dialog, APP_VERSION); + gtk_about_dialog_set_authors((GtkAboutDialog *)about_dialog, authors); + gtk_about_dialog_set_comments((GtkAboutDialog *)about_dialog, (const gchar *)APP_ABOUT); + gtk_about_dialog_set_name((GtkAboutDialog *)about_dialog, APP_NAME); + gtk_about_dialog_set_website((GtkAboutDialog *)about_dialog, APP_URL); + + g_signal_connect_swapped(about_dialog, "response", G_CALLBACK(gtk_widget_hide), about_dialog); + + gtk_widget_show(about_dialog); +} + +void menu_quit(GtkWidget *widget, gpointer data) { + // quit app + exit(0); +} + +void create_tray_icon() { + tray_icon = gtk_status_icon_new(); + + g_signal_connect(G_OBJECT(tray_icon), "activate", + G_CALLBACK(tray_icon_on_click), NULL); + g_signal_connect(G_OBJECT(tray_icon), "popup-menu", + G_CALLBACK(tray_icon_on_menu), NULL); + + gtk_status_icon_set_from_icon_name(tray_icon, "vkpc"); + gtk_status_icon_set_tooltip(tray_icon, APP_NAME); + gtk_status_icon_set_visible(tray_icon, true); +} + +void create_menu() { + GtkWidget *item; + menu = gtk_menu_new(); + + // About + item = gtk_image_menu_item_new_from_stock(GTK_STOCK_DIALOG_INFO, NULL); + gtk_menu_item_set_label((GtkMenuItem *)item, "About"); + g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(menu_about), NULL); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); + gtk_widget_show(item); + + // Quit + item = gtk_image_menu_item_new_from_stock(GTK_STOCK_QUIT, NULL); + gtk_menu_item_set_label((GtkMenuItem *)item, "Quit"); + g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(menu_quit), NULL); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), item); + gtk_widget_show(item); +} + +void handle_hotkeys(enum HotkeyEvent e) { + pthread_mutex_lock(&server_last_cmd_mutex); + switch (e) { + case HK_PLAY: + server_last_cmd = PLAY; + break; + + case HK_PAUSE: + server_last_cmd = PAUSE; + break; + + case HK_NEXT: + server_last_cmd = NEXT; + break; + + case HK_PREV: + server_last_cmd = PREV; + break; + + default: + break; + } + pthread_mutex_unlock(&server_last_cmd_mutex); +} + +void start_grab() { + int rc = pthread_create(&grab_thread, NULL, (void *)grab_init, handle_hotkeys); + if (rc) { + fprintf(stderr, "ERROR creating grab_thread, code = %d\n", rc); + exit(-1); + } +} + +void start_server() { + int rc = pthread_create(&server_thread, NULL, (void *)server_init, NULL); + if (rc) { + fprintf(stderr, "ERROR creating server_thread, code = %d\n", rc); + exit(-1); + } +} + +int main(int argc, char **argv) { + pthread_mutex_init(&server_last_cmd_mutex, NULL); + + start_grab(); + start_server(); + + gtk_init(&argc, &argv); + + create_tray_icon(); + create_menu(); + + gtk_main(); + return 0; +} diff --git a/desktop/server.c b/desktop/server.c new file mode 100644 index 0000000..a3c308f --- /dev/null +++ b/desktop/server.c @@ -0,0 +1,182 @@ +/** + * TODO: logging level + */ + +#include <string.h> +#include <stdio.h> +#include <stdlib.h> +#include <libwebsockets.h> +#include <stdbool.h> + +#include "server.h" +#include "vector.h" + +#define SERVER_PORT 52178 +#define SERVER_HOST "localhost" + +static struct libwebsocket_context *context; +static char *server_last_cmd_values[] = { + "none", "play", "pause", "next", "prev" +}; + +struct per_session_data { + bool established; + char *next_command; +}; +struct session { + struct per_session_data *pss; + struct libwebsocket *wsi; +}; +vector *sessions; + +static void add_session(struct libwebsocket *wsi, struct per_session_data *pss) { + struct session *s = malloc(sizeof(struct session)); + s->wsi = wsi; + s->pss = pss; + vector_add(sessions, s); +} + +static void delete_session(struct libwebsocket *wsi) { + for (int i = 0; i < vector_count(sessions); i++) { + struct session *s = vector_get(sessions, i); + if (s != NULL && s->wsi == wsi) { + printf("(delete_session) found, i=%d\n", i); + free(s); + vector_delete(sessions, i); + break; + } + } +} + +static void send_command_to_all(char *command) { + printf("Got command: %s\n", command); + for (int i = 0; i < vector_count(sessions); i++) { + struct session *s = (struct session *)vector_get(sessions, i); + s->pss->next_command = command; + libwebsocket_callback_on_writable(context, s->wsi); + } +} + +static int callback_http(struct libwebsocket_context *this, + struct libwebsocket *wsi, + enum libwebsocket_callback_reasons reason, + void *user, + void *in, + size_t len) +{ + switch (reason) { + case LWS_CALLBACK_HTTP: ; + libwebsocket_callback_on_writable(context, wsi); + break; + + case LWS_CALLBACK_HTTP_WRITEABLE: ; + char *response = "vkpc, world!"; + libwebsocket_write(wsi, (unsigned char *)response, strlen(response), LWS_WRITE_HTTP); + return -1; + + default: + break; + } + return 0; +} + +static int callback_signaling(struct libwebsocket_context *this, + struct libwebsocket *wsi, + enum libwebsocket_callback_reasons reason, + void *user, + void *in, + size_t len) +{ + struct per_session_data *pss = (struct per_session_data *)user; + + switch (reason) { + case LWS_CALLBACK_ESTABLISHED: + lwsl_info("Connection established"); + + pss->established = true; + pss->next_command = NULL; + add_session(wsi, pss); + + libwebsocket_callback_on_writable(context, wsi); + break; + + case LWS_CALLBACK_SERVER_WRITEABLE: + if (pss->next_command != NULL) { + int length = strlen(pss->next_command); + unsigned char buf[LWS_SEND_BUFFER_PRE_PADDING + length + LWS_SEND_BUFFER_POST_PADDING]; + unsigned char *p = &buf[LWS_SEND_BUFFER_PRE_PADDING]; + + strcpy((char *)p, pss->next_command); + int m = libwebsocket_write(wsi, p, length, LWS_WRITE_TEXT); + + if (m < length) { + lwsl_err("ERROR while writing %d bytes to socket\n", length); + return -1; + } + + pss->next_command = NULL; + } + break; + + case LWS_CALLBACK_RECEIVE: + lwsl_info("Received: %s, length: %d\n", + (char *)in, (int)strlen((char *)in)); + break; + + case LWS_CALLBACK_CLOSED: + lwsl_info("Connection closed\n"); + delete_session(wsi); + break; + + default: + break; + } + + return 0; +} + +static struct libwebsocket_protocols protocols[] = { + { "http-only", callback_http, 0, 0 }, + { "signaling-protocol", callback_signaling, sizeof(struct per_session_data), 0 }, + { NULL, NULL, 0 } +}; + +void server_init() { + sessions = vector_create(); + + struct lws_context_creation_info info; + memset(&info, 0, sizeof(info)); + + info.port = SERVER_PORT; + info.iface = SERVER_HOST; + info.protocols = protocols; + info.extensions = libwebsocket_get_internal_extensions(); + info.ssl_cert_filepath = NULL; + info.ssl_private_key_filepath = NULL; + info.gid = -1; + info.uid = -1; + info.options = 0; + + context = libwebsocket_create_context(&info); + if (context == NULL) { + fprintf(stderr, "libwebsocket init failed\n"); + return; + } + + enum server_last_cmd_enum last_cmd = NONE; + while (1) { + pthread_mutex_lock(&server_last_cmd_mutex); + last_cmd = server_last_cmd; + server_last_cmd = NONE; + pthread_mutex_unlock(&server_last_cmd_mutex); + + if (last_cmd != NONE) { + send_command_to_all(server_last_cmd_values[last_cmd]); + } + + libwebsocket_service(context, 50); + } + + libwebsocket_context_destroy(context); + return; +} diff --git a/desktop/server.h b/desktop/server.h new file mode 100644 index 0000000..1b7af3f --- /dev/null +++ b/desktop/server.h @@ -0,0 +1,14 @@ +#include <pthread.h> + +#ifndef SERVER_H__ +#define SERVER_H__ + +enum server_last_cmd_enum { + NONE = 0, PLAY, PAUSE, NEXT, PREV +}; +extern enum server_last_cmd_enum server_last_cmd; +extern pthread_mutex_t server_last_cmd_mutex; + +void server_init(); + +#endif diff --git a/desktop/vector.c b/desktop/vector.c new file mode 100644 index 0000000..16b8804 --- /dev/null +++ b/desktop/vector.c @@ -0,0 +1,74 @@ +/** + * Based on https://gist.github.com/EmilHernvall/953968 + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include "vector.h" + +vector * vector_create() { + vector *v; + v = malloc(sizeof(vector)); + + v->data = NULL; + v->size = 0; + v->count = 0; + + return v; +} + +int vector_count(vector *v) { + return v->count; +} + +void vector_add(vector *v, void *e) { + if (v->size == 0) { + v->size = 10; + v->data = malloc(sizeof(void *) * v->size); + memset(v->data, '\0', sizeof(void *) * v->size); + } + + if (v->size == v->count) { + v->size += 10; + v->data = realloc(v->data, sizeof(void *) * v->size); + } + + v->data[v->count++] = e; +} + +void vector_set(vector *v, int index, void *e) { + if (index >= v->count) { + return; + } + + v->data[index] = e; +} + +void * vector_get(vector *v, int index) { + if (index >= v->count) { + return NULL; + } + + return v->data[index]; +} + +void vector_delete(vector *v, int index) { + if (index >= v->count) { + return; + } + + for (int i = index+1; i < v->count; i++) { + v->data[i-1] = v->data[i]; + } + + v->data[--v->count] = NULL; +} + +void vector_free_data(vector *v) { + free(v->data); +} + +void vector_free(vector *v) { + free(v); +} diff --git a/desktop/vector.h b/desktop/vector.h new file mode 100644 index 0000000..c9e9c98 --- /dev/null +++ b/desktop/vector.h @@ -0,0 +1,23 @@ +/** + * Based on https://gist.github.com/EmilHernvall/953968 + */ + +#ifndef VECTOR_H__ +#define VECTOR_H__ + +typedef struct vector_ { + void **data; + int size; + int count; +} vector; + +vector * vector_create(); +int vector_count(vector *); +void vector_add(vector *, void *); +void vector_set(vector *, int, void *); +void *vector_get(vector *, int); +void vector_delete(vector*, int); +void vector_free_data(vector *); +void vector_free(vector *); + +#endif diff --git a/extensions/chrome/_locales/en/messages.json b/extensions/chrome/_locales/en/messages.json new file mode 100644 index 0000000..eebbc14 --- /dev/null +++ b/extensions/chrome/_locales/en/messages.json @@ -0,0 +1,8 @@ +{ + "title": { + "message": "VK Player Controller Client" + }, + "description_short": { + "message": "Chrome client for VK Player Controller app." + } +} diff --git a/extensions/chrome/common.js b/extensions/chrome/common.js new file mode 100644 index 0000000..30dc55a --- /dev/null +++ b/extensions/chrome/common.js @@ -0,0 +1,187 @@ +function init() { + // receive messages from webpage + chrome.runtime.onMessageExternal.addListener(receiveMessage); +} +function receiveMessage(msg, sender, sendResponse) { + if (msg.cmd == "injection_result") { + var obj = Injections.get(msg.id); + if (obj) obj.addResponse(sender.tab.id, msg.data); + } +} +function extend(dest, source) { + for (var i in source) { + dest[i] = source[i]; + } +} +function getWebSocket() { + return window.WebSocket || window.MozWebSocket; +} +function print() { + var msgs = [], i, tmp; + for (i = 0; i < arguments.length; i++) { + if (arguments[i] instanceof Error) tmp = [arguments[i], arguments[i].stack]; + else tmp = arguments[i]; + msgs.push(tmp); + } + + try { + console.log.apply(console, msgs); + } catch(e) {} +} +function getExtensionId() { + return chrome.i18n.getMessage("@@extension_id"); +} +function getVKTabs(callback) { + var vkTabs = []; + chrome.tabs.query({}, function(tabs) { + for (var i = 0; i < tabs.length; i++) { + var tab = tabs[i]; + if (tab.url.match(new RegExp('https?://vk.com/.*', 'gi'))) { + vkTabs.push(tab); + } + } + callback(vkTabs); + }); +} +function executeCommand(cmd) { + var injId = Injections.getNextId(); + var code_inj = "var el = document.createElement('script'); el.src = chrome.extension.getURL('inject_and_return.js'); document.body.appendChild(el); var el1 = document.createElement('script'); el1.textContent = 'window.__vkpc_extid=\""+getExtensionId()+"\"; window.__vkpc_injid="+injId+"'; document.body.appendChild(el1)"; + var code_exec = "var el = document.createElement('script'); el.src = chrome.extension.getURL('inject_exec.js'); document.body.appendChild(el); var el1 = document.createElement('script'); el1.textContent = 'window.__vkpc_cmd=\""+cmd+"\"'; document.body.appendChild(el1)"; + + getVKTabs(function(tabs) { + if (!tabs.length) return; + + var injResponses, activeTabId = null; + var onDone = function() { + var ok = {nowPlaying: null, lsSource: null, recentlyPlayed: null, active: activeTabId, last: null}; + var results = injResponses.results, lsSource = injResponses.lsSource; + + for (var i = 0; i < results.length; i++) { + var data = results[i].data, id = results[i].tab; + ok.last = id; + + if (data.havePlayer && (data.isPlaying || typeof data.trackId == 'string')) { + ok.recentlyPlayed = id; + } + if (data.isPlaying) { + ok.nowPlaying = id; + } + if (lsSource && lsSource == data.instanceId) { + ok.lsSource = id; + } + } + injResponses.unregister(); + + var rightId = ok.nowPlaying || ok.lsSource || ok.recentlyPlayed || ok.active || ok.last; + if (rightId) { + chrome.tabs.executeScript(rightId, {code: code_exec}); + } + }; + injResponses = new InjectionResponses(injId, tabs.length, onDone); + + for (var i = 0; i < tabs.length; i++) { + if (tabs[i].active) activeTabId = tabs[i].id; + chrome.tabs.executeScript(tabs[i].id, { + code: code_inj + }); + } + }); +} + +var Injections = { + id: 0, + objs: {}, + getNextId: function() { + return ++this.id; + }, + get: function(id) { + return this.objs[id] || false; + }, + register: function(id, obj) { + this.objs[id] = obj; + }, + unregister: function(id) { + if (this.objs[id] !== undefined) delete this.objs[id]; + } +}; + +var WSClient = new function() { + var STATUS_NONE = 0, STATUS_OK = 1, STATUS_ERR = 2; + var _ws = getWebSocket(), ws; + var _status = STATUS_NONE; + var ping_timer, reconnect_timer; + + if (!_ws) return; + + function setTimers() { + ping_timer = setInterval(function() { + if (ws) ws.send("PING"); + }, 30000); + } + function unsetTimers() { + clearInterval(ping_timer); + } + + function connect() { + _status = STATUS_NONE; + + print("[connect]"); + ws = new _ws("ws://localhost:52178", "signaling-protocol"); + ws.onopen = function() { + _status = STATUS_OK; + setTimers(); + }; + ws.onerror = function() { + unsetTimers(); + if (_status != STATUS_ERR) { + _status = STATUS_ERR; + tryToReconnect(); + } + } + ws.onclose = function() { + unsetTimers(); + if (_status != STATUS_ERR) { + _status = STATUS_ERR; + tryToReconnect(); + } + }; + ws.onmessage = function(e) { + onCommand(e.data); + }; + } + function tryToReconnect() { + print("[tryToReconnect]"); + + clearTimeout(reconnect_timer); + reconnect_timer = setTimeout(connect, 5000); + } + function onCommand(msg) { + executeCommand(msg); + } + + connect(); +}; + +function InjectionResponses(id, count, callback) { + this.id = id; + this.results = []; + this.lsSource = null; + this.maxCount = count; + this.callback = callback || function() {}; + + Injections.register(this.id, this); +} +extend(InjectionResponses.prototype, { + addResponse: function(id, response) { + this.results.push({tab: id, data: response}); + if (!this.lsSource && response && response.lastInstanceId) this.lsSource = response.lastInstanceId; + if (this.results.length == this.maxCount) { + this.callback(); + } + }, + unregister: function() { + Injections.unregister(this.id); + } +}); + +init(); diff --git a/extensions/chrome/inject_and_return.js b/extensions/chrome/inject_and_return.js new file mode 100644 index 0000000..ebb2827 --- /dev/null +++ b/extensions/chrome/inject_and_return.js @@ -0,0 +1,28 @@ +(function() { + function getLastInstanceId() { + var id = null, pp = ls.get('pad_playlist'); + if (pp && pp.source) id = pp.source; + return id; + } + + var data = {}; + try { + var havePlayer = window.audioPlayer !== undefined; + var havePlaylist = havePlayer && !!padAudioPlaylist(); + + data = { + havePlayer: havePlayer, + havePlaylist: havePlaylist, + isPlaying: havePlayer && window.audioPlayer.player && !window.audioPlayer.player.paused(), + instanceId: window.curNotifier && curNotifier.instance_id, + trackId: havePlayer && audioPlayer.id, + lastInstanceId: getLastInstanceId() + }; + } catch(e) {} + + chrome.runtime.sendMessage(window.__vkpc_extid, { + cmd: "injection_result", + id: parseInt(window.__vkpc_injid, 10), + data: data + }); +})(); diff --git a/extensions/chrome/inject_exec.js b/extensions/chrome/inject_exec.js new file mode 100644 index 0000000..c1df18f --- /dev/null +++ b/extensions/chrome/inject_exec.js @@ -0,0 +1,74 @@ +(function() { + function vkAudio__getPlayFirstId() { + var id = currentAudioId() || ls.get('audio_id') || (window.audioPlaylist && audioPlaylist.start); + return id || null; + } + function vkAudio__executeAfterPadLoading(f) { + Pads.show('mus'); + window.onPlaylistLoaded = function() { + if (f) { + try { + f(); + } catch(e) {} + } + setTimeout(function() { + Pads.show('mus'); + }, 10); + } + } + function vkAudio__next() { + console.log("Next"); + window.audioPlayer && audioPlayer.nextTrack(true, !window.audioPlaylist); + } + function vkAudio__prev() { + console.log("Prev"); + window.audioPlayer && audioPlayer.prevTrack(true, !window.audioPlaylist); + } + function vkAudio__playPause() { + console.log("PlayPause"); + if (!window.audioPlayer || !padAudioPlaylist()) { + stManager.add(['audioplayer.js'], function() { + vkAudio__executeAfterPadLoading(function() { + var plist = padAudioPlaylist(), id = vkAudio__getPlayFirstId(); + if (id) { + playAudioNew(id); + } else if (plist && plist.start) { + playAudioNew(plist.start); + } + }); + }); + } else { + if (window.audioPlayer && audioPlayer.player) { + if (audioPlayer.player.paused()) { + audioPlayer.playTrack(); + } else { + audioPlayer.pauseTrack(); + } + } + } + } + + try { + var data = window.__vkpc_cmd; + if (data) { + switch (data) { + case "next": + vkAudio__next(); + break; + + case "prev": + vkAudio__prev(); + break; + + case "play": + case "pause": + vkAudio__playPause(); + break; + } + + delete window.__vkpc_cmd; + } + } catch (e) { + console.log('[VKPC]', e, e.stack); + } +})(); diff --git a/extensions/chrome/manifest.json b/extensions/chrome/manifest.json new file mode 100644 index 0000000..f7b29d7 --- /dev/null +++ b/extensions/chrome/manifest.json @@ -0,0 +1,23 @@ +{ + "manifest_version": 2, + "name": "__MSG_title__", + "description": "__MSG_description_short__", + "version": "0.1", + "default_locale": "en", + "permissions": [ + "background", + "tabs", + "https://vk.com/*", + "http://vk.com/*", + "https://*.vk.com/*", + "http://*.vk.com/*" + ], + "background": { + "scripts": ["common.js"] + }, + "externally_connectable": { + "matches": ["https://vk.com/*", "http://vk.com/*", "https://*.vk.com/*", "http://*.vk.com/*"] + }, + "content_security_policy": "script-src 'self' 'unsafe-eval' https://vk.com; object-src 'self' 'unsafe-eval'", + "web_accessible_resources": ["inject_and_return.js", "inject_exec.js"] +} diff --git a/extensions/firefox/chrome.manifest b/extensions/firefox/chrome.manifest new file mode 100644 index 0000000..1d59370 --- /dev/null +++ b/extensions/firefox/chrome.manifest @@ -0,0 +1,2 @@ +content vkpc chrome/ +overlay chrome://browser/content/browser.xul chrome://vkpc/content/overlay.xul diff --git a/extensions/firefox/chrome/background.js b/extensions/firefox/chrome/background.js new file mode 100644 index 0000000..2795a8c --- /dev/null +++ b/extensions/firefox/chrome/background.js @@ -0,0 +1,258 @@ +var VKPC = new function() { + +function init() { + window.addEventListener("load", function load(event) { + window.removeEventListener("load", load, false); + injectOnLoad(); + }, false); + + WSClient.go(); +} +function extend(dest, source) { + for (var i in source) { + dest[i] = source[i]; + } +} +function remove(element) { + element.parentNode.removeChild(element); +} +function createCData(data) { + var docu = new DOMParser().parseFromString('<xml></xml>', "application/xml"); + var cdata = docu.createCDATASection(data); + docu.getElementsByTagName('xml')[0].appendChild(cdata); + return cdata; +} +function getWebSocket() { + return window.WebSocket || window.MozWebSocket; +} +function print() { + var msgs = [], i, tmp; + for (i = 0; i < arguments.length; i++) { + if (arguments[i] instanceof Error) tmp = [arguments[i], arguments[i].stack]; + else tmp = arguments[i]; + msgs.push(tmp); + } + + try { + console.log.apply(console, msgs); + } catch(e) {} +} +function injectOnLoad() { + function onPageLoaded(e) { + var doc = e.originalTarget, loc = doc.location; + if (!loc.href.match(/^https?:\/\/vk.com\/.*$/)) return; + + doc.addEventListener("VKPCInjectedMessage", function(e) { + var target = e.target, json = JSON.parse(target.data || "{}"), doc = target.ownerDocument; + receiveMessage(json, doc, target); + }, false); + + var loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"].getService(Components.interfaces.mozIJSSubScriptLoader); + loader.loadSubScript("chrome://vkpc/content/inject_on_load.js", doc); + } + + var appcontent = document.getElementById("appcontent"); + if (appcontent) { + appcontent.addEventListener("DOMContentLoaded", onPageLoaded, true); + } +} +function receiveMessage(json, doc, target) { + switch (json.cmd) { + case "register": + Documents.add(doc); + break; + + case "params": + var id = json.id; + var obj = Injections.get(id); + if (obj) { + obj.addResponse(doc, json.data); + } + break; + } + + try { + remove(target); + } catch (e) {} +} +function executeCommand(cmd) { + var injId = Injections.getNextId(); + + var tabsCount = Documents.getCount(); + if (!tabsCount) return; + + var injResponses; + var onDone = function() { + var ok = {nowPlaying: null, lsSource: null, recentlyPlayed: null, active: null, last: null}; + var results = injResponses.results, lsSource = injResponses.lsSource; + + for (var i = 0; i < results.length; i++) { + var data = results[i].data, doc = results[i].tab; + ok.last = doc; + + if (data.havePlayer && (data.isPlaying || typeof data.trackId == 'string')) { + ok.recentlyPlayed = doc; + } + if (data.isPlaying) { + ok.nowPlaying = doc; + } + if (lsSource && lsSource == data.instanceId) { + ok.lsSource = doc; + } + if (data.isFocused) { + ok.active = doc; + } + } + injResponses.unregister(); + + var rightDoc = ok.nowPlaying || ok.lsSource || ok.recentlyPlayed || ok.active || ok.last; + if (rightDoc) { + Documents.sendToDoc(rightDoc, { + cmd: "audioCommand", + command: cmd + }); + } + }; + + injResponses = new InjectionResponses(injId, tabsCount, onDone); + + Documents.send({ + cmd: "getParams", + id: injId + }); +} + +var Injections = { + id: 0, + objs: {}, + getNextId: function() { + return ++this.id; + }, + get: function(id) { + return this.objs[id] || false; + }, + register: function(id, obj) { + this.objs[id] = obj; + }, + unregister: function(id) { + if (this.objs[id] !== undefined) delete this.objs[id]; + } +}; + +var Documents = { + list: [], + add: function(doc) { + this.cleanup(); + this.list.push(doc); + }, + cleanup: function() { + this.list = this.list.filter(function(t) { + return Object.prototype.toString.call(t) != '[object DeadObject]'; + }); + }, + send: function(json) { + var self = this; + this.cleanup(); + + this.list.forEach(function(doc) { + self.sendToDoc(doc, json); + }); + }, + sendToDoc: function(doc, json) { + var cdata = createCData(JSON.stringify(json)); + doc.getElementById('utils').appendChild(cdata); + + var evt = doc.createEvent("Events"); + evt.initEvent("VKPCBgMessage", true, false); + cdata.dispatchEvent(evt); + }, + getCount: function() { + this.cleanup(); + return this.list.length; + } +}; + +var WSClient = new function() { + var STATUS_NONE = 0, STATUS_OK = 1, STATUS_ERR = 2; + var _ws = getWebSocket(), ws; + var _status = STATUS_NONE; + var ping_timer, reconnect_timer; + + if (!_ws) return; + + function setTimers() { + ping_timer = setInterval(function() { + if (ws) ws.send("PING"); + }, 30000); + } + function unsetTimers() { + clearInterval(ping_timer); + } + + function connect() { + _status = STATUS_NONE; + + print("[connect]"); + ws = new _ws("ws://localhost:52178", "signaling-protocol"); + ws.onopen = function() { + _status = STATUS_OK; + setTimers(); + }; + ws.onerror = function() { + unsetTimers(); + if (_status != STATUS_ERR) { + _status = STATUS_ERR; + tryToReconnect(); + } + } + ws.onclose = function() { + unsetTimers(); + if (_status != STATUS_ERR) { + _status = STATUS_ERR; + tryToReconnect(); + } + }; + ws.onmessage = function(e) { + onCommand(e.data); + }; + } + function tryToReconnect() { + print("[tryToReconnect]"); + + clearTimeout(reconnect_timer); + reconnect_timer = setTimeout(connect, 5000); + } + function onCommand(msg) { + executeCommand(msg); + } + + this.go = function() { + connect(); + } +}; + +function InjectionResponses(id, count, callback) { + this.id = id; + this.results = []; + this.lsSource = null; + this.maxCount = count; + this.callback = callback || function() {}; + + Injections.register(this.id, this); +} +extend(InjectionResponses.prototype, { + addResponse: function(doc, response) { + this.results.push({tab: doc, data: response}); + if (!this.lsSource && response && response.lastInstanceId) this.lsSource = response.lastInstanceId; + if (this.results.length == this.maxCount) { + this.callback(); + } + }, + unregister: function() { + Injections.unregister(this.id); + } +}); + +init(); + +}; diff --git a/extensions/firefox/chrome/inject_on_load.js b/extensions/firefox/chrome/inject_on_load.js new file mode 100644 index 0000000..faa4dbf --- /dev/null +++ b/extensions/firefox/chrome/inject_on_load.js @@ -0,0 +1,149 @@ +(function() { + var isFocused = true; + + function vkAudio__getLastInstanceId() { + var id = null, pp = ls.get('pad_playlist'); + if (pp && pp.source) id = pp.source; + return id; + } + function vkAudio__getParams() { + var data = {}; + try { + var havePlayer = window.audioPlayer !== undefined; + var havePlaylist = havePlayer && !!padAudioPlaylist(); + + data = { + havePlayer: havePlayer, + havePlaylist: havePlaylist, + isPlaying: havePlayer && window.audioPlayer.player && !window.audioPlayer.player.paused(), + instanceId: window.curNotifier && curNotifier.instance_id, + trackId: havePlayer && audioPlayer.id, + lastInstanceId: vkAudio__getLastInstanceId() + }; + } catch(e) {} + + return data; + } + function vkAudio__getPlayFirstId() { + var id = currentAudioId() || ls.get('audio_id') || (window.audioPlaylist && audioPlaylist.start); + return id || null; + } + function vkAudio__executeAfterPadLoading(f) { + Pads.show('mus'); + window.onPlaylistLoaded = function() { + if (f) { + try { + f(); + } catch(e) {} + } + setTimeout(function() { + Pads.show('mus'); + }, 10); + } + } + function vkAudio__next() { + console.log("Next"); + window.audioPlayer && audioPlayer.nextTrack(true, !window.audioPlaylist); + } + function vkAudio__prev() { + console.log("Prev"); + window.audioPlayer && audioPlayer.prevTrack(true, !window.audioPlaylist); + } + function vkAudio__playPause() { + console.log("PlayPause"); + if (!window.audioPlayer || !padAudioPlaylist()) { + stManager.add(['audioplayer.js'], function() { + vkAudio__executeAfterPadLoading(function() { + var plist = padAudioPlaylist(), id = vkAudio__getPlayFirstId(); + if (id) { + playAudioNew(id); + } else if (plist && plist.start) { + playAudioNew(plist.start); + } + }); + }); + } else { + if (window.audioPlayer && audioPlayer.player) { + if (audioPlayer.player.paused()) { + audioPlayer.playTrack(); + } else { + audioPlayer.pauseTrack(); + } + } + } + } + + function createCData(data) { + var docu = new DOMParser().parseFromString('<xml></xml>', "application/xml"); + var cdata = docu.createCDATASection(data); + docu.getElementsByTagName('xml')[0].appendChild(cdata); + return cdata; + } + function sendMessage(json) { + // Fucking crazy. + json.bg = 1; + + var cdata = createCData(JSON.stringify(json)); + document.getElementById('utils').appendChild(cdata); + + var evt = document.createEvent("Events"); + evt.initEvent("VKPCInjectedMessage", true, false); + cdata.dispatchEvent(evt); + } + function remove() { + remove.parentNode.removeChild(remove); + } + function receiveCommand(e) { + var target = e.target, json = JSON.parse(target.data || "{}"); + + switch (json.cmd) { + case "getParams": + var params = vkAudio__getParams(); + params.isFocused = isFocused; + sendMessage({ + data: params, + cmd: "params", + id: json.id + }); + break; + + case "audioCommand": + switch (json.command) { + case "play": + case "pause": + vkAudio__playPause(); + break; + + case "next": + vkAudio__next(); + break; + + case "prev": + vkAudio__prev(); + break; + } + break; + } + + try { + _VKPC.remove(target); + } catch (e) {} + } + + window.addEventListener("DOMContentLoaded", function(e) { + if (window.vk) { + document.addEventListener("VKPCBgMessage", receiveCommand, false); + + sendMessage({ + cmd: "register" + }); + } + }); + + window.addEventListener("focus", function(e) { + isFocused = true; + }, false); + window.addEventListener("blur", function(e) { + isFocused = false + }, false); +})(); diff --git a/extensions/firefox/chrome/overlay.xul b/extensions/firefox/chrome/overlay.xul new file mode 100644 index 0000000..7cf7229 --- /dev/null +++ b/extensions/firefox/chrome/overlay.xul @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!DOCTYPE overlay > +<overlay id="vkpc-overlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://vkpc/content/background.js"/> +</overlay> diff --git a/extensions/firefox/install.rdf b/extensions/firefox/install.rdf new file mode 100644 index 0000000..30690ef --- /dev/null +++ b/extensions/firefox/install.rdf @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:id>vkpc@ch1p.org</em:id> + <em:name>VK Player Controller Client</em:name> + <em:version>0.1</em:version> + <em:type>2</em:type> + <em:creator>ch1p</em:creator> + <em:description>Firefox client for VK Player Controller app.</em:description> + + <em:targetApplication> + <Description> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>4.0</em:minVersion> + <em:maxVersion>30.0</em:maxVersion> + </Description> + </em:targetApplication> + </Description> +</RDF> |