more Stuff

main
Charlotte Som 2023-05-08 16:49:04 +01:00
parent fd0d9127c8
commit 6aff9f52b0
8 changed files with 226 additions and 24 deletions

View File

@ -2,8 +2,11 @@ root = true
[*] [*]
indent_style = space indent_style = space
indent_size = 4 indent_size = 2
end_of_line = lf end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = false trim_trailing_whitespace = false
insert_final_newline = true insert_final_newline = true
[*.rs]
indent_size = 4

15
Cargo.lock generated
View File

@ -225,6 +225,19 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "dashmap"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
dependencies = [
"cfg-if",
"hashbrown",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.6" version = "0.10.6"
@ -1442,11 +1455,13 @@ version = "0.1.0"
dependencies = [ dependencies = [
"axum", "axum",
"confique", "confique",
"dashmap",
"futures", "futures",
"futures-util", "futures-util",
"miette", "miette",
"once_cell", "once_cell",
"serde", "serde",
"serde_json",
"tokio", "tokio",
"tower-http", "tower-http",
"uuid", "uuid",

View File

@ -14,3 +14,5 @@ futures = "0.3"
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] } futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
uuid = { version = "1.3.1", features = ["v4", "serde"] } uuid = { version = "1.3.1", features = ["v4", "serde"] }
once_cell = "1.17.1" once_cell = "1.17.1"
serde_json = "1.0.95"
dashmap = "5.4.0"

38
public/assets/main.css Normal file
View File

@ -0,0 +1,38 @@
:root {
--bg-rgb: 28, 23, 36;
--fg-rgb: 234, 234, 248;
--accent-rgb: 181, 127, 220;
--fg: rgb(var(--fg-rgb));
--bg: rgb(var(--bg-rgb));
--default-user-color: rgb(238, 126, 255);
--accent: rgb(var(--accent-rgb));
--fg-transparent: rgba(var(--fg-rgb), 0.25);
--bg-transparent: rgba(var(--bg-rgb), 0.25);
--autocomplete-bg: linear-gradient(
var(--fg-transparent),
var(--fg-transparent)
),
linear-gradient(var(--bg), var(--bg));
--chip-bg: linear-gradient(
var(--accent-transparent),
var(--accent-transparent)
),
linear-gradient(var(--bg), var(--bg));
--accent-transparent: rgba(var(--accent-rgb), 0.25);
}
html {
background-color: var(--bg);
color: var(--fg);
font-family: sans-serif;
font-size: 1.125rem;
}
html, body {
margin: 0;
padding: 0;
}
.hidden {
display: none;
}

14
public/index.html Normal file
View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>watch party</title>
<link rel="stylesheet" href="/assets/main.css">
</head>
<body>
<main id="pre-join">
<h1>watch party</h1>
</main>
<main class="hidden" id=""></main>
</body>
</html>

View File

@ -1,15 +1,28 @@
use std::net::{IpAddr, SocketAddr}; use std::{
net::{IpAddr, SocketAddr},
sync::{Arc, Mutex},
};
use axum::{ use axum::{
extract::{ws::WebSocket, Query, WebSocketUpgrade}, extract::{
ws::{Message, WebSocket},
Query, WebSocketUpgrade,
},
http::StatusCode,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
routing::get, routing::{get, post},
Router, Server, Json, Router, Server,
}; };
use confique::Config; use confique::Config;
use futures::{SinkExt, StreamExt};
use miette::{Context, IntoDiagnostic, Result}; use miette::{Context, IntoDiagnostic, Result};
use rooms::create_room;
use serde::Deserialize; use serde::Deserialize;
use sources::MediaSource;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use uuid::Uuid;
use crate::rooms::{find_room, Member, RoomRef};
mod rooms; mod rooms;
mod sources; mod sources;
@ -25,12 +38,61 @@ struct WatchPartyConfig {
#[derive(Deserialize)] #[derive(Deserialize)]
struct WsQuery { struct WsQuery {
room: String, room: String,
rejoin_id: Option<String>,
} }
async fn handle_socket(socket: WebSocket, room: String) {}
async fn ws_handler(ws: WebSocketUpgrade, Query(q): Query<WsQuery>) -> Response { async fn ws_handler(ws: WebSocketUpgrade, Query(q): Query<WsQuery>) -> Response {
ws.on_upgrade(move |s| handle_socket(s, q.room)) let Ok(room_id) = Uuid::parse_str(&q.room) else {
return (StatusCode::BAD_REQUEST, "Malformed Room ID").into_response()
};
let Some(room) = find_room(room_id).await else {
return (StatusCode::NOT_FOUND, "Room does not exist").into_response()
};
let rejoin_id = q.rejoin_id.and_then(|s| Uuid::parse_str(&s).ok());
ws.on_upgrade(move |s| handle_socket(s, room, rejoin_id))
}
async fn handle_socket(mut socket: WebSocket, room: RoomRef, rejoin_id: Option<Uuid>) {
let (mut tx, mut rx) = socket.split();
let mut member: Member = match rejoin_id {
Some(_rejoin_id) => todo!("find existing member"),
None => Member {
id: Uuid::new_v4(),
chat_identity: None,
connections: vec![],
},
};
member.connections.push(tx);
let member_id = member.id;
room.add_member(member).await;
while let Some(Ok(message)) = rx.next().await {
match message {
Message::Text(text) => {
// TODO: parse a proto message
}
Message::Close(_) => break,
_ => continue,
}
}
}
#[derive(Deserialize)]
struct CreateRoomPayload {
playlist: Vec<MediaSource>,
}
async fn create_room_handler(Json(body): Json<CreateRoomPayload>) -> Response {
if body.playlist.is_empty() {
return (StatusCode::BAD_REQUEST, "").into_response();
}
let room = create_room(body.playlist).await;
let id = room.get().await.id;
id.to_string().into_response()
} }
#[tokio::main] #[tokio::main]
@ -44,9 +106,12 @@ async fn main() -> Result<()> {
let app = Router::new() let app = Router::new()
.fallback_service(ServeDir::new("public")) .fallback_service(ServeDir::new("public"))
.route("/api/ws", get(ws_handler)); .route("/api/ws", get(ws_handler))
.route("/api/create", post(create_room_handler));
let bind_address: SocketAddr = (conf.address, conf.port).into(); let bind_address: SocketAddr = (conf.address, conf.port).into();
println!("Listening on http://{} ...", &bind_address);
Server::bind(&bind_address) Server::bind(&bind_address)
.serve(app.into_make_service()) .serve(app.into_make_service())
.await .await

View File

@ -1,29 +1,95 @@
use std::{ use std::{collections::HashMap, sync::Arc};
collections::HashMap,
sync::{Arc, Mutex},
};
use axum::extract::ws::WebSocket; use axum::extract::ws::{Message, WebSocket};
use miette::Result; use dashmap::DashMap;
use futures::{stream::SplitSink, SinkExt};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::Serialize; use tokio::sync::{Mutex, MutexGuard};
use uuid::Uuid; use uuid::Uuid;
use crate::sources::MediaSource; use crate::sources::MediaSource;
// TODO: How do we store the active rooms? #[derive(Clone)]
pub struct RoomRef {
room: Arc<Mutex<Room>>,
}
impl RoomRef {
fn new(room: Room) -> Self {
RoomRef {
room: Arc::new(Mutex::new(room)),
}
}
pub async fn get(&self) -> MutexGuard<Room> {
self.room.lock().await
}
pub async fn add_member(&self, member: Member) {
let mut room = self.room.lock().await;
room.members.insert(member.id, member);
}
pub async fn broadcast(&self, message: Message) {
let mut room = self.room.lock().await;
for member in room.members.values_mut() {
member.send(message.clone()).await
}
}
pub async fn list_chat_members(&self) -> Vec<ChatIdentity> {
let room = self.room.lock().await;
room.members
.values()
.flat_map(|m| &m.chat_identity)
.cloned()
.collect()
}
}
static ROOMS: Lazy<DashMap<Uuid, RoomRef>> = Lazy::new(DashMap::new);
pub async fn create_room(playlist: Vec<MediaSource>) -> RoomRef {
let id = Uuid::new_v4();
let room = Room {
id,
playlist,
currently_playing: 0,
members: HashMap::new(),
};
let room_ref = RoomRef::new(room);
ROOMS.insert(id, room_ref.clone());
room_ref
}
pub async fn find_room(id: Uuid) -> Option<RoomRef> {
ROOMS.get(&id).as_deref().cloned()
}
#[derive(Serialize)]
pub struct Room { pub struct Room {
pub id: Uuid, pub id: Uuid,
pub playlist: Vec<MediaSource>, pub playlist: Vec<MediaSource>,
// TODO: Currently playing information pub currently_playing: usize,
#[serde(skip)] pub members: HashMap<Uuid, Member>,
pub members: Vec<Member>, }
#[derive(Clone)]
pub struct ChatIdentity {
pub nickname: String,
pub color: String,
} }
pub struct Member { pub struct Member {
pub id: Uuid, pub id: Uuid,
pub nickname: Option<String>, pub chat_identity: Option<ChatIdentity>,
pub socket: WebSocket, pub connections: Vec<SplitSink<WebSocket, Message>>,
}
impl Member {
pub async fn send(&mut self, message: Message) {
for conn in self.connections.iter_mut() {
let _ = conn.send(message.clone()).await;
}
}
} }

View File

@ -1,5 +1,4 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct DirectMediaSource { pub struct DirectMediaSource {