vrc_log/
lib.rs

1#[macro_use]
2extern crate tracing;
3
4use std::{
5    env::Args,
6    fs::File,
7    io::{BufRead, BufReader, Error},
8    path::{Path, PathBuf},
9    process::{Command, Stdio},
10    sync::LazyLock,
11    time::Duration,
12};
13
14use anyhow::{bail, Context};
15use chrono::Local;
16use colored::{Color, Colorize};
17use crossbeam::channel::{Receiver, Sender};
18use lazy_regex::{lazy_regex, regex_replace_all, Lazy, Regex};
19use notify::{Config, Event, PollWatcher, RecursiveMode, Watcher};
20use parking_lot::RwLock;
21use terminal_link::Link;
22
23use crate::provider::{prelude::*, Providers, Type};
24
25#[cfg(feature = "discord")]
26pub mod discord;
27pub mod provider;
28pub mod vrchat;
29
30pub const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
31pub const CARGO_PKG_HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE");
32pub const USER_AGENT: &str = concat!(
33    "VRC-LOG/",
34    env!("CARGO_PKG_VERSION"),
35    " shaybox@shaybox.com"
36);
37
38#[must_use]
39pub fn get_local_time() -> String {
40    Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
41}
42
43/// # Errors
44/// Will return `Err` if it couldn't get the GitHub repository.
45pub fn check_for_updates() -> reqwest::Result<bool> {
46    let response = reqwest::blocking::get(CARGO_PKG_HOMEPAGE)?;
47    if let Some(mut segments) = response.url().path_segments() {
48        if let Some(remote_version) = segments.next_back() {
49            return Ok(remote_version > CARGO_PKG_VERSION);
50        }
51    }
52
53    Ok(false)
54}
55
56/// # Errors
57/// Will return `Err` if `PollWatcher::watch` errors
58pub fn watch<P: AsRef<Path>>(tx: Sender<PathBuf>, path: P) -> notify::Result<PollWatcher> {
59    let tx_clone = tx.clone();
60    let mut watcher = PollWatcher::with_initial_scan(
61        move |watch_event: notify::Result<Event>| {
62            if let Ok(event) = watch_event {
63                for path in event.paths {
64                    let _ = tx.send(path);
65                }
66            }
67        },
68        Config::default().with_poll_interval(Duration::from_secs(1)),
69        move |scan_event: notify::Result<PathBuf>| {
70            if let Ok(path) = scan_event {
71                let _ = tx_clone.send(path);
72            }
73        },
74    )?;
75
76    watcher.watch(path.as_ref(), RecursiveMode::NonRecursive)?;
77
78    Ok(watcher)
79}
80
81/// Steam Game Launch Options: `.../vrc-log(.exe) %command%`
82///
83/// # Errors
84/// Will return `Err` if `Command::spawn` errors
85/// # Panics
86/// Will panic if `Child::wait` panics
87pub fn launch_game(args: Args) -> anyhow::Result<()> {
88    let args = args.collect::<Vec<_>>();
89    if args.len() > 1 {
90        let mut child = Command::new(&args[1])
91            .args(args.iter().skip(2))
92            .stderr(Stdio::null())
93            .stdout(Stdio::null())
94            .spawn()?;
95
96        std::thread::spawn(move || {
97            child.wait().unwrap();
98            std::process::exit(0);
99        });
100    }
101
102    Ok(())
103}
104
105/// # Errors
106/// Will return `Err` if `Sqlite::new` or `Provider::send_avatar_id` errors
107pub fn process_avatars(rx: &Receiver<PathBuf>) -> anyhow::Result<()> {
108    #[cfg_attr(not(feature = "cache"), allow(unused_mut))]
109    let mut providers = Providers::from([
110        #[cfg(feature = "cache")]
111        (Type::CACHE, box_db!(Cache::new()?)),
112        #[cfg(feature = "avtrdb")]
113        (Type::AVTRDB, box_db!(AvtrDB::default())),
114        #[cfg(feature = "vrcwb")]
115        (Type::VRCWB, box_db!(VRCWB::default())),
116        #[cfg(feature = "vrcds")]
117        (Type::VRCDS, box_db!(VRCDS::default())),
118        #[cfg(feature = "vrcdb")]
119        (Type::VRCDB, box_db!(VRCDB::default())),
120    ]);
121
122    #[cfg(feature = "cache")]
123    let cache = providers.shift_remove(&Type::CACHE).context("None")?;
124
125    while let Ok(path) = rx.recv() {
126        let avatar_ids = parse_avatar_ids(&path);
127        for avatar_id in avatar_ids {
128            #[cfg(feature = "cache")] // Avatar already in cache
129            if !cache.check_avatar_id(&avatar_id).unwrap_or(true) {
130                continue;
131            }
132
133            #[cfg(feature = "cache")] // Don't send to cache if sending failed
134            let mut send_to_cache = true;
135
136            print_colorized(&avatar_id);
137
138            for (provider_type, provider) in &providers {
139                match provider.send_avatar_id(&avatar_id) {
140                    Ok(unique) => {
141                        if unique {
142                            info!("^ Successfully Submitted to {provider_type}");
143                        }
144                    }
145                    Err(error) => {
146                        send_to_cache = false;
147                        error!("^ Failed to submit to {provider_type}: {error}");
148                    }
149                }
150            }
151
152            #[cfg(feature = "cache")]
153            if send_to_cache {
154                cache.send_avatar_id(&avatar_id)?;
155            }
156        }
157    }
158
159    bail!("Channel Closed")
160}
161
162/// # Errors
163/// Will return `Err` if `std::fs::canonicalize` errors
164///
165/// # Panics
166/// Will panic if an environment variable doesn't exist
167pub fn parse_path_env(path: &str) -> Result<PathBuf, Error> {
168    let path = regex_replace_all!(r"(?:\$|%)(\w+)%?", path, |_, env| {
169        std::env::var(env).unwrap_or_else(|_| panic!("Environment Variable not found: {env}"))
170    });
171
172    std::fs::canonicalize(path.as_ref())
173}
174
175#[must_use]
176pub fn parse_avatar_ids(path: &PathBuf) -> Vec<String> {
177    static RE: Lazy<Regex> = lazy_regex!(r"avtr_\w{8}-\w{4}-\w{4}-\w{4}-\w{12}");
178
179    let Ok(file) = File::open(path) else {
180        return Vec::new(); // Directory
181    };
182
183    let mut reader = BufReader::new(file);
184    let mut avatar_ids = Vec::new();
185    let mut buf = Vec::new();
186
187    while reader.read_until(b'\n', &mut buf).unwrap_or(0) > 0 {
188        let line = String::from_utf8_lossy(&buf);
189        for mat in RE.find_iter(&line) {
190            avatar_ids.push(mat.as_str().to_string());
191        }
192        buf.clear();
193    }
194
195    avatar_ids
196}
197
198/// # Print with colorized rainbow rows for separation
199pub fn print_colorized(avatar_id: &str) {
200    static INDEX: LazyLock<RwLock<usize>> = LazyLock::new(|| RwLock::new(0));
201    static COLORS: LazyLock<[Color; 12]> = LazyLock::new(|| {
202        [
203            Color::Red,
204            Color::BrightRed,
205            Color::Yellow,
206            Color::BrightYellow,
207            Color::Green,
208            Color::BrightGreen,
209            Color::Blue,
210            Color::BrightBlue,
211            Color::Cyan,
212            Color::BrightCyan,
213            Color::Magenta,
214            Color::BrightMagenta,
215        ]
216    });
217
218    let index = *INDEX.read();
219    let color = COLORS[index];
220    *INDEX.write() = (index + 1) % COLORS.len();
221
222    let text = format!("vrcx://avatar/{avatar_id}");
223    let link = Link::new(&text, &text).to_string().color(color);
224    info!("{link}");
225}