Skip to main content

yts/core/
response.rs

1use scraper::{Html, Selector};
2
3use crate::{Genre, Quality};
4
5use super::model;
6
7/// Represents pagination information for a movie list page.
8#[derive(Debug)]
9pub struct Page {
10    /// The current page number.
11    pub current: u32,
12    /// The total number of pages.
13    pub of: u32,
14    /// The total number of movies available.
15    pub total: u32,
16}
17
18impl Page {
19    /// Creates a new `Page` instance calculating total pages based on total items.
20    ///
21    /// # Parameters
22    /// - `current`: The current page number.
23    /// - `total`: The total number of movies available.
24    ///
25    /// # Returns
26    /// A `Page` struct with calculated total pages (`of`).
27    ///
28    /// # Notes
29    /// Assumes 20 movies per page.
30    fn create(current: u32, total: u32) -> Self {
31        let of = if total > 20 {
32            (total / 20) + (if !total.is_multiple_of(20) { 1 } else { 0 })
33        } else {
34            1
35        };
36
37        Self { current, of, total }
38    }
39}
40
41/// Represents the response from a movie listing page.
42///
43/// Contains pagination info and a list of parsed movies.
44#[derive(Debug)]
45pub struct Response {
46    /// Pagination information.
47    pub page: Page,
48    /// List of movies parsed from the page.
49    pub movies: Vec<model::Movie>,
50}
51
52impl Response {
53    /// Parses HTML content to create a `Response` with movies and pagination info.
54    ///
55    /// # Parameters
56    /// - `html`: Raw HTML content of the page.
57    /// - `page`: Current page number.
58    ///
59    /// # Returns
60    /// A `Result` containing the parsed `Response` or an error.
61    ///
62    /// # Errors
63    /// Returns errors if parsing fails or required movie data is missing.
64    pub(crate) fn create(html: &str, page: u32) -> crate::Result<Self> {
65        let document = Html::parse_document(html);
66
67        let total: u32 = document
68            .select(&Selector::parse("div.container h2 b")?)
69            .next()
70            .and_then(|value| value.text().next())
71            .and_then(|value| value.parse().ok())
72            .unwrap_or_default();
73
74        let mut movies = Vec::new();
75        if let Some(div) = document.select(&Selector::parse("section div.row")?).next() {
76            for line in div.select(&Selector::parse("div.browse-movie-wrap")?) {
77                let link = line
78                    .select(&Selector::parse("a.browse-movie-link")?)
79                    .next()
80                    .and_then(|e| e.attr("href"))
81                    .unwrap_or_default()
82                    .to_string();
83
84                let image = line
85                    .select(&Selector::parse("img")?)
86                    .next()
87                    .and_then(|e| e.attr("src"))
88                    .unwrap_or_default()
89                    .to_string();
90
91                let info = line
92                    .text()
93                    .filter(|&t| !t.trim().is_empty() && t != "View Details")
94                    .collect::<Vec<_>>();
95
96                let rating = info.first().ok_or(crate::Error::MovieRatingError)?;
97                let rating = &rating[..1];
98                let rating: f32 = rating.parse()?;
99
100                let year: u32 = info.last().ok_or(crate::Error::MovieYearError)?.parse()?;
101
102                let name = info
103                    .get(info.len() - 2)
104                    .ok_or(crate::Error::MovieNameError)?
105                    .trim()
106                    .to_string();
107
108                let mut genres = Vec::new();
109                for &value in &info[1..info.len() - 2] {
110                    let value: Genre = value.into();
111                    genres.push(value);
112                }
113
114                movies.push(model::Movie::new(name, year, rating, genres, image, link));
115            }
116        }
117
118        Ok(Self {
119            page: Page::create(page, total),
120            movies,
121        })
122    }
123}
124
125/// Represents a torrent download option for a movie.
126#[derive(Debug)]
127pub struct Torrent {
128    /// The quality of the torrent (e.g., 720p, 1080p).
129    pub quality: Quality,
130    /// The size of the torrent file.
131    pub size: String,
132    /// The language of the torrent.
133    pub language: String,
134    /// The runtime of the movie.
135    pub runtime: String,
136    /// Information about peers and seeds.
137    pub peers_seeds: String,
138    /// Direct link to the torrent file.
139    pub link: String,
140}
141
142impl Torrent {
143    /// Creates a new `Torrent` instance from raw string data.
144    ///
145    /// # Parameters
146    /// - `quality`: Quality string (converted to `Quality` enum).
147    /// - `size`: Size of the torrent.
148    /// - `language`: Language of the torrent.
149    /// - `runtime`: Runtime of the movie.
150    /// - `peers_seeds`: Peers and seeds info.
151    /// - `link`: URL link to the torrent.
152    ///
153    /// # Returns
154    /// A new `Torrent` struct.
155    pub(crate) fn new(
156        quality: &str,
157        size: &str,
158        language: &str,
159        runtime: &str,
160        peers_seeds: &str,
161        link: String,
162    ) -> Self {
163        Self {
164            quality: quality.into(),
165            size: size.to_string(),
166            language: language.to_string(),
167            runtime: runtime.to_string(),
168            peers_seeds: peers_seeds.to_string(),
169            link,
170        }
171    }
172
173    /// Parses HTML content to extract a list of torrents.
174    ///
175    /// # Parameters
176    /// - `html`: Raw HTML content containing torrent info.
177    ///
178    /// # Returns
179    /// A `Result` containing a vector of `Torrent` structs or an error.
180    pub(crate) fn create(html: &str) -> crate::Result<Vec<Self>> {
181        let document = Html::parse_document(html);
182
183        let mut torrents = Vec::new();
184        if let Some(movie_tech_specs) = document
185            .select(&Selector::parse("div#movie-tech-specs")?)
186            .next()
187        {
188            let qualities = movie_tech_specs
189                .select(&Selector::parse("span.tech-quality")?)
190                .map(|line| line.text().collect::<Vec<_>>())
191                .collect::<Vec<_>>();
192
193            let data = movie_tech_specs
194                .select(&Selector::parse("div.tech-spec-info")?)
195                .map(|line| {
196                    line.text()
197                        .map(|t| t.trim())
198                        .filter(|&t| Self::skip_useless_str(t))
199                        .collect::<Vec<_>>()
200                })
201                .collect::<Vec<_>>();
202
203            if let Some(movie_info) = document
204                .select(&Selector::parse("div#movie-info p")?)
205                .next()
206            {
207                let data_len = data.len();
208                for (i, line) in movie_info.select(&Selector::parse("a")?).enumerate() {
209                    let link = line.attr("href").unwrap_or_default().to_string();
210
211                    if i < data_len {
212                        let data = &data[i];
213                        let qualities = &qualities[i];
214
215                        let index = if !data[3].contains("R") { 3 } else { 4 };
216
217                        torrents.push(Torrent::new(
218                            qualities[0],
219                            data[0],
220                            data[2],
221                            data[index],
222                            data[index + 1],
223                            link,
224                        ));
225                    }
226                }
227            }
228        }
229
230        Ok(torrents)
231    }
232
233    /// Helper function to filter out unwanted strings during parsing.
234    ///
235    /// Returns `true` if the string is useful, `false` if it should be ignored.
236    fn skip_useless_str(value: &str) -> bool {
237        !value.is_empty()
238            && value != "P/S"
239            && value != "Subtitles"
240            && value != "NR"
241            && !value.contains("fps")
242    }
243}