kopuz-server 0.8.0

A modern, lightweight music player built with Rust and Dioxus.
#[derive(Clone, Debug, PartialEq)]
pub enum DownloadStatus {
    Queued,
    Downloading,
    Done,
    Failed,
}

use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

#[derive(Clone, Debug, Default)]
pub struct DownloadProgress {
    pub per_item: HashMap<String, u64>,
    pub bytes_done_session: u64,
    pub session_elapsed_secs: f64,
}

#[derive(Clone, Debug)]
pub struct DownloadItem {
    pub id: String,
    pub title: String,
    pub artist: String,
    pub status: DownloadStatus,
    pub bytes_done: u64,
    pub bytes_total: u64,
}

#[derive(Clone, Debug, Default)]
pub struct DownloadQueue {
    pub items: Vec<DownloadItem>,
    pub is_running: bool,
    pub cancel_requested: bool,
    pub cancel_flag: Arc<AtomicBool>,
    pub bytes_done_session: u64,
    pub session_elapsed_secs: f64,
}

impl DownloadQueue {
    pub fn is_active(&self) -> bool {
        self.items.iter().any(|i| {
            matches!(
                i.status,
                DownloadStatus::Queued | DownloadStatus::Downloading
            )
        })
    }

    pub fn done_count(&self) -> usize {
        self.items
            .iter()
            .filter(|i| matches!(i.status, DownloadStatus::Done))
            .count()
    }

    pub fn total_non_failed(&self) -> usize {
        self.items
            .iter()
            .filter(|i| !matches!(i.status, DownloadStatus::Failed))
            .count()
    }

    pub fn current(&self) -> Option<&DownloadItem> {
        self.items
            .iter()
            .find(|i| matches!(i.status, DownloadStatus::Downloading))
    }

    pub fn eta_secs(&self) -> Option<u64> {
        let elapsed = self.session_elapsed_secs;
        if elapsed < 0.5 || self.bytes_done_session == 0 {
            return None;
        }
        let bps = self.bytes_done_session as f64 / elapsed;

        let avg_size: u64 = {
            let known: Vec<u64> = self
                .items
                .iter()
                .filter(|i| i.bytes_total > 0)
                .map(|i| i.bytes_total)
                .collect();
            if known.is_empty() {
                8_000_000
            } else {
                known.iter().sum::<u64>() / known.len() as u64
            }
        };

        let remaining: u64 = self
            .items
            .iter()
            .filter(|i| {
                matches!(
                    i.status,
                    DownloadStatus::Queued | DownloadStatus::Downloading
                )
            })
            .map(|i| {
                if i.bytes_total > 0 {
                    i.bytes_total.saturating_sub(i.bytes_done)
                } else {
                    avg_size
                }
            })
            .sum();

        if bps > 0.0 {
            Some((remaining as f64 / bps) as u64)
        } else {
            None
        }
    }

    pub fn dismiss(&mut self) {
        self.items.retain(|i| {
            matches!(
                i.status,
                DownloadStatus::Queued | DownloadStatus::Downloading
            )
        });
        if self.items.is_empty() {
            self.bytes_done_session = 0;
            self.session_elapsed_secs = 0.0;
        }
    }

    pub fn cancel_all(&mut self) {
        self.cancel_requested = true;
        self.cancel_flag.store(true, Ordering::Relaxed);
        for item in &mut self.items {
            if matches!(
                item.status,
                DownloadStatus::Queued | DownloadStatus::Downloading
            ) {
                item.status = DownloadStatus::Failed;
            }
        }
    }
}