CLI utility to ingest embedded json metadata from yt-dlp downloads to a SQLite database file
yt-dlp
1
fork

Configure Feed

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

feat: Introduce subcommands and new `retry-failed` command

0xBA5E64 de90b3b9 f4ffbc20

+132 -7
+20
.sqlx/query-4c74aeeb056982a4397d483c50debec5f1cbab2422ed938c0b437c3b4cef3f95.json
··· 1 + { 2 + "db_name": "SQLite", 3 + "query": "SELECT video_path FROM failed_videos;", 4 + "describe": { 5 + "columns": [ 6 + { 7 + "name": "video_path", 8 + "ordinal": 0, 9 + "type_info": "Text" 10 + } 11 + ], 12 + "parameters": { 13 + "Right": 0 14 + }, 15 + "nullable": [ 16 + false 17 + ] 18 + }, 19 + "hash": "4c74aeeb056982a4397d483c50debec5f1cbab2422ed938c0b437c3b4cef3f95" 20 + }
+12
.sqlx/query-a4161c19060ddf0e1d23a985d1568dbe7e82217019b47026a8631353eee0515a.json
··· 1 + { 2 + "db_name": "SQLite", 3 + "query": "DELETE FROM failed_videos WHERE video_path = ?1", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Right": 1 8 + }, 9 + "nullable": [] 10 + }, 11 + "hash": "a4161c19060ddf0e1d23a985d1568dbe7e82217019b47026a8631353eee0515a" 12 + }
+72
src/lib.rs
··· 142 142 } 143 143 Ok(()) 144 144 } 145 + 146 + pub async fn reindex_failed_videos( 147 + db_pool: &Pool<Sqlite>, 148 + removed_failed: bool, 149 + headless_mode: bool, 150 + multi_progress: MultiProgress, 151 + ) -> Result<(), IndexError> { 152 + // Get Vec<PathBuf> of all videos already indexed 153 + let previous_failed: Vec<PathBuf> = sqlx::query!("SELECT video_path FROM failed_videos;") 154 + .fetch_all(db_pool) 155 + .await 156 + .unwrap() 157 + .iter() 158 + .map(|i| PathBuf::from_str(&i.video_path).unwrap()) 159 + .collect(); 160 + 161 + if previous_failed.is_empty() { 162 + return Err(IndexError::NoVideos); 163 + } 164 + 165 + log::info!("Found {} previously failed videos.", previous_failed.len()); 166 + 167 + let bar = multi_progress.add(ProgressBar::new(previous_failed.len().try_into().unwrap())); 168 + 169 + if !headless_mode { 170 + bar.set_style( 171 + ProgressStyle::with_template( 172 + "[{elapsed_precise} / {duration_precise}] {wide_bar} [{human_pos}/{human_len}]", 173 + ) 174 + .unwrap(), 175 + ); 176 + } 177 + for path in previous_failed { 178 + let path_str = path.to_string_lossy().to_string(); 179 + let file_exists = path.try_exists().unwrap_or(false); 180 + 181 + if removed_failed && !file_exists { 182 + log::error!("Couldn't find \"{}\" - Removing from Database", path_str); 183 + sqlx::query!("DELETE FROM failed_videos WHERE video_path = ?1", path_str) 184 + .execute(db_pool) 185 + .await 186 + .map_err(|_| IndexError::DatabaseError)?; 187 + continue; 188 + } 189 + 190 + if let Err(error) = index_video(&path, db_pool).await { 191 + let err_str = error.to_string(); 192 + 193 + log::error!("Couldn't index \"{}\" - {}", path_str, err_str); 194 + 195 + sqlx::query!( 196 + "INSERT INTO failed_videos (video_path, error) VALUES (?1, ?2) ON CONFLICT (video_path) DO UPDATE SET error=excluded.error", 197 + path_str, 198 + err_str 199 + ) 200 + .execute(db_pool) 201 + .await 202 + .map_err(|_| IndexError::DatabaseError)?; 203 + continue; 204 + }; 205 + 206 + if !headless_mode { 207 + bar.inc(1); 208 + } 209 + } 210 + 211 + if !headless_mode { 212 + bar.finish(); 213 + } 214 + 215 + Ok(()) 216 + }
+28 -7
src/main.rs
··· 1 - use clap::Parser; 1 + use clap::{Parser, Subcommand}; 2 2 use sqlx::migrate; 3 3 use sqlx::sqlite::{SqliteAutoVacuum, SqliteConnectOptions, SqlitePoolOptions}; 4 4 use std::path::PathBuf; 5 - use ydjr::index_videos_recursively; 5 + use ydjr::{index_videos_recursively, reindex_failed_videos}; 6 6 7 - #[derive(Parser, Debug)] 7 + #[derive(Parser)] 8 8 #[command(version, about, long_about = None)] 9 9 struct Args { 10 - /// Location of file/files to rename 11 - path: PathBuf, 12 - 13 10 /// Where to write or open the database file from 14 11 #[arg(long, default_value = "./db.sqlite")] 15 12 db: PathBuf, ··· 17 14 /// Headless mode, fitting if invoked automatically 18 15 #[arg(long, short = 'H')] 19 16 headless: bool, 17 + 18 + #[command(subcommand)] 19 + cmd: CmdOption, 20 + } 21 + 22 + #[derive(Subcommand)] 23 + #[command()] 24 + enum CmdOption { 25 + /// Index a directory into the database 26 + #[command(name = "index")] 27 + IndexDirectory { path: PathBuf }, 28 + /// Retry failed indexings found in the database 29 + #[command(name = "retry-failed")] 30 + ReIndexFailed { 31 + #[arg(long, short, default_value = "false")] 32 + remove_failed: bool, 33 + }, 20 34 } 21 35 22 36 #[tokio::main] ··· 49 63 .await 50 64 .expect("Failed to apply database migrations"); 51 65 52 - index_videos_recursively(args.path, &db_pool, args.headless, multi_progress).await?; 66 + match args.cmd { 67 + CmdOption::IndexDirectory { path } => { 68 + index_videos_recursively(path, &db_pool, args.headless, multi_progress).await? 69 + } 70 + CmdOption::ReIndexFailed { remove_failed } => { 71 + reindex_failed_videos(&db_pool, remove_failed, args.headless, multi_progress).await? 72 + } 73 + } 53 74 Ok(()) 54 75 }