Browse Source

add accounts and web app WIP

Johann Woelper 6 years ago
parent
commit
fa7b8c25df
11 changed files with 1093 additions and 443 deletions
  1. 492 91
      Cargo.lock
  2. 8 2
      Cargo.toml
  3. 8 0
      accounts.json
  4. 92 72
      db.json
  5. 57 278
      src/main.rs
  6. 367 0
      src/query.rs
  7. 54 0
      src/server.rs
  8. 1 0
      webapp/lilac.min.css
  9. 1 0
      webapp/moment-duration-format.min.js
  10. 7 0
      webapp/moment.min.js
  11. 6 0
      webapp/vue.min.js

File diff suppressed because it is too large
+ 492 - 91
Cargo.lock


+ 8 - 2
Cargo.toml

@@ -7,9 +7,15 @@ edition = "2018"
 [dependencies]
 reqwest = "0.9.18"
 select = "0.4.2"
-#chrono = "0.4.6"
 chrono = { version = "0.4", features = ["serde"] }
 parse_duration = "1.0.1"
 serde = "1.0"
 serde_derive = "1.0"
-serde_json = "1.0"
+serde_json = "1.0"
+rocket = "0.4.1"
+lazy_static = "*"
+
+[dependencies.rocket_contrib]
+version = "0.4.1"
+default-features = false
+features = ["json", "serve"]

+ 8 - 0
accounts.json

@@ -0,0 +1,8 @@
+{
+  "offline": {
+    "queries": {
+      "http://localhost:8001": {},
+      "http://localhost:8002": {}
+    }
+  }
+}

+ 92 - 72
db.json

@@ -1,52 +1,92 @@
 [
   {
-    "price": 1409.0,
+    "price": 645.0,
+    "desc": "Remington Model 783 Accuracy",
+    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190630T131100/20190630T131100&text=Remington Model 783 Accuracy&location=&details=",
+    "thumb": "http://egun.de/market/images/picture.gif",
+    "remaining": 257760,
+    "url": "http://egun.de/market/item.php?id=7525551",
+    "timestamp": 1561893087,
+    "is_price_final": true
+  },
+  {
+    "price": 1399.0,
     "desc": "Accuracy International Chassis System (AICS-AX 2.0) FDE Remington 700 SA .308 Repetierbüchsen",
     "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190703T235700/20190703T235700&text=Accuracy International Chassis System (AICS-AX 2.0) FDE Remington 700 SA .308 Repetierbüchsen&location=&details=",
     "thumb": "http://egun.de/market/images/picture.gif",
-    "remaining": 1988040,
+    "remaining": 266880,
     "url": "http://egun.de/market/item.php?id=7020521",
-    "timestamp": 1562191074,
+    "timestamp": 1562191050,
     "is_price_final": false
   },
   {
-    "price": 149.0,
-    "desc": "Sig Arms SHR 970 Magazin, 30/06 ,7x64, 9,3x62 usw. Neu",
-    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190625T083400/20190625T083400&text=Sig Arms SHR 970 Magazin, 30/06 ,7x64, 9,3x62 usw. Neu&location=&details=",
-    "thumb": "http://egun.de/market/cache/aucimg/64x64/2.7506617.943962973.jpg",
-    "remaining": 1241460,
-    "url": "http://egun.de/market/item.php?id=7506617",
-    "timestamp": 1561444494,
+    "price": 46.35,
+    "desc": "MSZU Auktion Harris Zweibein 9\"-13\" 1A2-L",
+    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190614T121700/20190614T121700&text=MSZU Auktion Harris Zweibein 9\"-13\" 1A2-L&location=&details=",
+    "thumb": "http://egun.de/market/images/picture.gif",
+    "remaining": 304440,
+    "url": "http://egun.de/market/item.php?id=7481474",
+    "timestamp": 1560507477,
+    "is_price_final": true
+  },
+  {
+    "price": 655.0,
+    "desc": "LEE-ENFIELD MOD. NO 4 MK I, KAL. .308 WIN.",
+    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190707T215300/20190707T215300&text=LEE-ENFIELD MOD. NO 4 MK I, KAL. .308 WIN.&location=&details=",
+    "thumb": "http://egun.de/market/cache/aucimg/64x64/3.7517103.716985898.jpg",
+    "remaining": 605040,
+    "url": "http://egun.de/market/item.php?id=7517103",
+    "timestamp": 1562529212,
     "is_price_final": false
   },
   {
-    "price": 5649.0,
-    "desc": "Accuracy 308 Win Sniper Custom AICS-AX GREEN Klappschaft Syst Remington 700 L.W. HELICAL BULL Match",
-    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190625T192300/20190625T192300&text=Accuracy 308 Win Sniper Custom AICS-AX GREEN Klappschaft Syst Remington 700 L.W. HELICAL BULL Match&location=&details=",
-    "thumb": "http://egun.de/market/cache/aucimg/64x64/0.7495531.1277774880.jpg",
-    "remaining": 1280400,
-    "url": "http://egun.de/market/item.php?id=7495531",
-    "timestamp": 1561483437,
+    "price": 2199.0,
+    "desc": "LuxDefTec M14 Cal. 308 Win inkl. Montage",
+    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190708T110900/20190708T110900&text=LuxDefTec M14 Cal. 308 Win inkl. Montage&location=&details=",
+    "thumb": "http://egun.de/market/cache/aucimg/64x64/6.7510941.937973306.jpg",
+    "remaining": 652800,
+    "url": "http://egun.de/market/item.php?id=7510941",
+    "timestamp": 1562576969,
     "is_price_final": false
   },
   {
-    "price": 3249.0,
-    "desc": "@ Accuracy HOWA 1500 Kal. .308 Win Sniper Custom GRS BERSERK Schaft EBI 4-K Bremse ZF Target Master",
-    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190625T193100/20190625T193100&text=@ Accuracy HOWA 1500 Kal. .308 Win Sniper Custom GRS BERSERK Schaft EBI 4-K Bremse ZF Target Master&location=&details=",
-    "thumb": "http://egun.de/market/cache/aucimg/64x64/0.7495570.261427846.jpg",
-    "remaining": 1280880,
-    "url": "http://egun.de/market/item.php?id=7495570",
-    "timestamp": 1561483917,
+    "price": 108.95,
+    "desc": "Harris Zweibein 1A2-LM Höhe 9-13\"/ ca. 23-33cm/ Rastenverstellung/ gummierte Füße",
+    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190709T164800/20190709T164800&text=Harris Zweibein 1A2-LM Höhe 9-13\"/ ca. 23-33cm/ Rastenverstellung/ gummierte Füße&location=&details=",
+    "thumb": "http://egun.de/market/cache/aucimg/64x64/5.7474724.408757137.jpg",
+    "remaining": 759540,
+    "url": "http://egun.de/market/item.php?id=7474724",
+    "timestamp": 1562683711,
     "is_price_final": false
   },
   {
-    "price": 670.0,
-    "desc": "B&T GRS Gewehr Schalldämpfer .308 + B&T Kompensator - Accuracy International M18x1,5 Brügger Thomet",
-    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190618T130500/20190618T130500&text=B&T GRS Gewehr Schalldämpfer .308 + B&T Kompensator - Accuracy International M18x1,5 Brügger Thomet&location=&details=",
-    "thumb": "http://egun.de/market/images/picture.gif",
-    "remaining": 652920,
-    "url": "http://egun.de/market/item.php?id=6884512",
-    "timestamp": 1560855957,
+    "price": 650.0,
+    "desc": "Enfield Mod 2A1, Kal..308Win",
+    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190709T212600/20190709T212600&text=Enfield Mod 2A1, Kal..308Win&location=&details=",
+    "thumb": "http://egun.de/market/cache/aucimg/64x64/4.7216322.1511970536.jpg",
+    "remaining": 776220,
+    "url": "http://egun.de/market/item.php?id=7216322",
+    "timestamp": 1562700392,
+    "is_price_final": false
+  },
+  {
+    "price": 108.95,
+    "desc": "Harris Zweibein 1A2-L: Höhe 9-13\"/ ca. 23-33cm, stufenlos einstellbar, gummierte Füße",
+    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190622T152500/20190622T152500&text=Harris Zweibein 1A2-L: Höhe 9-13\"/ ca. 23-33cm, stufenlos einstellbar, gummierte Füße&location=&details=",
+    "thumb": "http://egun.de/market/cache/aucimg/64x64/2.7081930.306653604.jpg",
+    "remaining": 1006920,
+    "url": "http://egun.de/market/item.php?id=7081930",
+    "timestamp": 1561209957,
+    "is_price_final": true
+  },
+  {
+    "price": 103.0,
+    "desc": "Top Sammlerstück ! Lee Enfield Ishapore 2A1, Kaliber: .308 Winchester",
+    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190712T201400/20190712T201400&text=Top Sammlerstück ! Lee Enfield Ishapore 2A1, Kaliber: .308 Winchester&location=&details=",
+    "thumb": "http://egun.de/market/cache/aucimg/64x64/3.7524552.575823640.jpg",
+    "remaining": 1031100,
+    "url": "http://egun.de/market/item.php?id=7524552",
+    "timestamp": 1562955272,
     "is_price_final": false
   },
   {
@@ -57,7 +97,7 @@
     "remaining": 1280400,
     "url": "http://egun.de/market/item.php?id=7495531",
     "timestamp": 1561483437,
-    "is_price_final": false
+    "is_price_final": true
   },
   {
     "price": 3249.0,
@@ -67,56 +107,36 @@
     "remaining": 1280880,
     "url": "http://egun.de/market/item.php?id=7495570",
     "timestamp": 1561483917,
-    "is_price_final": false
+    "is_price_final": true
   },
   {
-    "price": 1495.0,
-    "desc": "Accuracy International AX Chassis System grün",
-    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190628T111700/20190628T111700&text=Accuracy International AX Chassis System grün&location=&details=",
-    "thumb": "http://egun.de/market/images/picture.gif",
-    "remaining": 1510440,
-    "url": "http://egun.de/market/item.php?id=7102212",
-    "timestamp": 1561713477,
+    "price": 149.0,
+    "desc": "Sig Arms SHR 970 Magazin, 30/06 ,7x64, 9,3x62 usw. Neu",
+    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190716T083500/20190716T083500&text=Sig Arms SHR 970 Magazin, 30/06 ,7x64, 9,3x62 usw. Neu&location=&details=",
+    "thumb": "http://egun.de/market/cache/aucimg/64x64/2.7506617.943962973.jpg",
+    "remaining": 1334760,
+    "url": "http://egun.de/market/item.php?id=7506617",
+    "timestamp": 1563258927,
     "is_price_final": false
   },
   {
-    "price": 1409.0,
-    "desc": "Accuracy International Chassis System (AICS-AX 2.0) FDE Remington 700 SA .308 Repetierbüchsen",
-    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190703T235700/20190703T235700&text=Accuracy International Chassis System (AICS-AX 2.0) FDE Remington 700 SA .308 Repetierbüchsen&location=&details=",
+    "price": 630.0,
+    "desc": "B&T GRS Gewehr Schalldämpfer .308 + B&T Kompensator - Accuracy International M18x1,5 Brügger Thomet",
+    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190718T130500/20190718T130500&text=B&T GRS Gewehr Schalldämpfer .308 + B&T Kompensator - Accuracy International M18x1,5 Brügger Thomet&location=&details=",
     "thumb": "http://egun.de/market/images/picture.gif",
-    "remaining": 1988040,
-    "url": "http://egun.de/market/item.php?id=7020521",
-    "timestamp": 1562191077,
+    "remaining": 1523760,
+    "url": "http://egun.de/market/item.php?id=6884512",
+    "timestamp": 1563447930,
     "is_price_final": false
   },
   {
-    "price": 46.35,
-    "desc": "MSZU Auktion Harris Zweibein 9\"-13\" 1A2-L",
-    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190614T121700/20190614T121700&text=MSZU Auktion Harris Zweibein 9\"-13\" 1A2-L&location=&details=",
+    "price": 1495.0,
+    "desc": "Accuracy International AX Chassis System grün",
+    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190728T111700/20190728T111700&text=Accuracy International AX Chassis System grün&location=&details=",
     "thumb": "http://egun.de/market/images/picture.gif",
-    "remaining": 304440,
-    "url": "http://egun.de/market/item.php?id=7481474",
-    "timestamp": 1560507477,
-    "is_price_final": false
-  },
-  {
-    "price": 108.95,
-    "desc": "Harris Zweibein 1A2-L: Höhe 9-13\"/ ca. 23-33cm, stufenlos einstellbar, gummierte Füße",
-    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190622T152500/20190622T152500&text=Harris Zweibein 1A2-L: Höhe 9-13\"/ ca. 23-33cm, stufenlos einstellbar, gummierte Füße&location=&details=",
-    "thumb": "http://egun.de/market/cache/aucimg/64x64/2.7081930.306653604.jpg",
-    "remaining": 1006920,
-    "url": "http://egun.de/market/item.php?id=7081930",
-    "timestamp": 1561209957,
-    "is_price_final": false
-  },
-  {
-    "price": 108.95,
-    "desc": "Harris Zweibein 1A2-LM Höhe 9-13\"/ ca. 23-33cm/ Rastenverstellung/ gummierte Füße",
-    "gcal": "http://www.google.com/calendar/event?action=TEMPLATE&dates=20190709T164800/20190709T164800&text=Harris Zweibein 1A2-LM Höhe 9-13\"/ ca. 23-33cm/ Rastenverstellung/ gummierte Füße&location=&details=",
-    "thumb": "http://egun.de/market/cache/aucimg/64x64/5.7474724.408757137.jpg",
-    "remaining": 2480700,
-    "url": "http://egun.de/market/item.php?id=7474724",
-    "timestamp": 1562683737,
+    "remaining": 2381280,
+    "url": "http://egun.de/market/item.php?id=7102212",
+    "timestamp": 1564305450,
     "is_price_final": false
   }
 ]

+ 57 - 278
src/main.rs

@@ -1,304 +1,83 @@
+#![feature(proc_macro_hygiene, decl_macro)]
 #[macro_use]
 extern crate serde_derive;
-use core::cmp::Ordering::Equal;
-use reqwest;
-use chrono::{Utc, Duration, Local, DateTime};
-use select::document::Document;
-use select::predicate::Name;
-use parse_duration::parse;
+#[macro_use]
+extern crate rocket;
+#[macro_use]
+extern crate rocket_contrib;
+#[macro_use]
+extern crate lazy_static;
+
+use chrono::{DateTime, Duration, Local, Utc};
 use serde_json;
-use std::io::BufWriter;
-use std::io::BufReader;
+use std::collections::HashMap;
 use std::fs::File;
+use std::io::BufReader;
+use std::io::BufWriter;
+use std::sync::Mutex;
 use std::{thread, time};
-use std::collections::HashMap;
 
 /// Fucking egun is a mess. It does not even use css and is built using tables. This is an attempt to parse it.
+mod query;
+use query::*;
+mod server;
+use server::run;
 
+lazy_static! {
+    // static ref ACCOUNTS: Mutex<HashMap<String, Account>> = Mutex::new(HashMap::new());
+    static ref ACCOUNTS: Mutex<HashMap<String, Account>> = Mutex::new(HashMap::new());
 
-
-#[derive(Serialize, Deserialize, Debug, Default, Clone)]
-struct Auction {
-    price: f32,
-    desc: String,
-    gcal: String,
-    thumb: String,
-    remaining: i64,
-    url: String,
-    timestamp: i64,
-    is_price_final: bool
-}
-
-#[derive(Serialize, Deserialize, Debug, Clone)]
-struct Query {
-    url: String,
-    auctions: Vec<Auction>,
-    frequency: i64,
 }
 
 
-impl Query {
-    fn run(&mut self) {
-        self.auctions = parse_url(&self.url);
-    }
+fn daemon() {
+    loop {
 
-    fn detect_frequency(&self) {
-        let _a = &self.auctions
-        .iter()
-        .map(|x| x)
-        .collect::<Vec<_>>();
-    }
-}
+        let mut accounts_unlocked = ACCOUNTS.lock().unwrap();
 
-impl Default for Query {
-    fn default () -> Query {
-        Query{
-            frequency: Duration::minutes(5).num_seconds(),
-            url: "".to_string(),
-            auctions: vec![]
+        for (account_name, account) in accounts_unlocked.clone() {
+            dbg!(&account.updated());
+            accounts_unlocked.insert(account_name.to_string(), account.updated());
         }
-    }
-}
 
+        drop(accounts_unlocked);
 
-#[derive(Serialize, Deserialize, Debug, Default)]
-struct Account {
-    queries: Vec<Query>,
-    name: String,
-}
-
-
-fn date_to_gcal(date: DateTime<Local>) -> String {
-    format!("{}",
-        date.format("%Y%m%dT%H%M00/%Y%m%dT%H%M00")
-    )
-}
-
-/// Parse a relative offset "x Tage HH:MM" into proper time
-// fn parse_end_date(timestring: &String) -> Option<DateTime<Local>> {
-//     match parse(&format!("{} minutes", timestring.replace(":", " hours ").replace("Tage", "days"))).ok() {
-//         Some(old_duration) => match Duration::from_std(old_duration).ok() {
-//             Some(chronoduration) => Some(Utc::now().with_timezone(&Local) + chronoduration),
-//             None => None
-//         }
-//         None => None
-//     }
-// }
-
-/// Parse a relative offset "x Tage HH:MM" into proper time
-fn parse_remaining(timestring: &String) -> Option<Duration> {
-    match parse(&format!("{} minutes", timestring.replace(":", " hours ").replace("Tage", "days"))).ok() {
-        Some(old_duration) => Duration::from_std(old_duration).ok(),
-        None => None
+        // let pause = time::Duration::from_secs(30);
+        // thread::sleep(pause);
+        break;
     }
 }
 
-fn parse_price(price: &str) -> Option<f32> {
-    price.to_string()
-    .replace(".", "")
-    .replace(",", ".")
-    .replace(" EUR", "")
-    .parse().ok()
-}
-
-
-fn parse_url(url: &str) -> Vec<Auction> {
-    let mut auctions = vec![];
-
-    if let Ok(mut resp) = reqwest::get(url) {
-
-        if !resp.status().is_success() {
-            println!("ERR {:?}", resp.text());
-            return auctions;
-        }
-
-        let text = resp.text().unwrap_or("".to_string());
-
-
-
-    for node in Document::from_read(text.as_bytes())
-        .unwrap()
-        // .find(Name("a"))
-        // .filter(|n| n.attr("href").is_some())
-        // .filter(|n| n.attr("href").unwrap().contains("item.php?id="))
-        .find(Name("tr"))
-        .filter(|x| x.attr("bgcolor").is_some())
-        .filter(|x| x.attr("align").is_none())
-
-        {
-
-            // Get auction name
-            if let Some(name) = node
-                .children().into_iter()
-                .filter(|x| x.name() == Some("td"))
-                .filter(|x| x.attr("align") == Some("LEFT"))
-                .flat_map(|x| x.children())
-                .filter(|x| x.name() == Some("a"))
-                .flat_map(|x| x.children())
-                .map(|x| x.text())
-                // .filter(|x| x.name() == Some("Text"))
-                .collect::<Vec<_>>().get(0) {
-
-                    // If we have the name, go on finding other details
-                    // instantiate mutable Auction
-                    let mut auction = Auction::default();
-                    auction.desc = name.clone();
-                    auction.thumb = format!("http://egun.de/market/images/picture.gif");
-
-
-                    // get image                
-                    if let Some(img) = node
-                    .children().into_iter()
-                    .filter(|x| x.name() == Some("td"))
-                    .filter(|x| x.attr("align") == Some("center"))
-                    .flat_map(|x| x.descendants())
-                    .filter(|x| x.name() == Some("img"))
-                    .filter(|x| match x.attr("src") {
-                        Some(src) => src.contains("cache"),
-                        None => false
-                    }  )
-                    .map(|x| x.attr("src").unwrap()) // we just tested
-
-                    .collect::<Vec<_>>().get(0) {
-                        auction.thumb = format!("http://egun.de/market/{}", img);
-                    }
-
-
-                    // get price                
-                    if let Some(price) = node
-                    .children()
-                    .filter(|x| x.text().contains("EUR"))
-                    .flat_map(|x| x.children())
-                    .map(|x| parse_price(&x.text()))
-                    .flat_map(|x| x)
-                    .collect::<Vec<_>>().get(0) {
-                        auction.price = price.clone();
-                    }
-
-
-                    // get article url
-                    if let Some(url) = node
-                    .children().into_iter()
-                        .filter(|x| x.name() == Some("td"))
-                        .filter(|x| x.attr("align") == Some("LEFT"))
-                        .flat_map(|x| x.children())
-                        .filter(|x| x.name() == Some("a"))
-                        .map(|x| x.attr("href"))
-                        .filter_map(|x| x)
-                    .collect::<Vec<_>>().get(0) {
-                        auction.url = format!("http://egun.de/market/{}", url);
-                    }
-                
-                    // TODO: check if https://doc.rust-lang.org/std/time/struct.SystemTime.html works too
-                    if let Some(remaining) = parse_remaining(
-                        &node
-                        .children()
-                        .filter(|x| x.attr("align") == Some("center"))
-                        .filter(|x| x.attr("nowrap").is_some())
-                        .flat_map(|x| x.children())
-                        .filter(|x| !x.text().is_empty())
-                        .map(|x| x.text())
-                        .collect::<Vec<_>>()
-                        .join(" ")
-                    ){
-                        // dbg!(&t_remaining.children());
-
-                        let end_date = Utc::now().with_timezone(&Local) + remaining;
-                        auction.gcal = format!("http://www.google.com/calendar/event?action=TEMPLATE&dates={}&text={}&location=&details=", date_to_gcal(end_date), auction.desc);
-                        auction.remaining = remaining.num_seconds();
-                        auction.timestamp = end_date.timestamp();
-                        // println!("ENDS\t{:?}", date_to_gcal(remaining));
+fn main() {
 
+    // let mut test_account: HashMap<String, Account> = HashMap::new();
+    // let q = Query::default();
+    // let auction = Auction::default();
+    // let mut a = Account::default();
+    // let mut auctions: HashMap<String, Auction> = HashMap::new();
+    // auctions.insert("some gun".to_string(), auction);
+
+    // a.queries.insert("http://bla".to_string(), auctions);
+    // test_account.insert("test".to_string(), a);
+    // let j = serde_json::to_string_pretty(&test_account).unwrap();
+    // print!("{}", j);
+
+    match File::open("accounts.json") {
+        Ok(f) => {
+            match serde_json::from_reader::<_, HashMap<String, Account>>(BufReader::new(f)) {
+                Ok(accounts) => {
+                    {
+                        // Lock once, shove loaded ccounts in
+                        let mut accounts_unlocked = ACCOUNTS.lock().unwrap();
+                        *accounts_unlocked = accounts;
                     }
-
-                    auctions.push(auction);
-                }
-        }
-
-    }
-    auctions
-}
-
-
-
-fn daemon(queries: Vec<Query>, loaded_auctions: Vec<Auction>) {
-    
-    // dbg!(&queries);
-    // let _s = loaded_auctions.into_iter().map(|x| (x.url.clone(), x.clone())).collect::<Vec<_>>();
-
-
-    // for auction in loaded_auctions {
-    //     if Utc::now().with_timezone(&Local).timestamp() > auction.timestamp {
-    //         //auction has expired, mark price as final
-    //         auction.is_price_final = true;
-    //         // let a = auction_map.get_mut(_key);
-    //         dbg!(&auction);
-    //     } 
-    // }
-
-    let auction_map: HashMap<String, Auction> = loaded_auctions
-        .into_iter()
-        .map(|mut x| {
-            if Utc::now().with_timezone(&Local).timestamp() > x.timestamp {
-                // auction has expired, mark price as final
-                x.is_price_final = true;
-                }
-                x
-            }
-            )
-        .map(|x| (x.url.clone(), x.clone()))
-        .collect();
-    
-
-    // return;
-    println!("Starting daemon with {} active queries", queries.len());
-    loop {
-        // TODO: see if we can get rid of clone
-        let mut auction_map = auction_map.clone();
-        // let mut auctions = vec![];
-        for mut query in queries.clone() {
-            query.run();
-            // dbg!(&query.auctions.into_iter().map(|x| x.desc).collect::<Vec<_>>());
-            // auctions.extend(query.auctions);
-            for auction in query.auctions {
-                dbg!(&auction.desc);
-                let a = auction.clone();
-                auction_map.insert(auction.url, a);
+                    // start daemon
+                    daemon();
+                    server::run();
+                },
+                Err(e) => println!("Parsing account da'a has failed: {:?}", e)
             }
         }
-        println!("{} auctions found", &auction_map.len());
-        let writer = BufWriter::new(File::create("db.json").unwrap());
-
-        let mut auctions: Vec<Auction> = auction_map.into_iter().map(|(_x, y)| y).collect();
-        auctions.sort_by_key(|k| k.remaining);
-
-        serde_json::to_writer_pretty(writer, &auctions).unwrap();
-        let pause = time::Duration::from_secs(300);
-        thread::sleep(pause);
+        Err(e) => println!("Opening accounts file failed. {:?}", e),
     }
-
-
 }
-
-
-fn main() {
-
-    
-
-    let reader = BufReader::new(File::open("urls.json").unwrap());
-    // let reader = BufReader::new(File::open("url_local.json").unwrap());
-    let urls: Vec<String> = serde_json::from_reader(reader).unwrap_or(vec![]);
-
-
-    let reader = BufReader::new(File::open("db.json").unwrap());
-    let auctions: Vec<Auction> = serde_json::from_reader(reader).unwrap_or(vec![]);
-
-
-    daemon(
-        urls.iter()
-        .map(|x| Query {url: x.to_string(), ..Default::default()})
-        .collect::<Vec<Query>>(),
-        auctions
-        );
-
-}

+ 367 - 0
src/query.rs

@@ -0,0 +1,367 @@
+
+
+use std::collections::HashMap;
+use chrono::{Utc, Duration, Local, DateTime};
+use select::document::Document;
+use select::predicate::Name;
+use parse_duration::parse;
+use reqwest;
+
+
+#[derive(Serialize, Deserialize, Debug, Default, Clone)]
+pub struct Auction {
+    pub price: f32,
+    pub desc: String,
+    pub gcal: String,
+    pub thumb: String,
+    pub remaining: i64,
+    pub url: String,
+    pub timestamp: i64,
+    pub is_price_final: bool
+}
+
+#[derive(Serialize, Deserialize, Debug, Default, Clone)]
+pub struct Account {
+    // SEARCH URL: {AUCTION_URL: AUCTION} 
+    pub queries: HashMap<String, HashMap<String, Auction>>
+}
+
+impl Account {
+    // pub fn run_queries(&mut self) {
+    //     // self.queries = self.queries.clone().into_iter().map(|mut q| q.run()).collect();
+    //     for url in &self.queries {
+    //         dbg!(&url);
+    //     }
+    // }
+    pub fn get_auctions(&self, account_name: &str) -> HashMap<String, Auction> {
+        match self.queries.get(account_name) {
+            Some(auctions) => auctions.clone(),
+            None => HashMap::new()
+        }
+    }
+
+    pub fn run_queries(&self) -> HashMap<String, HashMap<String, Auction>> {
+        self.queries
+        .clone()
+        .into_iter()
+        .map(|(url, mut auction_map)| (url.clone(), {
+            auction_map.extend(auctions_from_url(&url));
+            auction_map
+            }))
+        .map(|(url, mut auction_map)| {
+
+            (url, auction_map.into_iter().map(|(u, mut a)| {
+                if Utc::now().with_timezone(&Local).timestamp() > a.timestamp {
+                // auction has expired, mark price as final
+                a.is_price_final = true;
+                }
+                (u,a)
+            }).collect())
+        })
+        .collect()
+    }
+
+    pub fn updated(&self) -> Account {
+        Account {queries: self.run_queries()}
+    }
+
+}
+
+// #[derive(Serialize, Deserialize, Debug, Clone)]
+// pub struct Query {
+//     pub url: String,
+//     pub auctions: Vec<Auction>,
+//     pub frequency: i64,
+// }
+
+// impl Query {
+//     pub fn run(&mut self) -> Query {
+//         self.auctions = parse_url(&self.url);
+//         self.clone()
+//     }
+
+
+
+//     fn detect_frequency(&self) {
+//         let _a = &self.auctions
+//         .iter()
+//         .map(|x| x)
+//         .collect::<Vec<_>>();
+//     }
+// }
+
+// impl Default for Query {
+//     fn default () -> Query {
+//         Query{
+//             frequency: Duration::minutes(5).num_seconds(),
+//             url: "".to_string(),
+//             auctions: vec![]
+//         }
+//     }
+// }
+
+
+
+fn date_to_gcal(date: DateTime<Local>) -> String {
+    format!("{}",
+        date.format("%Y%m%dT%H%M00/%Y%m%dT%H%M00")
+    )
+}
+
+
+/// Parse a relative offset "x Tage HH:MM" into proper time
+fn parse_remaining(timestring: &String) -> Option<Duration> {
+    match parse(&format!("{} minutes", timestring.replace(":", " hours ").replace("Tage", "days"))).ok() {
+        Some(old_duration) => Duration::from_std(old_duration).ok(),
+        None => None
+    }
+}
+
+
+fn parse_price(price: &str) -> Option<f32> {
+    price.to_string()
+    .replace(".", "")
+    .replace(",", ".")
+    .replace(" EUR", "")
+    .parse().ok()
+}
+
+
+fn parse_url(url: &str) -> Vec<Auction> {
+    let mut auctions = vec![];
+    // dbg!(&url);
+
+    if let Ok(mut resp) = reqwest::get(url) {
+
+        if !resp.status().is_success() {
+            println!("ERR {:?}", resp.text());
+            return auctions;
+        }
+
+        let text = resp.text().unwrap_or("".to_string());
+
+
+
+    for node in Document::from_read(text.as_bytes())
+        .unwrap()
+        // .find(Name("a"))
+        // .filter(|n| n.attr("href").is_some())
+        // .filter(|n| n.attr("href").unwrap().contains("item.php?id="))
+        .find(Name("tr"))
+        .filter(|x| x.attr("bgcolor").is_some())
+        .filter(|x| x.attr("align").is_none())
+
+        {
+
+            // Get auction name
+            if let Some(name) = node
+                .children().into_iter()
+                .filter(|x| x.name() == Some("td"))
+                .filter(|x| x.attr("align") == Some("LEFT"))
+                .flat_map(|x| x.children())
+                .filter(|x| x.name() == Some("a"))
+                .flat_map(|x| x.children())
+                .map(|x| x.text())
+                // .filter(|x| x.name() == Some("Text"))
+                .collect::<Vec<_>>().get(0) {
+
+                    // If we have the name, go on finding other details
+                    // instantiate mutable Auction
+                    let mut auction = Auction::default();
+                    auction.desc = name.clone();
+                    auction.thumb = format!("http://egun.de/market/images/picture.gif");
+
+
+                    // get image                
+                    if let Some(img) = node
+                    .children().into_iter()
+                    .filter(|x| x.name() == Some("td"))
+                    .filter(|x| x.attr("align") == Some("center"))
+                    .flat_map(|x| x.descendants())
+                    .filter(|x| x.name() == Some("img"))
+                    .filter(|x| match x.attr("src") {
+                        Some(src) => src.contains("cache"),
+                        None => false
+                    }  )
+                    .map(|x| x.attr("src").unwrap()) // we just tested
+
+                    .collect::<Vec<_>>().get(0) {
+                        auction.thumb = format!("http://egun.de/market/{}", img);
+                    }
+
+
+                    // get price                
+                    if let Some(price) = node
+                    .children()
+                    .filter(|x| x.text().contains("EUR"))
+                    .flat_map(|x| x.children())
+                    .map(|x| parse_price(&x.text()))
+                    .flat_map(|x| x)
+                    .collect::<Vec<_>>().get(0) {
+                        auction.price = price.clone();
+                    }
+
+
+                    // get article url
+                    if let Some(url) = node
+                    .children().into_iter()
+                        .filter(|x| x.name() == Some("td"))
+                        .filter(|x| x.attr("align") == Some("LEFT"))
+                        .flat_map(|x| x.children())
+                        .filter(|x| x.name() == Some("a"))
+                        .map(|x| x.attr("href"))
+                        .filter_map(|x| x)
+                    .collect::<Vec<_>>().get(0) {
+                        auction.url = format!("http://egun.de/market/{}", url);
+                    }
+                
+                    // TODO: check if https://doc.rust-lang.org/std/time/struct.SystemTime.html works too
+                    if let Some(remaining) = parse_remaining(
+                        &node
+                        .children()
+                        .filter(|x| x.attr("align") == Some("center"))
+                        .filter(|x| x.attr("nowrap").is_some())
+                        .flat_map(|x| x.children())
+                        .filter(|x| !x.text().is_empty())
+                        .map(|x| x.text())
+                        .collect::<Vec<_>>()
+                        .join(" ")
+                    ){
+                        // dbg!(&t_remaining.children());
+
+                        let end_date = Utc::now().with_timezone(&Local) + remaining;
+                        auction.gcal = format!("http://www.google.com/calendar/event?action=TEMPLATE&dates={}&text={}&location=&details=", date_to_gcal(end_date), auction.desc);
+                        auction.remaining = remaining.num_seconds();
+                        auction.timestamp = end_date.timestamp();
+                        // println!("ENDS\t{:?}", date_to_gcal(remaining));
+
+                    }
+
+                    auctions.push(auction);
+                }
+        }
+
+    }
+    auctions
+}
+
+
+
+
+fn auctions_from_url(url: &str) -> HashMap<String, Auction> {
+    let mut auctions = HashMap::new();
+    // dbg!(&url);
+
+    if let Ok(mut resp) = reqwest::get(url) {
+
+        if !resp.status().is_success() {
+            println!("ERR {:?}", resp.text());
+            return auctions;
+        }
+
+        let text = resp.text().unwrap_or("".to_string());
+
+
+
+    for node in Document::from_read(text.as_bytes())
+        .unwrap()
+        // .find(Name("a"))
+        // .filter(|n| n.attr("href").is_some())
+        // .filter(|n| n.attr("href").unwrap().contains("item.php?id="))
+        .find(Name("tr"))
+        .filter(|x| x.attr("bgcolor").is_some())
+        .filter(|x| x.attr("align").is_none())
+
+        {
+
+            // Get auction name
+            if let Some(name) = node
+                .children().into_iter()
+                .filter(|x| x.name() == Some("td"))
+                .filter(|x| x.attr("align") == Some("LEFT"))
+                .flat_map(|x| x.children())
+                .filter(|x| x.name() == Some("a"))
+                .flat_map(|x| x.children())
+                .map(|x| x.text())
+                // .filter(|x| x.name() == Some("Text"))
+                .collect::<Vec<_>>().get(0) {
+
+                    // If we have the name, go on finding other details
+                    // instantiate mutable Auction
+                    let mut auction = Auction::default();
+                    auction.desc = name.clone();
+                    auction.thumb = format!("http://egun.de/market/images/picture.gif");
+
+
+                    // get image                
+                    if let Some(img) = node
+                    .children().into_iter()
+                    .filter(|x| x.name() == Some("td"))
+                    .filter(|x| x.attr("align") == Some("center"))
+                    .flat_map(|x| x.descendants())
+                    .filter(|x| x.name() == Some("img"))
+                    .filter(|x| match x.attr("src") {
+                        Some(src) => src.contains("cache"),
+                        None => false
+                    }  )
+                    .map(|x| x.attr("src").unwrap()) // we just tested
+
+                    .collect::<Vec<_>>().get(0) {
+                        auction.thumb = format!("http://egun.de/market/{}", img);
+                    }
+
+
+                    // get price                
+                    if let Some(price) = node
+                    .children()
+                    .filter(|x| x.text().contains("EUR"))
+                    .flat_map(|x| x.children())
+                    .map(|x| parse_price(&x.text()))
+                    .flat_map(|x| x)
+                    .collect::<Vec<_>>().get(0) {
+                        auction.price = price.clone();
+                    }
+
+
+                    // get article url
+                    if let Some(url) = node
+                    .children().into_iter()
+                        .filter(|x| x.name() == Some("td"))
+                        .filter(|x| x.attr("align") == Some("LEFT"))
+                        .flat_map(|x| x.children())
+                        .filter(|x| x.name() == Some("a"))
+                        .map(|x| x.attr("href"))
+                        .filter_map(|x| x)
+                    .collect::<Vec<_>>().get(0) {
+                        auction.url = format!("http://egun.de/market/{}", url);
+                    }
+                
+                    // TODO: check if https://doc.rust-lang.org/std/time/struct.SystemTime.html works too
+                    if let Some(remaining) = parse_remaining(
+                        &node
+                        .children()
+                        .filter(|x| x.attr("align") == Some("center"))
+                        .filter(|x| x.attr("nowrap").is_some())
+                        .flat_map(|x| x.children())
+                        .filter(|x| !x.text().is_empty())
+                        .map(|x| x.text())
+                        .collect::<Vec<_>>()
+                        .join(" ")
+                    ){
+                        // dbg!(&t_remaining.children());
+
+                        let end_date = Utc::now().with_timezone(&Local) + remaining;
+                        auction.gcal = format!("http://www.google.com/calendar/event?action=TEMPLATE&dates={}&text={}&location=&details=", date_to_gcal(end_date), auction.desc);
+                        auction.remaining = remaining.num_seconds();
+                        auction.timestamp = end_date.timestamp();
+                        // println!("ENDS\t{:?}", date_to_gcal(remaining));
+
+                    }
+
+                    auctions.insert(auction.url.clone(), auction);
+                }
+        }
+
+    }
+    auctions
+}

+ 54 - 0
src/server.rs

@@ -0,0 +1,54 @@
+use rocket_contrib::serve::StaticFiles;
+use rocket_contrib::json::Json;
+use rocket::response::content;
+use std::sync::Mutex;
+use super::ACCOUNTS;
+use super::query::*;
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+struct Task {
+    description: String,
+    complete: bool
+}
+
+lazy_static! {
+    static ref TASKS: Mutex<Vec<Task>> = Mutex::new(vec![]);
+}
+
+
+#[get("/")]
+fn index() -> &'static str {
+    let accounts = ACCOUNTS.lock().unwrap();
+    dbg!(&*accounts);
+    drop(accounts);
+    "Base url"
+}
+
+// receive json and make tasks from it (deserialize)
+#[post("/save", data = "<tasks>")] //<tasks> means that the function below will have a var called tasks
+fn save(tasks: Json<Vec<Task>>) -> &'static str {
+
+    let mut old_tasks = TASKS.lock().unwrap();
+    * old_tasks = tasks.to_vec();
+    "nice..."
+}
+
+// turn tasks into json (serialize)
+#[get("/load")]
+fn load() -> content::Json<String> {
+    // get a lock here on TASKS
+    let guard = ACCOUNTS.lock().unwrap();
+    // serialize all tasks
+    let json_string = serde_json::to_string(&*guard).unwrap();
+    // hand them out to the browser (or anything else) and tell them to expect json
+    content::Json(json_string)
+}
+
+pub fn run() {
+    rocket::ignite()
+    .mount("/", routes![index])
+    // .mount("/", routes![save])
+    .mount("/", routes![load])
+    // .mount("/static", StaticFiles::from("webapp"))
+    .launch();
+}

File diff suppressed because it is too large
+ 1 - 0
webapp/lilac.min.css


File diff suppressed because it is too large
+ 1 - 0
webapp/moment-duration-format.min.js


File diff suppressed because it is too large
+ 7 - 0
webapp/moment.min.js


File diff suppressed because it is too large
+ 6 - 0
webapp/vue.min.js