1
Fork 0

Merge branch 'master' of github.com:viktorstrate/photoview into favorites-checkobox-on-photos-and-album-page-viktorstrate/photoview#6

This commit is contained in:
stz184 2020-09-07 13:08:25 +03:00
commit 75e43aae80
53 changed files with 2668 additions and 2718 deletions

View File

@ -1,42 +0,0 @@
name: API
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
name: Build
runs-on: ubuntu-latest
defaults:
run:
working-directory: api
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: |
go get -v -t -d ./...
if [ -f Gopkg.toml ]; then
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
dep ensure
fi
- name: Build
run: go build -v .
- name: Test
run: go test -v ./...

90
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,90 @@
name: Docker builds
on:
schedule:
- cron: "0 10 * * *" # everyday at 10am
pull_request:
branches: master
push:
branches: master
tags:
- v*
jobs:
build:
name: Build and deploy docker images
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Cache Docker layers
uses: actions/cache@v2
id: cache
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Prepare
id: prepare
run: |
DOCKER_USERNAME=viktorstrate
DOCKER_IMAGE=viktorstrate/photoview
DOCKER_PLATFORMS=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
VERSION=edge
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/v}
fi
if [ "${{ github.event_name }}" = "schedule" ]; then
VERSION=nightly
fi
TAGS="--tag ${DOCKER_IMAGE}:${VERSION}"
if [[ $VERSION =~ ^(([0-9]{1,3})\.[0-9]{1,3})\.[0-9]{1,3}$ ]]; then
VERSION_MINOR=${BASH_REMATCH[1]}
VERSION_MAJOR=${BASH_REMATCH[2]}
TAGS="$TAGS --tag ${DOCKER_IMAGE}:latest --tag ${DOCKER_IMAGE}:${VERSION_MINOR} --tag ${DOCKER_IMAGE}:${VERSION_MAJOR}"
fi
echo ::set-output name=docker_username::${DOCKER_USERNAME}
echo ::set-output name=docker_image::${DOCKER_IMAGE}
echo ::set-output name=version::${VERSION}
echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--build-arg VERSION=${VERSION} \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg VCS_REF=${GITHUB_SHA::8} \
${TAGS} --file Dockerfile .
- name: Set up Docker Buildx
uses: crazy-max/ghaction-docker-buildx@v3
- name: Docker Buildx (build)
run: |
docker buildx build --output "type=image,push=false" ${{ steps.prepare.outputs.buildx_args }}
- name: Docker Login
if: success() && github.event_name != 'pull_request'
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "${DOCKER_PASSWORD}" | docker login --username "${{ steps.prepare.outputs.docker_username }}" --password-stdin
- name: Docker Buildx (push)
if: success() && github.event_name != 'pull_request'
run: |
docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }}
- name: Docker Check Manifest
if: always() && github.event_name != 'pull_request'
run: |
docker run --rm mplatform/mquery ${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.version }}
- name: Clear
if: always() && github.event_name != 'pull_request'
run: |
rm -f ${HOME}/.docker/config.json

40
.github/workflows/tests.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: Tests
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
test-api:
name: Test API
runs-on: ubuntu-latest
defaults:
run:
working-directory: api
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: |
go get -v -t -d ./...
if [ -f Gopkg.toml ]; then
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
dep ensure
fi
- name: Build
run: go build -v .
- name: Test
run: go test -v ./...

View File

@ -1,5 +1,3 @@
{
"eslint.workingDirectories": [
"ui", "api"
]
}
"eslint.workingDirectories": ["ui"]
}

View File

@ -1,5 +1,5 @@
# Build UI
FROM node:10 as ui
FROM --platform=${BUILDPLATFORM:-linux/amd64} node:10 as ui
ARG API_ENDPOINT
ENV API_ENDPOINT=${API_ENDPOINT}
@ -20,7 +20,8 @@ COPY ui /app
RUN npm run build -- --public-url $UI_PUBLIC_URL
# Build API
FROM golang:alpine AS api
FROM --platform=${BUILDPLATFORM:-linux/amd64} tonistiigi/xx:golang AS xgo
FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.14-alpine AS api
RUN mkdir -p /app
WORKDIR /app
@ -32,16 +33,20 @@ RUN go mod download
# Copy api source
COPY api /app
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o photoview .
ARG TARGETPLATFORM
ARG TARGETOS
ARG TARGETARCH
RUN go env
RUN go build -v -o photoview .
# Copy api and ui to production environment
FROM alpine:3.12
# Install darktable for converting RAW images
RUN apk --no-cache add darktable
# Install ffmpeg for encoding videos
RUN apk --no-cache add ffmpeg
# Install darktable for converting RAW images, and ffmpeg for encoding videos
# Ignore errors if packages are not supported for the specified platform
RUN apk --no-cache add darktable; exit 0
RUN apk --no-cache add ffmpeg; exit 0
COPY --from=ui /app/dist /ui
COPY --from=api /app/database/migrations /database/migrations

View File

@ -3,7 +3,7 @@
[![License](https://img.shields.io/github/license/viktorstrate/photoview)](./LICENSE.md)
[![GitHub contributors](https://img.shields.io/github/contributors/viktorstrate/photoview)](https://github.com/viktorstrate/photoview/graphs/contributors)
[![Docker Pulls](https://img.shields.io/docker/pulls/viktorstrate/photoview)](https://hub.docker.com/r/viktorstrate/photoview)
[![Docker Build Status](https://img.shields.io/docker/cloud/build/viktorstrate/photoview)](https://hub.docker.com/r/viktorstrate/photoview/builds)
[![Docker Build Status](https://img.shields.io/github/workflow/status/viktorstrate/photoview/Docker%20builds?label=docker%20build)](https://hub.docker.com/r/viktorstrate/photoview/)
![screenshot](./screenshots/main-window.png)
@ -30,9 +30,11 @@ Password: **demo**
- **Closely tied to the file system**. The website presents the images found on the local filesystem of the server, directories are mapped to albums.
- **User management**. Each user is created along with a path on the local filesystem, photos within that path can be accessed by that user.
- **Photo sharing**. Photos and albums can easily be shared with other users or publicly with a unique URL.
- **Made for photography**. The website is ment as a way to present photographies, and thus supports **RAW** file formats, and **EXIF** parsing.
- **Sharing**. Albums, as well as individual media, can easily be shared with a public link, the link can optinally be password protected.
- **Made for photography**. Photoview is built with photographers in mind, and thus supports **RAW** file formats, and **EXIF** parsing.
- **Video support**. Many common video formats are supported. Videos will automatically be optimized for web.
- **Performant**. Thumbnails are automatically generated and photos first load when they are visible on the screen. In full screen, thumbnails are displayed until the high resolution image has been fully loaded.
- **Secure**. All media resources are protected with a cookie-token, all passwords are properly hashed, and the API uses a strict [CORS policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS).
## Why yet another self-hosted photo gallery

View File

@ -0,0 +1,9 @@
DROP TABLE IF EXISTS site_info;
DROP TABLE IF EXISTS access_token;
DROP TABLE IF EXISTS media_url;
DROP TABLE IF EXISTS share_token;
DROP TABLE IF EXISTS media;
DROP TABLE IF EXISTS video_metadata;
DROP TABLE IF EXISTS media_exif;
DROP TABLE IF EXISTS album;
DROP TABLE IF EXISTS user;

View File

@ -0,0 +1,120 @@
-- Users and authentication
CREATE TABLE IF NOT EXISTS user (
user_id int NOT NULL AUTO_INCREMENT,
username varchar(256) NOT NULL UNIQUE,
password varchar(256),
root_path varchar(512),
admin boolean NOT NULL DEFAULT 0,
PRIMARY KEY (user_id)
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS access_token (
token_id int NOT NULL AUTO_INCREMENT,
user_id int NOT NULL,
value char(24) NOT NULL UNIQUE,
expire timestamp NOT NULL,
PRIMARY KEY (token_id),
FOREIGN KEY (user_id) REFERENCES user(user_id) ON DELETE CASCADE
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS site_info (
initial_setup boolean NOT NULL DEFAULT TRUE
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Video related
CREATE TABLE IF NOT EXISTS video_metadata (
metadata_id int NOT NULL AUTO_INCREMENT,
width int(6) NOT NULL,
height int(6) NOT NULL,
duration double NOT NULL,
codec varchar(128),
framerate double,
bitrate int(24),
color_profile varchar(128),
audio varchar(128),
PRIMARY KEY (metadata_id)
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Media related
CREATE TABLE IF NOT EXISTS album (
album_id int NOT NULL AUTO_INCREMENT,
title varchar(256) NOT NULL,
parent_album int,
owner_id int NOT NULL,
path varchar(1024) NOT NULL,
path_hash varchar(32) NOT NULL UNIQUE,
PRIMARY KEY (album_id),
FOREIGN KEY (parent_album) REFERENCES album(album_id) ON DELETE CASCADE,
FOREIGN KEY (owner_id) REFERENCES user(user_id) ON DELETE CASCADE
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS media_exif (
exif_id int NOT NULL AUTO_INCREMENT,
camera varchar(256),
maker varchar(256),
lens varchar(256),
date_shot timestamp NULL,
exposure varchar(256),
aperture float,
iso int(6),
focal_length float,
flash varchar(256),
orientation int(1),
exposure_program int(1),
gps_latitude float,
gps_longitude float,
PRIMARY KEY (exif_id)
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS media (
media_id int NOT NULL AUTO_INCREMENT,
title varchar(256) NOT NULL,
path varchar(1024) NOT NULL,
path_hash varchar(32) NOT NULL UNIQUE,
album_id int NOT NULL,
exif_id int,
date_shot datetime NOT NULL,
date_imported datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
favorite boolean DEFAULT FALSE,
media_type varchar(64) NOT NULL,
video_metadata_id int,
PRIMARY KEY (media_id),
FOREIGN KEY (album_id) REFERENCES album(album_id) ON DELETE CASCADE,
FOREIGN KEY (exif_id) REFERENCES media_exif(exif_id) ON DELETE CASCADE,
FOREIGN KEY (video_metadata_id) REFERENCES video_metadata(metadata_id) ON DELETE CASCADE
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS media_url (
url_id int NOT NULL AUTO_INCREMENT,
media_id int NOT NULL,
media_name varchar(512) NOT NULL,
width int NOT NULL,
height int NOT NULL,
purpose varchar(64) NOT NULL,
content_type varchar(64) NOT NULL,
file_size int NOT NULL,
PRIMARY KEY (url_id),
FOREIGN KEY (media_id) REFERENCES media(media_id) ON DELETE CASCADE
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Public shares
CREATE TABLE IF NOT EXISTS share_token (
token_id int AUTO_INCREMENT,
value char(24) NOT NULL UNIQUE,
owner_id int NOT NULL,
expire timestamp NULL DEFAULT NULL,
password varchar(256),
album_id int,
media_id int,
PRIMARY KEY (token_id)
-- CHECK (album_id IS NOT NULL OR media_id IS NOT NULL)
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@ -1,2 +0,0 @@
DROP TABLE IF EXISTS user;
DROP TABLE IF NOT EXISTS access_token;

View File

@ -1,19 +0,0 @@
CREATE TABLE IF NOT EXISTS user (
user_id int NOT NULL AUTO_INCREMENT,
username varchar(256) NOT NULL UNIQUE,
password varchar(256),
root_path varchar(512),
admin boolean NOT NULL DEFAULT 0,
PRIMARY KEY (user_id)
);
CREATE TABLE IF NOT EXISTS access_token (
token_id int NOT NULL AUTO_INCREMENT,
user_id int NOT NULL,
value char(24) NOT NULL UNIQUE,
expire timestamp NOT NULL,
PRIMARY KEY (token_id),
FOREIGN KEY (user_id) REFERENCES user(user_id) ON DELETE CASCADE
);

View File

@ -1,4 +0,0 @@
DROP TABLE IF EXISTS photo;
DROP TABLE IF EXISTS album;
DROP TABLE IF EXISTS photo_url;
DROP TABLE IF EXISTS photo_exif;

View File

@ -1,55 +0,0 @@
CREATE TABLE IF NOT EXISTS photo_exif (
exif_id int NOT NULL AUTO_INCREMENT,
camera varchar(256),
maker varchar(256),
lens varchar(256),
dateShot timestamp NULL,
exposure varchar(256),
aperture float,
iso int(6),
focal_length float,
flash varchar(256),
orientation int(1),
exposure_program int(1),
PRIMARY KEY (exif_id)
);
CREATE TABLE IF NOT EXISTS album (
album_id int NOT NULL AUTO_INCREMENT,
title varchar(256) NOT NULL,
parent_album int,
owner_id int NOT NULL,
path varchar(1024) NOT NULL,
path_hash varchar(32) NOT NULL UNIQUE,
PRIMARY KEY (album_id),
FOREIGN KEY (parent_album) REFERENCES album(album_id) ON DELETE CASCADE,
FOREIGN KEY (owner_id) REFERENCES user(user_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS photo (
photo_id int NOT NULL AUTO_INCREMENT,
title varchar(256) NOT NULL,
path varchar(1024) NOT NULL,
path_hash varchar(32) NOT NULL UNIQUE,
album_id int NOT NULL,
exif_id int,
PRIMARY KEY (photo_id),
FOREIGN KEY (album_id) REFERENCES album(album_id) ON DELETE CASCADE,
FOREIGN KEY (exif_id) REFERENCES photo_exif(exif_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS photo_url (
url_id int NOT NULL AUTO_INCREMENT,
photo_id int NOT NULL,
photo_name varchar(512) NOT NULL,
width int NOT NULL,
height int NOT NULL,
purpose varchar(64) NOT NULL,
content_type varchar(64) NOT NULL,
PRIMARY KEY (url_id),
FOREIGN KEY (photo_id) REFERENCES photo(photo_id) ON DELETE CASCADE
);

View File

@ -1 +0,0 @@
DROP TABLE IF EXISTS site_info;

View File

@ -1,3 +0,0 @@
CREATE TABLE IF NOT EXISTS site_info (
initial_setup boolean NOT NULL DEFAULT TRUE
);

View File

@ -1,2 +0,0 @@
DROP TABLE IF EXISTS share_token;

View File

@ -1,12 +0,0 @@
CREATE TABLE IF NOT EXISTS share_token (
token_id int AUTO_INCREMENT,
value char(24) NOT NULL UNIQUE,
owner_id int NOT NULL,
expire timestamp NULL DEFAULT NULL,
password varchar(256),
album_id int,
photo_id int,
PRIMARY KEY (token_id)
-- CHECK (album_id IS NOT NULL OR photo_id IS NOT NULL)
);

View File

@ -1,9 +0,0 @@
-- Migrate all tables in database to use utf8 for better language support
ALTER TABLE access_token CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;
ALTER TABLE album CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;
ALTER TABLE photo CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;
ALTER TABLE photo_exif CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;
ALTER TABLE photo_url CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;
ALTER TABLE share_token CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;
ALTER TABLE site_info CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;
ALTER TABLE user CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;

View File

@ -1,9 +0,0 @@
-- Migrate all tables in database to use utf8 for better language support
ALTER TABLE access_token CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE album CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE photo CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE photo_exif CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE photo_url CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE share_token CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE site_info CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@ -1,2 +0,0 @@
-- Add favorite attribute to photos
ALTER TABLE photo DROP favorite

View File

@ -1,2 +0,0 @@
-- Add favorite attribute to photos
ALTER TABLE photo ADD favorite BOOL DEFAULT false

View File

@ -1,43 +0,0 @@
-- Update database to hash indexed paths
CREATE PROCEDURE MigratePathHashIfNeeded()
BEGIN
-- Add path hash for photo table if it doesn't exist
IF NOT EXISTS( SELECT *
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'photo'
AND table_schema = DATABASE()
AND column_name = 'path_hash') THEN
-- Remove unique index from photo.path
ALTER TABLE photo DROP INDEX path;
-- Add path_hash and set it to the md5 hash based of the path attribute
ALTER TABLE photo ADD path_hash varchar(32) AFTER path;
UPDATE photo p SET path_hash = md5(p.path);
ALTER TABLE photo MODIFY path_hash varchar(32) NOT NULL UNIQUE;
END IF;
-- Add path hash for album table if it doesn't exist
IF NOT EXISTS( SELECT *
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'album'
AND table_schema = DATABASE()
AND column_name = 'path_hash') THEN
-- Remove unique index from album.path
ALTER TABLE album DROP INDEX path;
-- Add path_hash and set it to the md5 hash based of the path attribute
ALTER TABLE album ADD path_hash varchar(32) AFTER path;
UPDATE album a SET path_hash = md5(a.path);
ALTER TABLE album MODIFY path_hash varchar(32) NOT NULL UNIQUE;
END IF;
END; -- MigratePathHashIfNeeded procedure end
CALL MigratePathHashIfNeeded();
DROP PROCEDURE MigratePathHashIfNeeded;

View File

@ -1,17 +0,0 @@
ALTER TABLE media RENAME TO photo;
ALTER TABLE media_url RENAME TO photo_url;
ALTER TABLE media_exif RENAME TO photo_exif;
ALTER TABLE photo CHANGE COLUMN media_id photo_id int NOT NULL AUTO_INCREMENT;
ALTER TABLE photo_url CHANGE COLUMN media_id photo_id int NOT NULL;
ALTER TABLE photo_url CHANGE COLUMN media_name photo_name varchar(512) NOT NULL;
ALTER TABLE share_token CHANGE COLUMN media_id photo_id int;
ALTER TABLE photo DROP COLUMN media_type;
ALTER TABLE photo
DROP FOREIGN KEY photo_ibfk_3,
DROP COLUMN video_metadata_id;
DROP TABLE video_metadata;

View File

@ -1,31 +0,0 @@
ALTER TABLE photo RENAME TO media;
ALTER TABLE photo_url RENAME TO media_url;
ALTER TABLE photo_exif RENAME TO media_exif;
ALTER TABLE media RENAME COLUMN photo_id TO media_id;
ALTER TABLE media_url
RENAME COLUMN photo_id TO media_id,
RENAME COLUMN photo_name TO media_name;
ALTER TABLE share_token RENAME COLUMN photo_id TO media_id;
CREATE TABLE video_metadata (
metadata_id int NOT NULL AUTO_INCREMENT,
width int(6) NOT NULL,
height int(6) NOT NULL,
duration double NOT NULL,
codec varchar(128),
framerate double,
bitrate int(24),
color_profile varchar(128),
audio varchar(128),
PRIMARY KEY (metadata_id)
);
ALTER TABLE media
ADD COLUMN media_type varchar(64) NOT NULL DEFAULT "photo",
ADD COLUMN video_metadata_id int,
ADD FOREIGN KEY (video_metadata_id) REFERENCES video_metadata(metadata_id);

View File

@ -3,9 +3,9 @@ module github.com/viktorstrate/photoview/api
go 1.13
require (
github.com/99designs/gqlgen v0.11.3
github.com/99designs/gqlgen v0.12.1
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/agnivade/levenshtein v1.1.0 // indirect
github.com/disintegration/imaging v1.6.2
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v1.13.1 // indirect
@ -18,21 +18,15 @@ require (
github.com/h2non/filetype v1.1.0
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/joho/godotenv v1.3.0
github.com/matryer/moq v0.0.0-20200607124540-4638a53893e6 // indirect
github.com/mitchellh/mapstructure v1.3.2 // indirect
github.com/mitchellh/mapstructure v1.3.3 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pkg/errors v0.9.1
github.com/urfave/cli v1.22.4 // indirect
github.com/urfave/cli/v2 v2.2.0 // indirect
github.com/vektah/dataloaden v0.3.0 // indirect
github.com/vektah/gqlparser v1.3.1 // indirect
github.com/vektah/gqlparser/v2 v2.0.1
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0
github.com/xor-gate/goexif2 v1.1.0
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9
golang.org/x/image v0.0.0-20200618115811-c13761719519
golang.org/x/mod v0.3.0 // indirect
golang.org/x/tools v0.0.0-20200622192924-4fd1c64487bf // indirect
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
golang.org/x/image v0.0.0-20200801110659-972c09e46d76
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc // indirect
gopkg.in/vansante/go-ffprobe.v2 v2.0.2
gopkg.in/yaml.v2 v2.3.0 // indirect
)

View File

@ -1,9 +1,5 @@
github.com/99designs/gqlgen v0.10.2 h1:FfjCqIWejHDJeLpQTI0neoZo5vDO3sdo5oNCucet3A0=
github.com/99designs/gqlgen v0.10.2/go.mod h1:aDB7oabSAyZ4kUHLEySsLxnWrBy3lA0A2gWKU+qoHwI=
github.com/99designs/gqlgen v0.11.2 h1:qatIx2DY7YyaUIBd47ORY3Aj/+pJsPLoL7tyuuISJR0=
github.com/99designs/gqlgen v0.11.2/go.mod h1:RgX5GRRdDWNkh4pBrdzNpNPFVsdoUFY2+adM6nb1N+4=
github.com/99designs/gqlgen v0.11.3 h1:oFSxl1DFS9X///uHV3y6CEfpcXWrDUxVblR4Xib2bs4=
github.com/99designs/gqlgen v0.11.3/go.mod h1:RgX5GRRdDWNkh4pBrdzNpNPFVsdoUFY2+adM6nb1N+4=
github.com/99designs/gqlgen v0.12.1 h1:Qfi6HDi6uDxGVKvz5kg8/5iP9YF2XqhwIoBKAt+Nt6M=
github.com/99designs/gqlgen v0.12.1/go.mod h1:7zdGo6ry9u1YBp/qlb2uxSU5Mt2jQKLcBETQiKk+Bxo=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
@ -11,19 +7,21 @@ github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+s
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0=
github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs=
github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM=
github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c h1:TUuUh0Xgj97tLMNtWtNvI9mIV6isjEb9lBMNv+77IGM=
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
@ -46,16 +44,8 @@ github.com/gorilla/mux v1.6.1 h1:KOwqsTYZdeuMacU7CxjMNYEKeBvLbxW+psodrbcEa3A=
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/h2non/filetype v1.0.10 h1:z+SJfnL6thYJ9kAST+6nPRXp1lMxnOVbMZHNYHMar0s=
github.com/h2non/filetype v1.0.10/go.mod h1:isekKqOuhMj+s/7r3rIeTErIRy4Rub5uBWHfvMusLMU=
github.com/h2non/filetype v1.0.12 h1:yHCsIe0y2cvbDARtJhGBTD2ecvqMSTvlIcph9En/Zao=
github.com/h2non/filetype v1.0.12/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/h2non/filetype v1.1.0 h1:Or/gjocJrJRNK/Cri/TDEKFjAR+cfG6eK65NGYB6gBA=
github.com/h2non/filetype v1.1.0/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
@ -73,19 +63,13 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg=
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/matryer/moq v0.0.0-20200607124540-4638a53893e6 h1:Cx1ZvZ3SQTli1nKee9qvJ/NJP3vt11s+ilM7NF3QSL8=
github.com/matryer/moq v0.0.0-20200607124540-4638a53893e6/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5nrJJVjbXiDZcVhOBSzKn3o9LgRLLMRNuru8=
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg=
github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/nf/cr2 v0.0.0-20180623103828-4699471a17ed h1:QP63yO3XEt8tJ1DgBsNjbOyBGjy2eHy9ITK4Eisr9rg=
github.com/nf/cr2 v0.0.0-20180623103828-4699471a17ed/go.mod h1:HazDB3gS/i//QXMMRmTAV7Ni9gAi4mDNTH2HjZ5aVgU=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
@ -117,22 +101,10 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.3 h1:FpNT6zq26xNpHZy08emi755QwzLPs6Pukqjlc7RfOMU=
github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/dataloaden v0.3.0 h1:ZfVN2QD6swgvp+tDqdH/OIT/wu3Dhu0cus0k5gIZS84=
github.com/vektah/dataloaden v0.3.0/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/gqlparser v1.2.0 h1:ntkSCX7F5ZJKl+HIVnmLaO269MruasVpNiMOjX9kgo0=
github.com/vektah/gqlparser v1.2.0/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74=
github.com/vektah/gqlparser v1.3.1 h1:8b0IcD3qZKWJQHSzynbDlrtP3IxVydZ2DZepCGofqfU=
github.com/vektah/gqlparser v1.3.1/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74=
github.com/vektah/gqlparser/v2 v2.0.1 h1:xgl5abVnsd4hkN9rk65OJID9bfcLSMuTaTcZj777q1o=
@ -141,59 +113,44 @@ github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 h1:3UeQBvD0TFrlV
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM=
github.com/xor-gate/goexif2 v1.1.0 h1:OvTZ5iEvsDhRWFjV5xY3wT7uHFna28nSSP7ucau+cXQ=
github.com/xor-gate/goexif2 v1.1.0/go.mod h1:eRjn3VSkAwpNpxEx/CGmd0zg0JFGL3akrSMxnJ581AY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4 h1:QmwruyY+bKbDDL0BaglrbZABEali68eoMFhTZpCjYVA=
golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200119044424-58c23975cae1 h1:5h3ngYt7+vXCDZCup/HkCQgW5XwmSvR/nA2JmJ0RErg=
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200618115811-c13761719519 h1:1e2ufUJNM3lCHEY5jIgac/7UTjd6cgJNdatjPdFWf34=
golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200801110659-972c09e46d76 h1:U7GPaoQyQmX+CBRWXKrvRzWTbd+slqeSh8uARsIyhAw=
golang.org/x/image v0.0.0-20200801110659-972c09e46d76/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc h1:zK/HqS5bZxDptfPJNq8v7vJfXtkU7r9TLIoSr1bXaP4=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd h1:oMEQDWVXVNpceQoVd1JN3CQ7LYJJzs5qWqZIUcxXHHw=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 h1:rjUrONFu4kLchcZTfp3/96bR8bW8dIa8uz3cR5n0cgM=
golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200622192924-4fd1c64487bf h1:nXhK+swoyjE2slxjyxxa36VklQeCrnFyGuIZQGUsuxY=
golang.org/x/tools v0.0.0-20200622192924-4fd1c64487bf/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/vansante/go-ffprobe.v2 v2.0.2 h1:DdxSfFnlqeawPIVbIQEI6LR6OQHQNR7tNgWb2mWuC4w=

View File

@ -87,10 +87,8 @@ type ComplexityRoot struct {
}
MediaDownload struct {
Height func(childComplexity int) int
Title func(childComplexity int) int
URL func(childComplexity int) int
Width func(childComplexity int) int
MediaURL func(childComplexity int) int
Title func(childComplexity int) int
}
MediaExif struct {
@ -109,9 +107,10 @@ type ComplexityRoot struct {
}
MediaURL struct {
Height func(childComplexity int) int
URL func(childComplexity int) int
Width func(childComplexity int) int
FileSize func(childComplexity int) int
Height func(childComplexity int) int
URL func(childComplexity int) int
Width func(childComplexity int) int
}
Mutation struct {
@ -472,12 +471,12 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Media.VideoWeb(childComplexity), true
case "MediaDownload.height":
if e.complexity.MediaDownload.Height == nil {
case "MediaDownload.mediaUrl":
if e.complexity.MediaDownload.MediaURL == nil {
break
}
return e.complexity.MediaDownload.Height(childComplexity), true
return e.complexity.MediaDownload.MediaURL(childComplexity), true
case "MediaDownload.title":
if e.complexity.MediaDownload.Title == nil {
@ -486,20 +485,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.MediaDownload.Title(childComplexity), true
case "MediaDownload.url":
if e.complexity.MediaDownload.URL == nil {
break
}
return e.complexity.MediaDownload.URL(childComplexity), true
case "MediaDownload.width":
if e.complexity.MediaDownload.Width == nil {
break
}
return e.complexity.MediaDownload.Width(childComplexity), true
case "MediaEXIF.aperture":
if e.complexity.MediaExif.Aperture == nil {
break
@ -584,6 +569,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.MediaExif.Media(childComplexity), true
case "MediaURL.fileSize":
if e.complexity.MediaURL.FileSize == nil {
break
}
return e.complexity.MediaURL.FileSize(childComplexity), true
case "MediaURL.height":
if e.complexity.MediaURL.Height == nil {
break
@ -1410,13 +1402,13 @@ type MediaURL {
width: Int!
"Height of the image in pixels"
height: Int!
"The file size of the resource in bytes"
fileSize: Int!
}
type MediaDownload {
title: String!
width: Int!
height: Int!
url: String!
mediaUrl: MediaURL!
}
enum MediaType {
@ -2971,7 +2963,7 @@ func (ec *executionContext) _MediaDownload_title(ctx context.Context, field grap
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) _MediaDownload_width(ctx context.Context, field graphql.CollectedField, obj *models.MediaDownload) (ret graphql.Marshaler) {
func (ec *executionContext) _MediaDownload_mediaUrl(ctx context.Context, field graphql.CollectedField, obj *models.MediaDownload) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
@ -2988,7 +2980,7 @@ func (ec *executionContext) _MediaDownload_width(ctx context.Context, field grap
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Width, nil
return obj.MediaURL, nil
})
if err != nil {
ec.Error(ctx, err)
@ -3000,77 +2992,9 @@ func (ec *executionContext) _MediaDownload_width(ctx context.Context, field grap
}
return graphql.Null
}
res := resTmp.(int)
res := resTmp.(*models.MediaURL)
fc.Result = res
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) _MediaDownload_height(ctx context.Context, field graphql.CollectedField, obj *models.MediaDownload) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "MediaDownload",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Height, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(int)
fc.Result = res
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) _MediaDownload_url(ctx context.Context, field graphql.CollectedField, obj *models.MediaDownload) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "MediaDownload",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.URL, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
return ec.marshalNMediaURL2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaURL(ctx, field.Selections, res)
}
func (ec *executionContext) _MediaEXIF_id(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) {
@ -3553,6 +3477,40 @@ func (ec *executionContext) _MediaURL_height(ctx context.Context, field graphql.
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) _MediaURL_fileSize(ctx context.Context, field graphql.CollectedField, obj *models.MediaURL) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "MediaURL",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.FileSize, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(int)
fc.Result = res
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_authorizeUser(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -7325,18 +7283,8 @@ func (ec *executionContext) _MediaDownload(ctx context.Context, sel ast.Selectio
if out.Values[i] == graphql.Null {
invalids++
}
case "width":
out.Values[i] = ec._MediaDownload_width(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "height":
out.Values[i] = ec._MediaDownload_height(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "url":
out.Values[i] = ec._MediaDownload_url(ctx, field, obj)
case "mediaUrl":
out.Values[i] = ec._MediaDownload_mediaUrl(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
@ -7429,6 +7377,11 @@ func (ec *executionContext) _MediaURL(ctx context.Context, sel ast.SelectionSet,
if out.Values[i] == graphql.Null {
invalids++
}
case "fileSize":
out.Values[i] = ec._MediaURL_fileSize(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}

View File

@ -22,10 +22,8 @@ type Filter struct {
}
type MediaDownload struct {
Title string `json:"title"`
Width int `json:"width"`
Height int `json:"height"`
URL string `json:"url"`
Title string `json:"title"`
MediaURL *MediaURL `json:"mediaUrl"`
}
type Notification struct {

View File

@ -3,6 +3,7 @@ package models
import (
"database/sql"
"path"
"time"
"github.com/viktorstrate/photoview/api/utils"
)
@ -14,6 +15,8 @@ type Media struct {
PathHash string
AlbumId int
ExifId *int
DateShot time.Time
DateImported time.Time
Favorite bool
Type MediaType
VideoMetadataId *int
@ -41,12 +44,13 @@ type MediaURL struct {
Height int
Purpose MediaPurpose
ContentType string
FileSize int
}
func NewMediaFromRow(row *sql.Row) (*Media, error) {
media := Media{}
if err := row.Scan(&media.MediaID, &media.Title, &media.Path, &media.PathHash, &media.AlbumId, &media.ExifId, &media.Favorite, &media.Type, &media.VideoMetadataId); err != nil {
if err := row.Scan(&media.MediaID, &media.Title, &media.Path, &media.PathHash, &media.AlbumId, &media.ExifId, &media.DateShot, &media.DateImported, &media.Favorite, &media.Type, &media.VideoMetadataId); err != nil {
return nil, err
}
@ -58,7 +62,7 @@ func NewMediaFromRows(rows *sql.Rows) ([]*Media, error) {
for rows.Next() {
var media Media
if err := rows.Scan(&media.MediaID, &media.Title, &media.Path, &media.PathHash, &media.AlbumId, &media.ExifId, &media.Favorite, &media.Type, &media.VideoMetadataId); err != nil {
if err := rows.Scan(&media.MediaID, &media.Title, &media.Path, &media.PathHash, &media.AlbumId, &media.ExifId, &media.DateShot, &media.DateImported, &media.Favorite, &media.Type, &media.VideoMetadataId); err != nil {
return nil, err
}
medias = append(medias, &media)
@ -84,7 +88,7 @@ func (p *MediaURL) URL() string {
func NewMediaURLFromRow(row *sql.Row) (*MediaURL, error) {
url := MediaURL{}
if err := row.Scan(&url.UrlID, &url.MediaId, &url.MediaName, &url.Width, &url.Height, &url.Purpose, &url.ContentType); err != nil {
if err := row.Scan(&url.UrlID, &url.MediaId, &url.MediaName, &url.Width, &url.Height, &url.Purpose, &url.ContentType, &url.FileSize); err != nil {
return nil, err
}
@ -96,7 +100,7 @@ func NewMediaURLFromRows(rows *sql.Rows) ([]*MediaURL, error) {
for rows.Next() {
var url MediaURL
if err := rows.Scan(&url.UrlID, &url.MediaId, &url.MediaName, &url.Width, &url.Height, &url.Purpose, &url.ContentType); err != nil {
if err := rows.Scan(&url.UrlID, &url.MediaId, &url.MediaName, &url.Width, &url.Height, &url.Purpose, &url.ContentType, &url.FileSize); err != nil {
return nil, err
}
urls = append(urls, &url)

View File

@ -18,6 +18,8 @@ type MediaEXIF struct {
Flash *string
Orientation *int
ExposureProgram *int
GPSLatitude *float64
GPSLonitude *float64
}
func (exif *MediaEXIF) Media() *Media {
@ -31,7 +33,7 @@ func (exif *MediaEXIF) ID() int {
func NewMediaExifFromRow(row *sql.Row) (*MediaEXIF, error) {
exif := MediaEXIF{}
if err := row.Scan(&exif.ExifID, &exif.Camera, &exif.Maker, &exif.Lens, &exif.DateShot, &exif.Exposure, &exif.Aperture, &exif.Iso, &exif.FocalLength, &exif.Flash, &exif.Orientation, &exif.ExposureProgram); err != nil {
if err := row.Scan(&exif.ExifID, &exif.Camera, &exif.Maker, &exif.Lens, &exif.DateShot, &exif.Exposure, &exif.Aperture, &exif.Iso, &exif.FocalLength, &exif.Flash, &exif.Orientation, &exif.ExposureProgram, &exif.GPSLatitude, &exif.GPSLonitude); err != nil {
return nil, err
}

View File

@ -108,10 +108,8 @@ func (r *mediaResolver) Downloads(ctx context.Context, obj *models.Media) ([]*mo
}
downloads = append(downloads, &models.MediaDownload{
Title: title,
Width: url.Width,
Height: url.Height,
URL: url.URL(),
Title: title,
MediaURL: url,
})
}

View File

@ -195,13 +195,13 @@ type MediaURL {
width: Int!
"Height of the image in pixels"
height: Int!
"The file size of the resource in bytes"
fileSize: Int!
}
type MediaDownload {
title: String!
width: Int!
height: Int!
url: String!
mediaUrl: MediaURL!
}
enum MediaType {

View File

@ -11,27 +11,33 @@ import (
"github.com/viktorstrate/photoview/api/graphql/models"
)
func CleanupMedia(db *sql.DB, albumId int, albumPhotos []*models.Media) []error {
if len(albumPhotos) == 0 {
return nil
func CleanupMedia(db *sql.DB, albumId int, albumMedia []*models.Media) []error {
albumMediaIds := make([]interface{}, len(albumMedia))
for i, photo := range albumMedia {
albumMediaIds[i] = photo.MediaID
}
albumPhotoIds := make([]interface{}, len(albumPhotos))
for i, photo := range albumPhotos {
albumPhotoIds[i] = photo.MediaID
// Delete missing media
var rows *sql.Rows
var err error
// Select media from database that was not found on hard disk
if len(albumMedia) > 0 {
media_args := make([]interface{}, 0)
media_args = append(media_args, albumId)
media_args = append(media_args, albumMediaIds...)
media_questions := strings.Repeat("?,", len(albumMediaIds))[:len(albumMediaIds)*2-1]
rows, err = db.Query(
"SELECT media_id FROM media WHERE album_id = ? AND media_id NOT IN ("+media_questions+")",
media_args...,
)
} else {
rows, err = db.Query(
"SELECT media_id FROM media WHERE album_id = ?",
albumId,
)
}
// Delete missing photos
media_args := make([]interface{}, 0)
media_args = append(media_args, albumId)
media_args = append(media_args, albumPhotoIds...)
media_questions := strings.Repeat("?,", len(albumPhotoIds))[:len(albumPhotoIds)*2-1]
rows, err := db.Query(
"SELECT media_id FROM media WHERE album_id = ? AND media_id NOT IN ("+media_questions+")",
media_args...,
)
if err != nil {
return []error{errors.Wrap(err, "get media files to be deleted from database")}
}
@ -56,7 +62,7 @@ func CleanupMedia(db *sql.DB, albumId int, albumPhotos []*models.Media) []error
}
if len(deleted_media_ids) > 0 {
media_questions = strings.Repeat("?,", len(deleted_media_ids))[:len(deleted_media_ids)*2-1]
media_questions := strings.Repeat("?,", len(deleted_media_ids))[:len(deleted_media_ids)*2-1]
if _, err := db.Exec("DELETE FROM media WHERE media_id IN ("+media_questions+")", deleted_media_ids...); err != nil {
deleteErrors = append(deleteErrors, errors.Wrap(err, "delete old media from database"))

View File

@ -18,6 +18,21 @@ type PhotoDimensions struct {
Height int
}
func DecodeImage(imagePath string) (image.Image, error) {
file, err := os.Open(imagePath)
if err != nil {
return nil, errors.Wrapf(err, "failed to open file to decode image (%s)", imagePath)
}
defer file.Close()
image, err := imaging.Decode(file, imaging.AutoOrientation(true))
if err != nil {
return nil, errors.Wrapf(err, "failed to decode image (%s)", imagePath)
}
return image, nil
}
func PhotoDimensionsFromRect(rect image.Rectangle) PhotoDimensions {
return PhotoDimensions{
Width: rect.Bounds().Max.X,
@ -133,13 +148,7 @@ func (img *EncodeMediaData) EncodeHighRes(tx *sql.Tx, outputPath string) error {
}
func EncodeThumbnail(inputPath string, outputPath string) (*PhotoDimensions, error) {
inputFile, err := os.Open(inputPath)
if err != nil {
return nil, err
}
defer inputFile.Close()
inputImage, _, err := image.Decode(inputFile)
inputImage, err := DecodeImage(inputPath)
if err != nil {
return nil, err
}
@ -161,13 +170,7 @@ func (img *EncodeMediaData) photoImage(tx *sql.Tx) (image.Image, error) {
return img._photoImage, nil
}
photoFile, err := os.Open(img.media.Path)
if err != nil {
return nil, err
}
defer photoFile.Close()
photoImg, _, err := image.Decode(photoFile)
photoImg, err := DecodeImage(img.media.Path)
if err != nil {
return nil, utils.HandleError("image decoding", err)
}
@ -184,37 +187,6 @@ func (img *EncodeMediaData) photoImage(tx *sql.Tx) (image.Image, error) {
}
}
if orientation == nil {
defaultOrientation := 0
orientation = &defaultOrientation
}
switch *orientation {
case 2:
photoImg = imaging.FlipH(photoImg)
break
case 3:
photoImg = imaging.Rotate180(photoImg)
break
case 4:
photoImg = imaging.FlipV(photoImg)
break
case 5:
photoImg = imaging.Transpose(photoImg)
break
case 6:
photoImg = imaging.Rotate270(photoImg)
break
case 7:
photoImg = imaging.Transverse(photoImg)
break
case 8:
photoImg = imaging.Rotate90(photoImg)
break
default:
break
}
img._photoImage = photoImg
return img._photoImage, nil
}

View File

@ -77,8 +77,13 @@ func ScanEXIF(tx *sql.Tx, media *models.Media) (returnExif *models.MediaEXIF, re
date, err := exifTags.DateTime()
if err == nil {
valueNames = append(valueNames, "dateShot")
valueNames = append(valueNames, "date_shot")
exifValues = append(exifValues, date)
_, err := tx.Exec("UPDATE media SET date_shot = ? WHERE media_id = ?", date, media.MediaID)
if err != nil {
log.Printf("WARN: Failed to update date_shot for media %s: %s", media.Title, err)
}
}
exposure, err := readRationalTag(exifTags, exif.ExposureTime, media)
@ -148,6 +153,15 @@ func ScanEXIF(tx *sql.Tx, media *models.Media) (returnExif *models.MediaEXIF, re
exifValues = append(exifValues, *exposureProgram)
}
lat, long, err := exifTags.LatLong()
if err == nil {
valueNames = append(valueNames, "gps_latitude")
exifValues = append(exifValues, lat)
valueNames = append(valueNames, "gps_longitude")
exifValues = append(exifValues, long)
}
if len(valueNames) == 0 {
return nil, nil
}

View File

@ -4,6 +4,7 @@ import (
"io"
"log"
"os"
"path"
"path/filepath"
"strings"
@ -265,16 +266,21 @@ func getMediaType(path string) (*MediaType, error) {
return nil, nil
}
func isPathMedia(path string, cache *AlbumScannerCache) bool {
mediaType, err := cache.GetMediaType(path)
func isPathMedia(mediaPath string, cache *AlbumScannerCache) bool {
mediaType, err := cache.GetMediaType(mediaPath)
if err != nil {
ScannerError("%s (%s)", err, path)
ScannerError("%s (%s)", err, mediaPath)
return false
}
// Ignore hidden files
if path.Base(mediaPath)[0:1] == "." {
return false
}
if mediaType != nil {
// Make sure file isn't empty
fileStats, err := os.Stat(path)
fileStats, err := os.Stat(mediaPath)
if err != nil || fileStats.Size() == 0 {
return false
}
@ -282,6 +288,6 @@ func isPathMedia(path string, cache *AlbumScannerCache) bool {
return true
}
log.Printf("File is not a supported media %s\n", path)
log.Printf("File is not a supported media %s\n", mediaPath)
return false
}

View File

@ -128,8 +128,13 @@ func processPhoto(tx *sql.Tx, imageData *EncodeMediaData, photoCachePath *string
return false, err
}
_, err = tx.Exec("INSERT INTO media_url (media_id, media_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)",
photo.MediaID, highres_name, photoDimensions.Width, photoDimensions.Height, models.PhotoHighRes, "image/jpeg")
fileStats, err := os.Stat(baseImagePath)
if err != nil {
return false, errors.Wrap(err, "reading file stats of highres photo")
}
_, err = tx.Exec("INSERT INTO media_url (media_id, media_name, width, height, purpose, content_type, file_size) VALUES (?, ?, ?, ?, ?, ?, ?)",
photo.MediaID, highres_name, photoDimensions.Width, photoDimensions.Height, models.PhotoHighRes, "image/jpeg", fileStats.Size())
if err != nil {
return false, errors.Wrapf(err, "could not insert highres media url (%d, %s)", photo.MediaID, photo.Title)
}
@ -187,7 +192,12 @@ func processPhoto(tx *sql.Tx, imageData *EncodeMediaData, photoCachePath *string
return false, errors.Wrap(err, "could not create thumbnail cached image")
}
_, err = tx.Exec("INSERT INTO media_url (media_id, media_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo.MediaID, thumbnail_name, thumbSize.Width, thumbSize.Height, models.PhotoThumbnail, "image/jpeg")
fileStats, err := os.Stat(thumbOutputPath)
if err != nil {
return false, errors.Wrap(err, "reading file stats of thumbnail photo")
}
_, err = tx.Exec("INSERT INTO media_url (media_id, media_name, width, height, purpose, content_type, file_size) VALUES (?, ?, ?, ?, ?, ?, ?)", photo.MediaID, thumbnail_name, thumbSize.Width, thumbSize.Height, models.PhotoThumbnail, "image/jpeg", fileStats.Size())
if err != nil {
return false, err
}
@ -250,7 +260,12 @@ func saveOriginalPhotoToDB(tx *sql.Tx, photo *models.Media, imageData *EncodeMed
return err
}
_, err = tx.Exec("INSERT INTO media_url (media_id, media_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo.MediaID, original_image_name, photoDimensions.Width, photoDimensions.Height, models.MediaOriginal, contentType)
fileStats, err := os.Stat(photo.Path)
if err != nil {
return errors.Wrap(err, "reading file stats of original photo")
}
_, err = tx.Exec("INSERT INTO media_url (media_id, media_name, width, height, purpose, content_type, file_size) VALUES (?, ?, ?, ?, ?, ?, ?)", photo.MediaID, original_image_name, photoDimensions.Width, photoDimensions.Height, models.MediaOriginal, contentType, fileStats.Size())
if err != nil {
log.Printf("Could not insert original photo url: %d, %s\n", photo.MediaID, photoName)
return err

View File

@ -5,6 +5,7 @@ import (
"database/sql"
"fmt"
"log"
"os"
"path"
"strings"
"time"
@ -56,8 +57,13 @@ func processVideo(tx *sql.Tx, mediaData *EncodeMediaData, videoCachePath *string
return false, errors.Wrapf(err, "failed to read metadata for encoded web-video (%s)", video.Title)
}
_, err = tx.Exec("INSERT INTO media_url (media_id, media_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)",
video.MediaID, web_video_name, webMetadata.Width, webMetadata.Height, models.VideoWeb, "video/mp4")
fileStats, err := os.Stat(webVideoPath)
if err != nil {
return false, errors.Wrap(err, "reading file stats of web-optimized video")
}
_, err = tx.Exec("INSERT INTO media_url (media_id, media_name, width, height, purpose, content_type, file_size) VALUES (?, ?, ?, ?, ?, ?, ?)",
video.MediaID, web_video_name, webMetadata.Width, webMetadata.Height, models.VideoWeb, "video/mp4", fileStats.Size())
if err != nil {
return false, errors.Wrapf(err, "failed to insert encoded web-video into database (%s)", video.Title)
}
@ -83,8 +89,13 @@ func processVideo(tx *sql.Tx, mediaData *EncodeMediaData, videoCachePath *string
return false, errors.Wrap(err, "get dimensions of video thumbnail image")
}
_, err = tx.Exec("INSERT INTO media_url (media_id, media_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)",
video.MediaID, video_thumb_name, thumbDimensions.Width, thumbDimensions.Height, models.VideoThumbnail, "image/jpeg")
fileStats, err := os.Stat(thumbImagePath)
if err != nil {
return false, errors.Wrap(err, "reading file stats of video thumbnail")
}
_, err = tx.Exec("INSERT INTO media_url (media_id, media_name, width, height, purpose, content_type, file_size) VALUES (?, ?, ?, ?, ?, ?, ?)",
video.MediaID, video_thumb_name, thumbDimensions.Width, thumbDimensions.Height, models.VideoThumbnail, "image/jpeg", fileStats.Size())
if err != nil {
return false, errors.Wrapf(err, "failed to insert video thumbnail image into database (%s)", video.Title)
}

View File

@ -3,6 +3,7 @@ package scanner
import (
"database/sql"
"log"
"os"
"path"
"github.com/pkg/errors"
@ -40,7 +41,12 @@ func ScanMedia(tx *sql.Tx, mediaPath string, albumId int, cache *AlbumScannerCac
mediaTypeText = "photo"
}
result, err := tx.Exec("INSERT INTO media (title, path, path_hash, album_id, media_type) VALUES (?, ?, MD5(path), ?, ?)", mediaName, mediaPath, albumId, mediaTypeText)
stat, err := os.Stat(mediaPath)
if err != nil {
return nil, false, err
}
result, err := tx.Exec("INSERT INTO media (title, path, path_hash, album_id, media_type, date_shot) VALUES (?, ?, MD5(path), ?, ?, ?)", mediaName, mediaPath, albumId, mediaTypeText, stat.ModTime())
if err != nil {
return nil, false, errors.Wrap(err, "could not insert media into database")
}

View File

@ -1,12 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Chrome",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000",
"webRoot": "${workspaceRoot}/src"
}
]
}

4079
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
"license": "GPL-3.0",
"description": "UI app for Photoview",
"dependencies": {
"@babel/preset-env": "^7.10.2",
"@babel/preset-env": "^7.11.0",
"apollo-cache-inmemory": "^1.6.6",
"apollo-client": "^2.6.10",
"apollo-link": "^1.2.14",
@ -12,24 +12,24 @@
"apollo-link-error": "^1.1.13",
"apollo-link-http": "^1.5.17",
"apollo-link-ws": "^1.0.20",
"babel-plugin-styled-components": "^1.10.7",
"babel-plugin-styled-components": "^1.11.1",
"copy-to-clipboard": "^3.3.1",
"downloadjs": "^1.4.7",
"graphql": "^15.1.0",
"graphql-tag": "^2.10.3",
"graphql": "^15.3.0",
"graphql-tag": "^2.11.0",
"parcel-bundler": "^1.12.4",
"prop-types": "^15.7.2",
"react": "^16.13.1",
"react-apollo": "^3.1.5",
"react-dom": "^16.13.1",
"react-helmet": "^6.1.0",
"react-lazyload": "^2.6.8",
"react-lazyload": "^2.6.9",
"react-router-dom": "^5.2.0",
"react-spring": "^8.0.27",
"semantic-ui-css": "^2.4.1",
"semantic-ui-react": "^0.88.2",
"semantic-ui-react": "^1.2.0",
"styled-components": "^5.1.1",
"subscriptions-transport-ws": "^0.9.16",
"subscriptions-transport-ws": "^0.9.18",
"url-join": "^4.0.1"
},
"scripts": {
@ -37,16 +37,16 @@
"build": "parcel build src/index.html --no-source-maps"
},
"devDependencies": {
"@babel/core": "^7.10.2",
"@babel/plugin-transform-runtime": "^7.10.1",
"@babel/core": "^7.11.1",
"@babel/plugin-transform-runtime": "^7.11.0",
"babel-eslint": "^10.1.0",
"babel-plugin-graphql-tag": "^2.5.0",
"babel-plugin-graphql-tag": "^3.0.0",
"babel-plugin-transform-semantic-ui-react-imports": "^1.4.1",
"eslint": "^7.2.0",
"eslint-plugin-react": "^7.20.0",
"eslint-plugin-react-hooks": "^4.0.4",
"eslint": "^7.7.0",
"eslint-plugin-react": "^7.20.6",
"eslint-plugin-react-hooks": "^4.1.0",
"husky": "^4.2.5",
"lint-staged": "^10.2.10",
"lint-staged": "^10.2.11",
"parcel-plugin-sw-cache": "^0.3.1",
"prettier": "^2.0.5",
"react-router-prop-types": "^1.0.4"

View File

@ -21,9 +21,8 @@ const adminQuery = gql`
const Container = styled.div`
height: 100%;
display: flex;
/* margin-right: 500px; */
/* display: grid;
grid-template-columns: 80px 1fr 500px; */
overflow: hidden;
position: relative;
`
const SideMenu = styled.div`
@ -31,6 +30,18 @@ const SideMenu = styled.div`
width: 80px;
left: 0;
padding-top: 70px;
@media (max-width: 1000px) {
width: 100%;
height: 80px;
position: fixed;
background: white;
z-index: 10;
padding-top: 0;
display: flex;
bottom: 0;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
}
`
const Content = styled.div`

View File

@ -31,6 +31,8 @@ const photoQuery = gql`
}
highRes {
url
width
height
}
videoWeb {
url

View File

@ -1,22 +1,14 @@
import React, { useState } from 'react'
import gql from 'graphql-tag'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import RouterProps from 'react-router-prop-types'
import React, { useState } from 'react'
import { useQuery } from 'react-apollo'
import { Route, Switch } from 'react-router-dom'
import RouterProps from 'react-router-prop-types'
import { Form, Header, Icon, Input, Message } from 'semantic-ui-react'
import styled from 'styled-components'
import { getSharePassword, saveSharePassword } from '../../authentication'
import AlbumSharePage from './AlbumSharePage'
import MediaSharePage from './MediaSharePage'
import { useQuery } from 'react-apollo'
import gql from 'graphql-tag'
import {
Container,
Header,
Form,
Button,
Input,
Icon,
Message,
} from 'semantic-ui-react'
import { saveSharePassword, getSharePassword } from '../../authentication'
const shareTokenQuery = gql`
query SharePageToken($token: String!, $password: String) {
@ -61,9 +53,12 @@ const shareTokenQuery = gql`
}
downloads {
title
url
width
height
mediaUrl {
url
width
height
fileSize
}
}
highRes {
url

View File

@ -9,7 +9,7 @@ const Container = styled.div`
position: relative;
`
const AlbumBoxes = ({ loading, error, albums, getCustomLink }) => {
const AlbumBoxes = ({ error, albums, getCustomLink }) => {
if (error) return <div>Error {error.message}</div>
let albumElements = []
@ -28,12 +28,7 @@ const AlbumBoxes = ({ loading, error, albums, getCustomLink }) => {
}
}
return (
<Container>
{/* <Loader active={loading}>Loading albums</Loader> */}
{albumElements}
</Container>
)
return <Container>{albumElements}</Container>
}
AlbumBoxes.propTypes = {

View File

@ -21,6 +21,15 @@ const Title = styled.h1`
font-weight: 400;
padding: 2px 12px;
flex-grow: 1;
min-width: 245px;
@media (max-width: 400px) {
min-width: auto;
& span {
display: none;
}
}
`
const Logo = styled.img`

View File

@ -12,6 +12,10 @@ const Container = styled.div`
bottom: 20px;
right: 20px;
width: 500px;
@media (max-width: 1000px) {
display: none;
}
`
export let MessageState = {

View File

@ -15,6 +15,11 @@ const Gallery = styled.div`
min-height: 200px;
position: relative;
margin: -4px;
@media (max-width: 1000px) {
/* Compensate for tab bar on mobile */
margin-bottom: 76px;
}
`
const PhotoFiller = styled.div`

View File

@ -4,14 +4,10 @@ import PropTypes from 'prop-types'
const getProtectedUrl = url => {
const imgUrl = new URL(url)
if (localStorage.getItem('token') == null) {
// Get share token if not authorized
const tokenRegex = location.pathname.match(/^\/share\/([\d\w]+)(\/?.*)$/)
if (tokenRegex) {
const token = tokenRegex[1]
imgUrl.searchParams.set('token', token)
}
const tokenRegex = location.pathname.match(/^\/share\/([\d\w]+)(\/?.*)$/)
if (tokenRegex) {
const token = tokenRegex[1]
imgUrl.searchParams.set('token', token)
}
return imgUrl.href
@ -20,76 +16,30 @@ const getProtectedUrl = url => {
/**
* An image that needs authorization to load
*/
export const ProtectedImage = ({ src, ...props }) => {
// const [imgSrc, setImgSrc] = useState(null)
// useEffect(() => {
// if (imageCache[src]) return
// const fetchController = new AbortController()
// let canceled = false
// setImgSrc('')
// const imgUrl = new URL(src)
// const fetchHeaders = {}
// if (localStorage.getItem('token') == null) {
// // Get share token if not authorized
// const tokenRegex = location.pathname.match(/^\/share\/([\d\w]+)(\/?.*)$/)
// if (tokenRegex) {
// const token = tokenRegex[1]
// imgUrl.searchParams.set('token', token)
// const tokenPassword = sessionStorage.getItem(`share-token-pw-${token}`)
// if (tokenPassword) {
// fetchHeaders['TokenPassword'] = tokenPassword
// }
// }
// }
// fetchProtectedImage(imgUrl.href, {
// signal: fetchController.signal,
// headers: fetchHeaders,
// })
// .then(newSrc => {
// if (!canceled) {
// setImgSrc(newSrc)
// }
// })
// .catch(error => {
// console.log('Fetch image error', error.message)
// })
// return function cleanup() {
// canceled = true
// fetchController.abort()
// }
// }, [src])
return (
<img {...props} src={getProtectedUrl(src)} crossOrigin="use-credentials" />
)
}
export const ProtectedImage = ({ src, ...props }) => (
<img
key={src}
{...props}
src={getProtectedUrl(src)}
crossOrigin="use-credentials"
/>
)
ProtectedImage.propTypes = {
src: PropTypes.string.isRequired,
}
export const ProtectedVideo = ({ media, ...props }) => {
return (
<video
{...props}
controls
key={media.id}
crossOrigin="use-credentials"
poster={getProtectedUrl(media.thumbnail.url)}
>
<source src={getProtectedUrl(media.videoWeb.url)} type="video/mp4" />
</video>
)
}
export const ProtectedVideo = ({ media, ...props }) => (
<video
{...props}
controls
key={media.id}
crossOrigin="use-credentials"
poster={getProtectedUrl(media.thumbnail.url)}
>
<source src={getProtectedUrl(media.videoWeb.url)} type="video/mp4" />
</video>
)
ProtectedVideo.propTypes = {
media: PropTypes.object.isRequired,

View File

@ -1,4 +1,4 @@
import React from 'react'
import React, { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import { ProtectedImage, ProtectedVideo } from '../ProtectedMedia'

View File

@ -18,6 +18,7 @@ const OverlayButton = styled.button`
height: 64px;
background: none;
border: none;
outline: none;
cursor: pointer;
position: absolute;

View File

@ -1,6 +1,7 @@
import React, { createContext } from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import { Icon } from 'semantic-ui-react'
const SidebarContainer = styled.div`
width: 28vw;
@ -17,8 +18,23 @@ const SidebarContainer = styled.div`
@media (max-width: 700px) {
position: absolute;
width: 100%;
max-width: calc(100vw - 85px);
transform: translateX(100vw);
/* full height - header - tabbar */
height: calc(100% - 60px - 80px);
max-width: min(calc(100vw - 85px), 400px);
${({ highlighted }) => `right: ${highlighted ? 0 : -100}%;`}
padding-top: 45px;
}
transition: right 200ms ease-in-out;
`
const SidebarDismissButton = styled(Icon)`
position: absolute;
top: 10px;
right: 10px;
@media (min-width: 700px) {
display: none;
}
`
@ -46,8 +62,14 @@ class Sidebar extends React.Component {
{this.props.children}
<SidebarContext.Consumer>
{value => (
<SidebarContainer>
<SidebarContainer highlighted={value.content != null}>
{value.content}
<SidebarDismissButton
name="angle double right"
size="big"
link
onClick={() => this.setState({ content: null })}
/>
<div style={{ height: 100 }}></div>
</SidebarContainer>
)}

View File

@ -14,9 +14,12 @@ const downloadQuery = gql`
id
downloads {
title
url
width
height
mediaUrl {
url
width
height
fileSize
}
}
}
}
@ -171,10 +174,14 @@ const SidebarDownload = ({ photo }) => {
}
let downloadRows = downloads.map(x => (
<DownloadTableRow key={x.url} onClick={() => downloadPhoto(x.url)}>
<DownloadTableRow
key={x.mediaUrl.url}
onClick={() => downloadPhoto(x.mediaUrl.url)}
>
<Table.Cell>{`${x.title}`}</Table.Cell>
<Table.Cell>{`${x.width} x ${x.height}`}</Table.Cell>
<Table.Cell>{extractExtension(x.url)}</Table.Cell>
<Table.Cell>{`${x.mediaUrl.width} x ${x.mediaUrl.height}`}</Table.Cell>
<Table.Cell>{`${formatBytes(x.mediaUrl.fileSize)}`}</Table.Cell>
<Table.Cell>{extractExtension(x.mediaUrl.url)}</Table.Cell>
</DownloadTableRow>
))
@ -187,6 +194,7 @@ const SidebarDownload = ({ photo }) => {
<Table.Row>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Dimensions</Table.HeaderCell>
<Table.HeaderCell>Size</Table.HeaderCell>
<Table.HeaderCell>Type</Table.HeaderCell>
</Table.Row>
</Table.Header>