photosync/src/main.rs
Bill Thiede b0a10364b0 Base sync support.
Refactored list_albums to be useful in sync and list-albums.
2020-02-06 18:27:37 -08:00

193 lines
5.8 KiB
Rust

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<Regex>,
},
SearchMediaItems {
album_id: String,
},
Sync {
/// Optional album title to filter. Default will mirror all albums.
#[structopt(short, long)]
title_filter: Option<Regex>,
/// 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<google_photoslibrary1::Client, Box<dyn Error>> {
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<dyn Error>> {
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<Regex>,
output_dir: PathBuf,
) -> Result<(), Box<dyn Error>> {
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<Album>) {
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<Regex>,
) -> Result<Vec<Album>, Box<dyn Error>> {
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<dyn std::error::Error>> {
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),
}
}