Skip to content

Instantly share code, notes, and snippets.

@michaelowens
Last active June 20, 2023 04:02
Show Gist options
  • Save michaelowens/9e87302170bac06c6c14d7269086d494 to your computer and use it in GitHub Desktop.
Save michaelowens/9e87302170bac06c6c14d7269086d494 to your computer and use it in GitHub Desktop.
Show streamers live on kick in your Twitch sidebar
// ==UserScript==
// @name Twitch Kick Followers
// @namespace Violentmonkey Scripts
// @description Keep track of Kick streams.
// @run-at document-start
// @match *://twitch.tv/*
// @match *://www.twitch.tv/*
// @version 0.0.0
// @author xikeon
// @require https://cdn.jsdelivr.net/combine/npm/@violentmonkey/dom@2,npm/@violentmonkey/ui@0.7
// @updateURL https://gist.github.com/michaelowens/9e87302170bac06c6c14d7269086d494/raw/twitch-kick-followers.user.js
// @downloadURL https://gist.github.com/michaelowens/9e87302170bac06c6c14d7269086d494/raw/twitch-kick-followers.user.js
// @supportURL https://gist.github.com/michaelowens/9e87302170bac06c6c14d7269086d494#new_comment_field
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_setValue
// ==/UserScript==
(function () {
'use strict';
var css_248z = "";
var styles = {"title":"style-module_title__Hei5S","desc":"style-module_desc__LACEI"};
var stylesheet=".style-module_title__Hei5S{display:flex;font-weight:700;justify-content:space-between}.style-module_desc__LACEI{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity))}";
const store = {
get() {
return GM_getValue('following', []);
},
add(username) {
const value = store.get();
value.push(username);
GM_setValue('following', value);
},
remove(username) {
const value = store.get();
GM_setValue('following', value.filter(u => u !== username));
}
};
// global CSS
let panel;
GM_registerMenuCommand('Configure Followers', () => {
if (!panel) {
showPanel();
} else {
hidePanel();
}
});
function showPanel() {
if (panel) {
return;
}
panel = VM.getPanel({
content: VM.h(FollowingPanel, null),
theme: 'dark',
style: [css_248z, stylesheet].join('\n')
});
panel.wrapper.style.left = '50%';
panel.wrapper.style.top = '50%';
panel.wrapper.style.transform = 'translate(-50%, -50%)';
// panel.setMovable(true);
panel.show();
}
function hidePanel() {
panel.dispose();
panel = null;
}
function FollowingList() {
const following = store.get();
const deleteItem = (e, username) => {
e.preventDefault();
store.remove(username);
panel.body.querySelector('#following_list').replaceWith(VM.m(VM.h(FollowingList, null)));
};
return VM.h("ul", {
id: "following_list"
}, following.map(f => VM.h("li", null, f, ' ', VM.h("a", {
href: "#",
onClick: e => deleteItem(e, f),
style: "color: red"
}, "[x]"))));
}
function FollowingPanel() {
const addItem = () => {
const userInput = panel.body.querySelector('#addUserInput');
if (!userInput.value) return;
store.add(userInput.value);
panel.body.querySelector('#following_list').replaceWith(VM.m(VM.h(FollowingList, null)));
userInput.value = '';
};
return VM.h(VM.Fragment, null, VM.h("div", {
className: styles.title
}, VM.h("span", null, "Following on Kick"), VM.h("button", {
onClick: hidePanel
}, "Close")), VM.h("p", {
className: styles.desc
}, "Add user:", VM.h("input", {
type: "text",
id: "addUserInput",
style: "margin: 0 10px;"
}), VM.h("button", {
onClick: addItem
}, "Add")), VM.h("div", {
id: "following_list"
}, VM.h(FollowingList, null)));
}
const realFetch = unsafeWindow.fetch.bind(unsafeWindow);
unsafeWindow.fetch = async function (resource, options) {
const response = await realFetch(resource, options);
if (!(typeof resource === 'string' ? resource : resource.url).includes('gql.twitch.tv/gql')) {
return response;
}
const text = response.text.bind(response);
const json = response.json.bind(response);
response.text = async function () {
const e = await text();
try {
const parsedText = JSON.parse(e);
return await handleResponse(parsedText), JSON.stringify(parsedText);
} catch (t) {
return e;
}
};
response.json = async function () {
const parsedJson = await json();
try {
return await handleResponse(parsedJson), parsedJson;
} catch (t) {
return parsedJson;
}
};
return response;
};
async function handleResponse(e) {
if (Array.isArray(e)) for (const t of e) await handleGql(t);
return e;
}
async function handleGql(res) {
switch (res.extensions.operationName) {
case 'FollowingLive_CurrentUser':
{
console.log('injection: following live data');
break;
}
case 'PersonalSections':
{
const n = res.data.personalSections.find(e => 'FOLLOWED_SECTION' === e.type || 'RECS_FOLLOWED_SECTION' === e.type);
if (n) {
const following = store.get();
console.log('checking kick', following);
for (const u of following) {
var _kickData$livestream;
console.log(u);
const kickData = await getKickData(u);
const is_live = !!((_kickData$livestream = kickData.livestream) != null && _kickData$livestream.is_live);
if (!is_live) {
continue;
}
const id = (Math.random() * 10000).toFixed(0);
n.items.push({
trackingID: id,
promotionsCampaignID: '',
user: {
id: id,
login: kickData.user.username,
displayName: kickData.user.username,
profileImageURL: kickData.user.profile_pic,
primaryColorHex: '00FFE2',
broadcastSettings: {
id: id,
title: kickData.livestream.session_title,
__typename: 'BroadcastSettings'
},
channel: {
id: id,
creatorAnniversaries: {
id: id,
isAnniversary: !1,
__typename: 'CreatorAnniversaries'
},
__typename: 'Channel'
},
__typename: 'User'
},
label: 'NONE',
content: {
id: id,
previewImageURL: kickData.user.profile_pic,
broadcaster: {
id: id,
broadcastSettings: {
id: id,
title: kickData.livestream.session_title,
__typename: 'BroadcastSettings'
},
__typename: 'User'
},
viewersCount: kickData.livestream.viewer_count,
self: {
canWatch: !0,
isRestricted: !1,
restrictionType: null,
__typename: 'StreamSelfConnection'
},
game: {
id: '-1',
displayName: 'Kick',
name: 'Kick',
__typename: 'Game'
},
type: 'live',
__typename: 'Stream'
},
__typename: 'PersonalSectionChannel'
});
}
n.items.sort((e, t) => (t.content.viewersCount || 0) - (e.content.viewersCount || 0));
}
break;
}
case 'StreamMetadata':
{
console.log('injection: stream metadata');
break;
}
}
}
async function getKickData(username) {
const response = await fetch('https://kick.com/api/v1/channels/' + username);
return await response.json();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment