Skip to content

Commit

Permalink
Display track duration and task elapsed on the frontend
Browse files Browse the repository at this point in the history
switch to custom fork of rodio with the mp3dur patch applied
  • Loading branch information
beni69 committed Aug 25, 2023
1 parent 89c9ad8 commit 1d55c82
Show file tree
Hide file tree
Showing 12 changed files with 199 additions and 52 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ mail-send = { version = "0.4.0", default-features = false, features = ["dkim", "
mime_guess = "2.0.4"
pretty_env_logger = "0.5.0"
# https://github.com/RustAudio/rodio/pull/493 ChannelVolume bug
# https://github.com/RustAudio/rodio/pull/481 add mp3 durations
# symphonia-vorbis seems broken, replaced with lewton
rodio = { version = "0.17.1", default-features = false, features = ["symphonia-flac", "symphonia-isomp4", "symphonia-mp3", "symphonia-wav", "symphonia-aac", "vorbis"], git = "https://github.com/RustAudio/rodio.git", rev = "refs/pull/493/head" }
rodio = { version = "0.17.1", default-features = false, features = ["symphonia-flac", "symphonia-isomp4", "symphonia-mp3", "symphonia-wav", "symphonia-aac", "vorbis"], git = "https://github.com/beni69/rodio.git", branch = "master" }
rusqlite = { version = "0.29.0", features = ["bundled", "chrono"] }
rust-embed = "8.0.0"
serde = { version = "1.0.185", features = ["derive"] }
Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,4 @@ kelleni:
## todo

- [ ] task import
- [ ] relative duration format for tasks (maybe roll a custom localized impl on the server?)
- [ ] now playing duration and timestamp
- [ ] use [systemd credentials](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#Credentials) instead of an env file
5 changes: 3 additions & 2 deletions frontend/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,13 @@ htmx.on("#fileupload", "htmx:xhr:progress", function(e) {
});

const sleep = async ms => new Promise(r => setTimeout(r, ms));
(async () => {
document.addEventListener("DOMContentLoaded", async () => {
while (true) {
console.debug("sub realtime");
const res = await fetch("/htmx/status/realtime").then(r => r.text());
console.debug("recv realtime", res);
document.getElementById("status").outerHTML = res;
htmx.process(document.getElementById("status"));
await sleep(250);
}
})()
});
7 changes: 7 additions & 0 deletions frontend/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ header {
align-items: center;
width: 100%;
padding: 0 !important;
text-align: center;
}
#status-progress {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 1rem;
}

#title {
Expand Down
20 changes: 11 additions & 9 deletions src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ use rodio::{
};
use rusqlite::Connection;
use serde::Serialize;
use std::{collections::HashMap, io::Cursor, sync::Arc, time::Duration};
use std::{
collections::HashMap,
io::Cursor,
sync::Arc,
time::{Duration, Instant},
};
use tokio::sync::{
oneshot,
watch::{Receiver, Ref},
Expand All @@ -22,7 +27,7 @@ use tokio::sync::{

#[derive(Clone)]
pub struct Player {
pub controller: Controller,
controller: Controller,
pub conn: db::Db,
np_rx: Receiver<Option<NowPlaying>>,
cancel_map: Arc<Mutex<HashMap<String, oneshot::Sender<()>>>>,
Expand Down Expand Up @@ -75,10 +80,8 @@ impl Player {
fn inner(r: tokio::sync::watch::Ref<'_, Option<NowPlaying>>) -> Option<NowPlaying> {
r.to_owned()
}
let mut rx = self.np_realtime();
let mut rx = self.np_rx.clone();
async_stream::stream! {
let data = inner(rx.borrow());
yield data;
while rx.changed().await.is_ok() {
let data = inner(rx.borrow());
yield data;
Expand Down Expand Up @@ -152,10 +155,9 @@ impl PlayerLock<'_> {
})
}
}

#[derive(Debug, Clone, Serialize)]
#[derive(Debug, Clone)]
pub struct NowPlaying {
pub name: String,
// pos: Duration,
// len: Duration,
pub len: Option<Duration>,
pub started: Instant,
}
10 changes: 6 additions & 4 deletions src/scheduler.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{db, mail, player::Player, Task};
use crate::{db, mail, player::Player, templates::dur_human, Task};
use chrono::Local;
use futures_util::{stream::FuturesUnordered, StreamExt};
use tokio::{
Expand Down Expand Up @@ -27,8 +27,9 @@ pub async fn schedule(task: Task, player: Player) -> anyhow::Result<()> {
(time - Local::now()).to_std()?;

tokio::task::spawn(async move {
let diff = (time - Local::now()).to_std().unwrap();
debug!("{}: waiting {}s", name, diff.as_secs());
let diff = time - Local::now();
debug!("{}: {}", name, dur_human(&diff).0);
let diff = diff.to_std().unwrap();

let rx = player.create_cancel(name.to_owned()).await;
if select! {
Expand Down Expand Up @@ -76,7 +77,8 @@ pub async fn schedule(task: Task, player: Player) -> anyhow::Result<()> {
),
};
debug!(
"{name} (recurring): waiting {diff:.0?} for first play {}",
"{name} (recurring): {} {}",
dur_human(&chrono::Duration::from_std(diff.clone()).unwrap()).0,

Check warning on line 81 in src/scheduler.rs

View workflow job for this annotation

GitHub Actions / clippy

using `clone` on type `Duration` which implements the `Copy` trait

warning: using `clone` on type `Duration` which implements the `Copy` trait --> src/scheduler.rs:81:59 | 81 | dur_human(&chrono::Duration::from_std(diff.clone()).unwrap()).0, | ^^^^^^^^^^^^ help: try removing the `clone` call: `diff` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#clone_on_copy = note: `#[warn(clippy::clone_on_copy)]` on by default
if tmrw { "+" } else { "" }
);
let start: Instant = Instant::now() + diff;
Expand Down
48 changes: 28 additions & 20 deletions src/sink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,27 @@ use std::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
time::Duration,
time::{Duration, Instant},
};
use tokio::sync::watch::{self, Receiver, Sender};

struct Output {
controller: Controller,
np: Track,
track: Track,
np_tx: Sender<Option<NowPlaying>>,
}
impl Source for Output {
// should never return `None` or `0`
// see [`rodio::queue::SourcesQueueOutput::current_frame_len`]
#[inline]
fn current_frame_len(&self) -> Option<usize> {
if let Some(len) = self.np.src.current_frame_len() {
if let Some(len) = self.track.src.current_frame_len() {
if len > 0 {
return Some(len);
}
}

let (lower, _) = self.np.src.size_hint();
let (lower, _) = self.track.src.size_hint();
if lower > 0 {
return Some(lower);
}
Expand All @@ -41,12 +41,12 @@ impl Source for Output {

#[inline]
fn channels(&self) -> u16 {
self.np.src.channels()
self.track.src.channels()
}

#[inline]
fn sample_rate(&self) -> u32 {
self.np.src.sample_rate()
self.track.src.sample_rate()
}

#[inline]
Expand All @@ -60,39 +60,47 @@ impl Iterator for Output {
fn next(&mut self) -> Option<Self::Item> {
loop {
// keep playing current track
if let Some(sample) = self.np.src.next() {
if let Some(sample) = self.track.src.next() {
return Some(sample);
}

if let Some(name) = &self.np.name {
if let Some(name) = &self.track.name {
debug!("end of track: {:?}", name);
}

// get next track
let mut q = self.controller.q.lock().unwrap();
if let Some(next) = q.pop_front() {
self.np = next;
if let Some(name) = &self.np.name {
self.track = next;
if let Some(name) = &self.track.name {
info!("playing: {:?}", name);
}
} else {
// play a bit of silence
self.np = Track {
self.track = Track {
// this will give every play a worst-case 500ms delay, but in the context of this program and the benefits of lower resource usage, that's acceptable
src: Box::new(Zero::new(1, 44100).take_duration(Duration::from_millis(500))),
name: None,
};
}

// signal start of track
self.np_tx.send_if_modified(|np| {
// if self.np.name.is_none() && np.is_none() {
if self.np.name.as_ref() == np.as_ref().map(|n| &n.name) {
return false;
}
*np = self.np.name.clone().map(|name| NowPlaying { name });
true
});
self.np_tx
.send_if_modified(|prev| match (&prev, self.track.name.to_owned()) {
(None, None) => false,
(Some(_), None) => {
*prev = None;
true
}
(_, Some(name)) => {
*prev = Some(NowPlaying {
name,
len: self.track.src.total_duration(),
started: Instant::now(),
});
true
}
});
}
}
}
Expand All @@ -114,7 +122,7 @@ impl Controller {
};
let output = Output {
controller: controller.clone(),
np: Track {
track: Track {
src: Box::new(Empty::new()),
name: None,
},
Expand Down
Loading

0 comments on commit 1d55c82

Please sign in to comment.