libytdlp 0.1.0

Safe Rust wrapper around the libytdlp C ABI
use std::future::Future;
use std::pin::Pin;
use std::ptr;
use std::sync::Arc;
use std::sync::Mutex;
use std::task::Context;
use std::task::Poll;
use std::task::Waker;

use libytdlp_sys as sys;

use crate::error::Error;
use crate::error::Kind;
use crate::media::MediaInfo;
use crate::progress::ProgressEvent;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JobState {
    Running,
    Done,
    Failed,
    Cancelled,
}

impl JobState {
    fn from_raw(state: u32) -> Result<Self, Error> {
        match state {
            sys::LY_JOB_RUNNING => Ok(Self::Running),
            sys::LY_JOB_DONE => Ok(Self::Done),
            sys::LY_JOB_FAILED => Ok(Self::Failed),
            sys::LY_JOB_CANCELLED => Ok(Self::Cancelled),
            _ => Err(Error::new(Kind::Internal, "unknown job state")),
        }
    }
}

pub struct DownloadJob {
    raw: *mut sys::LyJob,
}

impl DownloadJob {
    pub(crate) fn from_raw(raw: *mut sys::LyJob) -> Self {
        Self { raw }
    }

    #[tracing::instrument(name = "libytdlp.poll", level = "debug", skip(self))]
    pub fn poll(&self) -> Result<(JobState, Vec<ProgressEvent>), Error> {
        let mut state: u32 = sys::LY_JOB_RUNNING;
        let mut events: *mut sys::LyResult = ptr::null_mut();
        let mut error: *mut sys::LyError = ptr::null_mut();

        let status = unsafe { sys::ly_job_poll(self.raw, &mut state, &mut events, &mut error) };

        if status != sys::LY_OK || events.is_null() {
            return Err(Error::from_ptr(error));
        }

        let text = crate::read_result_json(events)?;

        let state = JobState::from_raw(state)?;

        let values: Vec<serde_json::Value> = serde_json::from_str(&text)
            .map_err(|failed| Error::new(Kind::Internal, failed.to_string()))?;
        let values: Vec<ProgressEvent> =
            values.into_iter().map(ProgressEvent::from_value).collect();

        tracing::debug!(events = values.len(), "libytdlp poll drained");

        Ok((state, values))
    }

    #[tracing::instrument(name = "libytdlp.wait", level = "debug", skip(self))]
    pub fn wait(&self) -> Result<MediaInfo, Error> {
        let mut result: *mut sys::LyResult = ptr::null_mut();
        let mut error: *mut sys::LyError = ptr::null_mut();

        let status = unsafe { sys::ly_job_wait(self.raw, &mut result, &mut error) };

        if status != sys::LY_OK || result.is_null() {
            return Err(Error::from_ptr(error));
        }

        MediaInfo::from_raw(result)
    }

    pub fn wait_async(self) -> WaitForDownload {
        WaitForDownload {
            job: Some(self),
            shared: Arc::new(Shared {
                waker: Mutex::new(None),
                outcome: Mutex::new(None),
            }),
        }
    }

    pub fn cancel(&self) {
        unsafe {
            sys::ly_job_cancel(self.raw);
        }
    }
}

impl Drop for DownloadJob {
    fn drop(&mut self) {
        unsafe {
            sys::ly_job_release(self.raw);
        }
    }
}

unsafe impl Send for DownloadJob {}

unsafe impl Sync for DownloadJob {}

struct Shared {
    waker: Mutex<Option<Waker>>,
    outcome: Mutex<Option<Result<MediaInfo, Error>>>,
}

pub struct WaitForDownload {
    job: Option<DownloadJob>,
    shared: Arc<Shared>,
}

impl Future for WaitForDownload {
    type Output = Result<MediaInfo, Error>;

    fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
        if let Some(outcome) = self.shared.outcome.lock().expect("outcome lock").take() {
            return Poll::Ready(outcome);
        }

        *self.shared.waker.lock().expect("waker lock") = Some(context.waker().clone());

        if let Some(outcome) = self.shared.outcome.lock().expect("outcome lock").take() {
            return Poll::Ready(outcome);
        }

        let Some(job) = self.job.take() else {
            return Poll::Pending;
        };

        let shared = Arc::clone(&self.shared);

        std::thread::spawn(move || {
            let outcome = job.wait();

            *shared.outcome.lock().expect("outcome lock") = Some(outcome);

            let waker = shared.waker.lock().expect("waker lock").take();

            if let Some(waker) = waker {
                waker.wake();
            }
        });

        Poll::Pending
    }
}