Compare commits

..

No commits in common. "bd3aac5bc03b6bac36a16dff83b97224b7c5360f" and "e3182d4cf2141d4063bd863c9a74010b75e6f673" have entirely different histories.

9 changed files with 50 additions and 172 deletions

View File

@ -1,2 +0,0 @@
target
*/node_modules

View File

@ -1,11 +0,0 @@
FROM rust:latest AS build-env
COPY ./ /src/
COPY ./dockerfiles/netrc /root/.netrc
RUN mkdir /root/.cargo
COPY ./dockerfiles/cargo-config /.cargo/config
RUN apt-get update && apt-get install -y strace build-essential clang
WORKDIR /src
RUN cargo version && cargo install --path .
FROM rust:slim
COPY --from=build-env /usr/local/cargo/bin/photosync /usr/bin/

View File

@ -1 +0,0 @@
package="app/photosync"

View File

@ -1,2 +0,0 @@
[net]
git-fetch-with-cli = true

View File

@ -1 +0,0 @@
machine git.z.xinu.tv login wathiede password gitgit

View File

@ -3,16 +3,14 @@ body, html, #root {
} }
#ui { #ui {
top: 0; background-color: rgba(255, 255, 255, 0.5);
bottom: 0;
line-height: 3em; line-height: 3em;
position: absolute; position: 'absolute';
width: 100%; width: 100%;
height: 100%;
} }
#ui .meta { #ui .meta {
background-color: rgba(255, 255, 255, 0.5);
line-height: 3em;
text-align: center; text-align: center;
} }

View File

@ -5,8 +5,6 @@ import {
Route, Route,
useParams useParams
} from "react-router-dom"; } from "react-router-dom";
import 'animate.css';
import Random from './rand'; import Random from './rand';
import './App.css'; import './App.css';
@ -19,7 +17,7 @@ type Config = {
let CONFIG: Config; let CONFIG: Config;
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
CONFIG = { CONFIG = {
sleepTimeSeconds: 60, sleepTimeSeconds: 5 * 60,
showUI: false, showUI: false,
} }
} else { } else {
@ -62,11 +60,7 @@ class Slide {
constructor(items: Array<MediaItem>) { constructor(items: Array<MediaItem>) {
this.items = items; this.items = items;
} }
prefetchImages() { render() {
console.log(`prefetchImages, I have ${this.imageUrls.length} images`);
this.imageUrls.map(url => new Image().src = url);
}
get imageUrls(): Array<string> {
let w = window.innerWidth * window.devicePixelRatio; let w = window.innerWidth * window.devicePixelRatio;
let h = window.innerHeight * window.devicePixelRatio; let h = window.innerHeight * window.devicePixelRatio;
let ratio = w/h; let ratio = w/h;
@ -79,27 +73,18 @@ class Slide {
h = roundup(h, IMAGE_CHUNK); h = roundup(h, IMAGE_CHUNK);
w = Math.round(h/ratio); w = Math.round(h/ratio);
} }
//console.log(`Window size ${window.innerWidth}x${window.innerHeight} with a devicePixelRatio of ${window.devicePixelRatio} for a total size of ${w}x${h}`); console.log(`Window size ${window.innerWidth}x${window.innerHeight} with a devicePixelRatio of ${window.devicePixelRatio} for a total size of ${w}x${h}`);
return this.items.map(img => `/api/image/${img.id}?w=${w}&h=${h}`);
}
render() {
const imgs = this.imageUrls.map(url => {
let style: React.CSSProperties = { let style: React.CSSProperties = {
height: '100%', height: '100%',
width: '100%', width: '100%',
backgroundColor: 'black', backgroundColor: 'black',
backgroundImage: `url(${url})`, // TODO(wathiede): make this handle multiple items.
backgroundImage: `url(/api/image/${this.items[0].id}?w=${w}&h=${h})`,
backgroundRepeat: 'no-repeat', backgroundRepeat: 'no-repeat',
backgroundPosition: 'center center', backgroundPosition: 'center center',
backgroundSize: 'cover', backgroundSize: 'cover',
}; };
return <div key={url} style={style}></div>; return <div style={style}></div>
});
// TODO(wathiede): make sure the style handles multiple items.
return <div style={{
height: '100%',
width: '100%',
}}>{imgs}</div>;
} }
}; };
@ -198,7 +183,7 @@ class Album extends React.Component<AlbumProps, AlbumState> {
render() { render() {
// TODO(wathiede): fade transition. // TODO(wathiede): fade transition.
// TODO(wathiede): pair-up portrait orientation images. // TODO(wathiede): pair-up portrait orientation images.
let {curSlide, error, showUI} = this.state; let {curSlide, error, mediaItems, showUI} = this.state;
if (error !== null) { if (error !== null) {
return <h2>Error: {JSON.stringify(error)}</h2>; return <h2>Error: {JSON.stringify(error)}</h2>;
} else if (curSlide) { } else if (curSlide) {
@ -239,7 +224,6 @@ class Album extends React.Component<AlbumProps, AlbumState> {
}}>{ nextSlide?.render() }</div> }}>{ nextSlide?.render() }</div>
</div>; </div>;
} }
nextSlide?.prefetchImages();
return <div id="slide" onClick={(e)=>{ return <div id="slide" onClick={(e)=>{
e.stopPropagation(); e.stopPropagation();
this.setState({showUI: !showUI}) this.setState({showUI: !showUI})

View File

@ -2,14 +2,12 @@ use std::collections::HashMap;
use std::error::Error; use std::error::Error;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::PathBuf; use std::path::PathBuf;
use std::thread;
use std::time;
use google_api_auth; use google_api_auth;
use google_photoslibrary1 as photos; use google_photoslibrary1 as photos;
use hexihasher; use hexihasher;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use log::{debug, error, info}; use log::{debug, info};
use photos::schemas::{Album, MediaItem, SearchMediaItemsRequest}; use photos::schemas::{Album, MediaItem, SearchMediaItemsRequest};
use regex::Regex; use regex::Regex;
use structopt::StructOpt; use structopt::StructOpt;
@ -18,31 +16,6 @@ use yup_oauth2::{Authenticator, InstalledFlow};
use photosync::library::Library; use photosync::library::Library;
use photosync::web; use photosync::web;
fn parse_duration(src: &str) -> Result<time::Duration, std::num::ParseIntError> {
let secs = str::parse::<u64>(src)?;
Ok(time::Duration::from_secs(secs))
}
#[derive(Debug, StructOpt)]
struct Sync {
#[structopt(flatten)]
auth: Auth,
/// Optional album title to filter. Default will mirror all albums.
#[structopt(short, long)]
title_filter: Option<Regex>,
/// Directory to store sync.
root: PathBuf,
}
#[derive(Debug, StructOpt)]
struct Serve {
/// Directory of data fetched by `sync`.
root: PathBuf,
/// HTTP address to listen for web requests.
#[structopt(long = "addr", default_value = "0.0.0.0:0")]
addr: SocketAddr,
}
#[derive(Debug, StructOpt)] #[derive(Debug, StructOpt)]
enum Command { enum Command {
/// List albums for the user of the given credentials. Optionally title filter. /// List albums for the user of the given credentials. Optionally title filter.
@ -58,19 +31,16 @@ enum Command {
}, },
Sync { Sync {
#[structopt(flatten)] #[structopt(flatten)]
sync: Sync, auth: Auth,
/// Optional album title to filter. Default will mirror all albums.
#[structopt(short, long)]
title_filter: Option<Regex>,
/// Directory to store sync.
output: PathBuf,
}, },
Serve { Serve {
#[structopt(flatten)] /// Directory of data fetched by `sync`.
serve: Serve, root: PathBuf,
},
ServeAndSync {
/// Sync albums at given interval.
#[structopt(parse(try_from_str = parse_duration))]
interval: time::Duration,
#[structopt(flatten)]
sync: Sync,
/// HTTP address to listen for web requests. /// HTTP address to listen for web requests.
#[structopt(long = "addr", default_value = "0.0.0.0:0")] #[structopt(long = "addr", default_value = "0.0.0.0:0")]
addr: SocketAddr, addr: SocketAddr,
@ -222,9 +192,10 @@ lazy_static! {
fn sync_albums( fn sync_albums(
client: &photos::Client, client: &photos::Client,
title_filter: &Option<Regex>, title_filter: Option<Regex>,
lib: &Library, output_dir: PathBuf,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
let lib = Library::new(output_dir)?;
let albums = list_albums(client, title_filter)?; let albums = list_albums(client, title_filter)?;
info!("albums {:?}", albums); info!("albums {:?}", albums);
lib.create_album_index(&albums)?; lib.create_album_index(&albums)?;
@ -265,18 +236,12 @@ fn print_albums(albums: Vec<Album>) {
fn list_albums( fn list_albums(
client: &photos::Client, client: &photos::Client,
title_filter: &Option<Regex>, title_filter: Option<Regex>,
) -> Result<Vec<Album>, Box<dyn Error>> { ) -> Result<Vec<Album>, Box<dyn Error>> {
Ok(client Ok(client
.albums()
.list()
.iter_albums_with_all_fields()
.chain(
client
.shared_albums() .shared_albums()
.list() .list()
.iter_shared_albums_with_all_fields(), .iter_shared_albums_with_all_fields()
)
.filter_map(|a| a.ok()) .filter_map(|a| a.ok())
.filter(|a| { .filter(|a| {
match (&title_filter, &a.title) { match (&title_filter, &a.title) {
@ -293,23 +258,8 @@ fn list_albums(
.collect()) .collect())
} }
fn background_sync( pub fn serve(addr: SocketAddr, root: PathBuf) -> Result<(), Box<dyn Error>> {
client: photos::Client, web::run(addr, root)
interval: time::Duration,
title_filter: Option<Regex>,
lib: Library,
) -> Result<(), Box<dyn Error>> {
thread::spawn(move || loop {
if let Err(err) = sync_albums(&client, &title_filter, &lib) {
error!("Error syncing: {}", err);
}
thread::sleep(interval);
});
Ok(())
}
pub fn serve(addr: SocketAddr, lib: Library) -> Result<(), Box<dyn Error>> {
web::run(addr, lib)
} }
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
@ -323,7 +273,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
match opt.cmd { match opt.cmd {
Command::ListAlbums { auth, title_filter } => { Command::ListAlbums { auth, title_filter } => {
let client = new_client(&auth.credentials, &auth.token_cache)?; let client = new_client(&auth.credentials, &auth.token_cache)?;
print_albums(list_albums(&client, &title_filter)?); print_albums(list_albums(&client, title_filter)?);
Ok(()) Ok(())
} }
Command::SearchMediaItems { auth, album_id } => { Command::SearchMediaItems { auth, album_id } => {
@ -332,39 +282,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(()) Ok(())
} }
Command::Sync { Command::Sync {
sync:
Sync {
auth, auth,
title_filter, title_filter,
root, output,
},
} => { } => {
let client = new_client(&auth.credentials, &auth.token_cache)?; let client = new_client(&auth.credentials, &auth.token_cache)?;
let lib = Library::new(root)?; sync_albums(&client, title_filter, output)?;
sync_albums(&client, &title_filter, &lib)?;
Ok(())
}
Command::Serve {
serve: Serve { addr, root },
} => {
let lib = Library::new(root)?;
serve(addr, lib)
}
Command::ServeAndSync {
interval,
sync:
Sync {
auth,
title_filter,
root,
},
addr,
} => {
let client = new_client(&auth.credentials, &auth.token_cache)?;
let lib = Library::new(root)?;
background_sync(client, interval, title_filter, lib.clone())?;
serve(addr, lib)?;
Ok(()) Ok(())
} }
Command::Serve { addr, root } => serve(addr, root),
} }
} }

View File

@ -1,6 +1,6 @@
use std::error::Error; use std::error::Error;
use std::io::Write;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::PathBuf;
use log::warn; use log::warn;
use prometheus::Encoder; use prometheus::Encoder;
@ -98,27 +98,16 @@ fn image(
} }
#[derive(RustEmbed)] #[derive(RustEmbed)]
#[folder = "react-slideshow/build/"] #[folder = "react-debug/build/"]
struct Asset; struct Asset;
fn embedz() -> Result<impl warp::Reply, warp::Rejection> { pub fn run(addr: SocketAddr, root: PathBuf) -> Result<(), Box<dyn Error>> {
let mut w = Vec::new(); let lib = Library::new(root)?;
write!(w, "<html>").unwrap();
for path in Asset::iter() {
write!(w, r#"<div><a href="{0}">{0}</a></div>"#, path).unwrap();
}
Ok(warp::http::Response::builder()
.header("Content-Type", "text/html")
.body(w))
}
pub fn run(addr: SocketAddr, lib: Library) -> Result<(), Box<dyn Error>> {
let lib = warp::any().map(move || lib.clone()); let lib = warp::any().map(move || lib.clone());
let index = warp::get2().and(warp::path::full()).and_then(index); let index = warp::get2().and(warp::path::full()).and_then(index);
let albums = warp::path("albums").and(lib.clone()).and_then(albums); let albums = warp::path("albums").and(lib.clone()).and_then(albums);
let embedz = warp::path("embedz").and_then(embedz);
let album = warp::path("album") let album = warp::path("album")
.and(lib.clone()) .and(lib.clone())
@ -133,7 +122,6 @@ pub fn run(addr: SocketAddr, lib: Library) -> Result<(), Box<dyn Error>> {
let api = albums.or(album).or(image); let api = albums.or(album).or(image);
let api = warp::path("api").and(api); let api = warp::path("api").and(api);
let api = api.or(embedz);
// Fallback, always keep this last. // Fallback, always keep this last.
let api = api.or(index); let api = api.or(index);