Add files via upload
This commit is contained in:
parent
07f2ed525b
commit
6e7f0c1505
37 changed files with 4084 additions and 20 deletions
46
extras/rust/src/checksum.rs
Normal file
46
extras/rust/src/checksum.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// a small dependency-free crc32 (used nowhere, just a rust toy sitting
|
||||
// next to the real python download/convert pipeline).
|
||||
|
||||
const POLY: u32 = 0xEDB88320;
|
||||
|
||||
fn table() -> [u32; 256] {
|
||||
let mut table = [0u32; 256];
|
||||
let mut i = 0u32;
|
||||
while i < 256 {
|
||||
let mut c = i;
|
||||
let mut k = 0;
|
||||
while k < 8 {
|
||||
c = if c & 1 != 0 { POLY ^ (c >> 1) } else { c >> 1 };
|
||||
k += 1;
|
||||
}
|
||||
table[i as usize] = c;
|
||||
i += 1;
|
||||
}
|
||||
table
|
||||
}
|
||||
|
||||
pub fn crc32(data: &[u8]) -> u32 {
|
||||
let table = table();
|
||||
let mut crc: u32 = 0xFFFFFFFF;
|
||||
for &byte in data {
|
||||
let idx = ((crc ^ byte as u32) & 0xFF) as usize;
|
||||
crc = table[idx] ^ (crc >> 8);
|
||||
}
|
||||
crc ^ 0xFFFFFFFF
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn known_vector() {
|
||||
// crc32("123456789") == 0xCBF43926, the standard check value.
|
||||
assert_eq!(crc32(b"123456789"), 0xCBF43926);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_input() {
|
||||
assert_eq!(crc32(b""), 0);
|
||||
}
|
||||
}
|
||||
91
extras/rust/src/formats.rs
Normal file
91
extras/rust/src/formats.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
// mirrors the format list from app/backend/converter.py, just as a rust toy.
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AudioFormat {
|
||||
Mp3,
|
||||
Flac,
|
||||
Wav,
|
||||
M4a,
|
||||
Alac,
|
||||
Ogg,
|
||||
Opus,
|
||||
}
|
||||
|
||||
impl AudioFormat {
|
||||
pub fn all() -> &'static [AudioFormat] {
|
||||
use AudioFormat::*;
|
||||
&[Mp3, Flac, Wav, M4a, Alac, Ogg, Opus]
|
||||
}
|
||||
|
||||
pub fn extension(&self) -> &'static str {
|
||||
match self {
|
||||
AudioFormat::Mp3 => "mp3",
|
||||
AudioFormat::Flac => "flac",
|
||||
AudioFormat::Wav => "wav",
|
||||
AudioFormat::M4a => "m4a",
|
||||
AudioFormat::Alac => "alac",
|
||||
AudioFormat::Ogg => "ogg",
|
||||
AudioFormat::Opus => "opus",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_lossless(&self) -> bool {
|
||||
matches!(self, AudioFormat::Flac | AudioFormat::Wav | AudioFormat::Alac)
|
||||
}
|
||||
|
||||
pub fn from_extension(ext: &str) -> Option<AudioFormat> {
|
||||
AudioFormat::all()
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|f| f.extension().eq_ignore_ascii_case(ext))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ImageFormat {
|
||||
Jpg,
|
||||
Png,
|
||||
Webp,
|
||||
Bmp,
|
||||
Gif,
|
||||
Tiff,
|
||||
Ico,
|
||||
}
|
||||
|
||||
impl ImageFormat {
|
||||
pub fn all() -> &'static [ImageFormat] {
|
||||
use ImageFormat::*;
|
||||
&[Jpg, Png, Webp, Bmp, Gif, Tiff, Ico]
|
||||
}
|
||||
|
||||
pub fn extension(&self) -> &'static str {
|
||||
match self {
|
||||
ImageFormat::Jpg => "jpg",
|
||||
ImageFormat::Png => "png",
|
||||
ImageFormat::Webp => "webp",
|
||||
ImageFormat::Bmp => "bmp",
|
||||
ImageFormat::Gif => "gif",
|
||||
ImageFormat::Tiff => "tiff",
|
||||
ImageFormat::Ico => "ico",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn round_trips_extension() {
|
||||
for fmt in AudioFormat::all() {
|
||||
let ext = fmt.extension();
|
||||
assert_eq!(AudioFormat::from_extension(ext), Some(*fmt));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flac_is_lossless() {
|
||||
assert!(AudioFormat::Flac.is_lossless());
|
||||
assert!(!AudioFormat::Mp3.is_lossless());
|
||||
}
|
||||
}
|
||||
59
extras/rust/src/main.rs
Normal file
59
extras/rust/src/main.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// extras/rust — decorative toy crate, not wired into the actual nexdex.space
|
||||
// build (that's the C++ installer + python app). kept here as a small rust
|
||||
// playground alongside the app. build with: cargo run --manifest-path Cargo.toml
|
||||
|
||||
mod formats;
|
||||
mod tags;
|
||||
mod checksum;
|
||||
mod progress;
|
||||
mod playlist;
|
||||
|
||||
fn banner() -> &'static str {
|
||||
r#"
|
||||
_ __ _____ ____| | _____ __
|
||||
| '_ \ / _ \ \/ / _` |/ _ \ \/ /
|
||||
| | | | __/> < (_| | __/> <
|
||||
|_| |_|\___/_/\_\__,_|\___/_/\_\
|
||||
.space
|
||||
"#
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("{}", banner());
|
||||
|
||||
let track = tags::Tag {
|
||||
title: "MATADORA (Extended)".to_string(),
|
||||
artist: "SUKA.".to_string(),
|
||||
album: None,
|
||||
year: Some(2026),
|
||||
track_number: None,
|
||||
};
|
||||
println!("{}", track.describe());
|
||||
|
||||
for fmt in formats::AudioFormat::all() {
|
||||
println!(
|
||||
"{:<6} lossless={}",
|
||||
fmt.extension(),
|
||||
fmt.is_lossless()
|
||||
);
|
||||
}
|
||||
|
||||
let data = b"nexdex.space made by savsis";
|
||||
println!("crc32({:?}) = {:08x}", String::from_utf8_lossy(data), checksum::crc32(data));
|
||||
|
||||
progress::simulate_download("MATADORA (Extended)");
|
||||
|
||||
let mut pl = playlist::Playlist::new();
|
||||
pl.add(playlist::Track {
|
||||
tag: track.clone(),
|
||||
path: "downloads/MATADORA (Extended).flac".to_string(),
|
||||
duration_secs: 222,
|
||||
});
|
||||
println!(
|
||||
"playlist: {} track(s), total {}",
|
||||
pl.len(),
|
||||
playlist::Playlist::format_duration(pl.total_duration_secs())
|
||||
);
|
||||
|
||||
println!("made by savsis with <3");
|
||||
}
|
||||
111
extras/rust/src/playlist.rs
Normal file
111
extras/rust/src/playlist.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
// extras/rust — decorative toy: a tiny in-memory playlist model, echoing
|
||||
// the shape of data/history.json (backend/history.py) but with no I/O.
|
||||
|
||||
use crate::tags::Tag;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Track {
|
||||
pub tag: Tag,
|
||||
pub path: String,
|
||||
pub duration_secs: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Playlist {
|
||||
tracks: Vec<Track>,
|
||||
}
|
||||
|
||||
impl Playlist {
|
||||
pub fn new() -> Self {
|
||||
Playlist { tracks: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn add(&mut self, track: Track) {
|
||||
self.tracks.push(track);
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.tracks.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.tracks.is_empty()
|
||||
}
|
||||
|
||||
pub fn total_duration_secs(&self) -> u32 {
|
||||
self.tracks.iter().map(|t| t.duration_secs).sum()
|
||||
}
|
||||
|
||||
pub fn by_artist(&self, artist: &str) -> Vec<&Track> {
|
||||
self.tracks
|
||||
.iter()
|
||||
.filter(|t| t.tag.artist.eq_ignore_ascii_case(artist))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn sorted_by_title(&self) -> Vec<&Track> {
|
||||
let mut sorted: Vec<&Track> = self.tracks.iter().collect();
|
||||
sorted.sort_by(|a, b| a.tag.title.cmp(&b.tag.title));
|
||||
sorted
|
||||
}
|
||||
|
||||
pub fn longest(&self) -> Option<&Track> {
|
||||
self.tracks.iter().max_by_key(|t| t.duration_secs)
|
||||
}
|
||||
|
||||
pub fn format_duration(total_secs: u32) -> String {
|
||||
let hours = total_secs / 3600;
|
||||
let minutes = (total_secs % 3600) / 60;
|
||||
let seconds = total_secs % 60;
|
||||
if hours > 0 {
|
||||
format!("{}:{:02}:{:02}", hours, minutes, seconds)
|
||||
} else {
|
||||
format!("{}:{:02}", minutes, seconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tags::TagBuilder;
|
||||
|
||||
fn sample_track(title: &str, artist: &str, secs: u32) -> Track {
|
||||
Track {
|
||||
tag: TagBuilder::new(title, artist).build(),
|
||||
path: format!("downloads/{}.flac", title),
|
||||
duration_secs: secs,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn total_duration_sums_all_tracks() {
|
||||
let mut pl = Playlist::new();
|
||||
pl.add(sample_track("A", "X", 100));
|
||||
pl.add(sample_track("B", "Y", 200));
|
||||
assert_eq!(pl.total_duration_secs(), 300);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filters_by_artist_case_insensitively() {
|
||||
let mut pl = Playlist::new();
|
||||
pl.add(sample_track("A", "SUKA.", 100));
|
||||
pl.add(sample_track("B", "suka.", 50));
|
||||
pl.add(sample_track("C", "Someone Else", 10));
|
||||
assert_eq!(pl.by_artist("suka.").len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_longest_track() {
|
||||
let mut pl = Playlist::new();
|
||||
pl.add(sample_track("Short", "X", 60));
|
||||
pl.add(sample_track("Long", "X", 600));
|
||||
assert_eq!(pl.longest().unwrap().tag.title, "Long");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_duration_with_and_without_hours() {
|
||||
assert_eq!(Playlist::format_duration(65), "1:05");
|
||||
assert_eq!(Playlist::format_duration(3665), "1:01:05");
|
||||
}
|
||||
}
|
||||
61
extras/rust/src/progress.rs
Normal file
61
extras/rust/src/progress.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// a toy terminal progress bar, echoing the shape of the progress_hook
|
||||
// callbacks used by backend/jobs.py — just rendered with print! here
|
||||
// instead of pushed over the /api/job/<id> polling endpoint.
|
||||
|
||||
pub struct ProgressBar {
|
||||
width: usize,
|
||||
label: String,
|
||||
}
|
||||
|
||||
impl ProgressBar {
|
||||
pub fn new(label: impl Into<String>, width: usize) -> Self {
|
||||
ProgressBar {
|
||||
width,
|
||||
label: label.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&self, percent: u8) -> String {
|
||||
let percent = percent.min(100) as usize;
|
||||
let filled = (self.width * percent) / 100;
|
||||
let empty = self.width - filled;
|
||||
|
||||
format!(
|
||||
"{:<20} [{}{}] {:>3}%",
|
||||
self.label,
|
||||
"#".repeat(filled),
|
||||
"-".repeat(empty),
|
||||
percent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simulate_download(label: &str) {
|
||||
let bar = ProgressBar::new(label, 30);
|
||||
for pct in (0..=100).step_by(20) {
|
||||
println!("{}", bar.render(pct));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn zero_percent_is_all_empty() {
|
||||
let bar = ProgressBar::new("test", 10);
|
||||
assert_eq!(bar.render(0), format!("{:<20} [{}] {:>3}%", "test", "-".repeat(10), 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hundred_percent_is_all_filled() {
|
||||
let bar = ProgressBar::new("test", 10);
|
||||
assert_eq!(bar.render(100), format!("{:<20} [{}] {:>3}%", "test", "#".repeat(10), 100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clamps_above_100() {
|
||||
let bar = ProgressBar::new("test", 10);
|
||||
assert_eq!(bar.render(255), bar.render(100));
|
||||
}
|
||||
}
|
||||
86
extras/rust/src/tags.rs
Normal file
86
extras/rust/src/tags.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// a tiny stand-in for the tag struct app/backend/metadata.py works with —
|
||||
// decorative, the real read/write logic lives in python (mutagen).
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Tag {
|
||||
pub title: String,
|
||||
pub artist: String,
|
||||
pub album: Option<String>,
|
||||
pub year: Option<u32>,
|
||||
pub track_number: Option<u32>,
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
pub fn describe(&self) -> String {
|
||||
let mut parts = vec![format!("{} — {}", self.artist, self.title)];
|
||||
|
||||
if let Some(album) = &self.album {
|
||||
parts.push(format!("[{}]", album));
|
||||
}
|
||||
if let Some(year) = self.year {
|
||||
parts.push(format!("({})", year));
|
||||
}
|
||||
if let Some(track) = self.track_number {
|
||||
parts.push(format!("track #{}", track));
|
||||
}
|
||||
|
||||
parts.join(" ")
|
||||
}
|
||||
|
||||
pub fn is_complete(&self) -> bool {
|
||||
!self.title.is_empty() && !self.artist.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TagBuilder {
|
||||
tag: Tag,
|
||||
}
|
||||
|
||||
impl TagBuilder {
|
||||
pub fn new(title: impl Into<String>, artist: impl Into<String>) -> Self {
|
||||
TagBuilder {
|
||||
tag: Tag {
|
||||
title: title.into(),
|
||||
artist: artist.into(),
|
||||
album: None,
|
||||
year: None,
|
||||
track_number: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn album(mut self, album: impl Into<String>) -> Self {
|
||||
self.tag.album = Some(album.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn year(mut self, year: u32) -> Self {
|
||||
self.tag.year = Some(year);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn track_number(mut self, n: u32) -> Self {
|
||||
self.tag.track_number = Some(n);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Tag {
|
||||
self.tag
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn builder_produces_expected_tag() {
|
||||
let tag = TagBuilder::new("Aria Math", "Logan Feece")
|
||||
.album("C418 covers")
|
||||
.year(2024)
|
||||
.build();
|
||||
|
||||
assert!(tag.is_complete());
|
||||
assert_eq!(tag.describe(), "Logan Feece — Aria Math [C418 covers] (2024)");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue