initial commit

This commit is contained in:
ch1p 2013-12-29 17:43:49 +02:00
parent 8bd03b4f9c
commit bdc7ff9d08
53 changed files with 1323 additions and 2 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
desktop/icons/*_psd
extensions/chrome.crx
extensions/chrome.pem
extensions/chrome.zip
extensions/firefox.xpi
desktop/*.o
desktop/vkpc

View File

@ -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

View File

@ -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

29
desktop/Makefile Normal file
View File

@ -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

51
desktop/grab.c Normal file
View File

@ -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);
}

15
desktop/grab.h Normal file
View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 904 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 999 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

13
desktop/info.h Normal file
View File

@ -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

16
desktop/install_icons.sh Executable file
View File

@ -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

140
desktop/main.c Normal file
View File

@ -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;
}

182
desktop/server.c Normal file
View File

@ -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;
}

14
desktop/server.h Normal file
View File

@ -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

74
desktop/vector.c Normal file
View File

@ -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);
}

23
desktop/vector.h Normal file
View File

@ -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

View File

@ -0,0 +1,8 @@
{
"title": {
"message": "VK Player Controller Client"
},
"description_short": {
"message": "Chrome client for VK Player Controller app."
}
}

187
extensions/chrome/common.js Normal file
View File

@ -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();

View File

@ -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
});
})();

View File

@ -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);
}
})();

View File

@ -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"]
}

View File

@ -0,0 +1,2 @@
content vkpc chrome/
overlay chrome://browser/content/browser.xul chrome://vkpc/content/overlay.xul

View File

@ -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();
};

View File

@ -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);
})();

View File

@ -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>

View File

@ -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>