whatbpm 1.0.1

💓 Today's Trending Values for EDM Production
use crate::Key;
use crate::Meta;
use crate::TopPage;
use anyhow::{anyhow, Result};
use chrono::Duration;
use serde_json::Value;
use std::str::FromStr;

const START_PHRASE: &str = "window.Playables = ";
const END_PHRASE: &str = "window.Sliders";

#[derive(Debug)]
pub struct Track {
    pub bpm: u16,
    pub key: Key,
    pub label: String,
    pub duration: Duration,
    pub specific_genre: Option<String>,
}

struct Context {
    fill_genre: bool,
}

impl TryFrom<(&Value, Context)> for Track {
    type Error = anyhow::Error;

    fn try_from(value_with_ctx: (&Value, Context)) -> Result<Self> {
        let (value, ctx) = value_with_ctx;
        let object = value.as_object().ok_or_else(|| anyhow!("Missing object"))?;

        let bpm = object
            .get("bpm")
            .ok_or_else(|| anyhow!("Missing bpm"))?
            .as_u64()
            .ok_or_else(|| anyhow!("Invalid bpm"))? as u16;

        let key = object
            .get("key")
            .ok_or_else(|| anyhow!("Missing key"))?
            .as_str()
            .map(Key::from_str)
            .ok_or_else(|| anyhow!("Invalid key"))??;

        let label = object
            .get("label")
            .ok_or_else(|| anyhow!("Missing label"))?
            .get("name")
            .ok_or_else(|| anyhow!("Missing label name"))?
            .as_str()
            .ok_or_else(|| anyhow!("Invalid label name"))?
            .to_string();

        let duration_ms = object
            .get("duration")
            .ok_or_else(|| anyhow!("Missing duration"))?
            .get("milliseconds")
            .ok_or_else(|| anyhow!("Missing duration milliseconds"))?
            .as_i64()
            .ok_or_else(|| anyhow!("Invalid duration milliseconds"))?;

        let duration = Duration::milliseconds(duration_ms);

        let specific_genre = match ctx.fill_genre {
            true => Some(
                object
                    .get("genres")
                    .ok_or_else(|| anyhow!("Missing genres"))?
                    .as_array()
                    .ok_or_else(|| anyhow!("Invalid genres"))?
                    .iter()
                    .next()
                    .ok_or_else(|| anyhow!("Empty genres"))?
                    .get("name")
                    .ok_or_else(|| anyhow!("Missing genre name"))?
                    .as_str()
                    .ok_or_else(|| anyhow!("Invalid genre name"))?
                    .to_string(),
            ),
            false => None,
        };

        Ok(Track {
            bpm,
            key,
            label,
            duration,
            specific_genre,
        })
    }
}

pub struct TopTrackVec {
    pub meta: Meta,
    pub tracks: Vec<Track>,
}

impl TryFrom<TopPage> for TopTrackVec {
    type Error = anyhow::Error;

    fn try_from(top_page: TopPage) -> Result<Self> {
        let start_pos = top_page
            .text
            .find(START_PHRASE)
            .ok_or_else(|| anyhow!("Missing START_PHRASE"))?
            + START_PHRASE.len();
        let end_pos = top_page
            .text
            .find(END_PHRASE)
            .ok_or_else(|| anyhow!("Missing END_PHRASE"))?;
        let raw_json = top_page.text[start_pos..end_pos]
            .trim()
            .trim_end_matches(';');
        let json: Value = serde_json::from_str(raw_json)?;
        let fill_genre = top_page.meta.common_genre.is_none();
        Ok(TopTrackVec {
            meta: top_page.meta,
            tracks: json
                .get("tracks")
                .ok_or_else(|| anyhow!("Missing tracks"))?
                .as_array()
                .ok_or_else(|| anyhow!("Invalid tracks"))?
                .iter()
                .filter_map(|v| Track::try_from((v, Context { fill_genre })).ok())
                .collect(),
        })
    }
}