use std::error::Error; use std::fs; use std::path::PathBuf; use google_api_auth; use google_photoslibrary1; use google_photoslibrary1::schemas::Album; use google_photoslibrary1::schemas::SearchMediaItemsRequest; use log::debug; use log::info; use regex::Regex; use structopt::StructOpt; use yup_oauth2::{Authenticator, InstalledFlow}; #[derive(Debug, StructOpt)] enum Command { /// List albums for the user of the given credentials. Optionally title filter. ListAlbums { title_filter: Option, }, SearchMediaItems { album_id: String, }, Sync { /// Optional album title to filter. Default will mirror all albums. #[structopt(short, long)] title_filter: Option, /// Directory to store sync. output: PathBuf, }, } #[derive(Debug, StructOpt)] #[structopt( name = "photosync", about = "Utility for interacting with Google Photos API." )] struct Opt { /// Activate debug mode #[structopt(short, parse(from_occurrences))] verbose: usize, /// Path to json file containing Google client ID and secrets for out of band auth flow. #[structopt(long)] credentials: PathBuf, /// Path to json file where photosync will store auth tokens refreshed from Google. #[structopt(long)] token_cache: PathBuf, #[structopt(subcommand)] cmd: Command, } fn new_client( credentials: &PathBuf, token_cache: &PathBuf, ) -> Result> { let secret = yup_oauth2::read_application_secret(credentials)?; // Create an authenticator that uses an InstalledFlow to authenticate. The // authentication tokens are persisted to a file named tokencache.json. The // authenticator takes care of caching tokens to disk and refreshing tokens once // they've expired. let auth = Authenticator::new(InstalledFlow::new( secret, yup_oauth2::InstalledFlowReturnMethod::Interactive, )) .persist_tokens_to_disk(token_cache) .build() .unwrap(); let scopes = vec![google_photoslibrary1::scopes::PHOTOSLIBRARY_READONLY]; let auth = google_api_auth::yup_oauth2::from_authenticator(auth, scopes); Ok(google_photoslibrary1::Client::new(auth)) } fn search_media_items( client: google_photoslibrary1::Client, album_id: String, ) -> Result<(), Box> { let mut page_token = None; let mut total = 0; loop { let resp = client .media_items() .search(SearchMediaItemsRequest { album_id: Some(album_id.clone()), // 100 is the documented max. page_size: Some(100), page_token, ..Default::default() }) .execute_with_all_fields()?; let media_items = resp.media_items.ok_or("no results")?; println!("got ({}) items", media_items.len()); total += media_items.len(); for mi in media_items { println!( "{} {}", mi.id.unwrap_or("NO ID".to_string()), mi.filename.unwrap_or("NO FILENAME".to_string()) ); } page_token = resp.next_page_token; if page_token.is_none() { println!("({}) items total", total); return Ok(()); } } } fn sync_albums( client: google_photoslibrary1::Client, title_filter: Option, output_dir: PathBuf, ) -> Result<(), Box> { let albums = list_albums(client, title_filter)?; for a in &albums { let album_dir = output_dir.join(a.id.as_ref().expect("missing album id")); if !album_dir.exists() { info!("making album directory {}", album_dir.to_string_lossy()); fs::create_dir_all(album_dir)?; } } // Serialize it to a JSON string. let j = serde_json::to_string(&albums)?; let path = output_dir.join("albums.json"); info!("saving {}", path.to_string_lossy()); fs::write(path, j)?; Ok(()) } fn print_albums(albums: Vec) { for a in albums { println!( "album: {} {} ({} items)", a.id.unwrap_or("NO ID".to_string()), a.title.unwrap_or("NO TITLE".to_string()).to_string(), a.media_items_count.unwrap_or(0) ); } } fn list_albums( client: google_photoslibrary1::Client, title_filter: Option, ) -> Result, Box> { Ok(client .shared_albums() .list() .iter_shared_albums_with_all_fields() .filter_map(|a| a.ok()) .filter(|a| { match (&title_filter, &a.title) { // keep everything when no filter or title. (None, None) => true, // skip when filter given but the media item doesn't have a title (it can't match) (_, None) => false, // skip when the media item doesn't match the filter (Some(title_filter), Some(title)) if !title_filter.is_match(&title) => false, // keep everything else _ => true, } }) .collect()) } fn main() -> Result<(), Box> { let opt = Opt::from_args(); stderrlog::new() .module(module_path!()) .verbosity(opt.verbose) .init() .unwrap(); debug!("opt: {:?}", opt); let client = new_client(&opt.credentials, &opt.token_cache)?; match opt.cmd { Command::ListAlbums { title_filter } => { print_albums(list_albums(client, title_filter)?); Ok(()) } Command::SearchMediaItems { album_id } => search_media_items(client, album_id), Command::Sync { title_filter, output, } => sync_albums(client, title_filter, output), } }