Skip to content

Commit

Permalink
Merge pull request #72 from tsirysndr/feat/remote-stream
Browse files Browse the repository at this point in the history
network: add support for remote audio streams (http)
  • Loading branch information
tsirysndr authored Dec 24, 2024
2 parents be6bdcf + c0f1fe0 commit 33ab0b0
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 113 deletions.
12 changes: 12 additions & 0 deletions Cargo.lock

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

226 changes: 118 additions & 108 deletions crates/library/src/audio_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ use sqlx::{Pool, Sqlite};
use std::path::PathBuf;
use tokio::fs;

const AUDIO_EXTENSIONS: [&str; 17] = [
"mp3", "ogg", "flac", "m4a", "aac", "mp4", "alac", "wav", "wv", "mpc", "aiff", "ac3", "opus",
"spx", "sid", "ape", "wma",
const AUDIO_EXTENSIONS: [&str; 18] = [
"mp3", "ogg", "flac", "m4a", "aac", "mp4", "alac", "wav", "wv", "mpc", "aiff", "aif", "ac3",
"opus", "spx", "sid", "ape", "wma",
];

pub fn scan_audio_files(
Expand Down Expand Up @@ -44,111 +44,7 @@ pub fn scan_audio_files(
{
continue;
}
let filename = path.split('/').last().unwrap();
let dir = path.replace(filename, "");
println!(
"{} {}{}",
"Found".bright_green(),
dir,
filename.bright_yellow()
);
let entry = rb::metadata::get_metadata(-1, path);

let track_hash = format!("{:x}", md5::compute(entry.path.as_bytes()));
let artist_id = cuid::cuid1()?;
let album_id = cuid::cuid1()?;
let album_md5 = format!(
"{:x}",
md5::compute(
format!("{}{}{}", entry.albumartist, entry.album, entry.year).as_bytes()
)
);

let artist_id = repo::artist::save(
pool.clone(),
Artist {
id: artist_id.clone(),
name: match entry.albumartist.is_empty() {
true => entry.artist.clone(),
false => entry.albumartist.clone(),
},
bio: None,
image: None,
},
)
.await?;

let album_art = extract_and_save_album_cover(&entry.path)?;
let album_id = repo::album::save(
pool.clone(),
Album {
id: album_id,
title: entry.album.clone(),
artist: match entry.albumartist.is_empty() {
true => entry.artist.clone(),
false => entry.albumartist.clone(),
},
year: entry.year as u32,
year_string: entry.year_string.clone(),
album_art: album_art.clone(),
md5: album_md5,
artist_id: artist_id.clone(),
},
)
.await?;

let track_id = repo::track::save(
pool.clone(),
Track {
id: cuid::cuid1()?,
path: entry.path.clone(),
title: entry.title,
artist: entry.artist.clone(),
album: entry.album,
genre: match entry.genre_string.as_str() {
"" => None,
_ => Some(entry.genre_string),
},
year: Some(entry.year as u32),
track_number: Some(entry.tracknum as u32),
disc_number: entry.discnum as u32,
year_string: Some(entry.year_string),
composer: entry.composer,
album_artist: entry.albumartist.clone(),
bitrate: entry.bitrate,
frequency: entry.frequency as u32,
filesize: entry.filesize as u32,
length: entry.length as u32,
md5: track_hash,
created_at: Utc::now(),
updated_at: Utc::now(),
artist_id: artist_id.clone(),
album_id: album_id.clone(),
album_art,
..Default::default()
},
)
.await?;

repo::album_tracks::save(
pool.clone(),
AlbumTracks {
id: cuid::cuid1()?,
album_id,
track_id: track_id.clone(),
},
)
.await?;

repo::artist_tracks::save(
pool.clone(),
ArtistTracks {
id: cuid::cuid1()?,
artist_id,
track_id,
},
)
.await?;
save_audio_metadata(pool.clone(), path).await?;

let path = path.into();
result.push(path);
Expand All @@ -165,3 +61,117 @@ pub fn scan_audio_files(
Ok(result)
})
}

pub async fn save_audio_metadata(pool: Pool<Sqlite>, path: &str) -> Result<(), Error> {
if !AUDIO_EXTENSIONS
.into_iter()
.any(|ext| path.ends_with(&format!(".{}", ext)))
{
return Ok(());
}

let filename = path.split('/').last().unwrap();
let dir = path.replace(filename, "");
println!(
"{} {}{}",
"Found".bright_green(),
dir,
filename.bright_yellow()
);
let entry = rb::metadata::get_metadata(-1, path);

let track_hash = format!("{:x}", md5::compute(entry.path.as_bytes()));
let artist_id = cuid::cuid1()?;
let album_id = cuid::cuid1()?;
let album_md5 = format!(
"{:x}",
md5::compute(format!("{}{}{}", entry.albumartist, entry.album, entry.year).as_bytes())
);
let artist_id = repo::artist::save(
pool.clone(),
Artist {
id: artist_id.clone(),
name: match entry.albumartist.is_empty() {
true => entry.artist.clone(),
false => entry.albumartist.clone(),
},
bio: None,
image: None,
},
)
.await?;

let album_art = extract_and_save_album_cover(&entry.path)?;
let album_id = repo::album::save(
pool.clone(),
Album {
id: album_id,
title: entry.album.clone(),
artist: match entry.albumartist.is_empty() {
true => entry.artist.clone(),
false => entry.albumartist.clone(),
},
year: entry.year as u32,
year_string: entry.year_string.clone(),
album_art: album_art.clone(),
md5: album_md5,
artist_id: artist_id.clone(),
},
)
.await?;

let track_id = repo::track::save(
pool.clone(),
Track {
id: cuid::cuid1()?,
path: entry.path.clone(),
title: entry.title,
artist: entry.artist.clone(),
album: entry.album,
genre: match entry.genre_string.as_str() {
"" => None,
_ => Some(entry.genre_string),
},
year: Some(entry.year as u32),
track_number: Some(entry.tracknum as u32),
disc_number: entry.discnum as u32,
year_string: Some(entry.year_string),
composer: entry.composer,
album_artist: entry.albumartist.clone(),
bitrate: entry.bitrate,
frequency: entry.frequency as u32,
filesize: entry.filesize as u32,
length: entry.length as u32,
md5: track_hash,
created_at: Utc::now(),
updated_at: Utc::now(),
artist_id: artist_id.clone(),
album_id: album_id.clone(),
album_art,
..Default::default()
},
)
.await?;

repo::album_tracks::save(
pool.clone(),
AlbumTracks {
id: cuid::cuid1()?,
album_id,
track_id: track_id.clone(),
},
)
.await?;

repo::artist_tracks::save(
pool.clone(),
ArtistTracks {
id: cuid::cuid1()?,
artist_id,
track_id,
},
)
.await?;

Ok(())
}
14 changes: 14 additions & 0 deletions crates/network/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "rockbox-network"
version = "0.1.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true

[dependencies]
anyhow.workspace = true
dirs = "5.0.1"
md5 = "0.7.0"
reqwest.workspace = true
rockbox-library = { path = "../library" }
80 changes: 80 additions & 0 deletions crates/network/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use std::{fs, io::copy};

use anyhow::{anyhow, Error};
use rockbox_library::{audio_scan::save_audio_metadata, create_connection_pool};

pub async fn download(url: &str) -> Result<String, Error> {
if !url.starts_with("http") {
return Ok(url.to_string());
}

let home_dir = dirs::home_dir().unwrap();
let cache_dir = format!("{}/.cache/rockbox", home_dir.to_str().unwrap());
fs::create_dir_all(&cache_dir)?;

let client = reqwest::Client::new();
let response = client.head(url).send().await?;
let mime = response
.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap();
let extension = get_file_extension(mime)?;
let hash = md5::compute(url.as_bytes());
let file_path = format!("{}/{:x}.{}", cache_dir, hash, extension);

if fs::metadata(&file_path).is_ok() {
return Ok(file_path);
}

let client = reqwest::Client::new();
let response = client.get(url).send().await?;
let mut file = fs::File::create(&file_path)?;
let content = response.bytes().await?;

copy(&mut content.as_ref(), &mut file)?;

let pool = create_connection_pool().await?;
save_audio_metadata(pool, &file_path).await?;

Ok(file_path)
}

pub async fn download_tracks(urls: Vec<String>) -> Result<Vec<String>, Error> {
let mut files = Vec::new();
let urls: Vec<&str> = urls.iter().map(|url| url.as_str()).collect();
for url in urls {
match download(url).await {
Ok(path) => files.push(path),
Err(e) => eprintln!("Failed to download {}: {}", url, e),
}
}
Ok(files)
}

fn get_file_extension(mime: &str) -> Result<&str, Error> {
match mime {
"audio/mpeg" => Ok("mp3"),
"audio/ogg" => Ok("ogg"),
"audio/flac" => Ok("flac"),
"audio/x-m4a" => Ok("m4a"),
"audio/aac" => Ok("aac"),
"video/mp4" => Ok("mp4"),
"audio/wav" => Ok("wav"),
"audio/x-wav" => Ok("wav"),
"audio/x-wavpack" => Ok("wv"),
"audio/x-musepack" => Ok("mpc"),
"audio/aiff" => Ok("aiff"),
"audio/x-aiff" => Ok("aiff"),
"audio/ac3" => Ok("ac3"),
"audio/audio.vnd.dd-raw" => Ok("ac3"),
"audio/opus" => Ok("opus"),
"audio/x-speex" => Ok("spx"),
"audio/prs.sid" => Ok("sid"),
"audio/ape" => Ok("ape"),
"audio/x-monkeys-audio" => Ok("ape"),
"audio/x-ms-wma" => Ok("wma"),
_ => Err(anyhow!("Unsupported mime type: {}", mime)),
}
}
1 change: 1 addition & 0 deletions crates/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ rockbox-graphql = {path = "../graphql"}
rockbox-library = {path = "../library"}
rockbox-mpd = {path = "../mpd"}
rockbox-mpris = {path = "../mpris"}
rockbox-network = { path = "../network" }
rockbox-rpc = {path = "../rpc"}
rockbox-search = {path = "../search"}
rockbox-settings = {path = "../settings"}
Expand Down
Loading

0 comments on commit 33ab0b0

Please sign in to comment.