Skip to content

Commit 149e9a3

Browse files
author
PtiBouchon
committed
Should be working with a minimal README
1 parent 2b19d68 commit 149e9a3

29 files changed

+1400
-1
lines changed

.cargo/config.toml

Whitespace-only changes.

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ Cargo.lock
88

99
# These are backup files generated by rustfmt
1010
**/*.rs.bk
11+
12+
# IntelliJ IDEA
13+
/.idea/

Cargo.toml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "jukebox_rust"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[workspace]
9+
members = [".", "jukebox_axum", "jukebox_yew", "my_youtube_extractor"]
10+
11+
[dependencies]

README.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1-
# jukebox_rust
1+
# jukebox_rust
2+
3+
Trying to create a Jukebox using Rust (axum + yew)
4+
5+
## Run the server
6+
7+
Build the frontend using `trunk build` inside the jukebox_yew directory
8+
Then run `cargo run -p jukebox_axum`

jukebox_axum/.gitignore

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Generated by Cargo
2+
# will have compiled files and executables
3+
/target/
4+
5+
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
6+
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
7+
Cargo.lock
8+
9+
# These are backup files generated by rustfmt
10+
**/*.rs.bk
11+
12+
# IntelliJ IDEA
13+
/.idea/

jukebox_axum/Cargo.toml

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "jukebox_axum"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
9+
my_youtube_extractor = { path = "../my_youtube_extractor" }
10+
serde = { version = "1.0", features = ["derive"] }
11+
axum = { version = "0.6", features = ["headers", "macros", "form", "query", "ws"] }
12+
tokio = { version = "1.0", features = ["full"] }
13+
futures = "0.3"
14+
askama = "0.11"
15+
serde_json = "1.0"
16+
tracing = "0.1"
17+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
18+
tower-http = { version = "0.3", features = ["full"] }
19+
tower = "0.4"

jukebox_axum/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# JukeboxAxum
2+
3+
This is my version of the Jukebox CJ in Rust

jukebox_axum/askama.toml

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[general]
2+
# Directories to search for templates, relative to the crate root.
3+
dirs = ["templates"]
4+
# Unless you add a `-` in a block, whitespace characters won't be trimmed.
5+
whitespace = "preserve"
6+
7+
#[[escaper]]
8+
#path = "" # I have no idea what to do
9+
#extensions = ["askama"]

jukebox_axum/src/main.rs

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#![feature(let_chains)]
2+
#[allow(unused_imports)] // remove useless warning for developping
3+
4+
#[allow(unused_imports)]
5+
mod search;
6+
#[allow(unused_imports)]
7+
mod templates;
8+
#[allow(unused_imports)]
9+
mod websocket;
10+
11+
use crate::templates::index::IndexTemplate;
12+
use axum::body::{boxed, Body};
13+
use axum::extract::State;
14+
use axum::http::Response;
15+
use axum::http::StatusCode;
16+
use axum::response::Redirect;
17+
use axum::{
18+
response::IntoResponse,
19+
routing::{get, post},
20+
Router, Server,
21+
};
22+
use my_youtube_extractor::youtube_info::YtVideoPageInfo;
23+
use std::net::SocketAddr;
24+
use std::path::PathBuf;
25+
use std::str::FromStr;
26+
use std::sync::Arc;
27+
use std::time::Duration;
28+
use tokio::sync::{broadcast, Mutex};
29+
use tower::{ServiceBuilder, ServiceExt};
30+
use tower_http::services::ServeDir;
31+
use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt;
32+
use tracing_subscriber::util::SubscriberInitExt;
33+
34+
#[derive(Debug)]
35+
pub struct AppState {
36+
pub list: Mutex<Vec<YtVideoPageInfo>>,
37+
pub tx: broadcast::Sender<String>,
38+
}
39+
40+
#[derive(Debug)]
41+
pub enum MusicChange {
42+
NewMusic(YtVideoPageInfo),
43+
RemoveMusic(YtVideoPageInfo),
44+
}
45+
46+
// #[derive(Debug)]
47+
// pub enum MusicPlayerChange {
48+
// PlayFirstMusic,
49+
// FinishPlaying,
50+
// }
51+
52+
#[tokio::main]
53+
async fn main() {
54+
// Tracing
55+
tracing_subscriber::registry()
56+
.with(
57+
tracing_subscriber::EnvFilter::try_from_default_env()
58+
.unwrap_or_else(|_| "jukebox_axum=debug,tower_http=debug".into()),
59+
)
60+
.with(tracing_subscriber::fmt::layer())
61+
.init();
62+
63+
let (tx, _rx) = broadcast::channel(100);
64+
65+
// Channel between music player and axum web server
66+
let app_state = Arc::new(AppState {
67+
list: Mutex::new(vec![]),
68+
tx,
69+
});
70+
// let (mut send_new_music, mut receive_new_music): (Sender<MusicChange>, Receiver<MusicChange>) = tokio::sync::mpsc::channel(10);
71+
// let (mut send_player_update, mut receive_player_update): (Sender<MusicPlayerChange>, Receiver<MusicPlayerChange>) = tokio::sync::mpsc::channel(10);
72+
73+
let app_state_copy = app_state.clone();
74+
75+
// Music player
76+
let music_player = async move {
77+
let mut interval = tokio::time::interval(Duration::from_secs_f32(5.0));
78+
79+
loop {
80+
tokio::select! {
81+
_ = interval.tick() => {
82+
// tracing::debug!("20 seconds has passed");
83+
// let mut playlist = app_state_copy.list.lock().await;
84+
// if (playlist.len() > 0) {
85+
// playlist.remove(0);
86+
// }
87+
}
88+
}
89+
}
90+
};
91+
92+
tokio::spawn(music_player);
93+
94+
// Axum web server
95+
let app = Router::new()
96+
.route("/", get(|| async { Redirect::permanent("/index") }))
97+
// .route("/index", get(main_page))
98+
.fallback_service(get(|req| async move {
99+
match ServeDir::new("../jukebox_yew/dist/").oneshot(req).await {
100+
Ok(res) => {
101+
let status = res.status();
102+
match status {
103+
StatusCode::NOT_FOUND => {
104+
let index_path =
105+
PathBuf::from("../jukebox_yew/dist/").join("index.html");
106+
let index_content = match tokio::fs::read_to_string(index_path).await {
107+
Ok(index_content) => index_content,
108+
Err(_) => {
109+
return Response::builder()
110+
.status(StatusCode::NOT_FOUND)
111+
.body(boxed(Body::from("index file not found")))
112+
.unwrap()
113+
}
114+
};
115+
116+
Response::builder()
117+
.status(StatusCode::OK)
118+
.body(boxed(Body::from(index_content)))
119+
.unwrap()
120+
}
121+
_ => res.map(boxed),
122+
}
123+
}
124+
Err(err) => Response::builder()
125+
.status(StatusCode::INTERNAL_SERVER_ERROR)
126+
.body(boxed(Body::from(format!("error: {err}"))))
127+
.expect("error response"),
128+
}
129+
}))
130+
// .route("/search", post(search::search))
131+
// .route("/add_music", post(search::add_music))
132+
.route("/websocket", get(websocket::websocket_handler))
133+
.with_state(app_state);
134+
135+
let addr = SocketAddr::from_str("127.0.0.1:4000").unwrap();
136+
tracing::info!("Starting server on http://{addr}/index");
137+
138+
Server::bind(&addr)
139+
.serve(app.into_make_service())
140+
.await
141+
.unwrap();
142+
}
143+
144+
async fn main_page(State(app_state): State<Arc<AppState>>) -> impl IntoResponse {
145+
let playlist = app_state.list.lock().await;
146+
let template = IndexTemplate {
147+
username: "User".to_string(),
148+
playlist: playlist.clone(),
149+
searched_musics: vec![],
150+
};
151+
templates::HtmlTemplate(template)
152+
}

jukebox_axum/src/search.rs

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use std::sync::Arc;
2+
use axum::extract::State;
3+
use axum::Form;
4+
use axum::response::{IntoResponse, Redirect};
5+
use my_youtube_extractor::youtube_info::YtVideoPageInfo;
6+
use serde::Deserialize;
7+
use crate::{AppState, MusicChange, templates};
8+
use crate::templates::index::IndexTemplate;
9+
10+
#[derive(Debug, Deserialize)]
11+
pub struct SearchForm {
12+
pub search: String,
13+
}
14+
15+
#[axum::debug_handler]
16+
pub async fn search(
17+
State(app_state): State<Arc<AppState>>,
18+
search_form: Form<SearchForm>,
19+
) -> impl IntoResponse {
20+
let videos = my_youtube_extractor::search_videos(&search_form.search).await;
21+
let playlist: Vec<YtVideoPageInfo> = app_state.list.lock().await.clone();
22+
let template = IndexTemplate {
23+
username: "User".to_string(),
24+
playlist,
25+
searched_musics: videos,
26+
};
27+
templates::HtmlTemplate(template)
28+
}
29+
30+
#[derive(Debug, Deserialize)]
31+
pub struct AddMusicForm {
32+
pub video_serialized: String,
33+
}
34+
35+
#[axum::debug_handler]
36+
pub async fn add_music(State(app_state): State<Arc<AppState>>, index_input: Form<AddMusicForm>) -> Redirect {
37+
// TODO : do better
38+
if let Ok(video) = serde_json::from_str::<YtVideoPageInfo>(&index_input.video_serialized[1..]) { // Remove the first '#' caracter
39+
let mut added_video = false;
40+
let mut playlist = app_state.list.lock().await;
41+
added_video = true;
42+
tracing::debug!("Video Add to playlist");
43+
playlist.push(video.clone());
44+
}
45+
else {
46+
tracing::warn!("Canno't parse the video : {}", index_input.video_serialized);
47+
}
48+
Redirect::to("/")
49+
}

jukebox_axum/src/templates.rs

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
pub mod index;
2+
3+
use askama::Template;
4+
use axum::http::StatusCode;
5+
use axum::response::{Html, IntoResponse, Response};
6+
7+
pub struct HtmlTemplate<T>(pub T);
8+
9+
impl<T: Template> IntoResponse for HtmlTemplate<T> {
10+
fn into_response(self) -> Response {
11+
match self.0.render() {
12+
Ok(html) => Html(html).into_response(),
13+
Err(err) => (
14+
StatusCode::INTERNAL_SERVER_ERROR,
15+
format!("Failed to render template. Error: {err}"),
16+
)
17+
.into_response(),
18+
}
19+
}
20+
}

jukebox_axum/src/templates/index.rs

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use askama::Template;
2+
3+
#[derive(Template)]
4+
#[template(path = "index.html")]
5+
pub struct IndexTemplate {
6+
pub username: String,
7+
pub playlist: Vec<my_youtube_extractor::youtube_info::YtVideoPageInfo>,
8+
pub searched_musics: Vec<my_youtube_extractor::youtube_info::YtVideoPageInfo>,
9+
}

jukebox_axum/src/websocket.rs

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use std::sync::Arc;
2+
use axum::extract::{State, WebSocketUpgrade};
3+
use axum::extract::ws::{Message, WebSocket};
4+
use axum::response::IntoResponse;
5+
use futures::{sink::SinkExt, stream::StreamExt};
6+
use futures::stream::iter;
7+
use my_youtube_extractor::youtube_info::YtVideoPageInfo;
8+
use crate::AppState;
9+
10+
pub enum NetData {
11+
Remove(YtVideoPageInfo),
12+
Add(YtVideoPageInfo),
13+
Search(Vec<YtVideoPageInfo>),
14+
}
15+
16+
fn video_data_to_string(video_info: &YtVideoPageInfo) -> String {
17+
format!("({};{};{})", video_info.id, video_info.title, video_info.thumbnail)
18+
}
19+
20+
impl ToString for NetData {
21+
fn to_string(&self) -> String {
22+
match self {
23+
NetData::Remove(v) => format!("rem {}", video_data_to_string(v)),
24+
NetData::Add(v) => format!("add {}", video_data_to_string(v)),
25+
NetData::Search(s) => {
26+
let videos_stringified: Vec<String> = s.iter().map(video_data_to_string).collect();
27+
let res = videos_stringified.join("|");
28+
format!("sch [{res}]")
29+
}
30+
}
31+
}
32+
}
33+
34+
impl NetData {
35+
pub fn to_message(&self) -> Message {
36+
Message::Text(self.to_string())
37+
}
38+
}
39+
40+
pub async fn websocket_handler(
41+
ws: WebSocketUpgrade,
42+
State(state): State<Arc<AppState>>,
43+
) -> impl IntoResponse {
44+
ws.on_upgrade(|socket| websocket(socket, state))
45+
}
46+
47+
async fn websocket(stream: WebSocket, state: Arc<AppState>) {
48+
let (mut sender, mut receiver) = stream.split();
49+
50+
let mut rx = state.tx.subscribe();
51+
52+
let mut recv_user_task = tokio::spawn(async move {
53+
while let Some(Ok(data)) = receiver.next().await {
54+
if let Message::Text(data) = data {
55+
tracing::info!("REMOVE SOMETHING: {:?}", data);
56+
let video_id = &data[7..];
57+
let mut playlist = state.list.lock().await;
58+
if let Some((index, _)) = playlist.iter().enumerate().find(|(_, m)| m.id == video_id) {
59+
playlist.remove(index);
60+
state.tx.send(format!("rem {video_id}")).unwrap();
61+
}
62+
}
63+
}
64+
});
65+
66+
let mut broadcast_task = tokio::spawn(async move {
67+
while let Ok(msg) = rx.recv().await {
68+
if sender.send(Message::Text(msg)).await.is_err() {
69+
break;
70+
}
71+
}
72+
});
73+
74+
tokio::select! {
75+
_ = (&mut broadcast_task) => recv_user_task.abort(),
76+
_ = (&mut recv_user_task) => broadcast_task.abort(),
77+
}
78+
}

0 commit comments

Comments
 (0)