main.rs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. #[macro_use]
  2. extern crate serde_derive;
  3. use core::cmp::Ordering::Equal;
  4. use reqwest;
  5. use chrono::{Utc, Duration, Local, DateTime};
  6. use select::document::Document;
  7. use select::predicate::Name;
  8. use parse_duration::parse;
  9. use serde_json;
  10. use std::io::BufWriter;
  11. use std::io::BufReader;
  12. use std::fs::File;
  13. use std::{thread, time};
  14. use std::collections::HashMap;
  15. /// Fucking egun is a mess. It does not even use css and is built using tables. This is an attempt to parse it.
  16. #[derive(Serialize, Deserialize, Debug, Default, Clone)]
  17. struct Auction {
  18. price: f32,
  19. desc: String,
  20. gcal: String,
  21. thumb: String,
  22. remaining: i64,
  23. url: String,
  24. timestamp: i64,
  25. is_price_final: bool
  26. }
  27. #[derive(Serialize, Deserialize, Debug, Clone)]
  28. struct Query {
  29. url: String,
  30. auctions: Vec<Auction>,
  31. frequency: i64,
  32. }
  33. impl Query {
  34. fn run(&mut self) {
  35. self.auctions = parse_url(&self.url);
  36. }
  37. fn detect_frequency(&self) {
  38. let _a = &self.auctions
  39. .iter()
  40. .map(|x| x)
  41. .collect::<Vec<_>>();
  42. }
  43. }
  44. impl Default for Query {
  45. fn default () -> Query {
  46. Query{
  47. frequency: Duration::minutes(5).num_seconds(),
  48. url: "".to_string(),
  49. auctions: vec![]
  50. }
  51. }
  52. }
  53. #[derive(Serialize, Deserialize, Debug, Default)]
  54. struct Account {
  55. queries: Vec<Query>,
  56. name: String,
  57. }
  58. fn date_to_gcal(date: DateTime<Local>) -> String {
  59. format!("{}",
  60. date.format("%Y%m%dT%H%M00/%Y%m%dT%H%M00")
  61. )
  62. }
  63. /// Parse a relative offset "x Tage HH:MM" into proper time
  64. // fn parse_end_date(timestring: &String) -> Option<DateTime<Local>> {
  65. // match parse(&format!("{} minutes", timestring.replace(":", " hours ").replace("Tage", "days"))).ok() {
  66. // Some(old_duration) => match Duration::from_std(old_duration).ok() {
  67. // Some(chronoduration) => Some(Utc::now().with_timezone(&Local) + chronoduration),
  68. // None => None
  69. // }
  70. // None => None
  71. // }
  72. // }
  73. /// Parse a relative offset "x Tage HH:MM" into proper time
  74. fn parse_remaining(timestring: &String) -> Option<Duration> {
  75. match parse(&format!("{} minutes", timestring.replace(":", " hours ").replace("Tage", "days"))).ok() {
  76. Some(old_duration) => Duration::from_std(old_duration).ok(),
  77. None => None
  78. }
  79. }
  80. fn parse_price(price: &str) -> Option<f32> {
  81. price.to_string()
  82. .replace(".", "")
  83. .replace(",", ".")
  84. .replace(" EUR", "")
  85. .parse().ok()
  86. }
  87. fn parse_url(url: &str) -> Vec<Auction> {
  88. let mut auctions = vec![];
  89. if let Ok(mut resp) = reqwest::get(url) {
  90. if !resp.status().is_success() {
  91. println!("ERR {:?}", resp.text());
  92. return auctions;
  93. }
  94. let text = resp.text().unwrap_or("".to_string());
  95. for node in Document::from_read(text.as_bytes())
  96. .unwrap()
  97. // .find(Name("a"))
  98. // .filter(|n| n.attr("href").is_some())
  99. // .filter(|n| n.attr("href").unwrap().contains("item.php?id="))
  100. .find(Name("tr"))
  101. .filter(|x| x.attr("bgcolor").is_some())
  102. .filter(|x| x.attr("align").is_none())
  103. {
  104. // Get auction name
  105. if let Some(name) = node
  106. .children().into_iter()
  107. .filter(|x| x.name() == Some("td"))
  108. .filter(|x| x.attr("align") == Some("LEFT"))
  109. .flat_map(|x| x.children())
  110. .filter(|x| x.name() == Some("a"))
  111. .flat_map(|x| x.children())
  112. .map(|x| x.text())
  113. // .filter(|x| x.name() == Some("Text"))
  114. .collect::<Vec<_>>().get(0) {
  115. // If we have the name, go on finding other details
  116. // instantiate mutable Auction
  117. let mut auction = Auction::default();
  118. auction.desc = name.clone();
  119. auction.thumb = format!("http://egun.de/market/images/picture.gif");
  120. // get image
  121. if let Some(img) = node
  122. .children().into_iter()
  123. .filter(|x| x.name() == Some("td"))
  124. .filter(|x| x.attr("align") == Some("center"))
  125. .flat_map(|x| x.descendants())
  126. .filter(|x| x.name() == Some("img"))
  127. .filter(|x| match x.attr("src") {
  128. Some(src) => src.contains("cache"),
  129. None => false
  130. } )
  131. .map(|x| x.attr("src").unwrap()) // we just tested
  132. .collect::<Vec<_>>().get(0) {
  133. auction.thumb = format!("http://egun.de/market/{}", img);
  134. }
  135. // get price
  136. if let Some(price) = node
  137. .children()
  138. .filter(|x| x.text().contains("EUR"))
  139. .flat_map(|x| x.children())
  140. .map(|x| parse_price(&x.text()))
  141. .flat_map(|x| x)
  142. .collect::<Vec<_>>().get(0) {
  143. auction.price = price.clone();
  144. }
  145. // get article url
  146. if let Some(url) = node
  147. .children().into_iter()
  148. .filter(|x| x.name() == Some("td"))
  149. .filter(|x| x.attr("align") == Some("LEFT"))
  150. .flat_map(|x| x.children())
  151. .filter(|x| x.name() == Some("a"))
  152. .map(|x| x.attr("href"))
  153. .filter_map(|x| x)
  154. .collect::<Vec<_>>().get(0) {
  155. auction.url = format!("http://egun.de/market/{}", url);
  156. }
  157. // TODO: check if https://doc.rust-lang.org/std/time/struct.SystemTime.html works too
  158. if let Some(remaining) = parse_remaining(
  159. &node
  160. .children()
  161. .filter(|x| x.attr("align") == Some("center"))
  162. .filter(|x| x.attr("nowrap").is_some())
  163. .flat_map(|x| x.children())
  164. .filter(|x| !x.text().is_empty())
  165. .map(|x| x.text())
  166. .collect::<Vec<_>>()
  167. .join(" ")
  168. ){
  169. // dbg!(&t_remaining.children());
  170. let end_date = Utc::now().with_timezone(&Local) + remaining;
  171. auction.gcal = format!("http://www.google.com/calendar/event?action=TEMPLATE&dates={}&text={}&location=&details=", date_to_gcal(end_date), auction.desc);
  172. auction.remaining = remaining.num_seconds();
  173. auction.timestamp = end_date.timestamp();
  174. // println!("ENDS\t{:?}", date_to_gcal(remaining));
  175. }
  176. auctions.push(auction);
  177. }
  178. }
  179. }
  180. auctions
  181. }
  182. fn daemon(queries: Vec<Query>, loaded_auctions: Vec<Auction>) {
  183. // dbg!(&queries);
  184. // let _s = loaded_auctions.into_iter().map(|x| (x.url.clone(), x.clone())).collect::<Vec<_>>();
  185. // for auction in loaded_auctions {
  186. // if Utc::now().with_timezone(&Local).timestamp() > auction.timestamp {
  187. // //auction has expired, mark price as final
  188. // auction.is_price_final = true;
  189. // // let a = auction_map.get_mut(_key);
  190. // dbg!(&auction);
  191. // }
  192. // }
  193. let auction_map: HashMap<String, Auction> = loaded_auctions
  194. .into_iter()
  195. .map(|mut x| {
  196. if Utc::now().with_timezone(&Local).timestamp() > x.timestamp {
  197. // auction has expired, mark price as final
  198. x.is_price_final = true;
  199. }
  200. x
  201. }
  202. )
  203. .map(|x| (x.url.clone(), x.clone()))
  204. .collect();
  205. // return;
  206. println!("Starting daemon with {} active queries", queries.len());
  207. loop {
  208. // TODO: see if we can get rid of clone
  209. let mut auction_map = auction_map.clone();
  210. // let mut auctions = vec![];
  211. for mut query in queries.clone() {
  212. query.run();
  213. // dbg!(&query.auctions.into_iter().map(|x| x.desc).collect::<Vec<_>>());
  214. // auctions.extend(query.auctions);
  215. for auction in query.auctions {
  216. dbg!(&auction.desc);
  217. let a = auction.clone();
  218. auction_map.insert(auction.url, a);
  219. }
  220. }
  221. println!("{} auctions found", &auction_map.len());
  222. let writer = BufWriter::new(File::create("db.json").unwrap());
  223. let mut auctions: Vec<Auction> = auction_map.into_iter().map(|(_x, y)| y).collect();
  224. auctions.sort_by_key(|k| k.remaining);
  225. serde_json::to_writer_pretty(writer, &auctions).unwrap();
  226. let pause = time::Duration::from_secs(300);
  227. thread::sleep(pause);
  228. }
  229. }
  230. fn main() {
  231. let reader = BufReader::new(File::open("urls.json").unwrap());
  232. // let reader = BufReader::new(File::open("url_local.json").unwrap());
  233. let urls: Vec<String> = serde_json::from_reader(reader).unwrap_or(vec![]);
  234. let reader = BufReader::new(File::open("db.json").unwrap());
  235. let auctions: Vec<Auction> = serde_json::from_reader(reader).unwrap_or(vec![]);
  236. daemon(
  237. urls.iter()
  238. .map(|x| Query {url: x.to_string(), ..Default::default()})
  239. .collect::<Vec<Query>>(),
  240. auctions
  241. );
  242. }