Skip to content

Commit

Permalink
Code cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
JasonWei512 committed Aug 18, 2023
1 parent 557cbb0 commit 042f460
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 76 deletions.
1 change: 1 addition & 0 deletions reference/rest_api.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// For more information, see AzuraCast's API documentation:
// https://docs.azuracast.com/en/developers/apis/now-playing-data
// https://www.azuracast.com/api/index.html#/Now%20Playing/get_nowplaying

{
"station": {
"id": 2,
Expand Down
1 change: 1 addition & 0 deletions reference/server_sent_events_api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// For more information, see AzuraCast's API documentation:
// https://docs.azuracast.com/en/developers/apis/now-playing-data
// https://www.azuracast.com/api/index.html#/Now%20Playing/get_nowplaying

data: {"connect":{"client":"0973e698-6712-4f7a-aeac-b84a8d3bf96c","version":"0.0.0","subs":{"station:coderadio":{},"global:time":{}},"ping":25,"session":"a1593db6-264a-44aa-b617-30c8eecb4948"}}

data: {"channel":"station:coderadio","pub":{"data":{"np":{"station":{"id":2,"name":"freeCodeCamp.org Code Radio","shortcode":"coderadio","description":"","frontend":"icecast","backend":"liquidsoap","listen_url":"https:\/\/coderadio-admin-v2.freecodecamp.org\/listen\/coderadio\/radio.mp3","url":"https:\/\/coderadio.freecodecamp.org","public_player_url":"https:\/\/coderadio-admin-v2.freecodecamp.org\/public\/coderadio","playlist_pls_url":"https:\/\/coderadio-admin-v2.freecodecamp.org\/public\/coderadio\/playlist.pls","playlist_m3u_url":"https:\/\/coderadio-admin-v2.freecodecamp.org\/public\/coderadio\/playlist.m3u","is_public":true,"mounts":[{"id":2,"name":"\/radio.mp3 (128kbps MP3)","url":"https:\/\/coderadio-admin-v2.freecodecamp.org\/listen\/coderadio\/radio.mp3","bitrate":128,"format":"mp3","listeners":{"total":32,"unique":32,"current":32},"path":"\/radio.mp3","is_default":true},{"id":3,"name":"64kbps MP3","url":"https:\/\/coderadio-admin-v2.freecodecamp.org\/listen\/coderadio\/low.mp3","bitrate":64,"format":"mp3","listeners":{"total":16,"unique":16,"current":16},"path":"\/low.mp3","is_default":false}],"remotes":[],"hls_enabled":false,"hls_url":null,"hls_listeners":0},"listeners":{"total":48,"unique":48,"current":48},"live":{"is_live":false,"streamer_name":"","broadcast_start":null,"art":null},"now_playing":{"sh_id":429609,"played_at":1692248797,"duration":258,"playlist":"default","streamer":"","is_request":false,"song":{"id":"ec8eac58ccf43fcbd92a9164b69191c9","text":"saib. - West Lake","artist":"saib.","title":"West Lake","album":"Chillhop Essentials - Fall 2017","genre":"","isrc":"","lyrics":"","art":"https:\/\/coderadio-admin-v2.freecodecamp.org\/api\/station\/2\/art\/536571950758c84d3c25e259-1586028052.jpg","custom_fields":[]},"elapsed":149,"remaining":109},"playing_next":{"cued_at":1692248792,"played_at":1692249047,"duration":201,"playlist":"default","is_request":false,"song":{"id":"4b76986ef878f10f402b5f8e283588cf","text":"DJ Okawari - Ring","artist":"DJ Okawari","title":"Ring","album":"Diorama","genre":"","isrc":"","lyrics":"","art":"https:\/\/coderadio-admin-v2.freecodecamp.org\/api\/station\/2\/art\/3e7d5e3b5ed030b12141c16a-1586028052.jpg","custom_fields":[]}},"song_history":[{"sh_id":429608,"played_at":1692248551,"duration":250,"playlist":"default","streamer":"","is_request":false,"song":{"id":"e97e7d2f9308e2aeb998200ad793fddf","text":"Blazo - In Memory Of Nujabes","artist":"Blazo","title":"In Memory Of Nujabes","album":"Flowers For Jun","genre":"","isrc":"","lyrics":"","art":"https:\/\/coderadio-admin-v2.freecodecamp.org\/api\/station\/2\/art\/41e9f315dd471c29e0fcd46d-1586028052.jpg","custom_fields":[]}},{"sh_id":429607,"played_at":1692248333,"duration":221,"playlist":"default","streamer":"","is_request":false,"song":{"id":"fa6858fb7fa37188f884fc7a24a8278e","text":" - distant worlds","artist":"","title":"distant worlds","album":"Melancholy Hopeful (Instrumental Version)","genre":"","isrc":"","lyrics":"","art":"https:\/\/coderadio-admin-v2.freecodecamp.org\/api\/station\/2\/art\/0a0c273f6a4ec98973146ab3-1586028052.jpg","custom_fields":[]}},{"sh_id":429606,"played_at":1692248269,"duration":67,"playlist":"default","streamer":"","is_request":false,"song":{"id":"79165090f7bfd22e72dc6dc793ae01b6","text":"tkdwn. - flwrs.","artist":"tkdwn.","title":"flwrs.","album":"repeat.repeat.","genre":"","isrc":"","lyrics":"","art":"https:\/\/coderadio-admin-v2.freecodecamp.org\/api\/station\/2\/art\/86065c2e9190db230c3511bf-1586028052.jpg","custom_fields":[]}},{"sh_id":429605,"played_at":1692248050,"duration":222,"playlist":"default","streamer":"","is_request":false,"song":{"id":"010195d40e088dffd4c7fa6456d0d0a0","text":"Birocratic - Shakedown","artist":"Birocratic","title":"Shakedown","album":"Chillhop Essentials - Summer 2018","genre":"","isrc":"","lyrics":"","art":"https:\/\/coderadio-admin-v2.freecodecamp.org\/api\/station\/2\/art\/12b2bb00fd0f1ade187ed723-1586028052.jpg","custom_fields":[]}},{"sh_id":429604,"played_at":1692247842,"duration":211,"playlist":"default","streamer":"","is_request":false,"song":{"id":"ea0028799cb64034be74b79da69ed77b","text":"Misha, cocabona - Khaleesi","artist":"Misha, cocabona","title":"Khaleesi","album":"Chillhop Essentials Summer 2019","genre":"","isrc":"","lyrics":"","art":"https:\/\/coderadio-admin-v2.freecodecamp.org\/api\/station\/2\/art\/8863403d5dc4175c0ee53407-1586028052.jpg","custom_fields":[]}}],"is_online":true,"cache":"event"}},"offset":60987}}
Expand Down
74 changes: 74 additions & 0 deletions src/code_radio_api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use crate::models::{
code_radio::{CodeRadioMessage, Remote},
server_sent_events::{Np, SeverSentEventsChannelMessage},
};
use anyhow::{anyhow, Result};
use eventsource_client::{Client, SSE::Event};
use futures_util::{FutureExt, Stream, StreamExt, TryStreamExt};
use std::{pin::Pin, time::Duration};

const REST_API_URL: &str =
"https://coderadio-admin-v2.freecodecamp.org/api/nowplaying_static/coderadio.json";
const SERVER_SENT_EVENTS_API_URL: &str =
"https://coderadio-admin-v2.freecodecamp.org/api/live/nowplaying/sse?cf_connect=%7B%22subs%22%3A%7B%22station%3Acoderadio%22%3A%7B%7D%7D%7D";

/// Get a `CodeRadioMessage` with REST API.
pub async fn get_message() -> Result<CodeRadioMessage> {
let message: CodeRadioMessage = reqwest::get(REST_API_URL).await?.json().await?;
Ok(message)
}

/// Get a `CodeRadioMessage` stream with Server-Sent Events API.
pub fn get_message_stream() -> Pin<Box<dyn Stream<Item = Result<CodeRadioMessage>>>> {
let sse_client = eventsource_client::ClientBuilder::for_url(SERVER_SENT_EVENTS_API_URL)
.unwrap()
.reconnect(
eventsource_client::ReconnectOptions::reconnect(true)
.retry_initial(false)
.delay(Duration::from_secs(1))
.backoff_factor(2)
.delay_max(Duration::from_secs(20))
.build(),
)
.build();

let mut sse_stream = sse_client.stream();

sse_stream.next().now_or_never(); // Poll once to start connecting immediately

let sse_message_stream = sse_stream
.try_filter_map(|response| async move {
if let Event(event) = response {
if let Ok(message) =
serde_json::from_str::<SeverSentEventsChannelMessage<Np>>(&event.data)
{
return Ok(Some(message.r#pub.data.np));
}
}
Ok(None)
})
.map_err(|error| anyhow!("Server-Sent Events Error: {:#?}", error))
.into_stream();

Box::pin(sse_message_stream)
}

/// Get all stations with REST API.
pub async fn get_stations() -> Result<Vec<Remote>> {
let message = get_message().await?;
let stations = get_stations_from_message(&message);
Ok(stations)
}

/// Get all stations from an existing `CodeRadioMessage`.
pub fn get_stations_from_message(message: &CodeRadioMessage) -> Vec<Remote> {
let mut stations: Vec<Remote> = Vec::new();
for remote in &message.station.remotes {
stations.push(remote.clone());
}
for mount in &message.station.mounts {
stations.push(mount.clone().into());
}
stations.sort_by_key(|s| s.id);
stations
}
90 changes: 14 additions & 76 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod args;
mod code_radio_api;
mod models;
mod mp3_stream_decoder;
mod player;
Expand All @@ -10,23 +11,14 @@ use anyhow::{anyhow, Context, Result};
use args::Args;
use clap::Parser;
use colored::Colorize;
use eventsource_client::{Client, SSE::Event};
use futures_util::{FutureExt, StreamExt};
use futures_util::StreamExt;
use indicatif::{ProgressBar, ProgressState, ProgressStyle};
use inquire::Select;
use models::{
code_radio::{CodeRadioMessage, Remote},
server_sent_events::{Np, SeverSentEventsChannelMessage},
};
use models::code_radio::{CodeRadioMessage, Remote};
use player::Player;
use rodio::Source;
use std::{fmt::Write, sync::Mutex, thread, time::Duration};

const SERVER_SENT_EVENTS_API_URL: &str =
"https://coderadio-admin-v2.freecodecamp.org/api/live/nowplaying/sse?cf_connect=%7B%22subs%22%3A%7B%22station%3Acoderadio%22%3A%7B%7D%7D%7D";
const REST_API_URL: &str =
"https://coderadio-admin-v2.freecodecamp.org/api/nowplaying_static/coderadio.json";

const LOADING_SPINNER_TICK_INTERVAL: Duration = Duration::from_millis(120);

static PLAYER: Mutex<Option<Player>> = Mutex::new(None);
Expand Down Expand Up @@ -69,20 +61,10 @@ async fn start_playing(args: Args) -> Result<()> {
};

// Fetching data in background while creating `Player` to improve startup speed
let get_rest_api_message_task = tokio::spawn(get_rest_api_message());
let sse_client = eventsource_client::ClientBuilder::for_url(SERVER_SENT_EVENTS_API_URL)
.unwrap()
.reconnect(
eventsource_client::ReconnectOptions::reconnect(true)
.retry_initial(false)
.delay(Duration::from_secs(1))
.backoff_factor(2)
.delay_max(Duration::from_secs(20))
.build(),
)
.build();
let mut sse_stream = sse_client.stream();
sse_stream.next().now_or_never(); // Poll once to start connecting immediately
// Note: Here we use the REST API to get the first API message,
// because getting the first message from the Server-Sent Events stream may be slow
let get_message_task = tokio::spawn(code_radio_api::get_message());
let mut message_stream = code_radio_api::get_message_stream();

let loading_spinner = ProgressBar::new_spinner()
.with_style(ProgressStyle::with_template("{spinner} {msg}")?)
Expand All @@ -102,12 +84,10 @@ async fn start_playing(args: Args) -> Result<()> {
}

loading_spinner.set_message("Connecting...");

let message = get_rest_api_message_task.await??;

let message = get_message_task.await??;
loading_spinner.finish_and_clear();

let stations = get_stations_from_api_message(&message);
let stations = code_radio_api::get_stations_from_message(&message);

let listen_url = match selected_station {
Some(ref station) => stations
Expand Down Expand Up @@ -144,10 +124,11 @@ async fn start_playing(args: Args) -> Result<()> {
tokio::spawn(tick_progress_bar_progress());
thread::spawn(handle_keyboard_input);

loop {
let message = get_next_sse_message(&mut sse_stream).await?;
update_song_info_on_screen(message, &mut last_song_id);
while let Some(message) = message_stream.next().await {
update_song_info_on_screen(message?, &mut last_song_id);
}

Err(anyhow!("Server-Sent Events connection was closed"))
}

fn display_welcome_message(args: &Args) {
Expand Down Expand Up @@ -181,26 +162,6 @@ Run {} to get more help.",
println!();
}

async fn get_next_sse_message(
sse_stream: &mut std::pin::Pin<
Box<
dyn futures_util::Stream<
Item = std::result::Result<eventsource_client::SSE, eventsource_client::Error>,
> + Send
+ Sync,
>,
>,
) -> Result<CodeRadioMessage> {
loop {
if let Some(Ok(Event(event))) = sse_stream.next().await {
if let Ok(data) = serde_json::from_str::<SeverSentEventsChannelMessage<Np>>(&event.data)
{
return Ok(data.r#pub.data.np);
}
}
}
}

/// Update progress bar's progress and listeners count suffix.
///
/// If song id changes, print the new song's info on screen.
Expand Down Expand Up @@ -334,7 +295,7 @@ async fn select_station_interactively() -> Result<Remote> {
.with_message("Connecting...");
loading_spinner.enable_steady_tick(LOADING_SPINNER_TICK_INTERVAL);

let stations = get_stations_from_rest_api().await?;
let stations = code_radio_api::get_stations().await?;

loading_spinner.finish_and_clear();

Expand All @@ -353,26 +314,3 @@ async fn select_station_interactively() -> Result<Remote> {

Ok(selected_station)
}

async fn get_rest_api_message() -> Result<CodeRadioMessage> {
let message: CodeRadioMessage = reqwest::get(REST_API_URL).await?.json().await?;
Ok(message)
}

async fn get_stations_from_rest_api() -> Result<Vec<Remote>> {
let message = get_rest_api_message().await?;
let stations = get_stations_from_api_message(&message);
Ok(stations)
}

fn get_stations_from_api_message(message: &CodeRadioMessage) -> Vec<Remote> {
let mut stations: Vec<Remote> = Vec::new();
for remote in &message.station.remotes {
stations.push(remote.clone());
}
for mount in &message.station.mounts {
stations.push(mount.clone().into());
}
stations.sort_by_key(|s| s.id);
stations
}

0 comments on commit 042f460

Please sign in to comment.