more Stuff
parent
fd0d9127c8
commit
6aff9f52b0
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
81
src/main.rs
81
src/main.rs
|
@ -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
|
||||||
|
|
94
src/rooms.rs
94
src/rooms.rs
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue