Chunked song download boiler
This commit is contained in:
parent
120d991360
commit
71cc75feff
5 changed files with 50 additions and 10 deletions
|
@ -6,9 +6,11 @@ pub struct Metadata {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub artist: Option<String>,
|
pub artist: Option<String>,
|
||||||
pub playing: bool,
|
pub playing: bool,
|
||||||
pub duration: i32,
|
pub duration: u32,
|
||||||
pub current_time: i32,
|
pub current_time: u32,
|
||||||
pub cover: DynamicImage,
|
pub cover: DynamicImage,
|
||||||
|
pub size: u32,
|
||||||
|
pub bytes_received: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Metadata {
|
impl Metadata {
|
||||||
|
@ -20,6 +22,8 @@ impl Metadata {
|
||||||
duration: 0,
|
duration: 0,
|
||||||
current_time: 0,
|
current_time: 0,
|
||||||
cover: default_cover(),
|
cover: default_cover(),
|
||||||
|
size: 0,
|
||||||
|
bytes_received: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,5 +36,7 @@ impl Metadata {
|
||||||
self.artist = song.artist;
|
self.artist = song.artist;
|
||||||
self.current_time = 0;
|
self.current_time = 0;
|
||||||
self.duration = song.duration;
|
self.duration = song.duration;
|
||||||
|
self.bytes_received = 0;
|
||||||
|
self.size = song.size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,8 @@ use crossterm::event::{poll, read, Event, KeyCode};
|
||||||
use image::DynamicImage;
|
use image::DynamicImage;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ssonic::{response::Song, APIEvent},
|
audio::AudioEvent,
|
||||||
|
ssonic::{response::Song, APIEvent, MAX_CHUNK_SIZE},
|
||||||
utils::{default_cover, Error},
|
utils::{default_cover, Error},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -79,8 +80,20 @@ impl Player {
|
||||||
self.player_chan_in
|
self.player_chan_in
|
||||||
.send(PlayerEvent::UpdateCover(default_cover()))?;
|
.send(PlayerEvent::UpdateCover(default_cover()))?;
|
||||||
}
|
}
|
||||||
|
self.fetch_audio_chunk(song.id.clone())?;
|
||||||
self.tui_root.metadata.update_metadata(song);
|
self.tui_root.metadata.update_metadata(song);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fetch_audio_chunk(&mut self, id: String) -> Result<(), Error> {
|
||||||
|
let (mut start, mut end) = (0, 0);
|
||||||
|
if self.tui_root.metadata.bytes_received < self.tui_root.metadata.size {
|
||||||
|
start = self.tui_root.metadata.bytes_received;
|
||||||
|
let diff = self.tui_root.metadata.size - self.tui_root.metadata.bytes_received;
|
||||||
|
end = std::cmp::min(diff, MAX_CHUNK_SIZE);
|
||||||
|
self.api_chan.send(APIEvent::StreamSong(id, start, end))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::io::{BufReader, Read};
|
||||||
|
|
||||||
use reqwest::{
|
use reqwest::{
|
||||||
blocking::Response,
|
blocking::Response,
|
||||||
header::{ACCEPT, CONTENT_TYPE},
|
header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, RANGE},
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
@ -24,14 +24,29 @@ impl APIClient {
|
||||||
let player_resp = match self.api_requests.recv()? {
|
let player_resp = match self.api_requests.recv()? {
|
||||||
super::APIEvent::FetchRandom(n) => self.get_random(n)?,
|
super::APIEvent::FetchRandom(n) => self.get_random(n)?,
|
||||||
super::APIEvent::FetchCoverArt(id) => self.get_cover_art(id)?,
|
super::APIEvent::FetchCoverArt(id) => self.get_cover_art(id)?,
|
||||||
|
super::APIEvent::StreamSong(id, start, end) => self.stream_song(id, start, end)?,
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
};
|
};
|
||||||
self.player_chan.send(player_resp)?;
|
self.player_chan.send(player_resp)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn stream_song(
|
||||||
|
&mut self,
|
||||||
|
id: String,
|
||||||
|
byte_start: u32,
|
||||||
|
byte_end: u32,
|
||||||
|
) -> Result<PlayerEvent, Error> {
|
||||||
|
let range_header = format!("bytes={}-{}", byte_start, byte_end);
|
||||||
|
let mut header = HeaderMap::new();
|
||||||
|
header.insert(RANGE, range_header.parse()?);
|
||||||
|
let mut response = self.request("/stream", Some(&[("id", id)]), Some(header))?;
|
||||||
|
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
fn get_cover_art(&mut self, cover_id: String) -> Result<PlayerEvent, Error> {
|
fn get_cover_art(&mut self, cover_id: String) -> Result<PlayerEvent, Error> {
|
||||||
let mut response = self.request("/getCoverArt", Some(&[("id", cover_id)]))?;
|
let mut response = self.request("/getCoverArt", Some(&[("id", cover_id)]), None)?;
|
||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
return Err(Box::new(APIError::StatusError(
|
return Err(Box::new(APIError::StatusError(
|
||||||
response.status(),
|
response.status(),
|
||||||
|
@ -54,7 +69,7 @@ impl APIClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_random(&mut self, n: i32) -> Result<PlayerEvent, Error> {
|
fn get_random(&mut self, n: i32) -> Result<PlayerEvent, Error> {
|
||||||
let response = self.request("/getRandomSongs", Some(&[("size", n.to_string())]))?;
|
let response = self.request("/getRandomSongs", Some(&[("size", n.to_string())]), None)?;
|
||||||
let song_list = self
|
let song_list = self
|
||||||
.validate(response, "/getRandomSongs")?
|
.validate(response, "/getRandomSongs")?
|
||||||
.subsonic_response
|
.subsonic_response
|
||||||
|
@ -66,7 +81,7 @@ impl APIClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ping(&mut self) -> Result<(), Error> {
|
fn ping(&mut self) -> Result<(), Error> {
|
||||||
let response = self.request::<[(&str, String); 0]>("/ping.view", None)?;
|
let response = self.request::<[(&str, String); 0]>("/ping.view", None, None)?;
|
||||||
self.validate(response, "/ping.view")?;
|
self.validate(response, "/ping.view")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -98,6 +113,7 @@ impl APIClient {
|
||||||
&mut self,
|
&mut self,
|
||||||
uri: &str,
|
uri: &str,
|
||||||
extra_params: Option<&T>,
|
extra_params: Option<&T>,
|
||||||
|
extra_headers: Option<HeaderMap>,
|
||||||
) -> Result<Response, Error> {
|
) -> Result<Response, Error> {
|
||||||
let (token, salt) = self.generate_random_token();
|
let (token, salt) = self.generate_random_token();
|
||||||
let mut req = self
|
let mut req = self
|
||||||
|
@ -114,7 +130,9 @@ impl APIClient {
|
||||||
if let Some(params) = extra_params {
|
if let Some(params) = extra_params {
|
||||||
req = req.query(params);
|
req = req.query(params);
|
||||||
}
|
}
|
||||||
req = req.header(ACCEPT, "application/json");
|
if let Some(headers) = extra_headers {
|
||||||
|
req = req.headers(headers);
|
||||||
|
}
|
||||||
Ok(req.send()?)
|
Ok(req.send()?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,12 @@ use reqwest::blocking::Client;
|
||||||
|
|
||||||
use crate::{config::Settings, player::PlayerEvent, utils::Error};
|
use crate::{config::Settings, player::PlayerEvent, utils::Error};
|
||||||
|
|
||||||
|
pub const MAX_CHUNK_SIZE: u32 = 1000000;
|
||||||
|
|
||||||
pub enum APIEvent {
|
pub enum APIEvent {
|
||||||
FetchRandom(i32),
|
FetchRandom(i32),
|
||||||
FetchCoverArt(String),
|
FetchCoverArt(String),
|
||||||
StreamSong(String),
|
StreamSong(String, u32, u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct APIClient {
|
pub struct APIClient {
|
||||||
|
|
|
@ -36,5 +36,6 @@ pub struct Song {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
#[serde(alias = "coverArt")]
|
#[serde(alias = "coverArt")]
|
||||||
pub cover_art: Option<String>,
|
pub cover_art: Option<String>,
|
||||||
pub duration: i32,
|
pub duration: u32,
|
||||||
|
pub size: u32,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue