main.rs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. #[macro_use]
  2. extern crate serde_derive;
  3. use reqwest;
  4. use chrono::{Utc, Duration, Local, DateTime};
  5. use select::document::Document;
  6. use select::predicate::Name;
  7. use parse_duration::parse;
  8. use serde;
  9. use serde_json;
  10. use std::io::BufWriter;
  11. use std::io::BufReader;
  12. use std::fs::File;
  13. /// Fucking egun is a mess. It does not even have classes. This is an attempt to parse it.
  14. #[derive(Serialize, Deserialize, Debug, Default)]
  15. struct Auction {
  16. price: f32,
  17. desc: String,
  18. gcal: String,
  19. thumb: String,
  20. remaining: String,
  21. url: String,
  22. timestamp: i64,
  23. is_price_final: bool
  24. }
  25. #[derive(Serialize, Deserialize, Debug)]
  26. struct Query {
  27. url: String,
  28. auctions: Vec<Auction>,
  29. frequency: i64
  30. }
  31. impl Query {
  32. fn run(&mut self) {
  33. self.auctions = parse_url(&self.url);
  34. }
  35. }
  36. impl Default for Query {
  37. fn default () -> Query {
  38. Query{
  39. frequency: Duration::minutes(5).num_seconds(),
  40. url: "".to_string(),
  41. auctions: vec![]
  42. }
  43. }
  44. }
  45. #[derive(Serialize, Deserialize, Debug, Default)]
  46. struct Account {
  47. queries: Vec<Query>,
  48. name: String,
  49. }
  50. fn date_to_gcal(date: DateTime<Local>) -> String {
  51. format!("{}",
  52. date.format("%Y%m%dT%H%M00/%Y%m%dT%H%M00")
  53. )
  54. }
  55. /// Parse a relative offset "x Tage HH:MM" into proper time
  56. fn parse_end_date(timestring: &String) -> Option<DateTime<Local>> {
  57. match parse(&format!("{} minutes", timestring.replace(":", " hours ").replace("Tage", "days"))).ok() {
  58. Some(old_duration) => match Duration::from_std(old_duration).ok() {
  59. Some(chronoduration) => Some(Utc::now().with_timezone(&Local) + chronoduration),
  60. None => None
  61. }
  62. None => None
  63. }
  64. }
  65. fn parse_price(price: &str) -> Option<f32> {
  66. price.to_string()
  67. .replace(".", "")
  68. .replace(",", ".")
  69. .replace(" EUR", "")
  70. .parse().ok()
  71. }
  72. fn parse_url(url: &str) -> Vec<Auction> {
  73. let mut resp = reqwest::get(url).unwrap();
  74. assert!(resp.status().is_success());
  75. // dbg!(&resp.text());
  76. let text = resp.text().unwrap();
  77. let mut auctions = vec![];
  78. for node in Document::from_read(text.as_bytes())
  79. .unwrap()
  80. // .find(Name("a"))
  81. // .filter(|n| n.attr("href").is_some())
  82. // .filter(|n| n.attr("href").unwrap().contains("item.php?id="))
  83. .find(Name("tr"))
  84. .filter(|x| x.attr("bgcolor").is_some())
  85. .filter(|x| x.attr("align").is_none())
  86. {
  87. // Get auction name
  88. if let Some(name) = node
  89. .children().into_iter()
  90. .filter(|x| x.name() == Some("td"))
  91. .filter(|x| x.attr("align") == Some("LEFT"))
  92. .flat_map(|x| x.children())
  93. .filter(|x| x.name() == Some("a"))
  94. .flat_map(|x| x.children())
  95. .map(|x| x.text())
  96. // .filter(|x| x.name() == Some("Text"))
  97. .collect::<Vec<_>>().get(0) {
  98. // If we have the name, go on finding other details
  99. // instantiate mutable Auction
  100. let mut auction = Auction::default();
  101. auction.desc = name.clone();
  102. // get image
  103. if let Some(img) = node
  104. .children().into_iter()
  105. .filter(|x| x.name() == Some("td"))
  106. .filter(|x| x.attr("align") == Some("center"))
  107. .flat_map(|x| x.descendants())
  108. .filter(|x| x.name() == Some("img"))
  109. .filter(|x| match x.attr("src") {
  110. Some(src) => src.contains("cache"),
  111. None => false
  112. } )
  113. .map(|x| x.attr("src").unwrap()) // we just tested
  114. .collect::<Vec<_>>().get(0) {
  115. auction.thumb = format!("http://egun.de/market/{}", img);
  116. }
  117. // get price
  118. if let Some(price) = node
  119. .children()
  120. .filter(|x| x.text().contains("EUR"))
  121. .flat_map(|x| x.children())
  122. .map(|x| parse_price(&x.text()))
  123. .flat_map(|x| x)
  124. .collect::<Vec<_>>().get(0) {
  125. auction.price = price.clone();
  126. }
  127. // get article url
  128. if let Some(url) = node
  129. .children().into_iter()
  130. .filter(|x| x.name() == Some("td"))
  131. .filter(|x| x.attr("align") == Some("LEFT"))
  132. .flat_map(|x| x.children())
  133. .filter(|x| x.name() == Some("a"))
  134. .map(|x| x.attr("href"))
  135. .filter_map(|x| x)
  136. .collect::<Vec<_>>().get(0) {
  137. auction.url = format!("http://egun.de/market/{}", url);
  138. }
  139. // TODO: check if https://doc.rust-lang.org/std/time/struct.SystemTime.html works too
  140. if let Some(remaining) = parse_end_date(
  141. &node
  142. .children()
  143. .filter(|x| x.attr("align") == Some("center"))
  144. .filter(|x| x.attr("nowrap").is_some())
  145. .flat_map(|x| x.children())
  146. .filter(|x| !x.text().is_empty())
  147. .map(|x| x.text())
  148. .collect::<Vec<_>>()
  149. .join(" ")
  150. ){
  151. // dbg!(&t_remaining.children());
  152. auction.gcal = format!("http://www.google.com/calendar/event?action=TEMPLATE&dates={}&text={}&location=&details=", date_to_gcal(remaining), auction.desc);
  153. auction.timestamp = remaining.timestamp();
  154. // println!("ENDS\t{:?}", date_to_gcal(remaining));
  155. }
  156. auctions.push(auction);
  157. }
  158. }
  159. auctions
  160. }
  161. fn main() {
  162. let url = "http://www.egun.de/market/list_items.php?mode=qry&plusdescr=off&wheremode=and&query=sniper&quick=1";
  163. let mut query = Query {
  164. url: url.to_string(),
  165. ..Default::default()
  166. };
  167. query.run();
  168. dbg!(query);
  169. // write out the file
  170. let writer = BufWriter::new(File::create("db.json").unwrap());
  171. serde_json::to_writer_pretty(writer, &parse_url(url)).unwrap();
  172. }