main.rs 7.8 KB

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