blockpalettes_client/lib.rs
1//! A client for the Block Palettes API.
2//!
3//! This crate provides a convenient asynchronous client for interacting with the
4//! [Block Palettes](https://www.blockpalettes.com) website's API. It allows you to
5//! search for palettes, retrieve popular blocks, get detailed information about
6//! specific palettes, and even scrape some information directly from the website's
7//! HTML pages.
8//!
9//! The client is built on top of `reqwest` for HTTP requests and `serde` for
10//! JSON serialization/deserialization. It also uses `chrono` for date parsing
11//! and `scraper` for HTML parsing when scraping.
12//!
13//! # Features
14//!
15//! - Search for palettes based on contained blocks.
16//! - Retrieve lists of popular blocks.
17//! - Fetch detailed information for individual palettes.
18//! - Get similar palettes based on a given palette ID.
19//! - Scrape palette page details (blocks and similar palette IDs) directly from HTML.
20//! - Robust error handling with custom error types.
21//!
22//! # Error Handling
23//!
24//! The crate defines a custom error type, [`BlockPalettesError`], which
25//! encapsulates various issues that can occur, such as HTTP errors, API-specific
26//! errors, HTML parsing failures, and invalid date formats.
27//!
28//! # Data Structures
29//!
30//! Key data structures like [`Palette`], [`PaletteDetails`], and [`PopularBlock`]
31//! are provided to represent the API responses.
32
33use chrono::NaiveDateTime;
34use reqwest::Client;
35use scraper::{Html, Selector};
36use serde::{Deserialize, Serialize};
37use std::collections::HashSet;
38use thiserror::Error;
39
40/// Represents the possible errors that can occur when interacting with the
41/// Block Palettes API.
42#[derive(Debug, Error)]
43pub enum BlockPalettesError {
44 /// An HTTP request failed, typically due to network issues, DNS resolution,
45 /// or invalid URLs.
46 ///
47 /// This error wraps the underlying `reqwest::Error`.
48 #[error("HTTP request failed: {0}")]
49 Http(#[from] reqwest::Error),
50 /// The Block Palettes API returned an error message or indicated a failure
51 /// in its response.
52 ///
53 /// The contained `String` provides more details about the API-specific error.
54 #[error("API error: {0}")]
55 Api(String),
56 /// An error occurred during the parsing of HTML content, typically when
57 /// scraping a palette page.
58 ///
59 /// This can happen if the HTML structure changes unexpectedly.
60 #[error("HTML parsing error")]
61 HtmlParse,
62 /// The date string received from the API could not be parsed into a
63 /// `NaiveDateTime` object.
64 ///
65 /// This usually indicates an unexpected date format from the API.
66 #[error("Invalid date format")]
67 InvalidDateFormat,
68}
69
70/// A specialized `Result` type for Block Palettes operations.
71///
72/// This type is a convenience alias for `std::result::Result<T, BlockPalettesError>`.
73pub type Result<T, E = BlockPalettesError> = std::result::Result<T, E>;
74
75/// An asynchronous client for the Block Palettes API.
76///
77/// This struct provides methods to interact with various endpoints of the
78/// Block Palettes API, allowing you to search for palettes, retrieve block
79/// information, and get palette details.
80///
81/// # Examples
82///
83/// ```rust
84/// use blockpalettes_client::{BlockPalettesClient, SortOrder};
85///
86/// #[tokio::main]
87/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
88/// let client = BlockPalettesClient::new(reqwest::Client::new());
89/// let popular_blocks = client.popular_blocks().await?;
90/// println!("First popular block: {}", popular_blocks[0].name);
91/// Ok(())
92/// }
93/// ```
94#[derive(Debug, Clone)]
95pub struct BlockPalettesClient<'a> {
96 client: Client,
97 base_url: &'a str,
98}
99
100impl<'a> BlockPalettesClient<'a> {
101 /// Creates a new [`BlockPalettesClient`] instance.
102 ///
103 /// # Arguments
104 ///
105 /// * `client` - An instance of `reqwest::Client` to use for making HTTP requests.
106 ///
107 /// # Returns
108 ///
109 /// A new `BlockPalettesClient` configured to use the provided `reqwest::Client`
110 /// and the default base URL (`https://www.blockpalettes.com`).
111 ///
112 /// # Examples
113 ///
114 /// ```rust
115 /// use blockpalettes_client::BlockPalettesClient;
116 ///
117 /// let reqwest_client = reqwest::Client::new();
118 /// let bp_client = BlockPalettesClient::new(reqwest_client);
119 /// ```
120 pub const fn new(client: Client) -> Self {
121 Self {
122 client,
123 base_url: "https://www.blockpalettes.com",
124 }
125 }
126
127 /// Searches for blocks that match a given query string.
128 ///
129 /// This method queries the `/api/palettes/search-block.php` endpoint.
130 ///
131 /// # Arguments
132 ///
133 /// * `query` - The search string for blocks (e.g., "stone", "wood").
134 ///
135 /// # Returns
136 ///
137 /// A `Result` containing a `Vec<String>` of block names if successful,
138 /// or a [`BlockPalettesError`] if the request fails or the API returns an error.
139 ///
140 /// [`BlockPalettesError`]: enum.BlockPalettesError.html
141 ///
142 /// # Examples
143 ///
144 /// ```rust
145 /// use blockpalettes_client::BlockPalettesClient;
146 ///
147 /// #[tokio::main]
148 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
149 /// let client = BlockPalettesClient::new(reqwest::Client::new());
150 /// let blocks = client.search_blocks("dirt").await?;
151 /// println!("Found blocks: {:?}", blocks);
152 /// Ok(())
153 /// }
154 /// ```
155 pub async fn search_blocks(&self, query: impl AsRef<str>) -> Result<Vec<String>> {
156 let url = format!("{}/api/palettes/search-block.php", self.base_url);
157 let response = self
158 .client
159 .get(&url)
160 .query(&[("query", query.as_ref())])
161 .send()
162 .await?
163 .json::<BlockSearchResponse>()
164 .await?;
165
166 if response.success {
167 Ok(response.blocks)
168 } else {
169 Err(BlockPalettesError::Api("Search failed".into()))
170 }
171 }
172
173 /// Retrieves a list of popular blocks.
174 ///
175 /// This method queries the `/api/palettes/popular-blocks.php` endpoint.
176 ///
177 /// # Returns
178 ///
179 /// A `Result` containing a `Vec<PopularBlock>` if successful,
180 /// or a [`BlockPalettesError`] if the request fails or the API returns an error.
181 ///
182 /// [`PopularBlock`]: struct.PopularBlock.html
183 /// [`BlockPalettesError`]: enum.BlockPalettesError.html
184 ///
185 /// # Examples
186 ///
187 /// ```rust
188 /// use blockpalettes_client::BlockPalettesClient;
189 ///
190 /// #[tokio::main]
191 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
192 /// let client = BlockPalettesClient::new(reqwest::Client::new());
193 /// let popular_blocks = client.popular_blocks().await?;
194 /// for block in popular_blocks.iter().take(3) {
195 /// println!("Block: {}, Count: {}", block.name, block.count);
196 /// }
197 /// Ok(())
198 /// }
199 /// ```
200 pub async fn popular_blocks(&self) -> Result<Vec<PopularBlock>> {
201 let url = format!("{}/api/palettes/popular-blocks.php", self.base_url);
202 let response = self
203 .client
204 .get(&url)
205 .send()
206 .await?
207 .json::<PopularBlocksResponse>()
208 .await?;
209
210 if response.success {
211 Ok(response.blocks)
212 } else {
213 Err(BlockPalettesError::Api(
214 "Popular blocks request failed".into(),
215 ))
216 }
217 }
218
219 /// Retrieves a list of palettes based on specified blocks, sorting order,
220 /// pagination, and limit.
221 ///
222 /// This method queries the `/api/palettes/all_palettes.php` endpoint.
223 /// It internally filters the results to ensure all specified blocks are present
224 /// in the returned palettes.
225 ///
226 /// # Arguments
227 ///
228 /// * `blocks` - A slice of string references representing the blocks that
229 /// must be present in the palettes.
230 /// * `sort` - The desired sorting order for the palettes (e.g., `SortOrder::Recent`).
231 /// * `page` - The page number of the results to retrieve (1-indexed).
232 /// * `limit` - The maximum number of palettes to return per page.
233 ///
234 /// # Returns
235 ///
236 /// A `Result` containing a [`PaletteResponse`] if successful,
237 /// or a [`BlockPalettesError`] if the request fails or the API returns an error.
238 ///
239 /// # Examples
240 ///
241 /// ```rust
242 /// use blockpalettes_client::{BlockPalettesClient, SortOrder};
243 ///
244 /// #[tokio::main]
245 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
246 /// let client = BlockPalettesClient::new(reqwest::Client::new());
247 /// let blocks_to_search = &["oak_log", "dirt"];
248 /// let response = client
249 /// .get_palettes(blocks_to_search, SortOrder::Popular, 1, 5)
250 /// .await?;
251 ///
252 /// if let Some(palettes) = response.palettes {
253 /// println!("Found {} popular palettes containing oak_log and dirt:", palettes.len());
254 /// for palette in palettes {
255 /// println!("- ID: {}, Name: {:?}", palette.id, palette.name());
256 /// }
257 /// }
258 /// Ok(())
259 /// }
260 /// ```
261 pub async fn get_palettes(
262 &self,
263 blocks: &[&str],
264 sort: SortOrder,
265 page: u32,
266 limit: u32,
267 ) -> Result<PaletteResponse> {
268 let url = format!("{}/api/palettes/all_palettes.php", self.base_url);
269
270 let mut all_palettes = Vec::new();
271 let mut total_results = 0;
272 let mut total_pages = 0;
273
274 for &block in blocks {
275 let response = self
276 .client
277 .get(&url)
278 .query(&[
279 ("sort", sort.to_string()),
280 ("page", page.to_string()),
281 ("limit", limit.to_string()),
282 ("blocks", block.to_string()),
283 ])
284 .send()
285 .await?
286 .json::<PaletteResponse>()
287 .await?;
288
289 if total_results == 0 {
290 total_results = response.total_results;
291 total_pages = response.total_pages;
292 }
293
294 if let Some(mut ps) = response.palettes {
295 all_palettes.append(&mut ps);
296 }
297 }
298
299 // filter the collected palettes to ensure they contain ALL specified blocks
300 let filtered = all_palettes
301 .into_iter()
302 .filter(|p| p.contains_all_blocks(blocks))
303 .collect();
304
305 Ok(PaletteResponse {
306 success: true,
307 palettes: Some(filtered),
308 total_results,
309 total_pages,
310 })
311 }
312
313 /// Retrieves detailed information for a single palette by its ID.
314 ///
315 /// This method queries the `/api/palettes/single_palette.php` endpoint.
316 ///
317 /// # Arguments
318 ///
319 /// * `id` - The unique identifier of the palette.
320 ///
321 /// # Returns
322 ///
323 /// A `Result` containing a [`PaletteDetails`] if successful,
324 /// or a [`BlockPalettesError`] if the request fails, the API returns an error
325 /// (e.g., palette not found), or the response cannot be deserialized.
326 ///
327 /// # Examples
328 ///
329 /// ```rust
330 /// use blockpalettes_client::BlockPalettesClient;
331 ///
332 /// #[tokio::main]
333 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
334 /// let client = BlockPalettesClient::new(reqwest::Client::new());
335 /// let palette_id = 12345; // Replace with an actual palette ID
336 /// match client.get_palette_details(palette_id).await {
337 /// Ok(details) => println!("Palette ID {}: Username {}", details.id, details.username),
338 /// Err(e) => eprintln!("Failed to get palette details: {}", e),
339 /// }
340 /// Ok(())
341 /// }
342 /// ```
343 pub async fn get_palette_details(&self, id: u64) -> Result<PaletteDetails> {
344 let url = format!("{}/api/palettes/single_palette.php", self.base_url);
345 let response = self
346 .client
347 .get(&url)
348 .query(&[("id", id.to_string())])
349 .send()
350 .await?
351 .json::<SinglePaletteResponse>()
352 .await?;
353
354 if response.success {
355 Ok(response.palette)
356 } else {
357 Err(BlockPalettesError::Api("Palette not found".into()))
358 }
359 }
360
361 /// Retrieves a list of palettes similar to a given palette ID.
362 ///
363 /// This method queries the `/api/palettes/similar_palettes.php` endpoint.
364 ///
365 /// # Arguments
366 ///
367 /// * `palette_id` - The ID of the reference palette to find similar ones.
368 ///
369 /// # Returns
370 ///
371 /// A `Result` containing a `Vec<Palette>` of similar palettes if successful,
372 /// or a [`BlockPalettesError`] if the request fails, the API returns an error,
373 /// or the response cannot be deserialized.
374 ///
375 /// # Examples
376 ///
377 /// ```rust
378 /// use blockpalettes_client::BlockPalettesClient;
379 ///
380 /// #[tokio::main]
381 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
382 /// let client = BlockPalettesClient::new(reqwest::Client::new());
383 /// let reference_palette_id = 56655; // Replace with an actual palette ID
384 /// let similar_palettes = client.get_similar_palettes(reference_palette_id).await?;
385 /// println!("Found {} similar palettes for ID {}:", similar_palettes.len(), reference_palette_id);
386 /// for palette in similar_palettes.iter().take(3) {
387 /// println!("- ID: {}, Name: {:?}", palette.id, palette.name());
388 /// }
389 /// Ok(())
390 /// }
391 /// ```
392 pub async fn get_similar_palettes(&self, palette_id: u64) -> Result<Vec<Palette>> {
393 let url = format!("{}/api/palettes/similar_palettes.php", self.base_url);
394 let response = self
395 .client
396 .get(&url)
397 .query(&[("palette_id", palette_id.to_string())])
398 .send()
399 .await?
400 .json::<SimilarPalettesResponse>()
401 .await?;
402
403 if response.success {
404 Ok(response.palettes)
405 } else {
406 Err(BlockPalettesError::Api("Similar palettes not found".into()))
407 }
408 }
409
410 /// Scrapes details directly from a Block Palettes HTML page for a given palette ID.
411 ///
412 /// This method is useful for extracting information that might not be available
413 /// directly through the public API endpoints, such as the full list of blocks
414 /// displayed on the page or IDs of similar palettes linked on the page.
415 ///
416 /// # Arguments
417 ///
418 /// * `palette_id` - The ID of the palette whose page details are to be scraped.
419 ///
420 /// # Returns
421 ///
422 /// A `Result` containing a [`PalettePageDetails`] if successful,
423 /// or a [`BlockPalettesError`] if the request fails, HTML parsing fails,
424 /// or expected elements are not found.
425 ///
426 /// # Caveats
427 ///
428 /// This method relies on the specific HTML structure of `blockpalettes.com`.
429 /// Any changes to the website's front-end might break this scraping functionality.
430 ///
431 /// # Examples
432 ///
433 /// ```rust
434 /// use blockpalettes_client::BlockPalettesClient;
435 ///
436 /// #[tokio::main]
437 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
438 /// let client = BlockPalettesClient::new(reqwest::Client::new());
439 /// let palette_id = 12345; // Replace with an actual palette ID
440 /// match client.scrape_palette_page(palette_id).await {
441 /// Ok(details) => {
442 /// println!("Scraped blocks for palette {}: {:?}", palette_id, details.blocks);
443 /// println!("Similar palette IDs: {:?}", details.similar_palette_ids);
444 /// },
445 /// Err(e) => eprintln!("Failed to scrape palette page: {}", e),
446 /// }
447 /// Ok(())
448 /// }
449 /// ```
450 pub async fn scrape_palette_page(&self, palette_id: u64) -> Result<PalettePageDetails> {
451 let url = format!("{}/palette/{}", self.base_url, palette_id);
452 let html = self.client.get(&url).send().await?.text().await?;
453
454 let document = Html::parse_document(&html);
455
456 // extract palette blocks
457 let block_selector =
458 Selector::parse(".single-block").map_err(|_| BlockPalettesError::HtmlParse)?;
459 let mut blocks = Vec::new();
460
461 for element in document.select(&block_selector) {
462 if let Some(block_name) = element.text().last() {
463 blocks.push(block_name.trim().to_string());
464 }
465 }
466
467 // extract similar palettes if available
468 let similar_selector =
469 Selector::parse(".palette-card").map_err(|_| BlockPalettesError::HtmlParse)?;
470 let mut similar = Vec::new();
471
472 for element in document.select(&similar_selector) {
473 if let Some(id) = element
474 .value()
475 .attr("href")
476 .and_then(|href| href.split('/').next_back())
477 .and_then(|id| id.parse::<u64>().ok())
478 {
479 similar.push(id);
480 }
481 }
482
483 Ok(PalettePageDetails {
484 blocks,
485 similar_palette_ids: similar,
486 })
487 }
488}
489
490/// Represents the different sorting orders available for retrieving palettes.
491///
492/// These variants correspond to the `sort` parameter in the Block Palettes API.
493#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
494#[serde(rename_all = "snake_case")]
495pub enum SortOrder {
496 /// Sort by the most recently added palettes.
497 Recent,
498 /// Sort by the most popular palettes.
499 Popular,
500 /// Sort by the oldest palettes.
501 Oldest,
502 /// Sort by trending palettes.
503 Trending,
504}
505
506impl std::fmt::Display for SortOrder {
507 /// Converts the `SortOrder` enum variant into its corresponding API string representation.
508 ///
509 /// # Returns
510 ///
511 /// A `String` slice representing the sort order (e.g., "recent", "popular").
512 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
513 match self {
514 SortOrder::Recent => write!(f, "recent"),
515 SortOrder::Popular => write!(f, "popular"),
516 SortOrder::Oldest => write!(f, "oldest"),
517 SortOrder::Trending => write!(f, "trending"),
518 }
519 }
520}
521
522/// Internal struct for deserializing the response from the `/api/palettes/search-block.php` endpoint.
523#[derive(Debug, Deserialize)]
524struct BlockSearchResponse {
525 success: bool,
526 blocks: Vec<String>,
527}
528
529/// Internal struct for deserializing the response from the `/api/palettes/popular-blocks.php` endpoint.
530#[derive(Debug, Deserialize)]
531struct PopularBlocksResponse {
532 success: bool,
533 blocks: Vec<PopularBlock>,
534}
535
536/// Internal struct for deserializing the response from the `/api/palettes/single_palette.php` endpoint.
537#[derive(Debug, Deserialize)]
538struct SinglePaletteResponse {
539 success: bool,
540 palette: PaletteDetails,
541}
542
543/// Internal struct for deserializing the response from the `/api/palettes/similar_palettes.php` endpoint.
544#[derive(Debug, Deserialize)]
545struct SimilarPalettesResponse {
546 success: bool,
547 palettes: Vec<Palette>,
548}
549
550/// Represents a popular block returned by the API.
551#[derive(Debug, Deserialize, Serialize)]
552pub struct PopularBlock {
553 /// The name of the block (e.g., "stone", "dirt").
554 #[serde(rename = "block")]
555 pub name: String,
556 /// The number of palettes this block appears in.
557 pub count: u32,
558}
559
560/// Represents the response structure when fetching a list of palettes.
561#[derive(Debug, Deserialize, Serialize)]
562pub struct PaletteResponse {
563 /// Indicates if the API request was successful.
564 pub success: bool,
565 /// The total number of results found for the query.
566 pub total_results: u32,
567 /// The total number of pages available for the query.
568 pub total_pages: u32,
569 /// An optional vector of [`Palette`] objects. It will be `None` if no palettes were found.
570 pub palettes: Option<Vec<Palette>>,
571}
572
573/// Represents a single palette returned by the Block Palettes API.
574///
575/// This struct contains core information about a palette, including its ID,
576/// associated blocks, likes, and creation date.
577#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
578pub struct Palette {
579 /// The unique identifier for the palette.
580 pub id: u64,
581 /// The ID of the user who created the palette.
582 pub user_id: u64,
583 /// The creation date of the palette as a string (e.g., "YYYY-MM-DD HH:MM:SS").
584 pub date: String,
585 /// The number of likes the palette has received.
586 pub likes: u32,
587 /// The first block in the palette.
588 #[serde(rename = "blockOne")]
589 pub block_one: String,
590 /// The second block in the palette.
591 #[serde(rename = "blockTwo")]
592 pub block_two: String,
593 /// The third block in the palette.
594 #[serde(rename = "blockThree")]
595 pub block_three: String,
596 /// The fourth block in the palette.
597 #[serde(rename = "blockFour")]
598 pub block_four: String,
599 /// The fifth block in the palette.
600 #[serde(rename = "blockFive")]
601 pub block_five: String,
602 /// The sixth block in the palette.
603 #[serde(rename = "blockSix")]
604 pub block_six: String,
605 /// A flag indicating if the palette is hidden (0 for not hidden, 1 for hidden).
606 #[serde(default)]
607 pub hidden: Option<u8>,
608 /// A flag indicating if the palette is featured (0 for not featured, 1 for featured).
609 #[serde(default)]
610 pub featured: Option<u8>,
611 /// An optional hash associated with the palette.
612 pub hash: Option<String>,
613 /// A human-readable string indicating how long ago the palette was created (e.g., "2 days ago").
614 pub time_ago: String,
615}
616
617impl Palette {
618 /// Returns a vector containing all six block names from the palette.
619 ///
620 /// This is a convenience method to access all blocks without individually
621 /// referencing `block_one`, `block_two`, etc.
622 ///
623 /// # Returns
624 ///
625 /// A `Vec<String>` containing the names of the six blocks in the palette.
626 ///
627 /// # Examples
628 ///
629 /// ```rust
630 /// # use blockpalettes_client::Palette;
631 /// # let palette = Palette {
632 /// # id: 1, user_id: 1, date: "2023-01-01 12:00:00".to_string(), likes: 10,
633 /// # block_one: "stone".to_string(), block_two: "dirt".to_string(),
634 /// # block_three: "grass_block".to_string(), block_four: "oak_log".to_string(),
635 /// # block_five: "cobblestone".to_string(), block_six: "sand".to_string(),
636 /// # hidden: Some(0), featured: Some(0), hash: None, time_ago: "1 day ago".to_string()
637 /// # };
638 /// let blocks = palette.name();
639 /// assert_eq!(blocks.len(), 6);
640 /// assert!(blocks.contains(&"stone".to_string()));
641 /// ```
642 pub fn name(&self) -> Vec<String> {
643 vec![
644 self.block_one.clone(),
645 self.block_two.clone(),
646 self.block_three.clone(),
647 self.block_four.clone(),
648 self.block_five.clone(),
649 self.block_six.clone(),
650 ]
651 }
652
653 /// Checks if the palette contains all the specified blocks.
654 ///
655 /// This method is useful for client-side filtering of palettes.
656 ///
657 /// # Arguments
658 ///
659 /// * `blocks` - A slice of string references, where each string is a block name
660 /// to check for.
661 ///
662 /// # Returns
663 ///
664 /// `true` if the palette contains all blocks specified in the `blocks` slice,
665 /// `false` otherwise. The comparison is case-sensitive.
666 ///
667 /// # Examples
668 ///
669 /// ```rust
670 /// # use blockpalettes_client::Palette;
671 /// # let palette = Palette {
672 /// # id: 1, user_id: 1, date: "2023-01-01 12:00:00".to_string(), likes: 10,
673 /// # block_one: "stone".to_string(), block_two: "dirt".to_string(),
674 /// # block_three: "grass_block".to_string(), block_four: "oak_log".to_string(),
675 /// # block_five: "cobblestone".to_string(), block_six: "sand".to_string(),
676 /// # hidden: Some(0), featured: Some(0), hash: None, time_ago: "1 day ago".to_string()
677 /// # };
678 /// assert!(palette.contains_all_blocks(&["stone", "dirt"]));
679 /// assert!(!palette.contains_all_blocks(&["stone", "diamond_block"]));
680 /// ```
681 pub fn contains_all_blocks(&self, blocks: &[&str]) -> bool {
682 let palette_blocks: HashSet<&str> = HashSet::from([
683 self.block_one.as_str(),
684 self.block_two.as_str(),
685 self.block_three.as_str(),
686 self.block_four.as_str(),
687 self.block_five.as_str(),
688 self.block_six.as_str(),
689 ]);
690
691 blocks.iter().all(|&b| palette_blocks.contains(b))
692 }
693
694 /// Parses the `date` string of the palette into a `NaiveDateTime` object.
695 ///
696 /// This provides a more structured way to work with the palette's creation date.
697 ///
698 /// # Returns
699 ///
700 /// A `Result` containing a `NaiveDateTime` if the date string is successfully parsed,
701 /// or a [`BlockPalettesError::InvalidDateFormat`] if the string does not match
702 /// the expected format ("YYYY-MM-DD HH:MM:SS").
703 ///
704 /// # Examples
705 ///
706 /// ```rust
707 /// # use blockpalettes_client::Palette;
708 /// # use chrono::{NaiveDate, Timelike};
709 /// # let palette = Palette {
710 /// # id: 1, user_id: 1, date: "2023-01-01 12:30:00".to_string(), likes: 10,
711 /// # block_one: "stone".to_string(), block_two: "dirt".to_string(),
712 /// # block_three: "grass_block".to_string(), block_four: "oak_log".to_string(),
713 /// # block_five: "cobblestone".to_string(), block_six: "sand".to_string(),
714 /// # hidden: Some(0), featured: Some(0), hash: None, time_ago: "1 day ago".to_string()
715 /// # };
716 /// let datetime = palette.parse_date().unwrap();
717 /// assert_eq!(datetime.date(), NaiveDate::from_ymd_opt(2023, 1, 1).unwrap());
718 /// assert_eq!(datetime.hour(), 12);
719 /// ```
720 pub fn parse_date(&self) -> Result<NaiveDateTime> {
721 NaiveDateTime::parse_from_str(&self.date, "%Y-%m-%d %H:%M:%S")
722 .map_err(|_| BlockPalettesError::InvalidDateFormat)
723 }
724}
725
726/// Represents detailed information for a single palette, including the username.
727///
728/// This struct is typically returned by the [`BlockPalettesClient::get_palette_details`] method.
729/// It extends the basic [`Palette`] information with the `username` of the creator.
730#[derive(Debug, Deserialize, Serialize)]
731pub struct PaletteDetails {
732 /// The unique identifier for the palette.
733 pub id: u64,
734 /// The ID of the user who created the palette.
735 #[serde(rename = "user_id")]
736 pub user_id: u64,
737 /// The creation date of the palette as a string (e.g., "YYYY-MM-DD HH:MM:SS").
738 pub date: String,
739 /// The number of likes the palette has received.
740 pub likes: u32,
741 /// The first block in the palette.
742 #[serde(rename = "blockOne")]
743 pub block_one: String,
744 /// The second block in the palette.
745 #[serde(rename = "blockTwo")]
746 pub block_two: String,
747 /// The third block in the palette.
748 #[serde(rename = "blockThree")]
749 pub block_three: String,
750 /// The fourth block in the palette.
751 #[serde(rename = "blockFour")]
752 pub block_four: String,
753 /// The fifth block in the palette.
754 #[serde(rename = "blockFive")]
755 pub block_five: String,
756 /// The sixth block in the palette.
757 #[serde(rename = "blockSix")]
758 pub block_six: String,
759 /// A flag indicating if the palette is hidden (0 for not hidden, 1 for hidden).
760 pub hidden: u8,
761 /// A flag indicating if the palette is featured (0 for not featured, 1 for featured).
762 pub featured: u8,
763 /// The hash associated with the palette.
764 pub hash: String,
765 /// The username of the palette creator.
766 pub username: String,
767 /// A human-readable string indicating how long ago the palette was created (e.g., "2 days ago").
768 #[serde(rename = "time_ago")]
769 pub time_ago: String,
770}
771
772/// Represents details scraped directly from a palette's HTML page.
773///
774/// This struct is typically returned by the [`BlockPalettesClient::scrape_palette_page`] method.
775/// It contains information extracted by parsing the HTML, which might include
776/// blocks displayed on the page and IDs of similar palettes linked.
777///
778/// [`BlockPalettesClient::scrape_palette_page`]: struct.BlockPalettesClient.html#method.scrape_palette_page
779#[derive(Debug, Serialize)]
780pub struct PalettePageDetails {
781 /// A list of block names found on the palette's page.
782 pub blocks: Vec<String>,
783 /// A list of IDs of similar palettes linked on the page.
784 pub similar_palette_ids: Vec<u64>,
785}