Skip to content

Instantly share code, notes, and snippets.

@estshorter
Last active January 28, 2020 12:04
Show Gist options
  • Save estshorter/b20e93d785404947bfc746163c0bed49 to your computer and use it in GitHub Desktop.
Save estshorter/b20e93d785404947bfc746163c0bed49 to your computer and use it in GitHub Desktop.
Rust script for downloading and applying MusicBee Patches from https://getmusicbee.com/patches/
use std::fs::{self, File};
use std::io::{self, stdout, BufWriter, ErrorKind, Write};
use std::path::PathBuf;
use std::process::Command;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use std::time::SystemTime;
use chrono::{DateTime, Local, TimeZone, Utc};
use crossterm::event::{read, Event, KeyEvent};
use filetime::FileTime;
use reqwest;
use scraper::{Html, Selector};
use zip::read::ZipArchive;
fn main() {
let last_access_filename = r#"C:\tmp\mb_last_download_datetime.txt"#;
let (need_update, timestamp_str) = check_update("MusicBee33_Patched.zip", last_access_filename);
if !need_update {
println!("No need to download.");
timeout(3);
return;
}
println!("Downloading the zip file.");
let filepath = r#"C:\tmp\MusicBee_Patched.zip"#;
download_file(
filepath,
"https://getmusicbee.com/patches/MusicBee33_Patched.zip",
);
Command::new("taskkill")
.args(&["/im", "MusicBee.exe"])
.output()
.expect("failed to kill MB");
match extract_updated_files(filepath, r#"C:\Program Files (x86)\MusicBee\"#) {
0 => println!("All files are up-to-date."),
cnt => println!("Update/added {} file(s).", cnt),
}
write_download_date(&timestamp_str, r#"C:\tmp\mb_last_download_datetime.txt"#);
timeout(7);
Command::new(r#"C:\Program Files (x86)\MusicBee\MusicBee.exe"#)
.spawn()
.expect("failed to launch MB");
}
fn timeout(wait_second: usize) {
let (tx, rx) = mpsc::channel();
let tx1 = mpsc::Sender::clone(&tx);
thread::spawn(move || {
for idx in (0..wait_second).rev() {
tx1.send(Some(format!("{}秒後に終了します", idx + 1)))
.unwrap();
thread::sleep(Duration::from_secs(1));
}
tx1.send(Some(String::from("0秒後に終了します"))).unwrap();
tx1.send(None).unwrap();
});
thread::spawn(move || loop {
if let Event::Key(KeyEvent { .. }) = read().unwrap() {
tx.send(None).unwrap();
break;
}
});
for received in rx {
match received {
Some(msg) => {
print!("{}\r", msg);
stdout().flush().unwrap();
}
None => break,
}
}
}
fn write_download_date(timestamp: &str, filepath: &str) {
let f = File::create(filepath).expect("Unable to create a timestamp file");
let mut buffer = BufWriter::new(f);
buffer
.write_all(timestamp.as_bytes())
.expect("Unable to write timestamp");
}
fn extract_updated_files(src: &str, dst: &str) -> u32 {
let mut extract_cnt = 0;
let file = File::open(src).expect("Couldn't open file");
let mut zip = ZipArchive::new(file).unwrap();
for i in 0..zip.len() {
if extract_updated_file(i, &mut zip, &dst) == true {
extract_cnt += 1;
}
}
extract_cnt
}
fn extract_updated_file(idx: usize, zip: &mut ZipArchive<fs::File>, dst: &str) -> bool {
let mut file_in_zip = zip.by_index(idx).unwrap();
let mut path = PathBuf::new();
path.push(dst);
path.push(file_in_zip.name().replace("/", "\\"));
// dir
if file_in_zip.is_dir() {
if !path.exists() {
fs::create_dir_all(path).unwrap();
}
return false;
}
// file
let modified_exist: DateTime<Local> = if path.exists() {
let metadata = fs::metadata(&path).unwrap();
let systime = metadata.modified().unwrap();
systime.into()
} else {
SystemTime::UNIX_EPOCH.into()
};
let lm = file_in_zip.last_modified();
let modified_zip = Utc
.ymd(lm.year() as i32, lm.month() as u32, lm.day() as u32)
.and_hms(lm.hour() as u32, lm.minute() as u32, lm.second() as u32);
let modified_zip: DateTime<Local> = DateTime::from(modified_zip);
if modified_zip <= modified_exist {
return false;
}
let mut file = File::create(&path).expect("failed to create file");
io::copy(&mut file_in_zip, &mut file).expect("failed to copy content");
let mtime = FileTime::from_unix_time(
modified_zip.timestamp(),
modified_zip.timestamp_subsec_nanos(),
);
filetime::set_file_handle_times(&file, Some(mtime), Some(mtime)).unwrap();
let modified_zip_str = modified_zip.format("%Y/%m/%d %H:%M:%S %Z").to_string();
let modified_exist_str = modified_exist.format("%Y/%m/%d %H:%M:%S %Z").to_string();
println!("{} -> {}", modified_exist_str, modified_zip_str);
return true;
}
fn download_file(filepath: &str, url: &str) {
let mut resp = reqwest::blocking::get(url).expect("zip download request failed");
let mut out = File::create(filepath).expect("failed to create a zip file");
io::copy(&mut resp, &mut out).expect("failed to copy zip content");
}
fn get_last_access_date(filename: &str) -> String {
fs::read_to_string(filename).unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
String::from("2020-01-01 00:00")
} else {
panic!("Problem opening the file: {:?}", filename);
}
})
}
fn check_update(target_filename: &str, last_access_filename: &str) -> (bool, String) {
let site_timestamp_str = get_timestamp_from_site(target_filename);
let site_timestamp: DateTime<Local> = Local
.datetime_from_str(&site_timestamp_str, "%Y-%m-%d %H:%M")
.unwrap();
let last_access_date = get_last_access_date(last_access_filename);
let last_access_date: DateTime<Local> = Local
.datetime_from_str(&last_access_date, "%Y-%m-%d %H:%M")
.unwrap();
if site_timestamp > last_access_date {
return (true, site_timestamp_str);
}
return (false, site_timestamp_str);
}
fn get_timestamp_from_site(target_filename: &str) -> String {
let client = reqwest::blocking::Client::builder()
.user_agent("Mozilla/5.0")
.build()
.expect("failed to build client");
let html = client
.get("https://getmusicbee.com/patches/")
.send()
.unwrap()
.text()
.expect("failed to get web page");
let doc = Html::parse_fragment(&html);
let select_str = format!("a[href=\"{}\"]", target_filename);
let selector = Selector::parse(&select_str).unwrap();
doc.select(&selector)
.next()
.unwrap()
.parent()
.unwrap()
.next_sibling()
.unwrap()
.first_child()
.unwrap()
.value()
.as_text()
.unwrap()
.to_string()
.trim()
.to_string()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment