From 16a9bba6dd46c67965c4f361b7505d1aa2fad32f Mon Sep 17 00:00:00 2001 From: Mylloon Date: Mon, 18 Sep 2023 19:50:30 +0200 Subject: [PATCH 01/17] find the right timetable for the current semester/year combo --- src/main.rs | 47 +++++++++---------- src/timetable.rs | 114 +++++++++++++++++------------------------------ src/utils.rs | 11 +---- 3 files changed, 64 insertions(+), 108 deletions(-) diff --git a/src/main.rs b/src/main.rs index 189eb78..3828c65 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,14 +9,18 @@ mod utils; #[derive(Parser)] #[clap(version, about, long_about = None)] struct Args { - /// The class you want to get the timetable, i.e.: L2-A + /// The class you want to get the timetable, i.e.: M1-LP #[clap(value_parser)] class: String, - /// The semester you want (useful only in 3rd year, 1-2 use letter in class) + /// The semester you want (1 or 2) #[clap(short, long, value_parser, value_name = "SEMESTER NUMBER")] semester: Option, + /// The year, default to the current year + #[clap(short, long, value_parser, value_name = "YEAR")] + year: Option, + /// Export to iCalendar format (.ics) #[clap(short, long, value_name = "FILE NAME")] export: Option, @@ -30,38 +34,31 @@ struct Args { async fn main() { let args = Args::parse(); - let matches = Regex::new(r"[Ll](?P\d)[-–•·]?(?P.)?") - .unwrap() - .captures(&args.class) - .unwrap(); + let matches = + Regex::new(r"(?i)M(?P[1,2])[-–•·]?(?P(LP|IMPAIRS|DATA|GENIAL|MPRI))?") + .unwrap() + .captures(&args.class) + .unwrap(); - let year = matches - .name("year") + let level = matches + .name("level") .unwrap() .as_str() .parse::() .unwrap(); - let letter = matches - .name("letter") - .map(|c| c.as_str().chars().next().expect("Error in letter")); + let pathway = matches.name("pathway").unwrap().as_str(); - // Show a separator only if we need one - let seperator = match letter { - Some(_) => "-", - None => "", - }; - - let user_agent = format!("cal8tor/{}", env!("CARGO_PKG_VERSION")); + let user_agent = format!("cal7tor/{}", env!("CARGO_PKG_VERSION")); println!( - "Récupération de l'emploi du temps des L{}{}{}...", - year, - seperator, - letter.unwrap_or_default().to_uppercase() + "Récupération de l'emploi du temps des M{}-{}...", + level, + pathway.to_uppercase() ); - let timetable = timetable::timetable(year, args.semester, letter, &user_agent).await; + let timetable = + timetable::timetable(level, args.semester, args.year, pathway, &user_agent).await; - println!("Récupération des informations par rapport à l'année..."); + /* println!("Récupération des informations par rapport à l'année..."); let info = info::info(&user_agent).await; if args.export.is_some() { @@ -77,5 +74,5 @@ async fn main() { println!("Affichage..."); timetable::display(timetable, args.cl); println!("Vous devrez peut-être mettre votre terminal en plein écran si ce n'est pas déjà le cas."); - } + } */ } diff --git a/src/timetable.rs b/src/timetable.rs index 05bc6a7..9a8944d 100644 --- a/src/timetable.rs +++ b/src/timetable.rs @@ -12,19 +12,24 @@ pub mod models; /// Fetch the timetable for a class pub async fn timetable( - year: i8, + level: i8, semester_opt: Option, - letter: Option, + year_opt: Option, + pathway: &str, user_agent: &str, ) -> (Vec, (usize, Vec)) { - let semester = get_semester(semester_opt, letter); + let semester = get_semester(semester_opt); - let document = get_webpage(year, semester, letter, user_agent) + let year = get_year(year_opt, semester); + + let document = get_webpage(level, semester, &year, user_agent) .await .expect("Can't reach timetable website."); + (vec![], (0, vec![])) + // Selectors - let sel_table = Selector::parse("table").unwrap(); + /* let sel_table = Selector::parse("table").unwrap(); let sel_tr = Selector::parse("tr").unwrap(); let sel_tbody = Selector::parse("tbody").unwrap(); let sel_th = Selector::parse("th").unwrap(); @@ -116,59 +121,17 @@ pub async fn timetable( panic!("Error when building the timetable."); } - (schedules, (semester as usize, timetable)) + (schedules, (semester as usize, timetable)) */ } /// Get timetable webpage async fn get_webpage( - year: i8, + level: i8, semester: i8, - letter: Option, + year: &str, user_agent: &str, ) -> Result> { - let url = { - let panic_semester_message = "Unknown semester."; - let panic_letter_message = "Unknown letter."; - - let base_url = "https://informatique.up8.edu/licence-iv/edt"; - let allow_letters_1 = match semester { - 1 => ['a', 'b', 'c'], - 2 => ['x', 'y', 'z'], - _ => panic!("{}", panic_semester_message), - }; - let allow_letters_2_3 = match semester { - 1 => ['a', 'b'], - 2 => ['x', 'y'], - _ => panic!("{}", panic_semester_message), - }; - match year { - 1 => { - let c = letter.expect(panic_letter_message).to_ascii_lowercase(); - if allow_letters_1.contains(&c) { - format!("{}/l1-{}.html", base_url, c) - } else { - panic!("{}", panic_letter_message) - } - } - 2 => { - let c = letter.expect(panic_letter_message).to_ascii_lowercase(); - if allow_letters_2_3.contains(&c) { - format!("{}/l2-{}.html", base_url, c) - } else { - panic!("{}", panic_letter_message) - } - } - 3 => { - let c = letter.expect(panic_letter_message).to_ascii_lowercase(); - if allow_letters_2_3.contains(&c) { - format!("{}/l3-{}.html", base_url, c) - } else { - panic!("{}", panic_letter_message) - } - } - _ => panic!("Unknown year."), - } - }; + let url = format!("https://silice.informatique.univ-paris-diderot.fr/ufr/U{}/EDT/visualiserEmploiDuTemps.php?quoi=M{},{}", year, level, semester); // Use custom User-Agent let client = reqwest::Client::builder().user_agent(user_agent).build()?; @@ -314,34 +277,37 @@ pub fn build(timetable: T, dates: D) -> Vec { semester } -/// Get the current semester depending on the letter or the current date -fn get_semester(semester: Option, letter: Option) -> i8 { +/// Get the current semester depending on the current date +fn get_semester(semester: Option) -> i8 { match semester { // Force the asked semester Some(n) => n, // Find the potential semester - None => match letter { - // Based on letter (kinda accurate) - Some(c) => { - if c.to_ascii_uppercase() as i8 > 77 { - // If letter is N or after - 2 - } else { - // If letter is before N - 1 - } + None => { + if Utc::now().month() > 6 { + // From july to december + 1 + } else { + // from january to june + 2 } - // Based on the time (kinda less accurate) - None => { - if Utc::now().month() > 6 { - // From july to december - 1 - } else { - // from january to june - 2 - } - } - }, + } + } +} + +/// Get the current year depending on the current date +fn get_year(year: Option, semester: i8) -> String { + let wanted_year = match year { + // Force the asked semester + Some(n) => n, + // Find the potential semester + None => Utc::now().year(), + }; + + if semester == 1 { + format!("{}-{}", wanted_year, wanted_year + 1) + } else { + format!("{}-{}", wanted_year - 1, wanted_year) } } diff --git a/src/utils.rs b/src/utils.rs index c03101d..f7e3114 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,20 +2,13 @@ pub mod models; /// Panic if an error happened pub fn check_errors(html: &String, loc: &str) { + let no_timetable = "Aucun créneau horaire affecté"; match html { - t if t.contains(&err_code(429)) => panic!( - "URL: {} • HTTP 429: Slow down - Rate limited (too many access attempts detected)", - loc - ), + t if t.contains(no_timetable) => panic!("URL: {} • {}", loc, no_timetable), _ => (), } } -/// Create String error code -fn err_code(code: i32) -> String { - format!("HTTP Code : {}", code) -} - /// Print a line for the table pub fn line_table( cell_length_hours: usize, From ff95bf09b5628ce1a08d3bf91a1051673c9468e4 Mon Sep 17 00:00:00 2001 From: Mylloon Date: Mon, 18 Sep 2023 20:00:35 +0200 Subject: [PATCH 02/17] currently alpha --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ff6900f..88c35c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,7 +160,7 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cal7tor" -version = "0.5.0" +version = "0.5.0-alpha" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index e4be2d6..483cca5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cal7tor" -version = "0.5.0" +version = "0.5.0-alpha" authors = ["Mylloon"] edition = "2021" description = "Timetable extractor for the Paris Cité master's degree in IT" From 1dbfe1256623a23a137d88a14a89c0ec4c6daa01 Mon Sep 17 00:00:00 2001 From: Mylloon Date: Mon, 18 Sep 2023 20:52:18 +0200 Subject: [PATCH 03/17] find size of days --- src/timetable.rs | 115 ++++++++++++----------------------------------- 1 file changed, 29 insertions(+), 86 deletions(-) diff --git a/src/timetable.rs b/src/timetable.rs index 9a8944d..157211c 100644 --- a/src/timetable.rs +++ b/src/timetable.rs @@ -26,102 +26,45 @@ pub async fn timetable( .await .expect("Can't reach timetable website."); - (vec![], (0, vec![])) - // Selectors - /* let sel_table = Selector::parse("table").unwrap(); + let sel_table = Selector::parse("table").unwrap(); + let sel_thead = Selector::parse("thead").unwrap(); let sel_tr = Selector::parse("tr").unwrap(); - let sel_tbody = Selector::parse("tbody").unwrap(); let sel_th = Selector::parse("th").unwrap(); - let sel_td = Selector::parse("td").unwrap(); - let sel_em = Selector::parse("em").unwrap(); - let sel_small = Selector::parse("small").unwrap(); - let sel_strong = Selector::parse("strong").unwrap(); // Find the timetable let raw_timetable = document.select(&sel_table).next().unwrap(); - // Find the slots available for the timetable - let raw_schedules = raw_timetable.select(&sel_tr).next().unwrap(); - - // Find availables schedules - let mut schedules = Vec::new(); - for time in raw_schedules.select(&sel_th) { - schedules.push(time.inner_html()); - } - - // Find the timetable values - let raw_timetable_values = raw_timetable.select(&sel_tbody).next().unwrap(); - - // For each days - let mut timetable = Vec::new(); - let span_regex = Regex::new(r"").unwrap(); - for day in raw_timetable_values.select(&sel_tr) { - let mut courses_vec = Vec::new(); - let mut location_tracker = 0; - for course in day.select(&sel_td) { - if course.inner_html() == "—" { - courses_vec.push(None); - location_tracker += 1; - } else { - courses_vec.push(Some(models::Course { - name: match course.select(&sel_em).next() { - Some(value) => span_regex.replace(&value.inner_html(), " ").to_string(), - None => span_regex - .replace(course.inner_html().split("
").next().unwrap(), " ") - .to_string(), - }, - professor: match course - .select(&sel_small) - .next() - .unwrap() - .inner_html() - .split("
") - .next() - { - Some(data) => { - if data.contains("") { - // This is the room, so there is no professor assigned - // to this courses yet - None - } else { - Some(data.to_string()) - } - } - None => None, - }, - room: capitalize(&mut match course.select(&sel_strong).next() { - Some(el) => el.inner_html().replace("
", ""), - // Error in the site, silently passing... (the room is probably at the professor member) - None => String::new(), - }), - start: location_tracker, - size: match course.value().attr("colspan") { - Some(i) => i.parse().unwrap(), - None => 1, - }, - dtstart: None, - dtend: None, - })); - - match &courses_vec[courses_vec.len() - 1] { - Some(course) => location_tracker += course.size, - None => location_tracker += 1, - } - } - } - - timetable.push(models::Day { - name: day.select(&sel_th).next().unwrap().inner_html(), - courses: courses_vec, + // Find days + let days_size: Vec<_> = raw_timetable + .select(&sel_thead) + .next() + .unwrap() + .select(&sel_tr) + .next() + .unwrap() + .select(&sel_th) + .next() + .unwrap() + .next_siblings() + .flat_map(|i| { + let element = i.value().as_element().unwrap(); + element + .attrs() + .filter_map(|f| { + if f.0.contains("colspan") { + Some(f.1) + } else { + None + } + }) + .collect::>() }) - } + .collect(); - if !check_consistency(&schedules, &timetable) { - panic!("Error when building the timetable."); - } + println!("{:#?}", days); - (schedules, (semester as usize, timetable)) */ + (vec![], (0, vec![])) } /// Get timetable webpage From 9ba185b247e410c680a3f4ab88da3a786929954d Mon Sep 17 00:00:00 2001 From: Mylloon Date: Mon, 18 Sep 2023 20:54:48 +0200 Subject: [PATCH 04/17] oui --- src/timetable.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/timetable.rs b/src/timetable.rs index 157211c..73dc8d0 100644 --- a/src/timetable.rs +++ b/src/timetable.rs @@ -35,7 +35,10 @@ pub async fn timetable( // Find the timetable let raw_timetable = document.select(&sel_table).next().unwrap(); - // Find days + /* We are finding size of days in the timetable + * so we can increment it when we will cross the timetable */ + + // Find days size let days_size: Vec<_> = raw_timetable .select(&sel_thead) .next() @@ -62,9 +65,10 @@ pub async fn timetable( }) .collect(); - println!("{:#?}", days); + /* We are now iterating over all the 15-minute intervals to find courses */ + // TODO - (vec![], (0, vec![])) + todo!() } /// Get timetable webpage From b3fec122923678a60ea5f668c08bccbf4254ae5e Mon Sep 17 00:00:00 2001 From: Mylloon Date: Tue, 26 Sep 2023 19:16:15 +0200 Subject: [PATCH 05/17] I made a discovery that changed my life! --- src/timetable.rs | 46 ++++++++++++---------------------------------- 1 file changed, 12 insertions(+), 34 deletions(-) diff --git a/src/timetable.rs b/src/timetable.rs index 73dc8d0..f029c4c 100644 --- a/src/timetable.rs +++ b/src/timetable.rs @@ -28,45 +28,23 @@ pub async fn timetable( // Selectors let sel_table = Selector::parse("table").unwrap(); - let sel_thead = Selector::parse("thead").unwrap(); - let sel_tr = Selector::parse("tr").unwrap(); - let sel_th = Selector::parse("th").unwrap(); + let sel_tbody = Selector::parse("tbody").unwrap(); + let sel_td = Selector::parse("td").unwrap(); // Find the timetable let raw_timetable = document.select(&sel_table).next().unwrap(); - /* We are finding size of days in the timetable - * so we can increment it when we will cross the timetable */ - - // Find days size - let days_size: Vec<_> = raw_timetable - .select(&sel_thead) - .next() - .unwrap() - .select(&sel_tr) - .next() - .unwrap() - .select(&sel_th) - .next() - .unwrap() - .next_siblings() - .flat_map(|i| { - let element = i.value().as_element().unwrap(); - element - .attrs() - .filter_map(|f| { - if f.0.contains("colspan") { - Some(f.1) - } else { - None - } - }) - .collect::>() - }) - .collect(); - /* We are now iterating over all the 15-minute intervals to find courses */ - // TODO + for element in raw_timetable + .select(&sel_tbody) + .next() + .unwrap() + .select(&sel_td) + { + if let Some(i) = element.value().attr("title") { + println!("{}", i) + } + } todo!() } From 189b77cc4fe0b0c2625a052e9872d266ccf83d86 Mon Sep 17 00:00:00 2001 From: Mylloon Date: Tue, 26 Sep 2023 23:08:41 +0200 Subject: [PATCH 06/17] Correctly build the timetable from scratch --- src/main.rs | 7 ++-- src/timetable.rs | 86 ++++++++++++++++++++++++++++++++++++----- src/timetable/models.rs | 13 ++++++- 3 files changed, 92 insertions(+), 14 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3828c65..f907a05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -55,10 +55,9 @@ async fn main() { level, pathway.to_uppercase() ); - let timetable = - timetable::timetable(level, args.semester, args.year, pathway, &user_agent).await; + let timetable = timetable::timetable(level, args.semester, args.year, &user_agent).await; - /* println!("Récupération des informations par rapport à l'année..."); + println!("Récupération des informations par rapport à l'année..."); let info = info::info(&user_agent).await; if args.export.is_some() { @@ -74,5 +73,5 @@ async fn main() { println!("Affichage..."); timetable::display(timetable, args.cl); println!("Vous devrez peut-être mettre votre terminal en plein écran si ce n'est pas déjà le cas."); - } */ + } } diff --git a/src/timetable.rs b/src/timetable.rs index f029c4c..0c16348 100644 --- a/src/timetable.rs +++ b/src/timetable.rs @@ -4,7 +4,7 @@ use scraper::{Html, Selector}; use std::collections::HashMap; use crate::utils::{ - self, capitalize, + self, models::{Position, TabChar}, }; @@ -15,7 +15,6 @@ pub async fn timetable( level: i8, semester_opt: Option, year_opt: Option, - pathway: &str, user_agent: &str, ) -> (Vec, (usize, Vec)) { let semester = get_semester(semester_opt); @@ -30,23 +29,92 @@ pub async fn timetable( let sel_table = Selector::parse("table").unwrap(); let sel_tbody = Selector::parse("tbody").unwrap(); let sel_td = Selector::parse("td").unwrap(); + let sel_small = Selector::parse("small").unwrap(); + let sel_b = Selector::parse("b").unwrap(); // Find the timetable let raw_timetable = document.select(&sel_table).next().unwrap(); - /* We are now iterating over all the 15-minute intervals to find courses */ - for element in raw_timetable + let mut schedules = Vec::new(); + for hour in 8..=20 { + for minute in &[0, 15, 30, 45] { + let hour_str = format!("{}h{:02}", hour, minute); + schedules.push(hour_str); + } + } + + let mut timetable: Vec = Vec::new(); + + raw_timetable .select(&sel_tbody) .next() .unwrap() .select(&sel_td) - { - if let Some(i) = element.value().attr("title") { - println!("{}", i) - } + .filter(|element| element.value().attr("title").is_some()) + .for_each(|i| { + let matches = + Regex::new(r"(?PCOURS|TD|TP) (?P.*) : (?P(lundi|mardi|mercredi|jeudi|vendredi)) (?P.*) \(durée : (?P.*)\)") + .unwrap() + .captures(i.value().attr("title").unwrap()) + .unwrap(); + + let day: &str = matches + .name("day") + .unwrap() + .as_str(); + + + let binding = i.select(&sel_b).last().unwrap().inner_html(); + let course = models::Course{ + typee: match matches + .name("type") + .unwrap() + .as_str() { + "COURS" => models::Type::Cours, + "TP" => models::Type::TP, + "TD" => models::Type::TD, + _ => panic!("Unknown type of course") + }, + name: matches + .name("name") + .unwrap() + .as_str().to_owned(), + professor: match i.select(&sel_small).last().unwrap().inner_html() { + i if i.starts_with(" None, + i => Some(i), + }, + room: Regex::new(r"(|
.*?
.*?)
(?P.*?)
") + .unwrap() + .captures(&binding) + .unwrap().name("location") + .unwrap() + .as_str().to_owned(), + start: schedules.iter().position(|r| r == matches + .name("startime") + .unwrap() + .as_str()).unwrap(), + size: i.value().attr("rowspan").unwrap().parse::().unwrap(), + dtstart: None, + dtend: None, + }; + + // Search for the day in the timetable + if let Some(existing_day) = timetable.iter_mut().find(|x| x.name == day) { + existing_day.courses.push(Some(course)); + } else { + // Day with the name doesn't exist, create a new Day + timetable.push(models::Day { + name: day.to_owned(), + courses: vec![Some(course)], + }); + } + }); + + if !check_consistency(&schedules, &timetable) { + panic!("Error when building the timetable."); } - todo!() + (schedules, (semester as usize, timetable)) } /// Get timetable webpage diff --git a/src/timetable/models.rs b/src/timetable/models.rs index 1b67bcf..bb813a3 100644 --- a/src/timetable/models.rs +++ b/src/timetable/models.rs @@ -1,5 +1,15 @@ -#[derive(Clone)] +#[derive(Clone, Debug)] +pub enum Type { + Cours, + TP, + TD, +} + +#[derive(Clone, Debug)] pub struct Course { + /// Type du cours + pub typee: Type, + /// Course's name pub name: String, @@ -27,6 +37,7 @@ pub struct Course { pub dtend: Option>, } +#[derive(Debug)] pub struct Day { /// Day's name pub name: String, From bbee1d58be8a4b3e132544047871ff608bc03673 Mon Sep 17 00:00:00 2001 From: Mylloon Date: Tue, 26 Sep 2023 23:55:54 +0200 Subject: [PATCH 07/17] Should be better now ? --- src/timetable.rs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/timetable.rs b/src/timetable.rs index 0c16348..f5e08e6 100644 --- a/src/timetable.rs +++ b/src/timetable.rs @@ -1,7 +1,7 @@ -use chrono::{Datelike, Duration, TimeZone, Utc}; +use chrono::{Datelike, Duration, NaiveTime, TimeZone, Utc}; use regex::Regex; use scraper::{Html, Selector}; -use std::collections::HashMap; +use std::{collections::HashMap, str::FromStr}; use crate::utils::{ self, @@ -35,15 +35,16 @@ pub async fn timetable( // Find the timetable let raw_timetable = document.select(&sel_table).next().unwrap(); - let mut schedules = Vec::new(); + let mut hours = Vec::new(); for hour in 8..=20 { for minute in &[0, 15, 30, 45] { let hour_str = format!("{}h{:02}", hour, minute); - schedules.push(hour_str); + hours.push(hour_str); } } let mut timetable: Vec = Vec::new(); + let mut schedules = Vec::new(); raw_timetable .select(&sel_tbody) @@ -58,11 +59,15 @@ pub async fn timetable( .captures(i.value().attr("title").unwrap()) .unwrap(); - let day: &str = matches + let day = matches .name("day") .unwrap() .as_str(); + let startime = matches + .name("startime") + .unwrap() + .as_str(); let binding = i.select(&sel_b).last().unwrap().inner_html(); let course = models::Course{ @@ -89,10 +94,7 @@ pub async fn timetable( .unwrap().name("location") .unwrap() .as_str().to_owned(), - start: schedules.iter().position(|r| r == matches - .name("startime") - .unwrap() - .as_str()).unwrap(), + start: hours.iter().position(|r| r == startime).unwrap(), size: i.value().attr("rowspan").unwrap().parse::().unwrap(), dtstart: None, dtend: None, @@ -108,6 +110,18 @@ pub async fn timetable( courses: vec![Some(course)], }); } + + + let duration = Regex::new(r"(?P\d{1,2})h(?P\d{1,2})?") + .unwrap() + .captures(matches + .name("duration") + .unwrap() + .as_str()).unwrap(); + schedules.push(format!("{}-{}", startime, NaiveTime::from_str(&startime.replace('h', ":")).unwrap().overflowing_add_signed(Duration::minutes(duration.name("h").unwrap().as_str().parse::().unwrap() * 60 + match duration.name("m") { + Some(x) => x.as_str().parse::().unwrap(), + None => 0 + })).0.format("%Hh%M"))); }); if !check_consistency(&schedules, &timetable) { From c671587b7a6b175ffe2c7d8f930f64a400fbd4dc Mon Sep 17 00:00:00 2001 From: Mylloon Date: Thu, 28 Sep 2023 00:00:46 +0200 Subject: [PATCH 08/17] Sadly break-weeks are voted every year --- src/info.rs | 104 ++++++++++++++++++++++------------------------------ 1 file changed, 43 insertions(+), 61 deletions(-) diff --git a/src/info.rs b/src/info.rs index ac20468..e295501 100644 --- a/src/info.rs +++ b/src/info.rs @@ -1,76 +1,58 @@ -use chrono::{DateTime, Utc}; +use chrono::{DateTime, Duration, Utc}; use regex::{Captures, Regex}; -use scraper::{Html, Selector}; +use scraper::Selector; use std::collections::HashMap; -pub async fn info(user_agent: &str) -> HashMap, i64)>> { - let document = get_webpage(user_agent) +use crate::utils::{get_semester, get_webpage, get_year}; + +pub async fn info( + level: i8, + semester_opt: Option, + year_opt: Option, + user_agent: &str, +) -> HashMap, i64)>> { + let semester = get_semester(semester_opt); + + let year = get_year(year_opt, semester); + + let document = get_webpage(level, semester, &year, user_agent) .await .expect("Can't reach info website."); // Selectors - let sel_ul = Selector::parse("ul").unwrap(); - let sel_li = Selector::parse("li").unwrap(); + let sel_b = Selector::parse("b").unwrap(); + let sel_font = Selector::parse("font").unwrap(); - // Find the raw infos in html page - let mut raw_data = Vec::new(); - for (i, data) in document.select(&sel_ul).enumerate() { - if [1, 2].contains(&i) { - raw_data.push(data); - } - } + // Find when is the back-to-school date + let raw_data = document + .select(&sel_b) + .find(|element| element.select(&sel_font).next().is_some()) + .unwrap() + .inner_html(); - let mut data = HashMap::new(); - // d => date - // r => repetition - let re = Regex::new(r"(?P\d{1,2} \w+ \d{4}).+(?P\d)").unwrap(); - for (i, ul) in raw_data.into_iter().enumerate() { - for element in ul.select(&sel_li) { - match element.inner_html() { - e if e.starts_with("Début") => { - let captures = re.captures(&e).unwrap(); + let re = Regex::new(r"\d{1,2} (septembre|octobre)").unwrap(); + let date = re.captures(&raw_data).unwrap().get(0).unwrap().as_str(); - let start_date = get_date(captures.name("d").unwrap().as_str()); + let weeks_s1_1 = 6; // Number of weeks in the first part of the first semester + let date_s1_1 = get_date(&format!("{} {}", date, year.split_once('-').unwrap().0)); // Get week of back-to-school + let weeks_s1_2 = 7; // Number of weeks in the second part of the first semester + let date_s1_2 = date_s1_1 + Duration::weeks(weeks_s1_1 + 1); // Add past weeks with the break-week - let rep: i64 = captures.name("r").unwrap().as_str().parse().unwrap(); + let weeks_s2_1 = 6; // Number of weeks in the first part of the second semester + let date_s2_1 = date_s1_2 + Duration::weeks(weeks_s1_2 + 4); // 4 weeks of vacation between semester + let weeks_s2_2 = 7; // Number of weeks in the second part of the second semester + let date_s2_2 = date_s2_1 + Duration::weeks(weeks_s2_1 + 1); // Add past weeks with the break-week - data.insert(i + 1, vec![(start_date, rep)]); - } - e if e.starts_with("Reprise") => { - let captures = re.captures(&e).unwrap(); - captures.name("g"); - - let start_date = get_date(captures.name("d").unwrap().as_str()); - - let rep: i64 = captures.name("r").unwrap().as_str().parse().unwrap(); - - let it = i + 1; - - let mut vec = data.get(&it).unwrap().to_owned(); - vec.push((start_date, rep)); - - data.insert(it, vec); - } - _ => (), - } - } - } - - data -} - -/// Get info webpage -async fn get_webpage(user_agent: &str) -> Result> { - let url = "https://informatique.up8.edu/licence-iv/edt"; - - // Use custom User-Agent - let client = reqwest::Client::builder().user_agent(user_agent).build()?; - let html = client.get(url).send().await?.text().await?; - - // Panic on error - crate::utils::check_errors(&html, url); - - Ok(Html::parse_document(&html)) + HashMap::from([ + ( + 1_usize, + vec![(date_s1_1, weeks_s1_1), (date_s1_2, weeks_s1_2)], + ), + ( + 2_usize, + vec![(date_s2_1, weeks_s2_1), (date_s2_2, weeks_s2_2)], + ), + ]) } /// Turn a french date to an english one From be636f552a1f2cc3ba08d9b843177a081efe7bd2 Mon Sep 17 00:00:00 2001 From: Mylloon Date: Thu, 28 Sep 2023 00:01:48 +0200 Subject: [PATCH 09/17] Changes - We are now searching backtoschool day in the same page that the timetable - It should work now --- src/main.rs | 2 +- src/timetable.rs | 128 ++++++----------------------------------------- src/utils.rs | 62 ++++++++++++++++++++--- 3 files changed, 72 insertions(+), 120 deletions(-) diff --git a/src/main.rs b/src/main.rs index f907a05..801a6b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -58,7 +58,7 @@ async fn main() { let timetable = timetable::timetable(level, args.semester, args.year, &user_agent).await; println!("Récupération des informations par rapport à l'année..."); - let info = info::info(&user_agent).await; + let info = info::info(level, args.semester, args.year, &user_agent).await; if args.export.is_some() { // Export the calendar diff --git a/src/timetable.rs b/src/timetable.rs index f5e08e6..bc53162 100644 --- a/src/timetable.rs +++ b/src/timetable.rs @@ -1,10 +1,10 @@ -use chrono::{Datelike, Duration, NaiveTime, TimeZone, Utc}; +use chrono::{Datelike, Duration, TimeZone, Utc}; use regex::Regex; -use scraper::{Html, Selector}; -use std::{collections::HashMap, str::FromStr}; +use scraper::Selector; +use std::collections::HashMap; use crate::utils::{ - self, + self, get_semester, get_webpage, get_year, models::{Position, TabChar}, }; @@ -35,16 +35,21 @@ pub async fn timetable( // Find the timetable let raw_timetable = document.select(&sel_table).next().unwrap(); - let mut hours = Vec::new(); + let mut schedules = Vec::new(); for hour in 8..=20 { for minute in &[0, 15, 30, 45] { let hour_str = format!("{}h{:02}", hour, minute); - hours.push(hour_str); + if let Some(last_hour) = schedules.pop() { + schedules.push(format!("{}-{}", last_hour, hour_str)); + } + schedules.push(hour_str); } } + for _ in 0..4 { + schedules.pop(); + } let mut timetable: Vec = Vec::new(); - let mut schedules = Vec::new(); raw_timetable .select(&sel_tbody) @@ -94,8 +99,8 @@ pub async fn timetable( .unwrap().name("location") .unwrap() .as_str().to_owned(), - start: hours.iter().position(|r| r == startime).unwrap(), - size: i.value().attr("rowspan").unwrap().parse::().unwrap(), + start: schedules.iter().position(|r| r.starts_with(startime)).unwrap(), + size: i.value().attr("rowspan").unwrap().parse::().unwrap(), dtstart: None, dtend: None, }; @@ -110,78 +115,11 @@ pub async fn timetable( courses: vec![Some(course)], }); } - - - let duration = Regex::new(r"(?P\d{1,2})h(?P\d{1,2})?") - .unwrap() - .captures(matches - .name("duration") - .unwrap() - .as_str()).unwrap(); - schedules.push(format!("{}-{}", startime, NaiveTime::from_str(&startime.replace('h', ":")).unwrap().overflowing_add_signed(Duration::minutes(duration.name("h").unwrap().as_str().parse::().unwrap() * 60 + match duration.name("m") { - Some(x) => x.as_str().parse::().unwrap(), - None => 0 - })).0.format("%Hh%M"))); }); - if !check_consistency(&schedules, &timetable) { - panic!("Error when building the timetable."); - } - (schedules, (semester as usize, timetable)) } -/// Get timetable webpage -async fn get_webpage( - level: i8, - semester: i8, - year: &str, - user_agent: &str, -) -> Result> { - let url = format!("https://silice.informatique.univ-paris-diderot.fr/ufr/U{}/EDT/visualiserEmploiDuTemps.php?quoi=M{},{}", year, level, semester); - - // Use custom User-Agent - let client = reqwest::Client::builder().user_agent(user_agent).build()?; - let html = client.get(&url).send().await?.text().await?; - - // Panic on error - crate::utils::check_errors(&html, &url); - - // Parse document - let document = Html::parse_document(&html); - - Ok(document) -} - -/// Check if the timetable is well built -fn check_consistency(schedules: &[String], timetable: &Vec) -> bool { - let mut checker = true; - for day in timetable { - let mut i = 0; - for course in &day.courses { - match course { - Some(course_it) => { - // Checks the consistency of course start times - if i != course_it.start { - checker = false; - break; - } - // Keep the track of how many courses are in the day - i += course_it.size - } - None => i += 1, - } - } - // The counter should be the same as the amount of possible hours of the day - if i != schedules.len() { - checker = false; - break; - } - } - - checker -} - // Data builded in the timetable webpage type T = ( // Schedules @@ -202,9 +140,7 @@ pub fn build(timetable: T, dates: D) -> Vec { let mut schedules = Vec::new(); // h1 => heure de début | m1 => minute de début // h2 => heure de fin | m2 => minute de fin - let re = - Regex::new(r"(?P

\d{1,2})(h|:)(?P\d{1,2})?.(?P

\d{1,2})(h|:)(?P\d{1,2})?") - .unwrap(); + let re = Regex::new(r"(?P

\d{1,2})h(?P\d{2})-(?P

\d{1,2})h(?P\d{2})").unwrap(); for hour in timetable.0 { let captures = re.captures(&hour).unwrap(); @@ -284,40 +220,6 @@ pub fn build(timetable: T, dates: D) -> Vec { semester } -/// Get the current semester depending on the current date -fn get_semester(semester: Option) -> i8 { - match semester { - // Force the asked semester - Some(n) => n, - // Find the potential semester - None => { - if Utc::now().month() > 6 { - // From july to december - 1 - } else { - // from january to june - 2 - } - } - } -} - -/// Get the current year depending on the current date -fn get_year(year: Option, semester: i8) -> String { - let wanted_year = match year { - // Force the asked semester - Some(n) => n, - // Find the potential semester - None => Utc::now().year(), - }; - - if semester == 1 { - format!("{}-{}", wanted_year, wanted_year + 1) - } else { - format!("{}-{}", wanted_year - 1, wanted_year) - } -} - /// Display the timetable pub fn display(timetable: (Vec, (usize, Vec)), cell_length: usize) { // Cell length for hours diff --git a/src/utils.rs b/src/utils.rs index f7e3114..5ecc3f1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,3 +1,6 @@ +use chrono::{Datelike, Utc}; +use scraper::Html; + pub mod models; /// Panic if an error happened @@ -105,11 +108,58 @@ pub fn etc_str(text: &str) -> String { format!("{}...", split_half(text).0.trim()) } -// Capitalize string -pub fn capitalize(text: &mut str) -> String { - if let Some(r) = text.get_mut(0..1) { - r.make_ascii_uppercase(); - } +/// Get timetable webpage +pub async fn get_webpage( + level: i8, + semester: i8, + year: &str, + user_agent: &str, +) -> Result> { + let url = format!("https://silice.informatique.univ-paris-diderot.fr/ufr/U{}/EDT/visualiserEmploiDuTemps.php?quoi=M{},{}", year, level, semester); - text.to_string() + // Use custom User-Agent + let client = reqwest::Client::builder().user_agent(user_agent).build()?; + let html = client.get(&url).send().await?.text().await?; + + // Panic on error + crate::utils::check_errors(&html, &url); + + // Parse document + let document = Html::parse_document(&html); + + Ok(document) +} + +/// Get the current semester depending on the current date +pub fn get_semester(semester: Option) -> i8 { + match semester { + // Force the asked semester + Some(n) => n, + // Find the potential semester + None => { + if Utc::now().month() > 6 { + // From july to december + 1 + } else { + // from january to june + 2 + } + } + } +} + +/// Get the current year depending on the current date +pub fn get_year(year: Option, semester: i8) -> String { + let wanted_year = match year { + // Force the asked semester + Some(n) => n, + // Find the potential semester + None => Utc::now().year(), + }; + + if semester == 1 { + format!("{}-{}", wanted_year, wanted_year + 1) + } else { + format!("{}-{}", wanted_year - 1, wanted_year) + } } From dc3a5c6d8e495b4a722ce5685a1757c29592d2e4 Mon Sep 17 00:00:00 2001 From: Mylloon Date: Thu, 28 Sep 2023 00:01:51 +0200 Subject: [PATCH 10/17] fmt --- src/utils/models.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/models.rs b/src/utils/models.rs index 5d00157..de9502c 100644 --- a/src/utils/models.rs +++ b/src/utils/models.rs @@ -47,5 +47,5 @@ impl TabChar { pub enum Position { Top, Middle, - Bottom + Bottom, } From 5fc7d9209c8b9c1c09ba44c8172bd6c2eca98be9 Mon Sep 17 00:00:00 2001 From: Mylloon Date: Thu, 28 Sep 2023 00:23:51 +0200 Subject: [PATCH 11/17] Fix some bugs --- src/info.rs | 4 ++-- src/timetable.rs | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/info.rs b/src/info.rs index e295501..c6586cd 100644 --- a/src/info.rs +++ b/src/info.rs @@ -12,10 +12,10 @@ pub async fn info( user_agent: &str, ) -> HashMap, i64)>> { let semester = get_semester(semester_opt); - let year = get_year(year_opt, semester); - let document = get_webpage(level, semester, &year, user_agent) + // Fetch the timetable of the FIRST semester + let document = get_webpage(level, 1, &year, user_agent) .await .expect("Can't reach info website."); diff --git a/src/timetable.rs b/src/timetable.rs index bc53162..8adc0f9 100644 --- a/src/timetable.rs +++ b/src/timetable.rs @@ -89,11 +89,13 @@ pub async fn timetable( .name("name") .unwrap() .as_str().to_owned(), - professor: match i.select(&sel_small).last().unwrap().inner_html() { - i if i.starts_with(" None, - i => Some(i), - }, - room: Regex::new(r"(|
.*?
.*?)
(?P.*?)
") + professor: if let Some(raw_prof) = i.select(&sel_small).last() { + match raw_prof.inner_html() { + i if i.starts_with(" None, + i => Some(i), + } + } else { None }, + room: Regex::new(r"(|
.*?
.*?)?
(?P.*?)
") .unwrap() .captures(&binding) .unwrap().name("location") From 2ec3455d166c02cd47cdbf55ba63c3ec31a0664e Mon Sep 17 00:00:00 2001 From: Mylloon Date: Thu, 28 Sep 2023 00:27:16 +0200 Subject: [PATCH 12/17] refactor --- README.md | 18 +++++++++--------- src/main.rs | 16 +++++----------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 78e326a..44179ad 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # cal7tor • *cal*endar P*7* extrac*tor* -> !! Fork de [cal8tor](https://git.mylloon.fr/Anri/cal8tor) !! -> -> !! En cours de dev -> ne fonctionne pas !! +> Fork de [cal8tor](https://git.mylloon.fr/Anri/cal8tor) Extracteur d'emploi du temps pour les masters d'informatique de Paris Cité (Diderot) @@ -22,23 +20,25 @@ Pour afficher la page d'aide $ cal7tor --help ``` -## Voir le calendrier dans le terminal + ## Exporter le calendrier au format `.ics` -Pour les LP par exemple, lance : +Pour les M1 par exemple, lance : ```bash -$ cal8tor LP --export calendar.ics +$ cal8tor M1 --export calendar.ics ``` > Le fichier comprend le fuseau horaire pour `Europe/Paris` et est diff --git a/src/main.rs b/src/main.rs index 801a6b6..3cf8b6d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,11 +34,10 @@ struct Args { async fn main() { let args = Args::parse(); - let matches = - Regex::new(r"(?i)M(?P[1,2])[-–•·]?(?P(LP|IMPAIRS|DATA|GENIAL|MPRI))?") - .unwrap() - .captures(&args.class) - .unwrap(); + let matches = Regex::new(r"(?i)M(?P[1,2])") + .unwrap() + .captures(&args.class) + .unwrap(); let level = matches .name("level") @@ -46,15 +45,10 @@ async fn main() { .as_str() .parse::() .unwrap(); - let pathway = matches.name("pathway").unwrap().as_str(); let user_agent = format!("cal7tor/{}", env!("CARGO_PKG_VERSION")); - println!( - "Récupération de l'emploi du temps des M{}-{}...", - level, - pathway.to_uppercase() - ); + println!("Récupération de l'emploi du temps des M{}...", level,); let timetable = timetable::timetable(level, args.semester, args.year, &user_agent).await; println!("Récupération des informations par rapport à l'année..."); From 75832958d8a574fc698fe354a2521bcbceba0a1d Mon Sep 17 00:00:00 2001 From: Mylloon Date: Thu, 28 Sep 2023 00:30:33 +0200 Subject: [PATCH 13/17] add notice --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index 3cf8b6d..a2d7b70 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,6 +63,7 @@ async fn main() { println!("Fichier .ICS construit et exporté => {}", filename); } else { + println!("\x1b[93mNOTICE: IT WON'T WORK!!!\x1b[0m"); // Show the calendar println!("Affichage..."); timetable::display(timetable, args.cl); From 384dd9eaa988bcc45fe55f1373beb70caed4649b Mon Sep 17 00:00:00 2001 From: Mylloon Date: Thu, 28 Sep 2023 22:09:43 +0200 Subject: [PATCH 14/17] Capitalize + Type in models --- src/timetable.rs | 13 ++++--------- src/timetable/models.rs | 10 +++++++++- src/utils.rs | 16 ++++++++++++++++ 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/timetable.rs b/src/timetable.rs index 8adc0f9..bc02809 100644 --- a/src/timetable.rs +++ b/src/timetable.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use crate::utils::{ self, get_semester, get_webpage, get_year, models::{Position, TabChar}, + Capitalize, }; pub mod models; @@ -67,7 +68,8 @@ pub async fn timetable( let day = matches .name("day") .unwrap() - .as_str(); + .as_str() + .capitalize(); let startime = matches .name("startime") @@ -122,13 +124,6 @@ pub async fn timetable( (schedules, (semester as usize, timetable)) } -// Data builded in the timetable webpage -type T = ( - // Schedules - Vec, - // Timetable per days with the semester as the key - (usize, Vec), -); // Data builded in the info webpage type D = HashMap< // Semester @@ -138,7 +133,7 @@ type D = HashMap< >; /// Build the timetable -pub fn build(timetable: T, dates: D) -> Vec { +pub fn build(timetable: models::Timetable, dates: D) -> Vec { let mut schedules = Vec::new(); // h1 => heure de début | m1 => minute de début // h2 => heure de fin | m2 => minute de fin diff --git a/src/timetable/models.rs b/src/timetable/models.rs index bb813a3..0091185 100644 --- a/src/timetable/models.rs +++ b/src/timetable/models.rs @@ -37,10 +37,18 @@ pub struct Course { pub dtend: Option>, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Day { /// Day's name pub name: String, /// Ordered list of all the courses of the day pub courses: Vec>, } + +// Data builded in the timetable webpage +pub type Timetable = ( + // Schedules + Vec, + // Timetable per days with the semester as the key + (usize, Vec), +); diff --git a/src/utils.rs b/src/utils.rs index 5ecc3f1..b6a2ffa 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -163,3 +163,19 @@ pub fn get_year(year: Option, semester: i8) -> String { format!("{}-{}", wanted_year - 1, wanted_year) } } + +pub trait Capitalize { + /// Capitalize string + fn capitalize(&self) -> String; +} + +impl Capitalize for str { + fn capitalize(&self) -> String { + let mut string = self.to_owned(); + if let Some(r) = string.get_mut(0..1) { + r.make_ascii_uppercase(); + } + + string + } +} From 041b8625f1b2a60ec5c5e86a9aa053e4b4002cec Mon Sep 17 00:00:00 2001 From: Mylloon Date: Thu, 28 Sep 2023 23:16:39 +0200 Subject: [PATCH 15/17] WIP: filter --- Cargo.lock | 173 +++++++++++++++++++++++++++++++++++----- Cargo.toml | 1 + src/filter.rs | 108 +++++++++++++++++++++++++ src/main.rs | 5 +- src/timetable/models.rs | 6 +- 5 files changed, 268 insertions(+), 25 deletions(-) create mode 100644 src/filter.rs diff --git a/Cargo.lock b/Cargo.lock index 88c35c8..8c4e8e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,7 +88,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -98,7 +98,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -164,6 +164,7 @@ version = "0.5.0-alpha" dependencies = [ "chrono", "clap", + "dialoguer", "ics", "regex", "reqwest", @@ -198,7 +199,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -247,6 +248,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "console" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.45.0", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -297,6 +311,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + [[package]] name = "dtoa" version = "1.0.9" @@ -318,6 +345,12 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -335,7 +368,7 @@ checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ "errno-dragonfly", "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -743,7 +776,7 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -868,7 +901,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -1126,7 +1159,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1141,7 +1174,7 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1261,6 +1294,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -1308,7 +1347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1381,7 +1420,7 @@ dependencies = [ "fastrand", "redox_syscall", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1395,6 +1434,26 @@ dependencies = [ "utf-8", ] +[[package]] +name = "thiserror" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.31", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1426,7 +1485,7 @@ dependencies = [ "signal-hook-registry", "socket2 0.5.3", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1687,7 +1746,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", ] [[package]] @@ -1696,7 +1764,22 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -1705,51 +1788,93 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -1763,5 +1888,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", - "windows-sys", + "windows-sys 0.48.0", ] + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" diff --git a/Cargo.toml b/Cargo.toml index 483cca5..bea3741 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ chrono = "0.4.28" ics = "0.5" uuid = { version = "1.4", features = ["v4", "fast-rng"] } clap = { version = "4.4", features = ["derive"] } +dialoguer = "0.11" diff --git a/src/filter.rs b/src/filter.rs new file mode 100644 index 0000000..24ece05 --- /dev/null +++ b/src/filter.rs @@ -0,0 +1,108 @@ +use std::collections::HashMap; + +use dialoguer::MultiSelect; + +use crate::timetable::models::Course; +use crate::timetable::models::Timetable; +use crate::timetable::models::Type; + +const DISCLAIMER: &str = "(selection avec ESPACE, ENTRER pour valider)"; + +/// Filter the timetable +pub fn timetable(timetable: Timetable) -> Timetable { + let mut my_timetable = timetable; + + /* courses(&mut my_timetable); */ + tdtp(&mut my_timetable); + + my_timetable +} + +/// Exclude some courses +fn courses(timetable: &mut Timetable) { + let mut multiselected = vec![]; + timetable.1 .1.iter().for_each(|day| { + day.courses.iter().for_each(|course_opt| { + if let Some(course) = course_opt { + if !multiselected.contains(&course.name) { + multiselected.push(course.name.to_owned()); + } + } + }) + }); + + let defaults = vec![true; multiselected.len()]; + let selections = MultiSelect::new() + .with_prompt(format!("Choisis tes matières {}", DISCLAIMER)) + .items(&multiselected[..]) + .defaults(&defaults[..]) + .interact() + .unwrap(); + + for day in &mut timetable.1 .1 { + day.courses.retain(|course_opt| { + if let Some(course) = course_opt { + // Remove courses not followed + for i in &selections { + if course.name == multiselected[*i] { + return true; + } + } + } + + false + }); + } +} + +/// Filter the multiples TD/TP +fn tdtp(timetable: &mut Timetable) { + let get_entry = |course: &Course| format!("{} - {:?}", course.name, course.typee); + let mut td_or_tp = vec![]; + + let mut counts = HashMap::new(); + timetable.1 .1.iter().for_each(|day| { + day.courses.iter().for_each(|course_opt| { + if let Some(course) = course_opt { + match course.typee { + Type::TD | Type::TP => { + td_or_tp.push(course); + let count = counts.entry(get_entry(course)).or_insert(0); + *count += 1; + } + _ => (), + } + } + }) + }); + // Keep only elements who have multiples TD/TP + td_or_tp.retain(|&course| *counts.get(&get_entry(course)).unwrap() > 1); + + let multiselected = td_or_tp + .iter() + .map(|el| format!("{} - {}", el.name, el.size)) + .collect::>(); + + let defaults = vec![true; multiselected.len()]; + let selections = MultiSelect::new() + .with_prompt(format!("Choisis tes horaires de TD/TP {}", DISCLAIMER)) + .items(&multiselected[..]) + .defaults(&defaults[..]) + .interact() + .unwrap(); + + for day in &mut timetable.1 .1 { + day.courses.retain(|course_opt| { + if let Some(course) = course_opt { + // Remove courses not followed + for i in &selections { + if course.name == multiselected[*i] { + return true; + } + } + } + + false + }); + } +} diff --git a/src/main.rs b/src/main.rs index a2d7b70..f3e1e90 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use clap::Parser; use regex::Regex; +mod filter; mod ics; mod info; mod timetable; @@ -49,7 +50,9 @@ async fn main() { let user_agent = format!("cal7tor/{}", env!("CARGO_PKG_VERSION")); println!("Récupération de l'emploi du temps des M{}...", level,); - let timetable = timetable::timetable(level, args.semester, args.year, &user_agent).await; + let mut timetable = timetable::timetable(level, args.semester, args.year, &user_agent).await; + + timetable = filter::timetable(timetable); println!("Récupération des informations par rapport à l'année..."); let info = info::info(level, args.semester, args.year, &user_agent).await; diff --git a/src/timetable/models.rs b/src/timetable/models.rs index 0091185..77c2d71 100644 --- a/src/timetable/models.rs +++ b/src/timetable/models.rs @@ -1,11 +1,11 @@ -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum Type { Cours, TP, TD, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct Course { /// Type du cours pub typee: Type, @@ -37,7 +37,7 @@ pub struct Course { pub dtend: Option>, } -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct Day { /// Day's name pub name: String, From 6e139369801c6fef5cc392489a876e56631024f4 Mon Sep 17 00:00:00 2001 From: Mylloon Date: Fri, 29 Sep 2023 00:03:41 +0200 Subject: [PATCH 16/17] fill hours is useful for the filter --- src/timetable.rs | 15 ++------------- src/utils.rs | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/timetable.rs b/src/timetable.rs index bc02809..32bd650 100644 --- a/src/timetable.rs +++ b/src/timetable.rs @@ -4,7 +4,7 @@ use scraper::Selector; use std::collections::HashMap; use crate::utils::{ - self, get_semester, get_webpage, get_year, + self, fill_hours, get_semester, get_webpage, get_year, models::{Position, TabChar}, Capitalize, }; @@ -37,18 +37,7 @@ pub async fn timetable( let raw_timetable = document.select(&sel_table).next().unwrap(); let mut schedules = Vec::new(); - for hour in 8..=20 { - for minute in &[0, 15, 30, 45] { - let hour_str = format!("{}h{:02}", hour, minute); - if let Some(last_hour) = schedules.pop() { - schedules.push(format!("{}-{}", last_hour, hour_str)); - } - schedules.push(hour_str); - } - } - for _ in 0..4 { - schedules.pop(); - } + fill_hours(&mut schedules); let mut timetable: Vec = Vec::new(); diff --git a/src/utils.rs b/src/utils.rs index b6a2ffa..d5ca354 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -179,3 +179,18 @@ impl Capitalize for str { string } } + +pub fn fill_hours(hours: &mut Vec) { + for hour in 8..=20 { + for minute in &[0, 15, 30, 45] { + let hour_str = format!("{}h{:02}", hour, minute); + if let Some(last_hour) = hours.pop() { + hours.push(format!("{}-{}", last_hour, hour_str)); + } + hours.push(hour_str); + } + } + for _ in 0..4 { + hours.pop(); + } +} From 54317e235606b37f9431309a149a8b978b11d6ed Mon Sep 17 00:00:00 2001 From: Mylloon Date: Fri, 29 Sep 2023 00:03:45 +0200 Subject: [PATCH 17/17] filter TP/TD --- src/filter.rs | 50 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/src/filter.rs b/src/filter.rs index 24ece05..514f572 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -5,6 +5,7 @@ use dialoguer::MultiSelect; use crate::timetable::models::Course; use crate::timetable::models::Timetable; use crate::timetable::models::Type; +use crate::utils::fill_hours; const DISCLAIMER: &str = "(selection avec ESPACE, ENTRER pour valider)"; @@ -12,7 +13,7 @@ const DISCLAIMER: &str = "(selection avec ESPACE, ENTRER pour valider)"; pub fn timetable(timetable: Timetable) -> Timetable { let mut my_timetable = timetable; - /* courses(&mut my_timetable); */ + courses(&mut my_timetable); tdtp(&mut my_timetable); my_timetable @@ -57,16 +58,37 @@ fn courses(timetable: &mut Timetable) { /// Filter the multiples TD/TP fn tdtp(timetable: &mut Timetable) { + // Entry's name used for finding duplicates let get_entry = |course: &Course| format!("{} - {:?}", course.name, course.typee); + + let mut hours = vec![]; + fill_hours(&mut hours); + + // Names showed to the users + let get_selection = |data: &(&Course, String)| { + format!( + "{} - {} {}-{}", + data.0.name, + data.1, + hours[data.0.start].split_once('-').unwrap().0, + hours[data.0.start + data.0.size - 1] + .split_once('-') + .unwrap() + .1 + ) + }; + + // List of courses who will be TP/TD let mut td_or_tp = vec![]; + // Counter of appearing of TP/TD to know if a TP/TD have multiple possible course let mut counts = HashMap::new(); timetable.1 .1.iter().for_each(|day| { day.courses.iter().for_each(|course_opt| { if let Some(course) = course_opt { match course.typee { Type::TD | Type::TP => { - td_or_tp.push(course); + td_or_tp.push((course, day.name.to_owned())); let count = counts.entry(get_entry(course)).or_insert(0); *count += 1; } @@ -75,13 +97,12 @@ fn tdtp(timetable: &mut Timetable) { } }) }); - // Keep only elements who have multiples TD/TP - td_or_tp.retain(|&course| *counts.get(&get_entry(course)).unwrap() > 1); - let multiselected = td_or_tp - .iter() - .map(|el| format!("{} - {}", el.name, el.size)) - .collect::>(); + // Keep only elements who have multiples TD/TP + td_or_tp.retain(|course| *counts.get(&get_entry(course.0)).unwrap() > 1); + + let mut multiselected: Vec = td_or_tp.iter().map(|el| get_selection(el)).collect(); + multiselected.sort(); let defaults = vec![true; multiselected.len()]; let selections = MultiSelect::new() @@ -91,12 +112,23 @@ fn tdtp(timetable: &mut Timetable) { .interact() .unwrap(); + // Keep only wanted courses for day in &mut timetable.1 .1 { day.courses.retain(|course_opt| { if let Some(course) = course_opt { + // Keep if it's a course + if course.typee == Type::Cours { + return true; + } + + // Keep if its an -only one course- TD/TP + if *counts.get(&get_entry(course)).unwrap() == 1 { + return true; + } + // Remove courses not followed for i in &selections { - if course.name == multiselected[*i] { + if get_selection(&(course, day.name.to_owned())) == multiselected[*i] { return true; } }