diff --git a/CHANGELOG.md b/CHANGELOG.md index adc2deb..c8c0566 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v4.1.0 (2023-11-25) + +* Defaults for date params [#177](https://github.com/gerardcl/renfe-cli/issues/177) +* Enable wait time for search results page [#178](https://github.com/gerardcl/renfe-cli/issues/178) + ## v4.0.0 (2023-11-25) * Refactor to Rust [#175](https://github.com/gerardcl/renfe-cli/issues/175) diff --git a/Cargo.lock b/Cargo.lock index fcea2ea..a7797af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_colours" version = "1.2.2" @@ -115,6 +130,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + [[package]] name = "bytemuck" version = "1.14.0" @@ -170,6 +191,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.48.5", +] + [[package]] name = "cipher" version = "0.4.4" @@ -210,6 +245,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + [[package]] name = "cpufeatures" version = "0.2.11" @@ -707,6 +748,29 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -781,6 +845,15 @@ dependencies = [ "rayon", ] +[[package]] +name = "js-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1295,8 +1368,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "renfe-cli" -version = "4.0.0" +version = "4.1.0" dependencies = [ + "chrono", "getopts", "headless_chrome", "image", @@ -1865,6 +1939,60 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" + [[package]] name = "webpki-roots" version = "0.25.2" @@ -1921,6 +2049,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 99cabb7..edde46c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "renfe-cli" -version = "4.0.0" +version = "4.1.0" edition = "2021" +license = "BSD License" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] @@ -16,3 +17,4 @@ viuer = "0.7" scraper = "0.18" ureq = "2.9" getopts = "0.2" +chrono = "0.4" diff --git a/README.md b/README.md index 347aa04..760dce1 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,11 @@ Usage: renfe-cli [options] Options: -f ORIGIN Set From origin station -t DESTINATION Set To destination station - -d, --day DAY Set Day to search timetable for - -m, --month MONTH Set Month to search timetable for - -y, --year YEAR Set Year to search timetable for - -h, --help print this help menu + -d, --day DAY Set Day to search timetable for (default: today) + -m, --month MONTH Set Month to search timetable for (default: today's month) + -y, --year YEAR Set Year to search timetable for (default: today's year) + -w, --wait SECONDS Set Wait time in seconds for Renfe search result page (default: 2) + -h, --help Print this help menu ``` ### **Getting the timetable** @@ -52,7 +53,9 @@ Options: In this new major release there is still no interactive mode nor defaults; one must provide all inputs, like: ```bash -$ renfe-cli -f Tarr -t Mad -d 27 -m 11 -y 2023 +$ renfe-cli -f Barce -t Mad -d30 +Today is: 2023-11-25 +Searching timetable for date: 2023-11-30 loading headless chrome browser navigating to renfe timetable search page waiting for search page @@ -67,40 +70,110 @@ loading timetable =========================TIMETABLE========================= Train | Departure | Arrival | Duration ----------------------------------------------------------- -AVE | 06.25 | 09.10 | 2 h. 45 min. +AVE | 05.50 | 09.10 | 3 h. 20 min. ----------------------------------------------------------- -LD-AVE | 08.22 | 15.35 | 7 h. 13 min. +AVE | 06.20 | 08.50 | 2 h. 30 min. ----------------------------------------------------------- -AVE | 08.34 | 11.12 | 2 h. 38 min. +AVLO | 06.35 | 09.20 | 2 h. 45 min. ----------------------------------------------------------- -REG.EXP. | 10.11 | 18.09 | 7 h. 58 min. +AVE | 07.00 | 09.30 | 2 h. 30 min. ----------------------------------------------------------- -AVLO | 10.34 | 13.17 | 2 h. 43 min. +AVE | 07.40 | 10.10 | 2 h. 30 min. ----------------------------------------------------------- -LD-AVE | 10.51 | 15.35 | 4 h. 44 min. +LD-AVE | 07.45 | 15.35 | 7 h. 50 min. ----------------------------------------------------------- -AVE | 12.34 | 15.12 | 2 h. 38 min. +AVE | 08.00 | 11.12 | 3 h. 12 min. ----------------------------------------------------------- -AVE INT | 13.22 | 15.45 | 2 h. 23 min. +AVE | 08.25 | 10.55 | 2 h. 30 min. ----------------------------------------------------------- -AVE | 14.34 | 17.12 | 2 h. 38 min. +REG.EXP. | 08.43 | 18.09 | 9 h. 26 min. ----------------------------------------------------------- -AVE | 16.34 | 19.12 | 2 h. 38 min. +AVE | 09.00 | 11.45 | 2 h. 45 min. ----------------------------------------------------------- -AVE | 18.34 | 21.12 | 2 h. 38 min. +AVLO | 10.00 | 13.17 | 3 h. 17 min. ----------------------------------------------------------- -AVE | 19.14 | 21.45 | 2 h. 31 min. +AVE | 11.00 | 13.45 | 2 h. 45 min. ----------------------------------------------------------- -AVE | 20.34 | 23.12 | 2 h. 38 min. +AVE | 12.00 | 15.12 | 3 h. 12 min. +----------------------------------------------------------- +AVE INT | 12.50 | 15.45 | 2 h. 55 min. +----------------------------------------------------------- +AVE | 13.25 | 15.54 | 2 h. 29 min. +----------------------------------------------------------- +AVE | 14.00 | 17.12 | 3 h. 12 min. +----------------------------------------------------------- +AVLO | 15.00 | 17.45 | 2 h. 45 min. +----------------------------------------------------------- +AVE | 15.25 | 17.55 | 2 h. 30 min. +----------------------------------------------------------- +AVE | 16.00 | 19.12 | 3 h. 12 min. +----------------------------------------------------------- +AVE | 16.25 | 18.55 | 2 h. 30 min. +----------------------------------------------------------- +AVE | 17.00 | 19.45 | 2 h. 45 min. +----------------------------------------------------------- +AVE | 17.25 | 19.55 | 2 h. 30 min. +----------------------------------------------------------- +AVE | 18.00 | 21.12 | 3 h. 12 min. +----------------------------------------------------------- +AVE | 18.25 | 20.55 | 2 h. 30 min. +----------------------------------------------------------- +AVE | 18.40 | 21.45 | 3 h. 5 min. +----------------------------------------------------------- +AVE | 19.25 | 21.55 | 2 h. 30 min. +----------------------------------------------------------- +AVE | 20.00 | 23.12 | 3 h. 12 min. +----------------------------------------------------------- +AVLO | 21.00 | 23.45 | 2 h. 45 min. +----------------------------------------------------------- +AVE | 21.25 | 23.55 | 2 h. 30 min. =========================================================== ``` ---- - ## Usage (Library) -TBD +`renfe-cli` can be imported as a python package into your project, offering utilities when willing to deal with the Renfe search web site. + +```bash +$ python +Python 3.8.18 (default, Aug 25 2023, 13:20:30) +[GCC 11.4.0] on linux +Type "help", "copyright", "credits" or "license" for more information. +>>> import renfe_cli +>>> renfe_cli. +renfe_cli.load_stations( renfe_cli.main( renfe_cli.print_timetable( renfe_cli.renfe_cli renfe_cli.search_timetable( +>>> renfe_cli.load_stations() +['A Coruña', 'Abrantes', 'Alicante / Alacant ', 'Albacete ', 'Alcantarilla-Los Romanos', 'Alcázar de San Juan ', 'Algeciras ', 'Almería', 'Altet Bus', 'Aguadulce Bus', 'Aix En Provence ', 'Andorra-Bus', 'Antequera (ALL)', 'Avignon ', 'Avila ', 'Badajoz', 'Barcelona (ALL) ', 'Beziers', 'Benicassim', 'Bilbao (ALL)', 'Bobadilla ', 'Burgos Rosa Manzano', 'Cáceres ', 'Cádiz ', 'Calatayud ', 'Campus Rabanales', 'Cartagena ', 'Castellón /Castelló', 'Ciudad Real ', 'Córdoba', 'Cuenca (ALL) ', 'Denia-Bus', 'Elda-Petrer ', 'Elche AV/Elx AV ', 'Entroncamento', 'Estepona Bus', 'Ferrol ', 'Figueres', 'Figueres Bus', 'Figueres Vilafant', 'Gandía ', 'Gijón ', 'Girona ', 'Granada ', 'Guadalajara (ALL) ', 'Huelva', 'Huesca', 'Irun-Hendaya (ALL) ', 'Iruña/Pamplona', 'Jaca-Bus', 'Jaén ', 'Játiva/Xàtiva', 'Javea-Bus', 'Jerez de la Frontera ', 'La Hoya', 'León ', 'Linares-Baeza', 'Librilla', 'Lyon', 'Lleida ', 'Logroño ', 'Lorca-San Diego', 'Lorca-Sutullena ', 'Los Arenales Bus', 'Lugo ', 'Madrid (ALL) ', 'Málaga María Zambrano', 'Marbella Bus', 'Marseille St Charles', 'Marvao - Beira ', 'Medina del Campo (ALL) ', 'Mérida ', 'Miranda de Ebro ', 'Monforte de Lemos ', 'Monzón-Río Cinca ', 'Montpellier', 'Murcia', 'Narbonne', 'Navalmoral de la Mata ', 'Nimes ', 'Nine', 'Oporto-Porto Campanha', 'Orihuela-Miguel Hernádez', 'Oropesa del Mar/Orpesa', 'Ourense', 'Oviedo ', 'Padrón-Barbanza', 'Palencia ', 'Pamplona/Iruña ', 'Perpignan ', 'Ponferrada ', 'Pontevedra ', 'Portbou ', 'Porto Campanha-Oporto', 'Puebla de Sanabria (ALL)', 'Puente Genil (ALL)', 'Puerto Santa María ', 'Puertollano ', 'Redondela (ALL)', 'Reus ', 'Requena / Utiel ', 'Roquetas-Bus', 'Sahagún ', 'Salamanca (ALL) ', 'San Fernando (ALL) ', 'San Sebastián/Donostia ', 'Santa Pola Bus', 'Santander (ALL)', 'Santiago de Compostela ', 'Segovia (ALL)', 'Sevilla ', 'Soria ', 'Tarragona (ALL)', 'Teruel ', 'Toledo ', 'Tudela de Navarra ', 'Valdepeñas ', 'Valence', 'Valencia (ALL) ', 'Valença Do Minho', 'Valladolid Campo Grande', 'Vielha-Bus', 'Viana Da Castelo', 'Vigo (ALL)', 'Vva. de Córdoba-Los Pedroches', 'Villena ', 'Villena AV', 'Vitoria/Gasteiz ', 'Xàtiva/Játiva', 'Zafra', 'Zafra Feria', 'Zamora ', 'Zaragoza (ALL)'] +``` + +--- ## Contribute or Report with Issues If Renfe's website is changed or you find any issue to be fixed or nice enhancements to have, please: [create an issue](https://github.com/gerardcl/renfe-cli/issues). + +### Development + +This project makes use of Rust bindings for the Python interpreter thanks to [pyo3](https://pyo3.rs). It is already available as a dependency. + +To develop, build and publish, this project makes use of [maturin](https://www.maturin.rs/) project. See [usage](https://www.maturin.rs/#usage). + +Example of first time working with this repository: + +```bash +$ git clone https://github.com/gerardcl/renfe-cli.git && cd renfe-cli +$ python -m venv venv +$ . venv/bin/activate +$ pip install -U pip +$ pip install -U maturin +$ maturing develop +🔗 Found pyo3 bindings with abi3 support for Python ≥ 3.7 +🐍 Not using a specific python interpreter +📡 Using build options features from pyproject.toml + Compiling renfe-cli v4.1.0 (/path/to/renfe-cli) + Finished dev [unoptimized + debuginfo] target(s) in 7.07s +📦 Built wheel for abi3 Python ≥ 3.7 to /tmp/.tmpDsjowL/renfe_cli-4.1.0-cp37-abi3-linux_x86_64.whl +🛠 Installed renfe-cli-4.1.0 +``` + +Maturin takes care of compiling the rust code, generating the bindings for python and installing the package for local use (as library or binary/CLI). diff --git a/src/cli.rs b/src/cli.rs index 65c8856..19feb92 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,3 +1,4 @@ +use chrono::{Datelike, Utc}; use getopts::Options; use pyo3::pyfunction; use std::env; @@ -8,14 +9,9 @@ use crate::timetable::{print_timetable, search_timetable}; pub fn main() { let args: Vec = env::args().collect(); let program = args[0].clone(); + let now = Utc::now(); + let opts = set_opts(); - let mut opts = Options::new(); - opts.optopt("f", "", "Set From origin station", "ORIGIN"); - opts.optopt("t", "", "Set To destination station", "DESTINATION"); - opts.optopt("d", "day", "Set Day to search timetable for", "DAY"); - opts.optopt("m", "month", "Set Month to search timetable for", "MONTH"); - opts.optopt("y", "year", "Set Year to search timetable for", "YEAR"); - opts.optflag("h", "help", "print this help menu"); let matches = match opts.parse(&args[1..]) { Ok(m) => m, Err(f) => { @@ -28,17 +24,27 @@ pub fn main() { } let origin = matches.opt_str("f"); let destination = matches.opt_str("t"); - let day = matches.opt_str("d"); - let month = matches.opt_str("m"); - let year = matches.opt_str("y"); + let day = matches.opt_str("d").unwrap_or(now.day().to_string()); + let month = matches.opt_str("m").unwrap_or(now.month().to_string()); + let year = matches.opt_str("y").unwrap_or(now.year().to_string()); + let wait = matches + .opt_str("w") + .unwrap_or(2.to_string()) + .parse::() + .unwrap(); + + println!("Today is: {}-{}-{}", now.year(), now.month(), now.day()); + println!("Searching timetable for date: {}-{}-{}", year, month, day); let timetable = search_timetable( origin.unwrap(), destination.unwrap(), - day.unwrap(), - month.unwrap(), - year.unwrap(), + day, + month, + year, + wait, ); + print_timetable(timetable); } @@ -46,3 +52,36 @@ fn print_usage(program: &str, opts: Options) { let brief = format!("Usage: {} [options]", program); print!("{}", opts.usage(&brief)); } + +fn set_opts() -> Options { + let mut opts = Options::new(); + opts.optopt("f", "", "Set From origin station", "ORIGIN"); + opts.optopt("t", "", "Set To destination station", "DESTINATION"); + opts.optopt( + "d", + "day", + "Set Day to search timetable for (default: today)", + "DAY", + ); + opts.optopt( + "m", + "month", + "Set Month to search timetable for (default: today's month)", + "MONTH", + ); + opts.optopt( + "y", + "year", + "Set Year to search timetable for (default: today's year)", + "YEAR", + ); + opts.optopt( + "w", + "wait", + "Set Wait time in seconds for Renfe search result page (default: 2)", + "SECONDS", + ); + opts.optflag("h", "help", "Print this help menu"); + + opts +} diff --git a/src/timetable.rs b/src/timetable.rs index b202659..932f7b4 100644 --- a/src/timetable.rs +++ b/src/timetable.rs @@ -1,11 +1,10 @@ use headless_chrome::{Browser, LaunchOptions}; use pyo3::pyfunction; use scraper::{ElementRef, Html, Selector}; -use std::{collections::HashMap, time::Duration}; +use std::{collections::HashMap, thread::sleep, time::Duration}; trait VecParser { fn texts_parser(&self, selector: Selector) -> Vec; - fn alts_parser(&self, selector: Selector) -> Vec; } impl VecParser for ElementRef<'_> { @@ -17,14 +16,6 @@ impl VecParser for ElementRef<'_> { .filter(|x| !x.is_empty()) .collect() } - fn alts_parser(&self, selector: Selector) -> Vec { - self.select(&selector) - .flat_map(|el| el.value().attr("alt")) - .map(|t| t.to_string()) - .map(|x| x.trim().to_string()) - .filter(|x| !x.is_empty()) - .collect() - } } // Convenience function to avoid unwrap()ing all the time @@ -39,6 +30,7 @@ pub fn search_timetable( day: String, month: String, year: String, + wait: u64, ) -> Vec> { let months: HashMap<&str, &str> = HashMap::from([ ("1", "Ene"), @@ -140,6 +132,8 @@ pub fn search_timetable( println!("searching timetable"); elem.press_key("Tab").unwrap().press_key("Enter").unwrap(); + sleep(Duration::from_secs(wait)); + // wait on navigating to search result page tab.wait_until_navigated() .unwrap()