1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
use iced::futures::{channel::mpsc, StreamExt};
use iced::{subscription, Subscription};
use serde::{Deserialize, Serialize};

use crate::{command, Message};

pub enum ProgressState {
    Starting,
    Ready(mpsc::UnboundedReceiver<String>),
}

pub fn bind() -> Subscription<Message> {
    struct Progress;

    subscription::unfold(
        std::any::TypeId::of::<Progress>(),
        ProgressState::Starting,
        |state| async move {
            match state {
                ProgressState::Starting => {
                    let (sender, receiver) = mpsc::unbounded();

                    (Message::Ready(sender), ProgressState::Ready(receiver))
                }
                ProgressState::Ready(mut progress_receiver) => {
                    let received = progress_receiver.next().await;
                    if let Some(progress) = received {
                        tracing::debug!("received progress from yt-dlp: {progress}");
                        if progress.contains("has already been downloaded") {
                            progress_receiver.close();
                            return (
                                Message::Command(command::Message::AlreadyExists),
                                ProgressState::Starting,
                            );
                        } else if progress.contains("entry does not pass filter (!playlist)") {
                            progress_receiver.close();
                            return (
                                Message::Command(command::Message::PlaylistNotChecked),
                                ProgressState::Starting,
                            );
                        } else if let Some(progress) = progress.strip_prefix("stderr:ERROR") {
                            return (
                                Message::Command(command::Message::Error(progress.to_string())),
                                ProgressState::Ready(progress_receiver),
                            );
                        } else {
                            return (
                                Message::ProgressEvent(progress),
                                ProgressState::Ready(progress_receiver),
                            );
                        }
                    }

                    (Message::None, ProgressState::Ready(progress_receiver))
                }
            }
        },
    )
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
pub enum Progress {
    PreProcessing,
    PreDownload {
        video_id: String,
    },
    Downloading {
        video_title: String,
        eta: i32,
        downloaded_bytes: f32,
        total_bytes: f32,
        elapsed: f32,
        speed: f32,
        playlist_count: Option<i32>,
        playlist_index: Option<i32>,
    },
    EndOfVideo,
    EndOfPlaylist,
    PostProcessing {
        status: String,
    },
    Error(String),
}

pub fn parse_progress(input: String) -> Vec<Progress> {
    input.lines().map(|line| {
        if let Some(progress) = line.strip_prefix("__") {
            let progress = progress.replace(r#", "playlist_count": NA, "#, r#""#);
            let progress = progress.replace(r#""playlist_index": NA"#, r#""#);
            let progress = progress.replace("NA", "0");

            return Some(
                serde_json::from_str::<Progress>(&progress).unwrap_or_else(|e| {
                    tracing::error!(
                        "failed to parse yt-dlp progress: \noriginal-input: {input}\nstripped-input: {progress}\n{e:#?}"
                    );
                    panic!("failed to parse yt-dlp progress");
                }),
            );
        } else {
            None
        }
    }).flatten().collect::<Vec<Progress>>()
}