[READ-ONLY] Mirror of https://github.com/probablykasper/kadium. App for staying ontop of YouTube channels' uploads kadium.kasper.space
linux macos notifications tauri windows youtube
0

Configure Feed

Select the types of activity you want to include in your feed.

Fix lint warnings

Kasper (Nov 19, 2023, 2:29 AM +0100) c1fb938c 0b31fddd

+54 -66
+4 -4
src-tauri/src/api.rs
··· 15 15 16 16 match json.get("error") { 17 17 Some(error_obj) => { 18 - let code = error_obj.get("code").map(|v| v.as_i64()).flatten(); 18 + let code = error_obj.get("code").and_then(|v| v.as_i64()); 19 19 let code_str = code.map(|n| n.to_string()).unwrap_or_default(); 20 - let message = error_obj.get("message").map(|v| v.as_str()).flatten(); 20 + let message = error_obj.get("message").and_then(|v| v.as_str()); 21 21 println!("{:?}", json); 22 22 throw!("{} {}", code_str, message.unwrap_or_default()); 23 23 } ··· 37 37 let videos = yt_request::<videos::Response>(&url, key) 38 38 .await 39 39 .map_err(|e| format!("Failed to get video: {}", e))?; 40 - for video in videos.items { 41 - return Ok(video.snippet.channelId); 40 + if let Some(video) = videos.items.first() { 41 + return Ok(video.snippet.channelId.clone()); 42 42 } 43 43 Err("No video returned".to_string()) 44 44 }
+17 -24
src-tauri/src/background.rs
··· 74 74 run_once: bool, 75 75 window: tauri::Window, 76 76 ) -> Option<BgHandle> { 77 - if settings.channels.len() == 0 { 77 + if settings.channels.is_empty() { 78 78 return None; 79 79 } 80 80 81 81 let interval_map = new_intervals_map(&settings.channels); 82 - let interval_infos = to_interval_info_vector(interval_map); 82 + let interval_infos = interval_map.into_values().collect(); 83 83 84 84 let (stop_sender, _stop_receiver) = broadcast::channel(1); 85 85 ··· 91 91 window, 92 92 }; 93 93 94 - let tokio_thread = thread::spawn(move || { 95 - return start(options, interval_infos); 96 - }); 94 + let tokio_thread = thread::spawn(move || start(options, interval_infos)); 97 95 98 96 Some(BgHandle { 99 97 handle: tokio_thread, 100 - stop_sender: stop_sender, 98 + stop_sender, 101 99 }) 102 100 } 103 101 104 102 pub type IntervalMap = HashMap<u64, IntervalInfo>; 105 - fn to_interval_info_vector(map: IntervalMap) -> Vec<IntervalInfo> { 106 - map.into_iter().map(|(_ms, info)| info).collect() 107 - } 108 - fn new_intervals_map(channels: &Vec<settings::Channel>) -> IntervalMap { 103 + fn new_intervals_map(channels: &[settings::Channel]) -> IntervalMap { 109 104 let mut intervals_map: IntervalMap = HashMap::new(); 110 105 for channel in channels.iter() { 111 106 let default = IntervalInfo { ··· 118 113 interval_info.channels.push(ChannelInfo { 119 114 name: channel.name.to_string(), 120 115 uploads_playlist_id: channel.uploads_playlist_id.clone(), 121 - from_time: channel.from_time.clone(), 116 + from_time: channel.from_time, 122 117 }); 123 118 } 124 119 intervals_map ··· 141 136 let mut stop_receiver = options.stop_sender.subscribe(); 142 137 let handle = task::spawn(async move { 143 138 tokio::select! { 144 - _ = run(options, interval_info) => { 145 - return Ok(()) 146 - } 139 + _ = run(options, interval_info) => Ok(()), 147 140 result = stop_receiver.recv() => { 148 141 match result { 149 - Ok(_) => return Ok(()), 142 + Ok(_) => Ok(()), 150 143 Err(e) => { 151 - return Err(e.to_string()); 144 + Err(e.to_string()) 152 145 } 153 146 } 154 147 } ··· 202 195 let _ = options.window.emit("checking", ""); 203 196 } 204 197 for channel in &interval_info.channels { 205 - match check_channel(&options, &channel).await { 198 + match check_channel(options, channel).await { 206 199 Ok(()) => {} 207 200 Err(e) => { 208 201 let title = format!("Error checking {}", channel.name); ··· 229 222 .await 230 223 .map_err(|e| format!("Failed to get channel: {}", e))?; 231 224 232 - if uploads.items.len() == 0 { 225 + if uploads.items.is_empty() { 233 226 return Ok(()); // no channel videos returned 234 227 } 235 228 ··· 258 251 new_ids.push(fetched_video.contentDetails.videoId); 259 252 } 260 253 261 - if new_ids.len() == 0 { 254 + if new_ids.is_empty() { 262 255 return Ok(()); // no new videos 263 256 } 264 257 ··· 305 298 } 306 299 307 300 for video in &videos_to_add { 308 - db::insert_video(&video, &options.pool).await?; 301 + db::insert_video(video, &options.pool).await?; 309 302 } 310 - if videos_to_add.len() >= 1 { 303 + if !videos_to_add.is_empty() { 311 304 match options.window.emit("refresh", "") { 312 305 Ok(_) => {} 313 306 Err(e) => { ··· 319 312 } 320 313 321 314 pub fn parse_datetime(value: &str) -> Result<DateTime<FixedOffset>, String> { 322 - match DateTime::parse_from_rfc3339(&value) { 315 + match DateTime::parse_from_rfc3339(value) { 323 316 Ok(datetime) => Ok(datetime), 324 317 Err(e) => throw!("Unexpected video publish date: {}", e), 325 318 } ··· 328 321 /// years and months have different lengths depending on what month or year 329 322 /// it is. 330 323 pub fn parse_absolute_duration(value: &str) -> Result<i64, String> { 331 - match IsoDuration::parse(&value) { 324 + match IsoDuration::parse(value) { 332 325 Ok(duration) => { 333 326 if duration.month == 0.0 && duration.year == 0.0 { 334 327 let seconds = duration.second as f64 335 - + duration.minute as f64 as f64 * 60.0 328 + + duration.minute as f64 * 60.0 336 329 + duration.hour as f64 * 60.0 * 60.0 337 330 + duration.day as f64 * 60.0 * 60.0 * 24.0; 338 331 let ms = seconds * 1000.0;
+7 -7
src-tauri/src/data.rs
··· 8 8 use std::collections::HashSet; 9 9 use std::env; 10 10 use std::io::Write; 11 - use std::path::PathBuf; 11 + use std::path::{Path, PathBuf}; 12 12 use std::sync::Arc; 13 13 use tauri::{command, Config, State}; 14 14 use tokio::sync::Mutex; ··· 72 72 } 73 73 } 74 74 75 - pub fn ensure_parent_exists(file_path: &PathBuf) -> Result<(), String> { 75 + pub fn ensure_parent_exists(file_path: &Path) -> Result<(), String> { 76 76 if let Some(parent) = file_path.parent() { 77 77 if let Err(e) = std::fs::create_dir_all(parent) { 78 78 throw!("Error creating parent folder: {}", e.to_string()); ··· 82 82 } 83 83 84 84 pub fn write_atomically(file_path: &PathBuf, buf: &[u8]) -> Result<(), String> { 85 - ensure_parent_exists(&file_path)?; 86 - let af = AtomicFile::new(&file_path, OverwriteBehavior::AllowOverwrite); 87 - match af.write(|f| f.write_all(&buf)) { 85 + ensure_parent_exists(file_path)?; 86 + let af = AtomicFile::new(file_path, OverwriteBehavior::AllowOverwrite); 87 + match af.write(|f| f.write_all(buf)) { 88 88 Ok(_) => Ok(()), 89 89 Err(e) => Err(e.to_string()), 90 90 } ··· 155 155 if path_segments.next()? != "channel" { 156 156 return None; 157 157 } 158 - return Some(path_segments.next()?.to_string()); 158 + Some(path_segments.next()?.to_string()) 159 159 } 160 160 161 161 #[command] ··· 176 176 177 177 let id = if let Some(video_id) = url_parse_video_id(&options.url) { 178 178 let key = &settings.api_key_or_default(); 179 - api::channel_id_from_video_id(&video_id, &key).await? 179 + api::channel_id_from_video_id(&video_id, key).await? 180 180 } else if let Some(id) = url_parse_channel_id(&options.url) { 181 181 id 182 182 } else {
+12 -13
src-tauri/src/db.rs
··· 92 92 title: row.try_get("title")?, 93 93 description: row.try_get("description")?, 94 94 publishTimeMs: row.try_get("publishTimeMs")?, 95 - /// SQLite does not support unsigned integers 95 + // SQLite does not support unsigned integers 96 96 durationMs: row.try_get("durationMs")?, 97 97 thumbnailStandard: row.try_get("thumbnailStandard")?, 98 98 thumbnailMaxres: row.try_get("thumbnailMaxres")?, ··· 105 105 } 106 106 107 107 pub async fn insert_video(video: &Video, pool: &SqlitePool) -> Result<(), String> { 108 - let query_str = format!( 108 + let query_str = 109 109 "INSERT INTO videos (id,title,description,publishTimeMs,durationMs,thumbnailStandard,thumbnailMaxres,channelId,channelName) \ 110 - VALUES (?,?,?,?,?,?,?,?,?)" 111 - ); 112 - let query = sqlx::query(&query_str) 110 + VALUES (?,?,?,?,?,?,?,?,?)"; 111 + let query = sqlx::query(query_str) 113 112 .bind(&video.id) 114 113 .bind(&video.title) 115 114 .bind(&video.description) 116 - .bind(&video.publishTimeMs) 117 - .bind(&video.durationMs) 118 - .bind(&video.thumbnailStandard) 119 - .bind(&video.thumbnailMaxres) 115 + .bind(video.publishTimeMs) 116 + .bind(video.durationMs) 117 + .bind(video.thumbnailStandard) 118 + .bind(video.thumbnailMaxres) 120 119 .bind(&video.channelId) 121 120 .bind(&video.channelName); 122 121 let rows_affected = match query.execute(pool).await { ··· 191 190 192 191 let mut query_str = "SELECT ".to_owned() + &selects.join(","); 193 192 query_str.push_str(" FROM videos"); 194 - if wheres.len() > 0 { 193 + if !wheres.is_empty() { 195 194 query_str.push_str(" WHERE "); 196 195 query_str.push_str(&wheres.join(" AND ")); 197 196 } 198 197 query_str.push_str(" ORDER BY publishTimeMs DESC, id DESC"); 199 - query_str.push_str(&format!(" LIMIT {}", options.limit.to_string())); 198 + query_str.push_str(&format!(" LIMIT {}", options.limit)); 200 199 201 200 let mut query = sqlx::query_as(&query_str); 202 201 for binding in bindings { ··· 211 210 212 211 async fn set_archived(pool: &SqlitePool, id: &str, value: bool) -> Result<(), String> { 213 212 let query = sqlx::query("UPDATE videos SET archived = ? WHERE id = ?") 214 - .bind(&value) 215 - .bind(&id); 213 + .bind(value) 214 + .bind(id); 216 215 let rows_affected = match query.execute(pool).await { 217 216 Ok(result_rows) => result_rows.rows_affected(), 218 217 Err(e) => throw!("{}", e),
+11 -15
src-tauri/src/main.rs
··· 53 53 /// This can display dialogs, which needs to happen before tauri runs to not panic 54 54 async fn load_data(paths: &AppPaths) -> Result<(VersionedSettings, ImportedNote), String> { 55 55 if paths.settings_file.exists() { 56 - return match settings::VersionedSettings::load(&paths) { 56 + return match settings::VersionedSettings::load(paths) { 57 57 Ok(settings) => Ok((settings, None)), 58 58 Err(e) => Err(e), 59 59 }; 60 60 } 61 61 62 62 let will_import = match yt_email_notifier::can_import() { 63 - true => { 64 - let msg = "Do you want to import your data from YouTube Email Notifier?"; 65 - let wants_to_import = rfd::MessageDialog::new() 66 - .set_title("Import") 67 - .set_description(&msg) 68 - .set_buttons(rfd::MessageButtons::YesNo) 69 - .set_level(rfd::MessageLevel::Info) 70 - .show(); 71 - wants_to_import 72 - } 63 + true => rfd::MessageDialog::new() 64 + .set_title("Import") 65 + .set_description("Do you want to import your data from YouTube Email Notifier?") 66 + .set_buttons(rfd::MessageButtons::YesNo) 67 + .set_level(rfd::MessageLevel::Info) 68 + .show(), 73 69 false => false, 74 70 }; 75 71 if will_import { 76 72 let imported_stuff = yt_email_notifier::import()?; 77 73 let versioned_settings = imported_stuff.settings.wrap(); 78 - versioned_settings.save(&paths)?; 74 + versioned_settings.save(paths)?; 79 75 80 76 let import_note = Some(("Import note".to_string(), imported_stuff.update_note)); 81 77 return Ok((versioned_settings, import_note)); ··· 114 110 #[cfg(target_os = "macos")] 115 111 macos_app_nap::prevent(); 116 112 117 - let app_paths = AppPaths::from_tauri_config(&ctx.config()); 113 + let app_paths = AppPaths::from_tauri_config(ctx.config()); 118 114 119 115 let (mut settings, _note) = match load_data(&app_paths).await { 120 116 Ok(v) => v, ··· 291 287 )), 292 288 MenuEntry::Submenu(Submenu::new( 293 289 "Help", 294 - Menu::with_items([CustomMenuItem::new("Learn More", "Learn More").into()]).into(), 290 + Menu::with_items([CustomMenuItem::new("Learn More", "Learn More").into()]), 295 291 )), 296 292 ])) 297 293 .on_menu_event(|event| match event.menu_item_id() { 298 294 "Learn More" => { 299 295 let url = "https://github.com/probablykasper/kadium"; 300 - shell::open(&event.window().shell_scope(), url.to_string(), None).unwrap(); 296 + shell::open(&event.window().shell_scope(), url, None).unwrap(); 301 297 } 302 298 _ => {} 303 299 })
+3 -3
src-tauri/src/settings.rs
··· 49 49 Ok(_) => {} 50 50 Err(err) => throw!("Error reading file: {}", err), 51 51 }; 52 - match serde_json::from_str(&mut json_str) { 52 + match serde_json::from_str(&json_str) { 53 53 Ok(settings) => Ok(settings), 54 54 Err(err) => { 55 55 throw!("Error parsing file: {}", err.to_string()); ··· 147 147 pub instances: Vec<Instance>, 148 148 } 149 149 fn load_settings(file_path: PathBuf) -> Result<Settings, String> { 150 - let mut file = match File::open(&file_path) { 150 + let mut file = match File::open(file_path) { 151 151 Ok(file) => file, 152 152 Err(e) => throw!("Error opening file: {}", e.to_string()), 153 153 }; ··· 156 156 Ok(_) => {} 157 157 Err(err) => throw!("Error reading file: {}", err), 158 158 }; 159 - let settings: Settings = match serde_json::from_str(&mut json_str) { 159 + let settings: Settings = match serde_json::from_str(&json_str) { 160 160 Ok(v) => v, 161 161 Err(err) => { 162 162 throw!("Error parsing file: {}", err.to_string());