tail-fin-591 0.2.0

591.com.tw Taiwan rentals adapter for tail-fin: listings, communities, price history, crawl
Documentation
use serde::{Deserialize, Serialize};

/// A hot community listing (from `/api/community/rentHot`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Community {
    pub id: String,
    pub name: String,
}

/// Detailed info for a community (from `/api/community/detail`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommunityDetail {
    pub id: u64,
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub region: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub section: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub address: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub age: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub floor: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub house_holds: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub lat: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub lng: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub build_purpose: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub base_area: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub const_company: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub search_count: Option<String>,
}

/// A single transaction price record (from community detail `price.items`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PriceRecord {
    pub id: u64,
    /// ROC date string, e.g. "115-01-20"
    pub date: String,
    pub address: String,
    pub layout: String,
    pub build_area: String,
    pub total_price: String,
    pub unit_price: PriceValue,
    pub shift_floor: String,
    pub total_floor: String,
    pub build_purpose_str: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PriceValue {
    pub price: String,
    pub unit: String,
}

/// A sale listing near a community (from community detail `sale.rooms[].items[]`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SaleListing {
    pub houseid: u64,
    pub title: String,
    pub price_v: PriceValue,
    pub price_unit: String,
    pub room: String,
    pub address: String,
    pub area_v: AreaValue,
    pub floor: String,
    pub floor_en: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub photo_src: Option<String>,
    pub label: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AreaValue {
    pub area: String,
    pub unit: String,
}

/// A rental listing from the search API (requires browser session).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchListing {
    pub post_id: u64,
    pub title: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub price: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub price_unit: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub address: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub area: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub kind_name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub room: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub floor: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub photo_list: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tags: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub post_time: Option<String>,
}

/// Parameters for the rental search.
#[derive(Debug, Clone, Default)]
pub struct SearchParams {
    /// Region ID (1 = Taipei City). See `REGIONS` for full list.
    pub region_id: u32,
    /// House kind: 0=all, 1=整層住家, 2=獨立套房, 3=分租套房, 8=雅房, 24=車位
    pub kind: Option<u32>,
    /// Maximum monthly rent (NTD)
    pub price_max: Option<u32>,
    /// Minimum monthly rent (NTD)
    pub price_min: Option<u32>,
    /// Sort field: "posttime" (default) or "price"
    pub order: Option<String>,
    /// Max results to return (per page)
    pub limit: usize,
    /// Pagination offset (0, 30, 60, …). Used by `crawl` to walk through pages.
    pub first_row: usize,
}

/// A region with its ID and name.
#[derive(Debug, Clone, Serialize)]
pub struct Region {
    pub id: u32,
    pub name: &'static str,
}

// ---------------------------------------------------------------------------
// Internal response wrappers (not exposed publicly)
// ---------------------------------------------------------------------------

#[derive(Deserialize)]
pub(crate) struct HotResponse {
    pub status: i32,
    pub data: Option<Vec<Community>>,
}

#[derive(Deserialize)]
pub(crate) struct DetailResponse {
    pub status: i32,
    pub data: Option<DetailData>,
}

#[derive(Deserialize)]
pub(crate) struct DetailData {
    pub community: Option<CommunityDetail>,
    pub price: Option<PriceData>,
    pub sale: Option<SaleData>,
}

#[derive(Deserialize)]
pub(crate) struct PriceData {
    pub items: Vec<PriceRecord>,
}

#[derive(Deserialize)]
pub(crate) struct SaleData {
    pub total: u32,
    pub rooms: Vec<SaleRoom>,
}

#[derive(Deserialize)]
pub(crate) struct SaleRoom {
    pub items: Vec<SaleListing>,
}