use std::net::SocketAddr;
use std::sync::{mpsc, Arc, Mutex};
use musicdb_lib::data::album::Album;
use musicdb_lib::data::artist::Artist;
use musicdb_lib::data::database::{Database, UpdateEndpoint};
use musicdb_lib::data::queue::{Queue, QueueContent, QueueFolder};
use musicdb_lib::data::song::Song;
use musicdb_lib::data::SongId;
use musicdb_lib::server::{Action, Command, Req};
use rocket::futures::{SinkExt, StreamExt};
use rocket::response::content::RawHtml;
use rocket::{get, routes, Config, State};
use rocket_seek_stream::SeekStream;
use rocket_ws::{Message, WebSocket};
use tokio::select;
use tokio::sync::mpsc::Sender;
/*
23E9 ⏩︎ fast forward
23EA ⏪︎ rewind, fast backwards
23EB ⏫︎ fast increase
23EC ⏬︎ fast decrease
23ED ⏭︎ skip to end, next
23EE ⏮︎ skip to start, previous
23EF ⏯︎ play/pause toggle
23F1 ⏱︎ stopwatch
23F2 ⏲︎ timer clock
23F3 ⏳︎ hourglass
23F4 ⏴︎ reverse, back
23F5 ⏵︎ forward, next, play
23F6 ⏶︎ increase
23F7 ⏷︎ decrease
23F8 ⏸︎ pause
23F9 ⏹︎ stop
23FA ⏺︎ record
*/
const HTML_START: &'static str =
"
";
const HTML_SEP: &'static str = "";
const HTML_END: &'static str = "";
struct Data {
db: Arc>,
command_sender: mpsc::Sender<(Command, Option)>,
websocket_connections: Arc>>>,
}
#[get("/")]
fn index(data: &State) -> RawHtml {
let script = r#"
"#;
let script2 = r#"
"#;
let buttons = "";
let search = "
";
let playback_live = r#""#;
let db = data.db.lock().unwrap();
let now_playing = gen_now_playing(&db);
let mut queue = String::new();
gen_queue_html(&db.queue, &mut queue, &db);
drop(db);
RawHtml(format!(
"{HTML_START}MusicDb{script}{HTML_SEP}
no javascript? reload to see updated information.
{now_playing}
{buttons}
{playback_live}
{search}
{queue}
{script2}{HTML_END}",
))
}
#[get("/now-playing-html")]
fn now_playing_html(data: &State) -> RawHtml {
RawHtml(gen_now_playing(&*data.db.lock().unwrap()))
}
#[get("/now-playing-ids")]
fn now_playing_ids(data: &State) -> String {
let db = data.db.lock().unwrap();
let (c, n) = (
db.queue.get_current_song().copied(),
db.queue.get_next_song().copied(),
);
drop(db);
if let Some(c) = c {
if let Some(n) = n {
format!("{c}/{n}")
} else {
format!("{c}")
}
} else {
"".to_owned()
}
}
#[get("/song/")]
fn song1(data: &State, id: SongId) -> Option> {
song(data, id)
}
#[get("/song//<_>")]
fn song2(data: &State, id: SongId) -> Option> {
song(data, id)
}
fn song(data: &State, id: SongId) -> Option> {
let db = data.db.lock().unwrap();
if let Some(song) = db.get_song(&id) {
song.cached_data().cache_data_start_thread(&*db, song);
if let Some(bytes) = song.cached_data().cached_data_await() {
drop(db);
Some(SeekStream::new(std::io::Cursor::new(ArcBytes(bytes))))
} else {
None
}
} else {
None
}
}
struct ArcBytes(pub Arc>);
impl AsRef<[u8]> for ArcBytes {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
#[get("/queue-html")]
fn queue_html(data: &State) -> RawHtml {
let mut str = String::new();
let db = data.db.lock().unwrap();
gen_queue_html(&db.queue, &mut str, &db);
RawHtml(str)
}
fn gen_now_playing(db: &Database) -> String {
if let Some(current_song) = db.queue.get_current_song().and_then(|id| db.get_song(id)) {
format!(
"
");
str.push_str(&format!("");
str.push_str("");
if let Some(artist) = db.artists().get(&song.artist) {
str.push_str(" by ");
str.push_str(&html_escape::encode_text(&artist.name));
}
if let Some(album) = song.album.as_ref().and_then(|id| db.albums().get(id)) {
str.push_str(" on ");
str.push_str(&html_escape::encode_text(&album.name));
}
str.push_str(&format!(
""
));
str.push_str("
");
} else {
str.push_str("
unknown song
");
}
}
QueueContent::Folder(f) => {
let html_shuf: &'static str = " shuffled";
if f.content.is_empty() {
str.push_str("[0/0] ");
if active_highlight {
str.push_str("");
}
str.push_str(&html_escape::encode_text(&f.name));
if active_highlight {
str.push_str("");
}
if f.order.is_some() {
str.push_str(html_shuf);
}
} else {
str.push_str(&format!("[{}/{}] ", f.index + 1, f.content.len(),));
if active_highlight {
str.push_str("");
}
str.push_str(&html_escape::encode_text(&f.name));
if active_highlight {
str.push_str("");
}
if f.order.is_some() {
str.push_str(html_shuf);
}
str.push_str("");
for (i, v) in f.iter().enumerate() {
str.push_str("
");
if !path.is_empty() {
path.push('_');
}
path.push_str(&format!("{i}"));
gen_queue_html_impl(v, str, db, active_highlight && i == f.index, path);
while !(path.is_empty() || path.ends_with('_')) {
path.pop();
}
path.pop();
str.push_str("
");
}
str.push_str("");
}
}
QueueContent::Loop(d, t, i) => {
if active_highlight {
str.push_str("");
}
if *t == 0 {
str.push_str(&format!("[{}/∞]", d + 1));
} else {
str.push_str(&format!("[{}/{}]", d + 1, t));
}
if active_highlight {
str.push_str("");
}
if !path.is_empty() {
path.push('_');
}
path.push('0');
gen_queue_html_impl(i, str, db, active_highlight, path);
while !(path.is_empty() || path.ends_with('_')) {
path.pop();
}
path.pop();
}
}
}
#[get("/queue-remove/")]
fn queue_remove(data: &State, path: &str) {
if let Some(path) = path.split('_').map(|v| v.parse().ok()).collect() {
data.command_sender
.send((Action::QueueRemove(path).cmd(0xFFu8), None))
.unwrap();
}
}
#[get("/queue-goto/")]
fn queue_goto(data: &State, path: &str) {
if let Some(path) = path.split('_').map(|v| v.parse().ok()).collect() {
data.command_sender
.send((Action::QueueGoto(path).cmd(0xFFu8), None))
.unwrap();
}
}
#[get("/play")]
fn play(data: &State) {
data.command_sender
.send((Action::Resume.cmd(0xFFu8), None))
.unwrap();
}
#[get("/pause")]
fn pause(data: &State) {
data.command_sender
.send((Action::Pause.cmd(0xFFu8), None))
.unwrap();
}
#[get("/stop")]
fn stop(data: &State) {
data.command_sender
.send((Action::Stop.cmd(0xFFu8), None))
.unwrap();
}
#[get("/skip")]
fn skip(data: &State) {
data.command_sender
.send((Action::NextSong.cmd(0xFFu8), None))
.unwrap();
}
#[get("/clear-queue")]
fn clear_queue(data: &State) {
data.command_sender
.send((
Action::QueueUpdate(
vec![],
QueueContent::Folder(QueueFolder {
index: 0,
content: vec![],
name: String::new(),
order: None,
})
.into(),
Req::none(),
)
.cmd(0xFFu8),
None,
))
.unwrap();
}
#[get("/add-song/")]
fn add_song(data: &State, id: SongId) {
data.command_sender
.send((
Action::QueueAdd(vec![], vec![QueueContent::Song(id).into()], Req::none()).cmd(0xFFu8),
None,
))
.unwrap();
}
#[get("/search?&&&&&")]
fn search(
data: &State,
artist: Option<&str>,
album: Option<&str>,
title: Option<&str>,
artist_tags: Vec<&str>,
album_tags: Vec<&str>,
song_tags: Vec<&str>,
) -> RawHtml {
let db = data.db.lock().unwrap();
let mut out = String::new();
let artist = artist.map(|v| v.to_lowercase());
let artist = artist.as_ref().map(|v| v.as_str());
let album = album.map(|v| v.to_lowercase());
let album = album.as_ref().map(|v| v.as_str());
let title = title.map(|v| v.to_lowercase());
let title = title.as_ref().map(|v| v.as_str());
find1(
&*db,
artist,
album,
title,
&artist_tags,
&album_tags,
&song_tags,
&mut out,
);
fn find1(
db: &Database,
artist: Option<&str>,
album: Option<&str>,
title: Option<&str>,
artist_tags: &[&str],
album_tags: &[&str],
song_tags: &[&str],
out: &mut String,
) {
if let Some(f) = artist {
find2(
db,
db.artists()
.values()
.filter(|v| v.name.to_lowercase().contains(f)),
album,
title,
artist_tags,
album_tags,
song_tags,
out,
)
} else {
find2(
db,
db.artists().values(),
album,
title,
artist_tags,
album_tags,
song_tags,
out,
)
}
}
fn find2<'a>(
db: &'a Database,
artists: impl IntoIterator,
album: Option<&str>,
title: Option<&str>,
artist_tags: &[&str],
album_tags: &[&str],
song_tags: &[&str],
out: &mut String,
) {
for artist in artists {
if artist_tags
.iter()
.all(|t| artist.general.tags.iter().any(|v| v == t))
{
let mut func_artist = Some(|out: &mut String| {
out.push_str("
");
out.push_str(&artist.name);
out.push_str("
");
});
let mut func_album = None;
if false {
// so they have the same type
std::mem::swap(&mut func_artist, &mut func_album);
}
if album.is_none() && album_tags.is_empty() {
find4(
db,
artist.singles.iter().filter_map(|v| db.get_song(v)),
title,
song_tags,
out,
&mut func_artist,
&mut func_album,
);
}
let iter = artist.albums.iter().filter_map(|v| db.albums().get(v));
if let Some(f) = album {
find3(
db,
iter.filter(|v| v.name.to_lowercase().contains(f)),
title,
album_tags,
song_tags,
out,
&mut func_artist,
)
} else {
find3(
db,
iter,
title,
album_tags,
song_tags,
out,
&mut func_artist,
)
}
}
}
}
fn find3<'a>(
db: &'a Database,
albums: impl IntoIterator,
title: Option<&str>,
album_tags: &[&str],
song_tags: &[&str],
out: &mut String,
func_artist: &mut Option,
) {
for album in albums {
if album_tags
.iter()
.all(|t| album.general.tags.iter().any(|v| v == t))
{
let mut func_album = Some(|out: &mut String| {
out.push_str("
");
out.push_str(&album.name);
out.push_str("
");
});
find4(
db,
album.songs.iter().filter_map(|v| db.get_song(v)),
title,
song_tags,
out,
func_artist,
&mut func_album,
)
}
}
}
fn find4<'a>(
db: &'a Database,
songs: impl IntoIterator,
title: Option<&str>,
song_tags: &[&str],
out: &mut String,
func_artist: &mut Option,
func_album: &mut Option,
) {
if let Some(f) = title {
find5(
db,
songs
.into_iter()
.filter(|v| v.title.to_lowercase().contains(f)),
song_tags,
out,
func_artist,
func_album,
)
} else {
find5(db, songs, song_tags, out, func_artist, func_album)
}
}
fn find5<'a>(
db: &'a Database,
songs: impl IntoIterator,
song_tags: &[&str],
out: &mut String,
func_artist: &mut Option,
func_album: &mut Option,
) {
for song in songs {
if song_tags
.iter()
.all(|t| song.general.tags.iter().any(|v| v == t))
{
find6(db, song, out, func_artist, func_album)
}
}
}
fn find6<'a>(
_db: &Database,
song: &Song,
out: &mut String,
func_artist: &mut Option,
func_album: &mut Option,
) {
if let Some(f) = func_artist.take() {
f(out)
}
if let Some(f) = func_album.take() {
f(out)
}
out.push_str(" ");
}
RawHtml(out)
}
#[get("/ws")]
async fn websocket(websocket: WebSocket, state: &State) -> rocket_ws::Channel<'static> {
// a channel so other threads/tasks can send messages to this websocket client
let (sender, mut receiver) = tokio::sync::mpsc::channel(5);
state.websocket_connections.lock().await.push(sender);
let (db_playing, ()) = tokio::task::block_in_place(|| {
let db = state.db.lock().unwrap();
(db.playing, ())
});
// handle messages from the websocket and from the channel
websocket.channel(move |mut websocket| {
Box::pin(async move {
if db_playing {
let _ = websocket.send(Message::text("init/playing=true")).await;
} else {
let _ = websocket.send(Message::text("init/playing=false")).await;
}
loop {
// async magic:
// handle a message from the websocket client or from other
// threads/tasks in the server, whichever happens first
select! {
message = websocket.next() => {
if let Some(message) = message {
// server received `message` from the websocket client
match message? {
Message::Text(text) => {
// it was a text message, prefix it with "You sent: " and echo
websocket
.send(Message::text(format!("You sent: {text}")))
.await?
}
Message::Binary(_bytes) => {
// it was a binary message, ignore it
}
Message::Ping(payload) => {
websocket.send(Message::Pong(payload)).await?
}
Message::Close(close) => {
websocket.close(close).await?;
break;
}
// these messages get ignored
Message::Pong(_) | Message::Frame(_) => (),
}
} else {
// websocket connection was closed
break;
}
},
message_to_be_sent = receiver.recv() => {
if let Some(message) = message_to_be_sent {
// server received `message` from another thread/task
websocket.send(message).await?;
} else {
// channel has been closed, close websocket connection too
websocket.close(None).await?;
break;
}
},
}
}
Ok(())
})
})
}
pub fn main(
db: Arc>,
command_sender: mpsc::Sender<(Command, Option)>,
addr: SocketAddr,
) {
let websocket_connections = Arc::new(tokio::sync::Mutex::new(vec![]));
let data = Data {
db: Arc::clone(&db),
command_sender,
websocket_connections: Arc::clone(&websocket_connections),
};
let mut db = db.lock().unwrap();
let udepid = db.update_endpoints_id;
db.update_endpoints_id += 1;
db.update_endpoints.push((
udepid,
UpdateEndpoint::Custom(Box::new(move |cmd| {
let mut msgs = vec![];
fn action(a: &Action, msgs: &mut Vec) {
match a {
Action::Resume => msgs.push(Message::text("resume")),
Action::Pause => msgs.push(Message::text("pause")),
Action::Stop => msgs.push(Message::text("stop")),
Action::NextSong => msgs.push(Message::text("next")),
Action::SyncDatabase(..)
| Action::AddSong(..)
| Action::AddAlbum(..)
| Action::AddArtist(..)
| Action::AddCover(..)
| Action::ModifySong(..)
| Action::ModifyAlbum(..)
| Action::ModifyArtist(..)
| Action::RemoveSong(..)
| Action::RemoveAlbum(..)
| Action::RemoveArtist(..)
| Action::SetSongDuration(..)
| Action::TagSongFlagSet(..)
| Action::TagSongFlagUnset(..)
| Action::TagAlbumFlagSet(..)
| Action::TagAlbumFlagUnset(..)
| Action::TagArtistFlagSet(..)
| Action::TagArtistFlagUnset(..)
| Action::TagSongPropertySet(..)
| Action::TagSongPropertyUnset(..)
| Action::TagAlbumPropertySet(..)
| Action::TagAlbumPropertyUnset(..)
| Action::TagArtistPropertySet(..)
| Action::TagArtistPropertyUnset(..) => msgs.push(Message::text("update/data")),
Action::QueueUpdate(..)
| Action::QueueAdd(..)
| Action::QueueInsert(..)
| Action::QueueRemove(..)
| Action::QueueMove(..)
| Action::QueueMoveInto(..)
| Action::QueueGoto(..)
| Action::QueueShuffle(..)
| Action::QueueSetShuffle(..)
| Action::QueueUnshuffle(..) => msgs.push(Message::text("update/queue")),
Action::Multiple(actions) => {
for inner in actions {
action(inner, msgs);
}
}
Action::InitComplete
| Action::Save
| Action::ErrorInfo(..)
| Action::Denied(..) => {}
}
}
action(&cmd.action, &mut msgs);
if !msgs.is_empty() {
let mut ws_cons = websocket_connections.blocking_lock();
let mut rm = vec![];
for msg in msgs {
rm.clear();
for (i, con) in ws_cons.iter_mut().enumerate() {
if con.blocking_send(msg.clone()).is_err() {
rm.push(i);
}
}
for i in rm.iter().rev() {
ws_cons.remove(*i);
}
}
}
})),
));
drop(db);
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async_main(data, addr));
}
async fn async_main(data: Data, addr: SocketAddr) {
rocket::build()
.configure(Config {
address: addr.ip(),
port: addr.port(),
..Default::default()
})
.manage(data)
.mount(
"/",
routes![
index,
websocket,
play,
pause,
stop,
skip,
clear_queue,
queue_goto,
queue_remove,
add_song,
search,
now_playing_html,
now_playing_ids,
song1,
song2,
queue_html,
],
)
.launch()
.await
.unwrap();
}