Skip to content

Commit 084689f

Browse files
committed
Added multiple changes: UnifiedBus, Caching, Host heirarchy
1 parent 5af29e9 commit 084689f

10 files changed

Lines changed: 139 additions & 71 deletions

File tree

backend-go/internal/domain/types.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ type RoomSummary struct {
2929
}
3030

3131
type UserSummary struct {
32-
ID string `json:"id"`
33-
Username string `json:"username"`
34-
IsHost bool `json:"is_host"`
32+
ID string `json:"id"`
33+
Username string `json:"username"`
34+
IsHost bool `json:"is_host"`
35+
JoinedAt time.Time `json:"joined_at"`
3536
}
3637

3738
type Message struct {

backend-go/internal/service/room_manager.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ func (s *RoomService) JoinRoom(ctx context.Context, roomID, action string, clien
192192
ID: client.ID,
193193
Username: client.Username,
194194
IsHost: client.IsHost,
195+
JoinedAt: time.Now(),
195196
}
196197

197198
s.RegisterLocalClient(roomID, client)
@@ -245,10 +246,24 @@ func (s *RoomService) LeaveRoom(ctx context.Context, roomID string, client *doma
245246
}
246247
}
247248
if !hasHost {
248-
for id, u := range state.Clients {
249-
u.IsHost = true
250-
state.Clients[id] = u
251-
break
249+
if !hasHost {
250+
var oldestUser string
251+
var oldestTime time.Time
252+
first := true
253+
254+
for id, u := range state.Clients {
255+
if first || u.JoinedAt.Before(oldestTime) {
256+
oldestTime = u.JoinedAt
257+
oldestUser = id
258+
first = false
259+
}
260+
}
261+
262+
if oldestUser != "" {
263+
u := state.Clients[oldestUser]
264+
u.IsHost = true
265+
state.Clients[oldestUser] = u
266+
}
252267
}
253268
}
254269

backend-python/app/domain/schemas.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,15 @@ class VideoResponse(BaseModel):
4141
model_config = {"from_attributes": True}
4242

4343
class MetadataReq(BaseModel):
44-
video_ids: List[int]
44+
video_ids: List[int]
45+
46+
class PubSubMessage(BaseModel):
47+
type: str
48+
username: str
49+
user_id: str = ""
50+
content: str = ""
51+
timestamp: float = 0.0
52+
video_id: int = 0
53+
room: str
54+
is_host: bool = False
55+
data: Optional[dict] = None
Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import json
22
from google.cloud import pubsub_v1
3+
from app.domain.schemas import PubSubMessage # 🚨 Import the unified schema
34

45
class GCPPubSubPublisher:
56
def __init__(self, project_id: str, topic_id: str):
67
self.publisher = pubsub_v1.PublisherClient()
78
self.topic_path = self.publisher.topic_path(project_id, topic_id)
89

910
def broadcast_video_added(self, room_id: str, video_data: dict):
10-
"""Sends a message to Go indicating the playlist has changed."""
11-
message = {
12-
"type": "video_added",
13-
"room": room_id,
14-
"data": video_data
15-
}
16-
data = json.dumps(message).encode("utf-8")
11+
validated_msg = PubSubMessage(
12+
type="video_added",
13+
room=room_id,
14+
username="system", # Required by Go validator
15+
data=video_data
16+
)
17+
18+
data = json.dumps(validated_msg.model_dump()).encode("utf-8")
1719
self.publisher.publish(self.topic_path, data)
1820
print(f"📡 Broadcast video update for room: {room_id}")

backend-python/app/repository/video_repo.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,11 @@ def save_video(self, video_data: dict) -> VideoResponse:
2929

3030
def delete_videos_by_room(self, room: str) -> None:
3131
self.db.query(VideoModel).filter(VideoModel.room == room).delete()
32-
self.db.commit()
32+
self.db.commit()
33+
34+
# Add to VideoRepoImpl class
35+
def get_video_by_url(self, url: str) -> VideoResponse | None:
36+
db_video = self.db.query(VideoModel).filter(VideoModel.video_url == url).first()
37+
if db_video:
38+
return VideoResponse.model_validate(db_video)
39+
return None

backend-python/app/service/video_service.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,29 @@ def process_and_add_video(self, req: VideoCreateReq) -> VideoResponse:
2525

2626
return saved_video
2727

28-
# 2. Otherwise, fetch it securely WITHOUT yt-dlp
29-
metadata = fetch_video_metadata(str(req.url))
28+
# 🚨 2. Check the Database Cache First
29+
cached_video = self.repo.get_video_by_url(str(req.url))
30+
31+
if cached_video:
32+
title = cached_video.title
33+
thumbnail = cached_video.thumbnail
34+
else:
35+
# 3. Only scrape if it's a completely new URL
36+
metadata = fetch_video_metadata(str(req.url))
37+
title = metadata.get('title') or 'Unknown Video'
38+
thumbnail = metadata.get('thumbnail') or "https://via.placeholder.com/640x360.png?text=Video+Added"
3039

3140
data = {
32-
"title": metadata.get('title') or 'Unknown Video',
41+
"title": title,
3342
"video_url": str(req.url),
34-
# Use fetched thumbnail or a generic fallback image
35-
"thumbnail": metadata.get('thumbnail') or "https://via.placeholder.com/640x360.png?text=Video+Added",
43+
"thumbnail": thumbnail,
3644
"room": req.room
3745
}
3846

3947
saved_video = self.repo.save_video(data)
4048

4149
if self.publisher:
42-
video_json_data = saved_video.model_dump(mode='json')
43-
self.publisher.broadcast_video_added(req.room, video_json_data)
50+
self.publisher.broadcast_video_added(req.room, saved_video.model_dump(mode='json'))
4451

4552
return saved_video
4653

frontend/wpfe/src/components/AddVideoBar.jsx

Lines changed: 55 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,76 @@
1-
import React, { useState } from "react";
1+
import React, { useState, useEffect } from "react";
22
import axios from "axios";
3-
import { FaPlus } from "react-icons/fa";
3+
import { FaPlus, FaSearch } from "react-icons/fa";
44
import { API_URL } from "../components/Config";
55

66
const AddVideoBar = ({ room, onVideoAdded }) => {
7-
const [url, setUrl] = useState("");
7+
const [query, setQuery] = useState("");
8+
const [results, setResults] = useState([]);
89
const [loading, setLoading] = useState(false);
910

10-
const handleAdd = async (e) => {
11-
e.preventDefault();
12-
if (!url.trim()) return;
13-
14-
setLoading(true);
15-
try {
16-
let cleanUrl = url.trim();
11+
// Debounced search
12+
useEffect(() => {
13+
if (!query.trim() || query.includes("http")) {
14+
setResults([]);
15+
return;
16+
}
17+
const delayDebounceFn = setTimeout(async () => {
1718
try {
18-
const parsedUrl = new URL(cleanUrl);
19-
if (parsedUrl.hostname.includes("youtube.com")) {
20-
const v = parsedUrl.searchParams.get("v");
21-
if (v) cleanUrl = `https://www.youtube.com/watch?v=${v}`;
22-
} else if (parsedUrl.hostname.includes("youtu.be")) {
23-
const v = parsedUrl.pathname.slice(1);
24-
if (v) cleanUrl = `https://www.youtube.com/watch?v=${v}`;
25-
}
19+
// Free, open-source YouTube search API
20+
const res = await axios.get(`https://vid.puffyan.us/api/v1/search?q=${encodeURIComponent(query)}`);
21+
setResults(res.data.slice(0, 5)); // Top 5 results
2622
} catch (err) {
27-
console.warn("URL parsing failed, using raw input:", err);
28-
cleanUrl = url.trim();
23+
console.error("Search failed", err);
2924
}
25+
}, 500);
26+
return () => clearTimeout(delayDebounceFn);
27+
}, [query]);
3028

31-
32-
await axios.post(`${API_URL}/api/videos/add`, {
33-
url: cleanUrl,
34-
room,
35-
});
36-
37-
setUrl("");
29+
const handleAdd = async (videoUrl) => {
30+
setLoading(true);
31+
try {
32+
await axios.post(`${API_URL}/api/videos/add`, { url: videoUrl, room });
33+
setQuery("");
34+
setResults([]);
3835
if (onVideoAdded) onVideoAdded();
3936
} catch (err) {
40-
console.error("Add Video Error:", err);
41-
alert("Failed to add video. Please ensure the link is valid.");
37+
alert("Failed to add video.");
4238
} finally {
4339
setLoading(false);
4440
}
4541
};
4642

4743
return (
48-
<form onSubmit={handleAdd} className="add-video-form">
49-
<input
50-
type="text"
51-
placeholder="Paste YouTube URL..."
52-
value={url}
53-
onChange={(e) => setUrl(e.target.value)}
54-
className="add-video-input"
55-
/>
56-
<button type="submit" disabled={loading} className="add-video-btn"aria-label="Add Video">
57-
{loading ? "..." : <FaPlus />}
58-
</button>
59-
</form>
44+
<div className="add-video-container" style={{ position: "relative" }}>
45+
<form onSubmit={(e) => { e.preventDefault(); if(query.includes("http")) handleAdd(query); }} className="add-video-form">
46+
<input
47+
type="text"
48+
placeholder="Search YouTube or paste URL..."
49+
value={query}
50+
onChange={(e) => setQuery(e.target.value)}
51+
className="add-video-input"
52+
/>
53+
<button type="submit" disabled={loading} className="add-video-btn">
54+
{loading ? "..." : (query.includes("http") ? <FaPlus /> : <FaSearch />)}
55+
</button>
56+
</form>
57+
58+
{/* Dropdown Results */}
59+
{results.length > 0 && (
60+
<div className="search-results-dropdown" style={{ position: "absolute", top: "100%", width: "100%", background: "#1a1a1a", zIndex: 100, borderRadius: "4px" }}>
61+
{results.map((v) => (
62+
<div
63+
key={v.videoId}
64+
onClick={() => handleAdd(`https://www.youtube.com/watch?v=${v.videoId}`)}
65+
style={{ padding: "10px", borderBottom: "1px solid #333", cursor: "pointer", display: "flex", gap: "10px" }}
66+
>
67+
<img src={v.videoThumbnails[0]?.url} alt="" width="50" style={{ borderRadius: "4px" }} />
68+
<div style={{ fontSize: "0.85rem", color: "white" }}>{v.title}</div>
69+
</div>
70+
))}
71+
</div>
72+
)}
73+
</div>
6074
);
6175
};
6276

frontend/wpfe/src/components/Dashboard.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const Dashboard = ({ user, onLogout }) => {
4444
changeVideo,
4545
sendTypingSignal,
4646
typingUsers,
47+
onEnded
4748
} = useWatchParty(roomId, action);
4849

4950
useEffect(() => {
@@ -133,6 +134,7 @@ const Dashboard = ({ user, onLogout }) => {
133134
onPlay={onPlay}
134135
onPause={onPause}
135136
onSeek={onSeek}
137+
onEnded={onEnded}
136138
/>
137139
) : (
138140
<div

frontend/wpfe/src/components/VideoPlayer.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const GuestControls = ({
3434
);
3535

3636
const VideoPlayer = forwardRef(
37-
({ url, playing, onReady, onPlay, onPause, onSeek, isHost }, ref) => {
37+
({ url, playing, onReady, onPlay, onPause, onSeek, isHost, onEnded }, ref) => {
3838
const [volume, setVolume] = useState(0.8);
3939
const [muted, setMuted] = useState(true);
4040
const wrapperRef = useRef(null);
@@ -117,6 +117,9 @@ const VideoPlayer = forwardRef(
117117
internalPlayer.playVideo();
118118
}
119119
}}
120+
onEnded={() => {
121+
if (onEnded) onEnded();
122+
}}
120123
onProgress={(state) => {
121124
const current = state.playedSeconds;
122125
const diff = Math.abs(current - lastProgressRef.current);

frontend/wpfe/src/hooks/useWatchParty.jsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,17 @@ export const useWatchParty = (urlRoom = null, action = "join") => {
314314
}
315315
};
316316

317+
const onEnded = () => {
318+
if (!isHostRef.current) return; // Only the host controls the queue
319+
320+
const currentIndex = videosRef.current.findIndex(v => v.id === currentVideoRef.current?.id);
321+
if (currentIndex !== -1 && currentIndex + 1 < videosRef.current.length) {
322+
const nextVideo = videosRef.current[currentIndex + 1];
323+
// Auto-play the next video
324+
changeVideo(nextVideo.id);
325+
}
326+
};
327+
317328
const sendSignal = (type, payload = null) => {
318329
if (ws.current?.readyState !== WebSocket.OPEN) return;
319330
const payloadMsg = {
@@ -412,12 +423,6 @@ export const useWatchParty = (urlRoom = null, action = "join") => {
412423
};
413424
}, [room, username, action]);
414425

415-
return () => {
416-
intentionalClose.current = true;
417-
if (ws.current) ws.current.close();
418-
};
419-
}, [room, username, action]);
420-
421426
const sendMessage = (text) =>
422427
ws.current.send(
423428
JSON.stringify({
@@ -464,5 +469,6 @@ export const useWatchParty = (urlRoom = null, action = "join") => {
464469
changeVideo,
465470
sendTypingSignal,
466471
setRoom,
472+
onEnded,
467473
};
468474
};

0 commit comments

Comments
 (0)