kodik-parser 3.0.0

A Rust library for getting direct links to files from Kodik.
Documentation
use crate::{KODIK_STATE, scraper::Response};
use base64::{Engine as _, engine::general_purpose};
use kodik_utils::KodikError;

const MIN_SHIFT: u8 = 0;
const MAX_SHIFT: u8 = 26;

/// Decodes links in the Kodik response.
///
/// # Errors
///
/// Returns a `KodikError` if decoding fails for any of the links.
pub fn decode_links(kodik_response: &mut Response) -> Result<(), KodikError> {
    log::debug!("Decoding links...");

    for link in &mut kodik_response.links.quality_360 {
        link.src = decode_link(&link.src)?;
    }

    let base_360 = kodik_response.links.quality_360.first();

    for link in &mut kodik_response.links.quality_480 {
        link.src = match base_360 {
            Some(link) => link.src.replace("/360.mp4", "/480.mp4"),
            None => decode_link(&link.src)?,
        };
    }

    for link in &mut kodik_response.links.quality_720 {
        link.src = match base_360 {
            Some(link) => link.src.replace("/360.mp4", "/720.mp4"),
            None => decode_link(&link.src)?,
        };
    }

    log::trace!("Decoded links: {:#?}", kodik_response.links);
    Ok(())
}

pub fn decode_link(src: &str) -> Result<String, KodikError> {
    let shift = KODIK_STATE.shift().clamp(MIN_SHIFT, MAX_SHIFT);

    if let Ok(decoded) = try_decode(src, shift) {
        return Ok(decoded);
    }

    for shift in MIN_SHIFT..=MAX_SHIFT {
        if let Ok(decoded) = try_decode(src, shift) {
            KODIK_STATE.set_shift(shift);
            return Ok(decoded);
        }
    }

    Err(KodikError::LinkCannotBeDecoded(src.to_owned()))
}

pub fn try_decode(encoded: &str, shift: u8) -> Result<String, KodikError> {
    let mut decoded_caesar = caesar_cipher(encoded, shift);

    while !decoded_caesar.len().is_multiple_of(4) {
        decoded_caesar.push('=');
    }

    let decode_result = decode_base64(&decoded_caesar);

    if let Ok(mut decoded) = decode_result {
        if !decoded.starts_with("https:") {
            decoded.insert_str(0, "https:");
        }
        return Ok(decoded);
    }

    decode_result
}

pub fn caesar_cipher(text: &str, shift: u8) -> String {
    text.chars()
        .map(|c| {
            if c.is_ascii_alphabetic() {
                let base = if c.is_ascii_lowercase() { b'a' } else { b'A' };
                let pos = c as u8 - base;
                let new_pos = (pos + MAX_SHIFT - shift) % MAX_SHIFT;
                (base + new_pos) as char
            } else {
                c
            }
        })
        .collect()
}

/// Decodes a base64-encoded string.
///
/// # Errors
///
/// Returns a `KodikError` if decoding fails due to invalid base64 input or invalid UTF-8.
pub fn decode_base64(input: &str) -> Result<String, KodikError> {
    let decoded_input = general_purpose::STANDARD.decode(input)?;
    Ok(String::from_utf8(decoded_input)?)
}