Compare commits
6 Commits
e3182d4cf2
...
bd3aac5bc0
| Author | SHA1 | Date | |
|---|---|---|---|
| bd3aac5bc0 | |||
| 737b290cc0 | |||
| 2ee2a98c7d | |||
| a96fe1da9d | |||
| 89037b6b24 | |||
| 9e4fdf7644 |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
target
|
||||||
|
*/node_modules
|
||||||
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
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/
|
||||||
1
config.dbuild
Normal file
1
config.dbuild
Normal file
@ -0,0 +1 @@
|
|||||||
|
package="app/photosync"
|
||||||
2
dockerfiles/cargo-config
Normal file
2
dockerfiles/cargo-config
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[net]
|
||||||
|
git-fetch-with-cli = true
|
||||||
1
dockerfiles/netrc
Normal file
1
dockerfiles/netrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
machine git.z.xinu.tv login wathiede password gitgit
|
||||||
@ -3,14 +3,16 @@ body, html, #root {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ui {
|
#ui {
|
||||||
background-color: rgba(255, 255, 255, 0.5);
|
top: 0;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,8 @@ 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';
|
||||||
|
|
||||||
@ -17,7 +19,7 @@ type Config = {
|
|||||||
let CONFIG: Config;
|
let CONFIG: Config;
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
CONFIG = {
|
CONFIG = {
|
||||||
sleepTimeSeconds: 5 * 60,
|
sleepTimeSeconds: 60,
|
||||||
showUI: false,
|
showUI: false,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -60,7 +62,11 @@ class Slide {
|
|||||||
constructor(items: Array<MediaItem>) {
|
constructor(items: Array<MediaItem>) {
|
||||||
this.items = items;
|
this.items = items;
|
||||||
}
|
}
|
||||||
render() {
|
prefetchImages() {
|
||||||
|
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;
|
||||||
@ -68,23 +74,32 @@ class Slide {
|
|||||||
// Landscape image
|
// Landscape image
|
||||||
w = roundup(w, IMAGE_CHUNK);
|
w = roundup(w, IMAGE_CHUNK);
|
||||||
h = Math.round(w/ratio);
|
h = Math.round(w/ratio);
|
||||||
} else {
|
} else {
|
||||||
// Portrait image
|
// Portrait image
|
||||||
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}`);
|
||||||
let style: React.CSSProperties = {
|
return this.items.map(img => `/api/image/${img.id}?w=${w}&h=${h}`);
|
||||||
height: '100%',
|
}
|
||||||
width: '100%',
|
render() {
|
||||||
backgroundColor: 'black',
|
const imgs = this.imageUrls.map(url => {
|
||||||
// TODO(wathiede): make this handle multiple items.
|
let style: React.CSSProperties = {
|
||||||
backgroundImage: `url(/api/image/${this.items[0].id}?w=${w}&h=${h})`,
|
height: '100%',
|
||||||
backgroundRepeat: 'no-repeat',
|
width: '100%',
|
||||||
backgroundPosition: 'center center',
|
backgroundColor: 'black',
|
||||||
backgroundSize: 'cover',
|
backgroundImage: `url(${url})`,
|
||||||
};
|
backgroundRepeat: 'no-repeat',
|
||||||
return <div style={style}></div>
|
backgroundPosition: 'center center',
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
};
|
||||||
|
return <div key={url} style={style}></div>;
|
||||||
|
});
|
||||||
|
// TODO(wathiede): make sure the style handles multiple items.
|
||||||
|
return <div style={{
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
}}>{imgs}</div>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -183,7 +198,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, mediaItems, showUI} = this.state;
|
let {curSlide, error, 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) {
|
||||||
@ -224,6 +239,7 @@ 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})
|
||||||
|
|||||||
121
src/main.rs
121
src/main.rs
@ -2,12 +2,14 @@ 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, info};
|
use log::{debug, error, 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;
|
||||||
@ -16,6 +18,31 @@ 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.
|
||||||
@ -31,16 +58,19 @@ enum Command {
|
|||||||
},
|
},
|
||||||
Sync {
|
Sync {
|
||||||
#[structopt(flatten)]
|
#[structopt(flatten)]
|
||||||
auth: Auth,
|
sync: Sync,
|
||||||
/// 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 {
|
||||||
/// Directory of data fetched by `sync`.
|
#[structopt(flatten)]
|
||||||
root: PathBuf,
|
serve: Serve,
|
||||||
|
},
|
||||||
|
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,
|
||||||
@ -192,10 +222,9 @@ lazy_static! {
|
|||||||
|
|
||||||
fn sync_albums(
|
fn sync_albums(
|
||||||
client: &photos::Client,
|
client: &photos::Client,
|
||||||
title_filter: Option<Regex>,
|
title_filter: &Option<Regex>,
|
||||||
output_dir: PathBuf,
|
lib: &Library,
|
||||||
) -> 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)?;
|
||||||
@ -236,12 +265,18 @@ 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
|
||||||
.shared_albums()
|
.albums()
|
||||||
.list()
|
.list()
|
||||||
.iter_shared_albums_with_all_fields()
|
.iter_albums_with_all_fields()
|
||||||
|
.chain(
|
||||||
|
client
|
||||||
|
.shared_albums()
|
||||||
|
.list()
|
||||||
|
.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) {
|
||||||
@ -258,8 +293,23 @@ fn list_albums(
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serve(addr: SocketAddr, root: PathBuf) -> Result<(), Box<dyn Error>> {
|
fn background_sync(
|
||||||
web::run(addr, root)
|
client: photos::Client,
|
||||||
|
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>> {
|
||||||
@ -273,7 +323,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 } => {
|
||||||
@ -282,14 +332,39 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Command::Sync {
|
Command::Sync {
|
||||||
auth,
|
sync:
|
||||||
title_filter,
|
Sync {
|
||||||
output,
|
auth,
|
||||||
|
title_filter,
|
||||||
|
root,
|
||||||
|
},
|
||||||
} => {
|
} => {
|
||||||
let client = new_client(&auth.credentials, &auth.token_cache)?;
|
let client = new_client(&auth.credentials, &auth.token_cache)?;
|
||||||
sync_albums(&client, title_filter, output)?;
|
let lib = Library::new(root)?;
|
||||||
|
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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/web.rs
20
src/web.rs
@ -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,16 +98,27 @@ fn image(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(RustEmbed)]
|
#[derive(RustEmbed)]
|
||||||
#[folder = "react-debug/build/"]
|
#[folder = "react-slideshow/build/"]
|
||||||
struct Asset;
|
struct Asset;
|
||||||
|
|
||||||
pub fn run(addr: SocketAddr, root: PathBuf) -> Result<(), Box<dyn Error>> {
|
fn embedz() -> Result<impl warp::Reply, warp::Rejection> {
|
||||||
let lib = Library::new(root)?;
|
let mut w = Vec::new();
|
||||||
|
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())
|
||||||
@ -122,6 +133,7 @@ pub fn run(addr: SocketAddr, root: PathBuf) -> 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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user