Last active
December 10, 2025 20:47
-
-
Save SerJaimeLannister/dd61a9ae641414b7d0c3ed8da12a0468 to your computer and use it in GitHub Desktop.
gosync is goated single file synctube esque alternative created for personal use.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """) | |
| .replace(/'/g, "'"); | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| ` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment