Skip to content

Instantly share code, notes, and snippets.

@SerJaimeLannister
Last active December 10, 2025 20:47
Show Gist options
  • Select an option

  • Save SerJaimeLannister/dd61a9ae641414b7d0c3ed8da12a0468 to your computer and use it in GitHub Desktop.

Select an option

Save SerJaimeLannister/dd61a9ae641414b7d0c3ed8da12a0468 to your computer and use it in GitHub Desktop.
gosync is goated single file synctube esque alternative created for personal use.
package main
import (
"flag"
"fmt"
"html/template"
"log"
"net/http"
"sync"
"github.com/gorilla/websocket"
)
// --- Configuration & Constants ---
var addr = flag.String("addr", ":8080", "http service address")
// --- Data Structures ---
// Message represents the JSON object sent between client and server
type Message struct {
Type string `json:"type"` // "chat", "video_id", "sync"
Payload string `json:"payload"` // Chat message or Video ID
Timestamp float64 `json:"timestamp"` // Current video time
State int `json:"state"` // 1=Playing, 2=Paused
User string `json:"user"` // Username
}
// Global state to handle connected clients and current playback state
type Hub struct {
clients map[*websocket.Conn]bool
broadcast chan Message
register chan *websocket.Conn
unregister chan *websocket.Conn
mutex sync.Mutex
// Current room state (for new joiners)
currentVideoID string
currentTime float64
currentState int // 1=play, 2=pause
}
var hub = Hub{
clients: make(map[*websocket.Conn]bool),
broadcast: make(chan Message),
register: make(chan *websocket.Conn),
unregister: make(chan *websocket.Conn),
// Default starting video
currentVideoID: "jfKfPfyJRdk", // Lofi Girl as default
currentState: 2, // Paused
}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
// --- Application Logic ---
func main() {
flag.Parse()
fmt.Println("░█▀▀░█▀█░▄▄▄░█▀▀░█░█░█▀█░█▀▀")
fmt.Println("░█░█░█░█░▄▄▄░▀▀█░░█░░█░█░█░░")
fmt.Println("░▀▀▀░▀▀▀░░░░░▀▀▀░░▀░░▀░▀░▀▀▀")
fmt.Printf(">> SYSTEM ONLINE. LISTENING ON %s\n", *addr)
// Start the Hub manager in a goroutine
go hub.run()
http.HandleFunc("/", serveHome)
http.HandleFunc("/ws", serveWs)
err := http.ListenAndServe(*addr, nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
func (h *Hub) run() {
for {
select {
case client := <-h.register:
h.mutex.Lock()
h.clients[client] = true
h.mutex.Unlock()
// Send current state to the new user
initMsg := Message{
Type: "init",
Payload: h.currentVideoID,
Timestamp: h.currentTime,
State: h.currentState,
}
client.WriteJSON(initMsg)
case client := <-h.unregister:
h.mutex.Lock()
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
client.Close()
}
h.mutex.Unlock()
case msg := <-h.broadcast:
// Update server state if it's a sync event
if msg.Type == "video_id" {
h.currentVideoID = msg.Payload
h.currentTime = 0
h.currentState = 1 // Auto play on new video
} else if msg.Type == "sync" {
h.currentTime = msg.Timestamp
h.currentState = msg.State
}
// Broadcast to all
h.mutex.Lock()
for client := range h.clients {
err := client.WriteJSON(msg)
if err != nil {
client.Close()
delete(h.clients, client)
}
}
h.mutex.Unlock()
}
}
}
// serveHome renders the embedded HTML/CSS/JS
func serveHome(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.New("index").Parse(htmlTemplate)
if err != nil {
http.Error(w, "Template Error", 500)
return
}
tmpl.Execute(w, nil)
}
// serveWs handles websocket requests from the peer
func serveWs(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
hub.register <- ws
// Read Loop
for {
var msg Message
err := ws.ReadJSON(&msg)
if err != nil {
hub.unregister <- ws
break
}
hub.broadcast <- msg
}
}
// --- Frontend Assets (Embedded) ---
const htmlTemplate = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GO-SYNC // TERMINAL</title>
<style>
:root {
--bg: #0d0d0d;
--term-green: #0f0;
--term-dim: #004400;
--alert: #ff0055;
--font: 'Courier New', Courier, monospace;
}
body {
background-color: var(--bg);
color: var(--term-green);
font-family: var(--font);
margin: 0;
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
text-shadow: 0 0 5px var(--term-dim);
}
/* CRT Scanline Effect */
body::after {
content: " ";
display: block;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
z-index: 2;
background-size: 100% 2px, 3px 100%;
pointer-events: none;
}
header {
border-bottom: 2px solid var(--term-green);
padding: 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
h1 { margin: 0; font-size: 1.5rem; letter-spacing: 2px; }
.status { font-size: 0.8rem; animation: blink 2s infinite; }
.container {
display: flex;
flex: 1;
height: 100%;
}
/* Video Section */
.video-area {
flex: 3;
display: flex;
flex-direction: column;
border-right: 2px solid var(--term-green);
padding: 10px;
position: relative;
}
.input-group {
display: flex;
margin-bottom: 10px;
border: 1px solid var(--term-green);
}
input {
background: transparent;
border: none;
color: var(--term-green);
font-family: var(--font);
padding: 10px;
flex: 1;
outline: none;
}
button {
background: var(--term-green);
color: var(--bg);
border: none;
font-family: var(--font);
font-weight: bold;
padding: 0 20px;
cursor: pointer;
}
button:hover { background: #ccffcc; }
.video-wrapper {
position: relative;
padding-bottom: 56.25%; /* 16:9 */
height: 0;
background: #000;
border: 1px dashed var(--term-dim);
}
.video-wrapper iframe {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
}
/* Chat Section */
.sidebar {
flex: 1;
display: flex;
flex-direction: column;
background: #050505;
}
.chat-log {
flex: 1;
overflow-y: auto;
padding: 10px;
font-size: 0.9rem;
}
.chat-log p { margin: 5px 0; word-break: break-all; }
.sys-msg { color: #888; font-style: italic; }
.user-msg .name { font-weight: bold; color: #ccffcc; }
@keyframes blink { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--term-green); }
</style>
</head>
<body>
<header>
<h1>GO-SYNC <span style="font-size:0.5em">v1.0</span></h1>
<div class="status">● CONNECTION ESTABLISHED</div>
</header>
<div class="container">
<div class="video-area">
<div class="input-group">
<input type="text" id="vidUrl" placeholder="PASTE YOUTUBE URL HERE...">
<button onclick="changeVideo()">LOAD</button>
</div>
<div class="video-wrapper">
<div id="player"></div>
</div>
<div style="margin-top:10px; font-size: 0.8rem; color: var(--alert);">
// CONTROLS ARE SYNCED. DON'T BE A TROLL.
</div>
</div>
<div class="sidebar">
<div class="chat-log" id="chatLog">
<p class="sys-msg">> SYSTEM: Welcome to the encrypted channel.</p>
</div>
<div class="input-group" style="border-left:none; border-right:none; border-bottom:none;">
<input type="text" id="chatInput" placeholder="TYPE MESSAGE..." onkeypress="handleChat(event)">
<button onclick="sendChat()">SEND</button>
</div>
</div>
</div>
<script>
// --- WebSocket Setup ---
const socket = new WebSocket("ws://" + window.location.host + "/ws");
const username = "USER_" + Math.floor(Math.random() * 1000);
let isRemoteUpdate = false; // Flag to prevent infinite sync loops
let player;
// --- YouTube API Setup ---
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
function onYouTubeIframeAPIReady() {
player = new YT.Player('player', {
height: '100%',
width: '100%',
videoId: '', // Will be set by init message
playerVars: {
'playsinline': 1,
'controls': 1,
'disablekb': 0,
'rel': 0
},
events: {
'onReady': onPlayerReady,
'onStateChange': onPlayerStateChange
}
});
}
function onPlayerReady(event) {
logSystem("Video Engine Initialized.");
}
function onPlayerStateChange(event) {
// If the state change was triggered by a socket message, ignore sending it back
if (isRemoteUpdate) return;
// 1 = Playing, 2 = Paused
if (event.data === YT.PlayerState.PLAYING || event.data === YT.PlayerState.PAUSED) {
socket.send(JSON.stringify({
type: "sync",
state: event.data,
timestamp: player.getCurrentTime(),
user: username
}));
}
}
// --- Socket Logic ---
socket.onmessage = function(event) {
const msg = JSON.parse(event.data);
switch(msg.type) {
case "init":
if(player && player.loadVideoById) {
isRemoteUpdate = true;
player.loadVideoById(msg.payload, msg.timestamp);
if (msg.state === 2) { player.pauseVideo(); }
setTimeout(() => isRemoteUpdate = false, 1000);
} else {
// Queue if player not ready (simple retry)
setTimeout(() => socket.onmessage(event), 500);
}
break;
case "video_id":
isRemoteUpdate = true;
player.loadVideoById(msg.payload);
logSystem(msg.user + " loaded new video.");
setTimeout(() => isRemoteUpdate = false, 1000);
break;
case "sync":
if (!player) return;
// Calculate lag
let timeDiff = Math.abs(player.getCurrentTime() - msg.timestamp);
isRemoteUpdate = true;
// Only seek if time difference is significant (>1s) to avoid jitters
if (timeDiff > 1.0) {
player.seekTo(msg.timestamp, true);
}
if (msg.state === 1) { // Play
player.playVideo();
} else if (msg.state === 2) { // Pause
player.pauseVideo();
}
setTimeout(() => isRemoteUpdate = false, 500);
break;
case "chat":
addChatMessage(msg.user, msg.payload);
break;
}
};
// --- UI Interactions ---
function changeVideo() {
const url = document.getElementById('vidUrl').value;
const vidId = extractVideoID(url);
if (vidId) {
socket.send(JSON.stringify({
type: "video_id",
payload: vidId,
user: username
}));
document.getElementById('vidUrl').value = "";
} else {
alert("INVALID URL DETECTED");
}
}
function extractVideoID(url) {
var regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
var match = url.match(regExp);
return (match && match[2].length == 11) ? match[2] : null;
}
function handleChat(e) {
if (e.key === 'Enter') sendChat();
}
function sendChat() {
const input = document.getElementById('chatInput');
if (input.value.trim() === "") return;
socket.send(JSON.stringify({
type: "chat",
payload: input.value,
user: username
}));
input.value = "";
}
function addChatMessage(user, text) {
const log = document.getElementById('chatLog');
const div = document.createElement('div');
div.className = "user-msg";
div.innerHTML = "<span class='name'>[" + user + "]</span>: " + escapeHtml(text);
log.appendChild(div);
log.scrollTop = log.scrollHeight;
}
function logSystem(text) {
const log = document.getElementById('chatLog');
const p = document.createElement('p');
p.className = "sys-msg";
p.innerText = ">> SYSTEM: " + text;
log.appendChild(p);
log.scrollTop = log.scrollHeight;
}
function escapeHtml(text) {
if (!text) return text;
return text.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
</script>
</body>
</html>
`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment