Skip to main content

ytmapi_rs/
parse.rs

1//! Results from parsing Innertube queries.
2//! # Implementation example
3//! ```no_run
4//! # struct GetDateQuery;
5//! use serde::Deserialize;
6//! #[derive(Debug, Deserialize)]
7//! struct Date {
8//!     date_string: String,
9//!     date_timestamp: usize,
10//! }
11//! impl ytmapi_rs::parse::ParseFrom<GetDateQuery> for Date {
12//!     fn parse_from(
13//!         p: ytmapi_rs::parse::ProcessedResult<GetDateQuery>,
14//!     ) -> ytmapi_rs::Result<Self> {
15//!         ytmapi_rs::json::from_json(p.json)
16//!     }
17//! }
18//! ```
19//! # Alternative implementation
20//! An alternative to working directly with [`crate::json::Json`] is to add
21//! `json-crawler` as a dependency and use the provided
22//! `From<ProcessedResult> for JsonCrawlerOwned` implementation.
23use crate::auth::AuthToken;
24use crate::common::{AlbumID, ArtistChannelID, Thumbnail};
25use crate::json::Json;
26use crate::nav_consts::*;
27use crate::{RawResult, Result, error};
28use json_crawler::{JsonCrawler, JsonCrawlerOwned};
29use serde::de::DeserializeOwned;
30use serde::{Deserialize, Serialize};
31use std::fmt::Debug;
32
33mod album;
34pub use album::*;
35mod artist;
36pub use artist::*;
37mod history;
38pub use history::*;
39mod library;
40pub use library::*;
41mod playlist;
42pub use playlist::*;
43mod podcasts;
44pub use podcasts::*;
45mod rate;
46// Whilst rate doesn't define anything - for consistency we still write the `pub
47// use` statement.
48#[allow(unused_imports)]
49pub use rate::*;
50mod recommendations;
51pub use recommendations::*;
52mod search;
53pub use search::*;
54mod song;
55pub use song::*;
56mod upload;
57pub use upload::*;
58mod user;
59pub use user::*;
60
61/// Describes how to parse the ProcessedResult from a Query into the target
62/// type.
63// By requiring ParseFrom to also implement Debug, this simplifies our Query ->
64// String API.
65pub trait ParseFrom<Q>: Debug + Sized {
66    fn parse_from(p: ProcessedResult<Q>) -> crate::Result<Self>;
67}
68
69#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
70pub enum EpisodeDate {
71    Live,
72    Recorded { date: String },
73}
74
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
76pub enum EpisodeDuration {
77    Live,
78    Recorded { duration: String },
79}
80
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82// Intentionally not marked non_exhaustive - not expecting this to change.
83pub struct ParsedSongArtist {
84    pub name: String,
85    pub id: Option<ArtistChannelID<'static>>,
86}
87#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88// Intentionally not marked non_exhaustive - not expecting this to change.
89pub struct ParsedSongAlbum {
90    pub name: String,
91    pub id: AlbumID<'static>,
92}
93
94/// A result from the api that has been checked for errors and processed into
95/// JSON.
96pub struct ProcessedResult<'a, Q> {
97    pub query: &'a Q,
98    /// The raw string output returned from the web request to YouTube.
99    pub source: String,
100    /// The result once it has been deserialized from Json and processed to
101    /// remove errors.
102    pub json: Json,
103}
104
105impl<'a, Q, A: AuthToken> TryFrom<RawResult<'a, Q, A>> for ProcessedResult<'a, Q> {
106    type Error = crate::Error;
107    fn try_from(value: RawResult<'a, Q, A>) -> Result<Self> {
108        let RawResult {
109            json: source,
110            query,
111            ..
112        } = value;
113        let json = match source.as_str() {
114            // Workaround for Get request returning empty string.
115            "" => serde_json::Value::Null,
116            other => {
117                serde_json::from_str(other).map_err(|e| error::Error::response(format!("{e:?}")))?
118            }
119        };
120        let json = Json::new(json);
121        Ok(Self {
122            query,
123            source,
124            json,
125        })
126    }
127}
128
129impl<'a, Q> ProcessedResult<'a, Q> {
130    pub(crate) fn destructure(self) -> (&'a Q, String, serde_json::Value) {
131        let ProcessedResult {
132            query,
133            source,
134            json,
135        } = self;
136        (query, source, json.inner)
137    }
138    pub(crate) fn get_json(&self) -> &serde_json::Value {
139        &self.json.inner
140    }
141}
142
143impl<Q> ProcessedResult<'_, Q> {
144    pub fn parse_into<O: ParseFrom<Q>>(self) -> Result<O> {
145        O::parse_from(self)
146    }
147}
148
149impl<Q> From<ProcessedResult<'_, Q>> for JsonCrawlerOwned {
150    fn from(value: ProcessedResult<Q>) -> Self {
151        let (_, source, crawler) = value.destructure();
152        JsonCrawlerOwned::new(source, crawler)
153    }
154}
155
156fn fixed_column_item_pointer(col_idx: usize) -> String {
157    format!("/fixedColumns/{col_idx}/musicResponsiveListItemFixedColumnRenderer")
158}
159
160fn flex_column_item_pointer(col_idx: usize) -> String {
161    format!("/flexColumns/{col_idx}/musicResponsiveListItemFlexColumnRenderer")
162}
163
164// Should take FlexColumnItem? or Data?. Regular serde_json::Value could tryInto
165// fixedcolumnitem also. Not sure if this should error.
166// XXX: I think this should return none instead of error.
167fn parse_song_artists(
168    data: &mut impl JsonCrawler,
169    col_idx: usize,
170) -> Result<Vec<ParsedSongArtist>> {
171    data.borrow_pointer(format!("{}/text/runs", flex_column_item_pointer(col_idx)))?
172        .try_into_iter()?
173        .step_by(2)
174        .map(|mut item| parse_song_artist(&mut item))
175        .collect()
176}
177
178fn parse_song_artist(data: &mut impl JsonCrawler) -> Result<ParsedSongArtist> {
179    Ok(ParsedSongArtist {
180        name: data.take_value_pointer("/text")?,
181        id: data.take_value_pointer(NAVIGATION_BROWSE_ID).ok(),
182    })
183}
184
185fn parse_song_album(data: &mut impl JsonCrawler, col_idx: usize) -> Result<ParsedSongAlbum> {
186    Ok(ParsedSongAlbum {
187        name: parse_flex_column_item(data, col_idx, 0)?,
188        id: data.take_value_pointer(format!(
189            "{}/text/runs/0{}",
190            flex_column_item_pointer(col_idx),
191            NAVIGATION_BROWSE_ID
192        ))?,
193    })
194}
195
196fn parse_flex_column_item<T: DeserializeOwned>(
197    item: &mut impl JsonCrawler,
198    col_idx: usize,
199    run_idx: usize,
200) -> Result<T> {
201    let pointer = format!(
202        "{}/text/runs/{run_idx}/text",
203        flex_column_item_pointer(col_idx)
204    );
205    Ok(item.take_value_pointer(pointer)?)
206}
207
208fn parse_fixed_column_item<T: DeserializeOwned>(
209    item: &mut impl JsonCrawler,
210    col_idx: usize,
211) -> Result<T> {
212    let pointer = format!("{}/text/runs/0/text", fixed_column_item_pointer(col_idx));
213    Ok(item.take_value_pointer(pointer)?)
214}